煎蛋段子爬蟲prototype

目標

完成一個爬蟲原型,能夠爬取全部煎蛋段子,並將數據持久化到本地。

步驟

不用擔心,這並不是教你怎麼畫馬。。。

總共六個部分

  1. 獲取頁數數據
  2. 測試非同步並發
  3. 抓取頁面並解析
  4. 數據持久化之命令行參數解析
  5. 數據持久化之文件存儲方案
  6. 數據持久化之MongoDB存儲

涉及到的知識點在前面的一些文章里有講,歡迎對照參考。

不過在做這六步之前需要搭建es6的開發環境,請參考es6開發環境搭建。

1 獲取頁面數據

觀察jandan.net/duan 頁面結構

發現頁數有個專門的classname叫current-comment-page,搜索下來只有三個地方有用到,可以用來提取。

首先安裝需要的npm包

npm i --save superagent cheerio

superagent用於網路請求,

cheerio用於解析頁面,其api類似jQuery

代碼如下

import superagent from superagent;import cheerio from cheerio;const baseUrl = http://jandan.net/duan;superagent.get(baseUrl).end((err, response) => { if (err) { return console.log(err); } let $ = cheerio.load(response.text); let currentMaximumPage = $(.current-comment-page).first().text(); currentMaximumPage = parseInt(currentMaximumPage.substr(1, currentMaximumPage.length-2)); console.log("current maximum page: " + currentMaximumPage);})

運行並輸出如下

2 非同步並發控制測試

研究發現,可以用 jandan.net/duan/page-[頁數], 這個url來遍歷所有頁面

為什麼要控制並發?因為同時發送過多請求可能會觸發對方網站的反爬蟲機制,而且高並發會給對方伺服器造成過大的流量壓力,不道德,我們要「悄悄的進村,打槍的不要」。並發控制我選用的是async這個包,需要提一下這個async和async/await不是一回事。

安裝async包

npm i --save aysnc;

使用mapLimit方法,控制並發數量,async的mapLimit使用方法參看 mapLimit。

測試一下該方法的用法,新代碼如下

import superagent from superagent;import cheerio from cheerio;import mapLimit from async/mapLimit;const baseUrl = http://jandan.net/duan;var currentCount = 0;superagent.get(baseUrl).end((err, response) => { if (err) { console.log("get baseUrl failed"); return console.log(err); } let $ = cheerio.load(response.text); let currentMaximumPage = $(.current-comment-page).first().text(); currentMaximumPage = parseInt(currentMaximumPage.substr(1, currentMaximumPage.length-2)); // generate url, test first 20 page let urls = []; for(let i = currentMaximumPage; i > currentMaximumPage-10; i--) { let url = "http://jandan.net/duan/page-" + i; urls.push(url); } console.log(urls.length); // async crawl mapLimit(urls, 3, (url, callback)=>{ console.log("current url is " + url); // create a random delay let delay = Math.random()*2000; setTimeout(function(){ currentCount--; callback(null, "url is " + url + " delay is " + delay); },delay); }, (err, result)=>{ if (err) { console.log("error happened"); console.log(err) }else { console.log("mapLimit done"); console.log(result) } })})

運行命令和輸出

3 抓取頁面並解析

抓取和解析函數如下

function crawlAndParsePage(url, callback) { superagent.get(url) .end((err, response) => { if (err) { console.log("get " + url + " failed"); return console.log(err); } let $ = cheerio.load(response.text); let duanziStore = duanziExtraction($); console.log(url + " extraction result") console.log(duanziStore); // create a random delay and call callback function let delay = Math.random()*2000; setTimeout(function(){ currentCount--; callback(null, "url is " + url + " delay is " + delay); },delay); })}function duanziExtraction($){ let duanziStore = []; $(.commentlist li).each(function(idx, elem) { let commentLike = parseInt($(elem).find(.tucao-like-container span).text()); let commentUnlike = parseInt($(elem).find(.tucao-unlike-container span).text()); if (commentUnlike+ commentLike >= 50 && (commentLike / commentUnlike) < 0.618){ // bad duanzi }else { let duanziId = $(elem).find(".righttext a").text() let pArray = $(elem).find("p") let duanziContent = "" pArray.each(function(index, element){ duanziContent += $(element).text() + "
"
}) duanziStore.push({ duanziId, duanziContent, commentLike, commentUnlike }) } }) return duanziStore;}

