【nodeJS爬蟲】前端爬蟲系列

寫這篇 blog 其實一開始我是拒絕的,由於爬蟲爬的就是cnblog博客園。搞很差編輯看到了就把個人帳號給封了:)。javascript

言歸正傳,前端同窗可能向來對爬蟲不是很感冒,以爲爬蟲須要用偏後端的語言,諸如 php , python 等。固然這是在 nodejs 前了,nodejs 的出現,使得 Javascript 也能夠用來寫爬蟲了。因爲 nodejs 強大的異步特性,讓咱們能夠輕鬆以異步高併發去爬取網站,固然這裏的輕鬆指的是 cpu 的開銷。
要讀懂本文,其實只須要有
  • 能看懂 Javascript 及 JQueryphp

  • 簡單的nodejs基礎css

  • http 網絡抓包 和 URL 基礎html

本文較長且圖多,但若是能耐下心讀完本文,你會發現,簡單的一個爬蟲實現並不難,而且能從中學到不少東西。前端

本文中的完整的爬蟲代碼,在個人github上能夠下載。主要的邏輯代碼在 server.js 中,建議邊對照代碼邊往下看。java

在詳細說爬蟲前,先來簡單看看要達成的最終目標,入口爲 http://www.cnblogs.com/ ,博客園文章列表頁每頁有20篇文章,最多能夠翻到200頁。我這個爬蟲要作的就是異步併發去爬取這4000篇文章的具體內容,拿到一些咱們想要的關鍵數據。node

 

   爬蟲流程

看到了最終結果,那麼咱們接下來看看該如何一步一步經過一個簡單的 nodejs 爬蟲拿到咱們想要的數據,首先簡單科普一下爬蟲的流程,要完成一個爬蟲,主要的步驟分爲:python

抓取

爬蟲爬蟲,最重要的步驟就是如何把想要的頁面抓取回來。而且能兼顧時間效率,可以併發的同時爬取多個頁面。jquery

同時,要獲取目標內容,須要咱們分析頁面結構,由於 ajax 的盛行,許多頁面內容並不是是一個url就能請求的的回來的,一般一個頁面的內容是通過屢次請求異步生成的。因此這就要求咱們可以利用抓包工具分析頁面結構。git

若是深刻作下去,你會發現要面對不一樣的網頁要求,好比有認證的,不一樣文件格式、編碼處理,各類奇怪的url合規化處理、重複抓取問題、cookies 跟隨問題、多線程多進程抓取、多節點抓取、抓取調度、資源壓縮等一系列問題。

因此第一步就是拉網頁回來,慢慢你會發現各類問題待你優化。

存儲

當把頁面內容抓回來後,通常不會直接分析,而是用必定策略存下來,我的以爲更好的架構應該是把分析和抓取分離,更加鬆散,每一個環節出了問題可以隔離另一個環節可能出現的問題,好排查也好更新發布。
那麼存文件系統、SQL or NOSQL 數據庫、內存數據庫,如何去存就是這個環節的重點。

分析

對網頁進行文本分析,提取連接也好,提取正文也好,總之看你的需求,可是必定要作的就是分析連接了。一般分析與存儲會交替進行。能夠用你認爲最快最優的辦法,好比正則表達式。而後將分析後的結果應用與其餘環節。

展現

要是你作了一堆事情,一點展現輸出都沒有,如何展示價值?
因此找到好的展現組件,去show出肌肉也是關鍵。
若是你爲了作個站去寫爬蟲,抑或你要分析某個東西的數據,都不要忘了這個環節,更好地把結果展現出來給別人感覺。

 

   編寫爬蟲代碼

Step.1 頁面分析

如今咱們一步一步來完成咱們的爬蟲,目標是爬取博客園第1頁至第200頁內的4000篇文章,獲取其中的做者信息,並保存分析。

共4000篇文章,因此首先咱們要得到這個4000篇文章的入口,而後再異步併發的去請求4000篇文章的內容。可是這個4000篇文章的入口 URL 分佈在200個頁面中。因此咱們要作的第一步是 從這個200個頁面當中,提取出4000個 URL 。而且是經過異步併發的方式,當收集完4000個 URL 再進行下一步。那麼如今咱們的目標就很明確了:

 

Step2.獲取4000個文章入口URL

要獲取這麼多 URL ,首先仍是得從分析單頁面開始,F12 打開 devtools 。很容易發現文章入口連接保存在 class 爲 titlelnk 的 <a> 標籤中,因此4000個 URL 就須要咱們輪詢 200個列表頁 ,將每頁的20個 連接保存起來。那麼該如何異步併發的從200個頁面去收集這4000個 URL 呢,繼續尋找規律,看看每一頁的列表頁的 URL 結構:

那麼,1~200頁的列表頁 URL 應該是這個樣子的:

1
2
3
for ( var  i=1 ; i<= 200 ; i++){
     pageUrls.push( 'http://www.cnblogs.com/#p' +i);
}

