1. 程式人生 > >【nodeJS爬蟲】前端爬蟲系列

【nodeJS爬蟲】前端爬蟲系列

取數 能夠 bsp blank 介紹 數據 ports exports 時間段

寫這篇 blog 其實一開始我是拒絕的,因為爬蟲爬的就是cnblog博客園。搞不好編輯看到了就把我的賬號給封了:)。

言歸正傳,前端同學可能向來對爬蟲不是很感冒,覺得爬蟲需要用偏後端的語言,諸如 php , python 等。當然這是在 nodejs 前了,nodejs 的出現,使得 Javascript 也可以用來寫爬蟲了。由於 nodejs 強大的異步特性,讓我們可以輕松以異步高並發去爬取網站,當然這裏的輕松指的是 cpu 的開銷。 要讀懂本文,其實只需要有
  • 能看懂 Javascript 及 JQuery

  • 簡單的nodejs基礎

  • http 網絡抓包 和 URL 基礎

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

本文中的完整的爬蟲代碼,在我的github上可以下載。主要的邏輯代碼在 server.js 中,建議邊對照代碼邊往下看。

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

技術分享

爬蟲流程

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

抓取

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

同時,要獲取目標內容,需要我們分析頁面結構,因為 ajax 的盛行,許多頁面內容並非是一個url就能請求的的回來的,通常一個頁面的內容是經過多次請求異步生成的。所以這就要求我們能夠利用抓包工具分析頁面結構。

如果深入做下去,你會發現要面對不同的網頁要求,比如有認證的,不同文件格式、編碼處理,各種奇怪的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 包教不包會》。

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

【nodeJS爬蟲】前端爬蟲系列