將mapLimit函數更新如下

// async crawlmapLimit(urls, 3, (url, callback)=>{ console.log("current url is " + url); crawlAndParsePage(url, callback);}, (err, result)=>{ if (err) { console.log("error happened"); console.log(err) }else { console.log("mapLimit done"); // console.log(result) }})

使用如下命令運行程序,將結果輸出到output.txt文件

babel-node index.js > output.txt

沒有遇到報錯,順利爬完,內容輸出到了output.txt。

4 數據持久化之命令行參數解析

數據持久化我有兩種方案,1. 本地文件保存,2. 本地mongodb資料庫。

打算使用命令行參數來控制持久化方法的選擇,即運行之前需要用戶給參數。這裡使用yargs包來實現這一功能。

安裝yargs包

npm i --save yargs

yargs用法參見本專欄前一篇文章,npm小工具之yargs。

創建一個utils文件夾,在裡面創建args.js。

import yargs from yargs;const args = yargs.option(file, { alias: f, string: true, default: "output.txt", describe: "output file name"}).option(mongodb, { alias: m, boolean: true, default: false, describe: choose to use local mongodb as default db}).option(port, { alias: p, string: true, default: 27017, describe: "get port number"}).argv;export default args;

最後需要export出去,這樣別的模塊才能引用。

5 數據持久化之文件存儲

首先需要在index.js中引入args模塊以及內置的fs模塊。

import args from ./utils/args;import fs from fs;

然後在crawlAndParsePage函數中增加根據命令行參數切換存儲方案的代碼

// storeif (args.mongodb == false) { // use file to store data fs.appendFileSync(args.file, JSON.stringify(duanziStore)+"
"
)}else{ // use mongodb to store data}

這地方才用同步追加讀寫函數appendFileSync,是為了防止並發函數的回調同時操作一個文件可能產生錯誤。不過做實驗的時用非同步的追加讀寫函數也沒有產生錯誤,可能nodejs單線程的特點本身就能避免這個問題?在此也提供非同步讀寫的demo,非同步文件讀寫參考程序。

6 數據持久化之MongoDB存儲

如果沒有用過mongoDB,可以參考本專欄的一篇文章,Async/Await用於mongodb driver,文章中也有如何搭建本地mongoDB環境的鏈接可供參考。

修改後的代碼如下

// storeif (args.mongodb == false) { // use file to store data fs.appendFileSync(args.file, JSON.stringify(duanziStore)+"
"
)}else{ mongoConnect("mongodb://localhost:27017/duanzi") .then( async (db)=>{ try { const collection = db.collection(jandan); const res = await collection.insertMany(duanziStore); console.log("save content from " + url +" success"); } catch(err) { console.log("save error"); db.close(); throw err; } db.close(); }, (err)=>{ console.log("db connection error") throw err; } ) .catch((err)=>{ console.log(err); })}

至於為什麼用async/await,Async/Await用於mongodb driver 文中已有提到,在此不贅述。使用try catch是為了捕獲錯誤,提高程序的健壯性。關於這一點可以參看這篇文章,Callback Promise Generator Async-Await 和異常處理的演進。

運行命令

babel-node index.js -m

-m 表示使用本地mongoDB存儲

因為前面解析段子的部分,計算了段子是否會被超載雞標記為不受歡迎,不受歡迎的段子不會存儲下來,因此爬取10頁跑下來存到資料庫里的記錄是到不了250條的(一頁25條,最新的一頁一般不滿25條),我得到173條數據。

至此煎蛋段子全站爬取的大部分就完成了。v0.1 release。

但是該程序還存在一些問題,比如爬取頁面還是過快,需要適當調整下間歇時間;比如沒有完善的logging;比如沒有完善的error處理等等。

參考資料

node.js 學習筆記005:使用async控制並發訪問數量

致謝

感謝 @惰性氣體 指導了下封面圖片的設計排版~


推薦閱讀:

MongoDB的安裝和配置
MongoDB 存儲引擎 mongorocks 原理解析

TAG:Nodejs | 爬虫 | MongoDB |