有了存放200個文章列表頁的 URL ,再要獲取4000個文章入口就不難了,下面貼出關鍵代碼,一些最基本的nodejs語法(譬如如何搭建一個http服務器)默認你們都已經會了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 一些依賴庫
var  http = require( "http" ),
     url = require( "url" ),
     superagent = require( "superagent" ),
     cheerio = require( "cheerio" ),
     async = require( "async" ),
     eventproxy = require( 'eventproxy' );
 
var  ep =  new  eventproxy(),
     urlsArray = [],  //存放爬取網址
     pageUrls = [],   //存放收集文章頁面網站
     pageNum = 200;   //要爬取文章的頁數
 
for ( var  i=1 ; i<= 200 ; i++){
     pageUrls.push( 'http://www.cnblogs.com/#p' +i);
}
 
// 主start程序
function  start(){
     function  onRequest(req, res){  
         // 輪詢 全部文章列表頁
         pageUrls.forEach( function (pageUrl){
             superagent.get(pageUrl)
                 .end( function (err,pres){
               // pres.text 裏面存儲着請求返回的 html 內容,將它傳給 cheerio.load 以後
               // 就能夠獲得一個實現了 jquery 接口的變量,咱們習慣性地將它命名爲 `$`
               // 剩下就都是利用$ 使用 jquery 的語法了
               var  $ = cheerio.load(pres.text);
               var  curPageUrls = $( '.titlelnk' );
 
               for ( var  i = 0 ; i < curPageUrls.length ; i++){
                 var  articleUrl = curPageUrls.eq(i).attr( 'href' );
                 urlsArray.push(articleUrl);
                 // 至關於一個計數器
                 ep.emit( 'BlogArticleHtml' , articleUrl);
               }
             });
         });
 
         ep.after( 'BlogArticleHtml' , pageUrls.length*20 , function (articleUrls){
         // 當全部 'BlogArticleHtml' 事件完成後的回調觸發下面事件
         // ...
         });
     }
     http.createServer(onRequest).listen(3000);
}
exports.start= start;
這裏咱們用到了三個庫,superagent 、 cheerio 、 eventproxy。
分別簡單介紹一下:

superagent

