node.js寫爬蟲程序抓取維基百科(wikiSpider)

任務說明

抓取維基百科中文站某幾個分類到本地,包括圖片資源,能在單機直接瀏覽。css

基本思路

思路一(origin:master):從維基百科的某個分類(好比:航空母艦(key))頁面開始,找出連接的title屬性中包含key(航空母艦)的全部目標,加入到待抓取隊列中。這樣,抓一個頁面的代碼及其圖片的同時,也獲取這個網頁上全部與key相關的其它網頁的地址,採起一個類廣度優先遍歷的算法來完成此任務。
思路二(origin:cat):按分類進行抓取。注意到,維基百科上,分類都以Category:開頭,因爲維基百科有很好的文檔結構,很容易從任一個分類,開始,一直把其下的全部分類全都抓取下來。這個算法對分類頁面,提取子分類,且並行抓取其下全部頁面,速度快,能夠把分類結構保存下來,但其實有不少的重複頁面,不過這個能夠後期寫個腳本就能很容易的處理。html

庫的選擇

開始想用jsdom,雖然感受它功能強大,但也比較「重」,最要命的是說明文檔不夠好,只說了它的優點,沒一個全面的說明。所以,換成cheerio,輕量級,功能比較全,至少文檔一看就能有一個總體概念。其實作到後來,才發現根本不須要庫,用正則表達式就能搞定一切!用庫只是少寫了一點正則而矣。git

關鍵點

全局變量設定

var regKey = ['航空母艦','航空母艦','航母'];    //連接中若包含此中關鍵詞,即爲目標
var allKeys = [];                            //連接的title,也是頁面標識,避免重複抓取
var keys = ['Category:%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0'];    //等待隊列,起始頁

圖片下載

使用request庫的流式操做,讓每個下載操做造成閉包。注意異步操做可能帶來的反作用。另外,圖片名字要從新設定,開始我取原名,不知道爲何,有的圖明明存在,就是顯示不出來;而且要把srcset屬性清理掉,否則本面顯示不出來。github

$ = cheer.load(downHtml);
  var rsHtml = $.html();
  var imgs = $('#bodyContent .image');        //圖片都由這個樣式修飾
  for(img in imgs){
    if(typeof imgs[img].attribs === 'undefined' || typeof imgs[img].attribs.href === 'undefined')
      {continue;}    //結構爲連接下的圖片,連接不存在,跳過
    else
      {
        var picUrl = imgs[img].children[0].attribs.src;    //圖片地址
        var dirs = picUrl.split('.');
        var filename = baseDir+uuid.v1()+'.'+dirs[dirs.length -1];    //從新命名

        request("https:"+picUrl).pipe(fs.createWriteStream('pages/'+filename));    //下載

        rsHtml = rsHtml.replace(picUrl,filename);    //換成本地路徑
        // console.log(picUrl);
      }
  }

廣度優先遍歷

開始沒能徹底理解異步的概念,以循環方式來作,覺得使用了Promise,就已經全轉化爲同步了,但其實只是能保證交給promise的操做會有序進行,並不能讓這些操做與其它的操做有序化!如,下面的代碼就是不正確的。正則表達式

var keys = ['航空母艦'];
var key = keys.shift();
while(key){
  data.get({
    url:encodeURI(key),
    qs:null
  }).then(function(downHtml){
       ...
       keys.push(key);                //(1)
    }
  });
key = keys.shift();                    //(2)
}

上面的操做看試很正常,但其實(2)會在(1)之間被運行!哪怎麼辦?
我使用遞歸來解決這個問題。以下示例代碼:算法

var key = keys.shift();
(function doNext(key){
  data.get({
    url:key,
    qs:null
  }).then(function(downHtml){
    ...
    keys.push(href);
    ...
    key = keys.shift();
    if(key){
      doNext(key);
    }else{
      console.log('抓取任務順利完成。')
    }
  })
})(key);

正則清理

使用正則表達式清理無用的頁面代碼,由於有不少模式須要處理,寫了一個循環統一處理。編程

var regs = [/<link rel=\"stylesheet\" href=\"?[^\"]*\">/g,
    /<script>?[^<]*<\/script>/g,
  /<style>?[^<]*<\/style>/g,
  /<a ?[^>]*>/g,
  /<\/a>/g,
  /srcset=(\"?[^\"]*\")/g
  ]
  regs.forEach(function(rs){
    var mactches = rsHtml.match(rs);
    for (var i=0;i < mactches.length ; i++)
    {
      rsHtml = rsHtml.replace(mactches[i],mactches[i].indexOf('stylesheet')>-1?'<link rel="stylesheet" href="wiki'+(i+1)+'.css"':'');
    }
  })

運行效果

上維基中文是須要FQ的,試運行了一下,抓取 航空母艦 分類,運行過程當中,發現了三百左右的相關連接(包括分類頁面,這些頁面我是隻取有效連接,不下載),最終正確的下載了209個,手工測試了一些出錯連接,發現都爲無效連接,顯示該詞條還未創建,整個過程大概花了不到十五分鐘,壓縮後近三十M,感受效果還不錯。promise

源代碼

https://github.com/zhoutk/wikiSpider

小結

到昨晚基本完成任務,思路一可以抓取內容比較準確的頁面,並且頁面不重複,但抓取效率不高,分類信息沒法準確得到;思路二可以按維基百科的分類,自動抓取並分門別類的把文件存儲到本地,效率高(實測,抓取【軍艦】類,共抓取頁面近六千個,費時五十來分鐘,每分鐘能抓取超過一百個頁面),能準確的保存分類信息。
最大的收穫在於深入的理解了異步編程的總體流程控制。閉包

相關文章
相關標籤/搜索