基本思路
思路一(origin:master):從維基百科的某個分類(比如:航空母艦(key))頁面開始,找出鏈接的title屬性中包含key(航空母艦)的所有目標,加入到待抓取隊列中。這樣,抓一個頁面的代碼及其圖片的同時,也獲取這個網頁上所有與key相關的其它網頁的地址,採取一個類廣度優先遍歷的算法來完成此任務。
思路二(origin:cat):按分類進行抓取。注意到,維基百科上,分類都以Category:開頭,由於維基百科有很好的文檔結構,很容易從任一個分類,開始,一直把其下的所有分類全都抓取下來。這個算法對分類頁面,提取子分類,且並行抓取其下所有頁面,速度快,可以把分類結構保存下來,但其實有很多的重複頁面,不過這個可以後期寫個腳本就能很容易的處理。
庫的選擇
開始想用jsdom,雖然感覺它功能強大,但也比較“重”,最要命的是說明文檔不夠好,只說了它的優勢,沒一個全面的說明。因此,換成cheerio,輕量級,功能比較全,至少文檔一看就能有一個整體概念。其實做到後來,才發現根本不需要庫,用正則表達式就能搞定一切!用庫只是少寫了一點正則而矣。
關鍵點
全局變量設定:
var regKey = ['航空母艦','航空母','航母']; //鏈接中若包含此中關鍵詞,即為目標var allKeys = []; //鏈接的title,也是頁面標識,避免重複抓取var keys = ['Category:%E8%88%AA%E7%A9%BA%E6%AF%8D%E8%88%B0']; //等待隊列,起始頁
圖片下載
使用request庫的流式操作,讓每一個下載操作形成閉包。注意異步操作可能帶來的副作用。另外,圖片名字要重新設定,開始我取原名,不知道為什麼,有的圖明明存在,就是顯示不出來;並且要把srcset屬性清理掉,不然本面顯示不出來。
$ = 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,感覺效果還不錯。
原始碼
https://github.com/zhoutk/wikiSpider
小結
到昨晚基本完成任務,思路一能夠抓取內容比較準確的頁面,而且頁面不重複,但抓取效率不高,分類信息無法準確獲得;思路二能夠按維基百科的分類,自動抓取並分門別類的把文件存儲到本地,效率高(實測,抓取【軍艦】類,共抓取頁面近六千個,費時五十來分鐘,每分鐘能抓取超過一百個頁面),能準確的保存分類信息。
最大的收穫在於深刻的理解了異步編程的整體流程控制。