Nodejs爬蟲進階=>異步并發控制
來自: http://www.cnblogs.com/tianheila/p/5183733.html
之前寫了個現在看來很不完美的小爬蟲,很多地方沒有處理好,比如說在知乎點開一個問題的時候,它的所有回答并不是全部加載好了的,當你拉到回答的尾部時,點擊加載更多,回答才會再加載一部分,所以說如果直接發送一個問題的請求鏈接,取得的頁面是不完整的。還有就是我們通過發送鏈接下載圖片的時候,是一張一張來下的,如果圖片數量太多的話,真的是下到你睡完覺它還在下,而且我們用nodejs寫的爬蟲,卻竟然沒有用到nodejs最牛逼的異步并發的特性,太浪費了啊。
思路
這次的的爬蟲是上次那個的升級版,不過呢,上次那個雖然是簡單,但是很適合新手學習啊。這次的爬蟲代碼在我的github上可以找到=> NodeSpider。
整個爬蟲的思路是這樣的:在一開始我們通過請求問題的鏈接抓取到部分頁面數據,接下來我們在代碼中模擬ajax請求截取剩余頁面的數據,當然在這里也是可以通過異步來實現并發的,對于小規模的異步流程控制,可以用這個模塊=>eventproxy,但這里我就沒有用啦!我們通過分析獲取到的頁面從中截取出所有圖片的鏈接,再通過異步并發來實現對這些圖片的批量下載。
抓取頁面初始的數據很簡單啊,這里就不做多解釋啦
1 /*獲取首屏所有圖片鏈接*/ 2 var getInitUrlList=function(){ 3 request.get("https://www.zhihu.com/question/34937418") 4 .end(function(err,res){ 5 if(err){ 6 console.log(err); 7 }else{ 8 var $=cheerio.load(res.text); 9 var answerList=$(".zm-item-answer"); 10 answerList.map(function(i,answer){ 11 var images=$(answer).find('.zm-item-rich-text img'); 12 images.map(function(i,image){ 13 photos.push($(image).attr("src")); 14 }); 15 }); 16 console.log("已成功抓取"+photos.length+"張圖片的鏈接"); 17 getIAjaxUrlList(20); 18 } 19 }); 20 }
模擬ajax請求獲取完整頁面
接下來就是怎么去模擬點擊加載更多時發出的ajax請求了,去知乎看一下吧!
有了這些信息,就可以來模擬發送相同的請求來獲得這些數據啦。
1 /*每隔100毫秒模擬發送ajax請求,并獲取請求結果中所有的圖片鏈接*/ 2 var getIAjaxUrlList=function(offset){ 3 request.post("https://www.zhihu.com/node/QuestionAnswerListV2") 4 .set(config) 5 .send("method=next¶ms=%7B%22url_token%22%3A34937418%2C%22pagesize%22%3A20%2C%22offset%22%3A" +offset+ "%7D&_xsrf=98360a2df02783902146dee374772e51") 6 .end(function(err,res){ 7 if(err){ 8 console.log(err); 9 }else{ 10 var response=JSON.parse(res.text);/*想用json的話對json序列化即可,提交json的話需要對json進行反序列化*/ 11 if(response.msg&&response.msg.length){ 12 var $=cheerio.load(response.msg.join(""));/*把所有的數組元素拼接在一起,以空白符分隔,不要這樣join(),它會默認數組元素以逗號分隔*/ 13 var answerList=$(".zm-item-answer"); 14 answerList.map(function(i,answer){ 15 var images=$(answer).find('.zm-item-rich-text img'); 16 images.map(function(i,image){ 17 photos.push($(image).attr("src")); 18 }); 19 }); 20 setTimeout(function(){ 21 offset+=20; 22 console.log("已成功抓取"+photos.length+"張圖片的鏈接"); 23 getIAjaxUrlList(offset); 24 },100); 25 }else{ 26 console.log("圖片鏈接全部獲取完畢,一共有"+photos.length+"條圖片鏈接"); 27 // console.log(photos); 28 return downloadImg(50); 29 } 30 } 31 }); 32 }
在代碼中post這條請求https://www.zhihu.com/node/QuestionAnswerListV2,把原請求頭和請求參數復制下來,作為我們的請求頭和請求參數,superagent的set方法可
用來設置請求頭,send方法可以用來發送請求參數。我們把請求參數中的offset初始為20,每隔一定時間offset再加20,再重新發送請求,這樣就相當于我們每隔一定時間發送
了一條ajax請求,獲取到最新的20條數據,每獲取到了數據,我們再對這些數據進行一定的處理,讓它們變成一整段的html,便于后面的提取鏈接處理。異步并發控制下載圖片
再獲取完了所有的圖片鏈接之后,即判定response.msg為空時,我們就要對這些圖片進行下載了,不可能一條一條下對不對,因為如你所看到的,我們的圖片足足有
沒錯,2萬多張,不過幸好nodejs擁有神奇的單線程異步特性,我們可以同時對這些圖片進行下載。但這個時候問題來了,聽說同時發送請求太多的話會被網站封ip噠!這是真的嗎?我不知道啊,沒試過,因為我也不想去試( ̄ー ̄〃),所以這個時候我們就需要對異步并發數量進行一些控制了。
在這里用到了一個神奇的模塊=>async,它不僅能幫我們拜托難以維護的回調金字塔惡魔,還能輕松的幫我們進行異步流程的管理。具體看文檔啦,因為我自己也不怎么會用,這里就只用到了一個強大的async.mapLimit方法。真的很厲害哦。
1 var requestAndwrite=function(url,callback){ 2 request.get(url).end(function(err,res){ 3 if(err){ 4 console.log(err); 5 console.log("有一張圖片請求失敗啦..."); 6 }else{ 7 var fileName=path.basename(url); 8 fs.writeFile("./img1/"+fileName,res.body,function(err){ 9 if(err){ 10 console.log(err); 11 console.log("有一張圖片寫入失敗啦..."); 12 }else{ 13 console.log("圖片下載成功啦"); 14 callback(null,"successful !"); 15 /*callback貌似必須調用,第二個參數為下一個回調函數的result參數*/ 16 } 17 }); 18 } 19 }); 20 } 21 22 var downloadImg=function(asyncNum){ 23 /*有一些圖片鏈接地址不完整沒有“http:”頭部,幫它們拼接完整*/ 24 for(var i=0;i<photos.length;i++){ 25 if(photos[i].indexOf("http")===-1){ 26 photos[i]="http:"+photos[i]; 27 } 28 } 29 console.log("即將異步并發下載圖片,當前并發數為:"+asyncNum); 30 async.mapLimit(photos,asyncNum,function(photo,callback){ 31 console.log("已有"+asyncNum+"張圖片進入下載隊列"); 32 requestAndwrite(photo,callback); 33 },function(err,result){ 34 if(err){ 35 console.log(err); 36 }else{ 37 // console.log(result);<=會輸出一個有2萬多個“successful”字符串的數組 38 console.log("全部已下載完畢!"); 39 } 40 }); 41 42 };
先看這里=>
mapLimit方法的第一個參數photos是所有圖片鏈接的數組,也是我們并發請求的對象,asyncNum是限制并發請求的數量,如果沒有這個參數的話,將會有同時兩萬多條請求發送過去,嗯,你的ip就會被成功的封掉,但當我們有這個參數時,比如它的值是10,則它一次就只會幫我們從數組中取10條鏈接,執行并發的請求,這10條請求都得到響應后,再發送下10條請求。告訴泥萌,并發到同時100條沒有事的,下載速度超級快,再往上就不知道咯,你們來告訴我...
結尾
哎呀呀,明天就是除夕了,大年三十晚我要再發一篇react-router的東西
</div> </span></span>