superagent(http://visionmedia.github.io/superagent/ ) 是個輕量的的 http 方面的庫,是nodejs裏一個很是方便的客戶端請求代理模塊,當咱們須要進行 get 、 post 、 head 等網絡請求時,嘗試下它吧。

cheerio

cheerio(https://github.com/cheeriojs/cheerio ) 你們能夠理解成一個 Node.js 版的 jquery,用來從網頁中以 css selector 取數據,使用方式跟 jquery 同樣同樣的。

eventproxy

eventproxy(https://github.com/JacksonTian/eventproxy ) 很是輕量的工具,可是可以帶來一種事件式編程的思惟變化。

用 js 寫過異步的同窗應該都知道,若是你要併發異步獲取兩三個地址的數據,而且要在獲取到數據以後,對這些數據一塊兒進行利用的話,常規的寫法是本身維護一個計數器。

先定義一個 var count = 0,而後每次抓取成功之後,就 count++。若是你是要抓取三個源的數據,因爲你根本不知道這些異步操做到底誰先完成,那麼每次當抓取成功的時候,就判斷一下count === 3。當值爲真時,使用另外一個函數繼續完成操做。

而 eventproxy 就起到了這個計數器的做用,它來幫你管理到底這些異步操做是否完成,完成以後,它會自動調用你提供的處理函數,並將抓取到的數據當參數傳過來。

OK,運行一下上面的函數,假設上面的內容咱們保存在 server.js 中,而咱們有一個這樣的啓動頁面 index.js,

如今咱們在回調裏增長几行代碼,打印出結果:

打開node命令行,鍵入指令,在瀏覽器打開 http://localhost:3000/ ,能夠看到:

1
node index.js

成功了!咱們成功收集到了4000個 URL ,可是我將這個4000個 URL 去重後發現,只有20個 URL 剩下,也就是說我將每一個 URL  push 進數組了200次,必定是哪裏錯,看到200這個數字,我立馬回頭查看 200 個 文章列表頁。

我發現,當我用 http://www.cnblogs.com/#p1 ~ 200 訪問頁面的時候,返回的都是博客園的首頁。 而真正的列表頁,藏在這個異步請求下面:

看看這個請求的參數:

把請求參數提取出來,咱們試一下這個 URL,訪問第15頁列表頁:http://www.cnblogs.com/?CategoryId=808&CategoryType=%22SiteHome%22&ItemListActionName=%22PostList%22&PageIndex=15&ParentCategoryId=0 。

成功了,那麼咱們稍微修改下上面的代碼:

1
2
3
4
5
6
7
//for(var i=1 ; i<= 200 ; i++){
//  pageUrls.push('http://www.cnblogs.com/#p'+i);
//}
//改成
for ( var  i=1 ; i<= 200 ; i++){
     pageUrls.push( 'http://www.cnblogs.com/?CategoryId=808&CategoryType=%22SiteHome%22&ItemListActionName=%22PostList%22&PageIndex=' + i + '&ParentCategoryId=0' );
}

再試一次,發現此次成功收集到了4000個沒有重複的 URL 。第二步完成!

 

Step.3 爬取具體頁面內容 使用 async 控制異步併發數量 

獲取到4000個 URL ,而且回調入口也有了,接下來咱們只須要在回調函數裏繼續爬取4000個具體頁面,並收集咱們想要的信息就行了。其實剛剛咱們已經經歷了第一輪爬蟲爬取,只是有一點作的很差的地方是咱們剛剛並無限制併發的數量,這也是我發現 cnblog 能夠改善的一點,否則很容易被單IP的巨量 URL 請求攻擊到崩潰。爲了作一個好公民,也爲了減輕網站的壓力(其實爲了避免被封IP),這4000個URL 我限制了同時併發量最高爲5。這裏用到了另外一個很是強大的庫 async ,讓咱們控制併發量變得十分輕鬆,簡單的介紹以下。

async

async(https://github.com/caolan/async#queueworker-concurrency),async是一個流程控制工具包,提供了直接而強大的異步功能mapLimit(arr, limit, iterator, callback)。

此次咱們要介紹的是 async 的 mapLimit(arr, limit, iterator, callback) 接口。另外,還有個經常使用的控制併發鏈接數的接口是 queue(worker, concurrency) ,你們能夠去看看它的API。

繼續咱們的爬蟲,進到具體的文章頁面,發現咱們想獲取的信息也不在直接請求而來的 html 頁面中,而是以下這個 ajax 請求異步生成的,不過慶幸的是咱們上一步收集的 URL 包含了這個請求所須要的參數,因此咱們僅僅須要多作一層處理,將這個參數從 URL 中取出來再從新拼接成一個ajax URL 請求。

下面,貼出代碼,在咱們剛剛的回調函數中,繼續咱們4000個頁面的爬取,而且控制併發數爲5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ep.after( 'BlogArticleHtml' ,pageUrls.length*20, function (articleUrls){
     // 當全部 'BlogArticleHtml' 事件完成後的回調觸發下面事件
     // 控制併發數
     var  curCount = 0;
     var  reptileMove =  function (url,callback){
         //延遲毫秒數
         var  delay = parseInt((Math.random() * 30000000) % 1000, 10);
       curCount++;
       console.log( '如今的併發數是' , curCount,  ',正在抓取的是' , url,  ',耗時'  + delay +  '毫秒' ); 
     
     superagent.get(url)
         .end( function (err,sres){
             // sres.text 裏面存儲着請求返回的 html 內容
             var  $ = cheerio.load(sres.text);
             // 收集數據
             // 拼接URL
             var  currentBlogApp = url.split( '/p/' )[0].split( '/' )[3],
                 appUrl =  "http://www.cnblogs.com/mvc/blog/news.aspx?blogApp=" + currentBlogApp;
             // 具體收集函數
             personInfo(appUrl);
         });
 
     setTimeout( function () {
         curCount--;
         callback( null ,url + 'Call back content' );
     }, delay);     
     };
 
// 使用async控制異步抓取   
// mapLimit(arr, limit, iterator, [callback])
// 異步回調
async.mapLimit(articleUrls, 5 , function  (url, callback) {
       reptileMove(url, callback);
     },  function  (err,result) {
         // 4000 個 URL 訪問完成的回調函數
         // ...
     });
});

根據從新拼接而來的 URL ,再寫一個具體的 personInfo(URL) 函數,具體獲取咱們要的暱稱、園齡、粉絲數等信息。

這樣,咱們把抓取回來的信息以 JSON 串的形式存儲在 catchDate 這個數組當中,

node index.js 運行一下程序,將結果打印出來,能夠看到中間過程及結果:

至此,第三步就完成了,咱們也收集到了4000條咱們想要的原始數據。

 

Step.4 分析 展現

原本想將爬來的數據存入 mongoDB ,但由於這裏我只抓取了4000條數據,相對於動不動爬幾百萬幾千萬的量級而言不值一提,故就不添加額外的操做 mongoDB 代碼,專一於爬蟲自己。

收集到數據以後,就想看你想怎麼展現了,這裏推薦使用 Highcharts 純JS圖表庫去展現咱們的成果。固然這裏我偷懶了沒有作,直接用最原始的方法展現結果。

下面是我不一樣時間段爬取,通過簡單處理後的的幾張結果圖:

(結果圖的耗時均在併發量控制爲 5 的狀況下)


 

   後記

OK,至此,整個爬蟲就完成了,其實代碼量不多,我以爲寫爬蟲更多的時間是花在在處理各種問題,分析頁面結構。

完整的爬蟲代碼,在個人github上能夠下載。若是仍有疑問,能夠把代碼 down 到本地,從新從文章開頭對照代碼再實踐一次,相信不少問題會迎刃而解。

由於代碼開源,本着負責任的心態,但願你們能夠照着代碼寫寫其餘網站的爬蟲,若是都拿cnblog來爬,服務器可能會承受不住的:)

參考文章:《Node.js 包教不包會》

原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。

相關文章
相關標籤/搜索