node-crawler Doc
Crawler DOC 翻譯
在使用這個框架一段時間之后阅虫,發(fā)現(xiàn)這個doc 有些亂托酸,并且缺少完整的案例褒颈。 面對不同的情況,想讓crawler正常運作的話励堡,需要查看依賴的庫谷丸,如request , cheerio等应结。在此我記錄一下我個人對這個框架的使用說明刨疼,附案例。本人非科班出身摊趾,非javaScript專業(yè)用戶币狠,文內如有偏頗游两,歡迎指正砾层!
pre:如果你對javaScript、Nodejs沒有基礎贱案,建議先閱讀我的學習筆記:(目前沒整理出來肛炮,比較懶!23333)
目錄:
一宝踪、框架機制 :
二侨糟、常用參數(shù):
三、案例:豆瓣圖書
四瘩燥、How to Debug
一秕重、框架機制
nodejs 作用機制是“單線程異步”“非阻塞式IO模型”,贅述一下厉膀,就是主線程是單線程溶耘,而處理主線程“分發(fā)”的事件是交由ChromeV8異步處理的二拐。
1.1 crawler
所以針對這個機制,crawler 維護一個任務隊列/請求隊列queue凳兵, 主線程遇到加入queue的請求百新,會把新請求丟入隊列,如果這個請求中有callback庐扫,則callback會被交給異步線程處理饭望,主線程繼續(xù)向下執(zhí)行,直到程序done形庭。隨后铅辞,主線程會不斷從queue頭部取新任務處理,形成閉環(huán)碘勉,直到隊列為空巷挥。
類似上圖中右側圖,在額外操作一個任務隊列验靡。
1.2 other spider
當然倍宾,nodejs 的單線程異步,可能會讓其他語言“轉職”過來的人迷惑胜嗓。由于我沒了解過其他語言爬蟲機制高职,說一下我對其機制的猜測(阻塞式IO):
爬蟲程序由function1請求入口頁面。假設入口頁面有100個list url辞州,在function1中循環(huán)100次怔锌,請求fucntion2進入list頁面。假設每個list頁面有10個detail url变过, 則在function2中循環(huán)10次請求detail函數(shù)寫入database埃元,程序完成交回爬蟲程序done。這樣完成了一個單線程阻塞式模型媚狰,清楚知道爬蟲程序運行到哪里岛杀,該在哪debug。在這種情況下要開多線程崭孤,則可以先準備好10個線程的線程池类嗤,在入口頁面函數(shù)function1中將100個list request交給10個線程處理,每條線程依照上面的步驟跑到底辨宠,空閑則回到線程池遗锣,爬蟲主線程會再從剩下的90個list req中分給線程任務,直到threadPool 為空嗤形。
上述過程相對來說更符合自然人操作邏輯精偿,更好理解。具體不同框架肯定對線程池的調度有著不同的優(yōu)化,例如開啟的額外線程可能會每完成一個請求函數(shù)笔咽,就回到線程池墓阀, 在總爬蟲程序程序構造方法處進行線程池設置。也可能添加callback函數(shù)優(yōu)化翻頁邏輯拓轻,這些我不得而知斯撮。
1.3 總結
在理解了框架工作機制后,不難發(fā)現(xiàn)盡管crawler只有一個主線程扶叉,但工作效率并不低勿锅,可以用于生產(chǎn)環(huán)境。唯一不足是因為框架本身輕量枣氧,欠缺了一些魯棒性溢十。
其實,爬蟲無非就是請求request和響應response达吞,下文簡寫req與res张弛。如果你的req與瀏覽器一致,那么你的到的res也必然相同酪劫,剩下的事情就是解析res得到自己想要的數(shù)據(jù)吞鸭。至于所有的爬蟲框架就是在這最本質的內核上錦上添花、方便使用覆糟,crawler 的分布式版本 floodesh 刻剥,即,將crawler維護的queue 改為分布式DB MongoDB滩字,增加了主機index與客戶端worker造虏,分別負責任務調度與爬取工作。
floodesh DOC文檔
二麦箍、常用參數(shù)
2.1 依賴包
java 習慣稱之為包漓藕, 也可叫模塊、輪子……whatever挟裂!源碼如下:
var path = require('path')//解決一些path 問題享钞,如不同系統(tǒng)\ /,絕對话瞧、相對路徑
, util = require('util')//node核心模塊嫩与,解決一些回調繼承的問題
, EventEmitter = require('events').EventEmitter//nodejs異步io事件隊列
, request = require('request')//發(fā)送請求
, _ = require('lodash')//優(yōu)化一些js對象操作寝姿,提供方便使用的接口
, cheerio = require('cheerio')//jquery選擇器
, fs = require('fs')//file 的io操作
, Bottleneck = require('bottleneckp')//任務調度以及限制速率
, seenreq = require('seenreq')//req url 去重
, iconvLite = require('iconv-lite')//編碼轉換
, typeis = require('type-is').is;//js 類型檢查器
日常使用的話交排,不需要了解所有包的全部功能, 需要的話可以查閱文檔:
https://www.npmjs.com/
最常用的的如request 饵筑、cheerio還是建議了解一下 DOC埃篓。
2.2 參數(shù)
對于crawler維護的任務隊列, 其實是一個包含options對象的json數(shù)組根资,源碼:
Crawler.prototype.queue = function queue (options) {
var self = this;
// Did you get a single object or string? Make it compatible.
options = _.isArray(options) ? options : [options];
options = _.flattenDeep(options);
for(var i = 0; i < options.length; ++i) {
if(self.isIllegal(options[i])) {
log('warn','Illegal queue option: ', JSON.stringify(options[i]));
continue;
}
self._pushToQueue(
_.isString(options[i]) ? {uri: options[i]} : options[i]
);
}
};
option可以全局傳給crawler架专,這樣會對每一次請求生效同窘, 也可以給把獨立的option傳給queue,關于這點doc寫的很清楚部脚。option常用參數(shù)和默認值見源碼:
var defaultOptions = {
autoWindowClose: true,
forceUTF8: true,
gzip: true,
incomingEncoding: null,
jQuery: true,//res 是否注入 cheerio想邦,doc有詳細說明
maxConnections: 10,//只有在rateLimit == 0時起作用,限制并發(fā)數(shù)
method: 'GET',
priority: 5,//queue請求優(yōu)先級委刘,模擬用戶行為
priorityRange: 10,
rateLimit: 0,//請求最小間隔
referer: false,
retries: 3,//重試次數(shù)丧没,請求不成功會重試3次
retryTimeout: 10000,//重試間隔
timeout: 15000,//15s req無響應,req失敗
skipDuplicates: false,//url去重锡移,建議框架外單讀使用seenreq
rotateUA: false,//數(shù)組多組UA
homogeneous: false
};
第一章有提到呕童,爬蟲最重要的是req和res , crawler在req部分使用的是 request.js API :https://github.com/request/request#requestoptions-callback
可以在options中使用request.js 淆珊,諸如body夺饲、form 、header…具體可以見第三章的實例代碼施符。
2.3 常識
在簡介 crawler event 之前往声,要提到一些爬蟲的常識,因為我有看到github上有人對crawler提問戳吝,提問的原因是自己常識不足烁挟!
爬蟲實際情況大概分兩種: 一、針對異步API接口 骨坑,二撼嗓、針對url返回的html頁面。一般來講欢唾,前者返回可解析的json數(shù)據(jù)且警,而后者返回的是html文本,你需要用正則regex匹配自己想要的礁遣,也可以cheerio注入jquery得到自己想要的斑芜。
如果已經(jīng)想清楚自己是那種情況,仍然得不到res的話祟霍,排除服務器端加密情況杏头,多半就是你req沒有發(fā)對,建議chrome 多按按F12沸呐、request.js doc醇王、cheerio doc。
2.4 事件
crawler doc中這部份表述非常清晰崭添,提一下常用情況:
queue : 推任務到queue
schedule : 任務被推queue時候觸發(fā)寓娩,多用于添加代理
drain : queue為空時觸發(fā) , 多用于關閉數(shù)據(jù)庫、關閉寫入流
如果你在queue為空后棘伴,異步重新把任務推入queue寞埠,會頻繁觸發(fā)drain。
三焊夸、案例:豆瓣圖書
爬取豆瓣圖書TOP250總榜仁连,這是一個返回html頁面的案例,算是爬蟲屆的HelloWorld 阱穗!
"use strict";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const fs = require('fs');
const moment = require('moment');
const Crawler = require('crawler');
const _prgname = 'doubanTop250';
class Douban{
constructor() {
this.writeStream = fs.createWriteStream('../result/' + _prgname + '_book_' + moment().format('YYYY-MM-DD') + '.csv');
this.header = ['排名','標題','信息','評分','url','抓取時間'];
this.rank = 1;
this.crawler = new Crawler({
maxConnection: 1,
forceUTF8: true,
rateLimit: 2000,
jar: true,
time: true,
headers: {
'User-Agent':`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36`//,
}
});
this.crawler.on('drain', () => {
console.log('Job done');
//end stream
this.writeStream.end();
}).on('schedule', options => {
//options.proxy = 'http://xxx.xxx.xx.xxx:xxxx';
options.limiter = Math.floor(Math.random() * 10);//并發(fā)10
});
}
start() {
let self = this;
self.writeStream.write(`\ufeff${self.header}\n`);
console.log(`start`);
this.crawler.queue({
uri: 'https://book.douban.com/top250?icn=index-book250-all' ,
method:'GET',
gene:{
page : 1
},
callback: this.pageList.bind(this)
});
}
pageList(err, res, done) {
let self = this;
if (err) {
console.log(`pageList got erro : ${err.stack}`);
return done();
}
const gene = res.options.gene;
const $ = res.$;
$('#content > div > div.article > div.indent >table').map(function (){
const title = $('tr > td:nth-child(2) > div.pl2 a ',this).text().trim().replace(/[,\r\n]/g, '');
const src = $('tr > td:nth-child(2) > div.pl2 a',this).attr("href");
const info = $('tr > td:nth-child(2) p.pl',this).text();
const rate = $('tr > td:nth-child(2) span.rating_nums',this).text();
const time = moment().format('YYYY-MM-DD HH:mm:ss');
const result = [self.rank++, title, info, rate, src, time];
console.log(`${result}\n`);
self.writeStream.write(`${result}\n`);
});
if(gene.page <= 10){
console.log(`currentPage : ${gene.page}`);
this.crawler.queue({
uri: 'https://book.douban.com/top250?start=' + gene.page*25,
method:'GET',
gene : {
page : gene.page + 1
},
callback: self.pageList.bind(self)
});
}
return done();
}
}
const douban = new Douban();
douban.start();
install 相關的包怖糊,在上級目錄建好result文件夾,腳本可以直接跑颇象。
注:
1伍伤、 gene 為自定義通過option傳入回調的json對象。
2遣钳、 使用jquery 時扰魂,作用域this覆蓋問題,可以用self指向本類this蕴茴。
四劝评、How to Debug
crawler 可以使用docker debug 稍微復雜有空單起一篇文章。
但是一般比較簡單的腳本使用log在關鍵節(jié)點記錄一下就可以查出問題倦淀。
見案例代碼console.log()
多為debug服務蒋畜。