node-crawler Doc
Crawler DOC 翻译
在使用这个框架一段时间之后,发现这个doc 有些乱,并且缺少完整的案例。 面对不同的情况,想让crawler正常运作的话,需要查看依赖的库,如request , cheerio等。在此我记录一下我个人对这个框架的使用说明,附案例。本人非科班出身,非javaScript专业用户,文内如有偏颇,欢迎指正!
pre:如果你对javaScript、Nodejs没有基础,建议先阅读我的学习笔记:(目前没整理出来,比较懒!23333)
目录:
一、框架机制 :
二、常用参数:
三、案例:豆瓣图书
四、How to Debug
一、框架机制
nodejs 作用机制是“单线程异步”“非阻塞式IO模型”,赘述一下,就是主线程是单线程,而处理主线程“分发”的事件是交由ChromeV8异步处理的。
1.1 crawler
所以针对这个机制,crawler 维护一个任务队列/请求队列queue, 主线程遇到加入queue的请求,会把新请求丢入队列,如果这个请求中有callback,则callback会被交给异步线程处理,主线程继续向下执行,直到程序done。随后,主线程会不断从queue头部取新任务处理,形成闭环,直到队列为空。
类似上图中右侧图,在额外操作一个任务队列。
1.2 other spider
当然,nodejs 的单线程异步,可能会让其他语言“转职”过来的人迷惑。由于我没了解过其他语言爬虫机制,说一下我对其机制的猜测(阻塞式IO):
爬虫程序由function1请求入口页面。假设入口页面有100个list url,在function1中循环100次,请求fucntion2进入list页面。假设每个list页面有10个detail url, 则在function2中循环10次请求detail函数写入database,程序完成交回爬虫程序done。这样完成了一个单线程阻塞式模型,清楚知道爬虫程序运行到哪里,该在哪debug。在这种情况下要开多线程,则可以先准备好10个线程的线程池,在入口页面函数function1中将100个list request交给10个线程处理,每条线程依照上面的步骤跑到底,空闲则回到线程池,爬虫主线程会再从剩下的90个list req中分给线程任务,直到threadPool 为空。
上述过程相对来说更符合自然人操作逻辑,更好理解。具体不同框架肯定对线程池的调度有着不同的优化,例如开启的额外线程可能会每完成一个请求函数,就回到线程池, 在总爬虫程序程序构造方法处进行线程池设置。也可能添加callback函数优化翻页逻辑,这些我不得而知。
1.3 总结
在理解了框架工作机制后,不难发现尽管crawler只有一个主线程,但工作效率并不低,可以用于生产环境。唯一不足是因为框架本身轻量,欠缺了一些鲁棒性。
其实,爬虫无非就是请求request和响应response,下文简写req与res。如果你的req与浏览器一致,那么你的到的res也必然相同,剩下的事情就是解析res得到自己想要的数据。至于所有的爬虫框架就是在这最本质的内核上锦上添花、方便使用,crawler 的分布式版本 floodesh ,即,将crawler维护的queue 改为分布式DB MongoDB,增加了主机index与客户端worker,分别负责任务调度与爬取工作。
floodesh DOC文档
二、常用参数
2.1 依赖包
java 习惯称之为包, 也可叫模块、轮子……whatever!源码如下:
var path = require('path')//解决一些path 问题,如不同系统\ /,绝对、相对路径
, util = require('util')//node核心模块,解决一些回调继承的问题
, EventEmitter = require('events').EventEmitter//nodejs异步io事件队列
, request = require('request')//发送请求
, _ = require('lodash')//优化一些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 参数
对于crawler维护的任务队列, 其实是一个包含options对象的json数组,源码:
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常用参数和默认值见源码:
var defaultOptions = {
autoWindowClose: true,
forceUTF8: true,
gzip: true,
incomingEncoding: null,
jQuery: true,//res 是否注入 cheerio,doc有详细说明
maxConnections: 10,//只有在rateLimit == 0时起作用,限制并发数
method: 'GET',
priority: 5,//queue请求优先级,模拟用户行为
priorityRange: 10,
rateLimit: 0,//请求最小间隔
referer: false,
retries: 3,//重试次数,请求不成功会重试3次
retryTimeout: 10000,//重试间隔
timeout: 15000,//15s req无响应,req失败
skipDuplicates: false,//url去重,建议框架外单读使用seenreq
rotateUA: false,//数组多组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数据,而后者返回的是html文本,你需要用正则regex匹配自己想要的,也可以cheerio注入jquery得到自己想要的。
如果已经想清楚自己是那种情况,仍然得不到res的话,排除服务器端加密情况,多半就是你req没有发对,建议chrome 多按按F12、request.js doc、cheerio doc。
2.4 事件
crawler doc中这部份表述非常清晰,提一下常用情况:
queue : 推任务到queue
schedule : 任务被推queue时候触发,多用于添加代理
drain : queue为空时触发 , 多用于关闭数据库、关闭写入流
如果你在queue为空后,异步重新把任务推入queue,会频繁触发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);//并发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在关键节点记录一下就可以查出问题。
见案例代码console.log()
多为debug服务。