這篇文章講解一下yunshare項(xiàng)目的爬蟲(chóng)模型。

使用nodejs開(kāi)發(fā)爬蟲(chóng)很簡(jiǎn)單,不需要類似python的scrapy這樣的爬蟲(chóng)框架,只需要用request或者superagent這樣的http庫(kù)就能完成大部分的爬蟲(chóng)工作了。

使用nodejs開(kāi)發(fā)爬蟲(chóng)半年左右了,爬蟲(chóng)可以很簡(jiǎn)單,也可以很復(fù)雜。簡(jiǎn)單的爬蟲(chóng)定向爬取一個(gè)網(wǎng)站,可能有個(gè)幾萬(wàn)或者幾十萬(wàn)的頁(yè)面請(qǐng)求,復(fù)雜的爬蟲(chóng)類似google bot這樣搜索引擎的蜘蛛爬蟲(chóng),要每時(shí)每刻爬取互聯(lián)網(wǎng)上最新的內(nèi)容。

一般的個(gè)人開(kāi)發(fā)者都是用爬蟲(chóng)定向爬取一些網(wǎng)站,然后提取一些結(jié)構(gòu)化的數(shù)據(jù),使用api接口獲取數(shù)據(jù)也可以歸到這一類。如果想簡(jiǎn)單的練習(xí)爬蟲(chóng)技術(shù),可以嘗試爬取豆瓣電影數(shù)據(jù)和書(shū)籍?dāng)?shù)據(jù)的,使用api接口和爬取html頁(yè)面都能完成這個(gè)任務(wù)。

爬蟲(chóng)的說(shuō)白了就是一個(gè)http客戶端,通過(guò)http協(xié)議和遠(yuǎn)程http服務(wù)器通信,獲取html頁(yè)面內(nèi)容或者其他的種子文件,pdf文件等等。和瀏覽器不同的一點(diǎn)就是爬蟲(chóng)不會(huì)把抓取的內(nèi)容渲染出來(lái),而是解析頁(yè)面內(nèi)容然后保存到數(shù)據(jù)庫(kù)里面。

在開(kāi)始學(xué)習(xí)爬蟲(chóng)的時(shí)候我考慮的是怎么爬取html頁(yè)面內(nèi)容,怎么解析html頁(yè)面之間的鏈接規(guī)則,后來(lái)遇到了頁(yè)面編碼的問(wèn)題。

統(tǒng)一utf8編碼

國(guó)內(nèi)網(wǎng)站主要是使用html和gbk這兩種編碼方式,解決編碼有兩種思路,第一個(gè)是在獲取頁(yè)面內(nèi)容的時(shí)候根據(jù)頁(yè)面的<meta charset='gbk'>編碼把內(nèi)容統(tǒng)一轉(zhuǎn)碼成utf8的,因?yàn)閚odejs字符串默認(rèn)編碼就是utf8。

這個(gè)方案充滿了不確定性。

問(wèn)題1:不同網(wǎng)站的指定編碼的方式不一樣,除了前面提到的那種方式,還有<meta http-equiv="Content-Type" content="text/html; charset=gbk">這種方式指定編碼,這個(gè)問(wèn)題還不是很大,很多的http工具庫(kù)都能正確的解析這兩種編碼,問(wèn)題是還有很多網(wǎng)站沒(méi)有指定編碼,又或者指定的編碼和文件的實(shí)際編碼不一致(遇到過(guò)真實(shí)的案例)。

問(wèn)題2:如果你把gbk編碼的html文件轉(zhuǎn)成utf8編碼保存到本地,用瀏覽器直接打開(kāi)這個(gè)文件的時(shí)候會(huì)顯示亂碼,非常不利于開(kāi)發(fā)過(guò)程中的查找問(wèn)題。

不轉(zhuǎn)碼html內(nèi)容

既然前面的方案有這么多的問(wèn)題,剩下的方法就是把html內(nèi)容直接按照原來(lái)的編碼保存到本地,然后解析的時(shí)候指定編碼。

這個(gè)方法有2個(gè)好處:1、簡(jiǎn)化了爬蟲(chóng)模型,2、可以用瀏覽器打開(kāi)html文件,不會(huì)亂碼。唯一的缺點(diǎn)是不同網(wǎng)站文件內(nèi)容解析的時(shí)候似乎需要指定編碼,對(duì)于小規(guī)模爬蟲(chóng)這個(gè)問(wèn)題其實(shí)影響不大。

統(tǒng)一爬蟲(chóng)模型

前面的編碼方案解決了爬取不同網(wǎng)站html文件的編碼問(wèn)題,我們可以用一個(gè)統(tǒng)一的爬蟲(chóng)方法爬取不同網(wǎng)站的內(nèi)容,那如果你想爬取非html內(nèi)容呢?

是不是又要重新寫(xiě)一個(gè)爬蟲(chóng)方法,解決這個(gè)問(wèn)題的方法就是http協(xié)議,假設(shè)我們寫(xiě)的這個(gè)爬蟲(chóng)方法就是一個(gè)完整的http客戶端,那理論上這個(gè)客戶端是不是能根據(jù)Content-Typ獲取各種格式的文件。

那到底能不能用一個(gè)簡(jiǎn)單的方法就能實(shí)現(xiàn)上述的功能呢?下面的方法就是我采用request寫(xiě)的nodejs簡(jiǎn)單高效的爬蟲(chóng)模型。

function fetch(url) { console.log(`down ${url} started`); const deferred = Q.defer(); const file = getfile(url);
  fs.ensureDirSync(path.dirname(file)); const stream = request
    .get(url)
    .on('error', (err) => {
      deferred.reject(`down ${url}:${err}`);
    })
    .on('response', (res) => { if (res.statusCode !== 200) {
        deferred.reject(`down ${url}:${res.statusCode}`);
      } else { console.log(`down ${url}:${res.statusCode}`);
      }
    })
    .pipe(fs.createWriteStream(`${file}`));

  stream.on('finish', () => {
    deferred.resolve();
  }); return deferred.promise;
}

這段代碼在yunshare/src/util/fetch.js里面,當(dāng)然這個(gè)方法不能單獨(dú)運(yùn)行,但是關(guān)鍵的邏輯就是這么簡(jiǎn)單。

不管是什么格式的http請(qǐng)求,json,html,torrent等都統(tǒng)一把返回的二進(jìn)制格式文件保存到以md5(url)為文件名的位置。上面的getfile就是用來(lái)獲取文件路徑的。

模型擴(kuò)展

使用MD5散列還是有發(fā)生沖突的風(fēng)險(xiǎn)的,如果你想要爬取上億的網(wǎng)頁(yè),可能還需要對(duì)上面的模型進(jìn)行擴(kuò)展。一個(gè)簡(jiǎn)單的思路就是把網(wǎng)頁(yè)路徑中的域名提取出來(lái),不同網(wǎng)站的內(nèi)容保存在對(duì)應(yīng)的域文件夾下面。

其他的類似的思路也行,需要注意的就是如果爬蟲(chóng)保存文件和解析文件是分開(kāi)的,你需要保證在解析文件的時(shí)候能用同樣的方法定位這個(gè)文件。共同的參數(shù)就是url,所以你生成文件名的時(shí)候不能用一些隨時(shí)間變化的參數(shù)。

最后,獻(xiàn)上第一個(gè)使用node全棧開(kāi)發(fā)的網(wǎng)站:嗶哩搜索,目前索引百度網(wǎng)盤(pán)資源1000w條了。