Node Crawler 使用说明

Node Crawler 使用说明_第1张图片
image

node-crawler Doc
Crawler DOC 翻译

在使用这个框架一段时间之后,发现这个doc 有些乱,并且缺少完整的案例。 面对不同的情况,想让crawler正常运作的话,需要查看依赖的库,如request , cheerio等。在此我记录一下我个人对这个框架的使用说明,附案例。本人非科班出身,非javaScript专业用户,文内如有偏颇,欢迎指正!
pre:如果你对javaScript、Nodejs没有基础,建议先阅读我的学习笔记:(目前没整理出来,比较懒!23333)

目录:
一、框架机制 :
二、常用参数:
三、案例:豆瓣图书
四、How to Debug


一、框架机制

nodejs 作用机制是“单线程异步”“非阻塞式IO模型”,赘述一下,就是主线程是单线程,而处理主线程“分发”的事件是交由ChromeV8异步处理的。


Node Crawler 使用说明_第2张图片
机制.jpg

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服务。

你可能感兴趣的:(Node Crawler 使用说明)