web spider built by puppeteer, support task-queue and task-scheduling by decorators,support nedb / mongodb, support data visualization; 基于puppeteer的web爬虫框架,提供灵活的任务队列管理调度方案,提供便捷的数据保存方案(nedb/mongodb),提供数据可视化和用户交互的实现方案
npm install -g typescript
推荐使用 IDEA
IDEA一定要下载 Ultimate 版本,否则有很多功能无法使用
IDEA中 nodejs 和 javascript 配置
ppspider_example github 地址
https://github.com/xiyuan-fengyu/ppspider_example
需要先安装git,并在IDEA中配置git的可执行文件的路径
点击 IDEA 底下工具栏的 Terminal,打开命令行面板,输入如下命令安装依赖
npm install
在安装依赖过程中,puppeteer会自动下载chromium,国内用户大概率下载失败
如果 chromium 下载失败,可以删除 node_modules/puppeteer 文件夹,如下更改 chromium 镜像地址,然后继续安装依赖
# win 系统
set PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors/
# unix 系统
export PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors/
npm install
tsc 是 typescript 的编译工具,将 typescript 代码编译为 js,之后便可右键 js 文件运行了(通过nodejs执行)
tsc 在运行期间会监听 ts 文件变化,自动编译有变动的 ts 文件
运行方式:
1.(推荐)右键 package.json,点击 Show npm Scripts,双击 auto build
2.在 IDEA 中打开一个 terminal, 运行 tsc
右键运行 lib/quickstart/App.js
用浏览器打开 http://localhost:9000 可以实时查看爬虫系统的运行情况
https://github.com/xiyuan-fengyu/ppspider_docker_deploy
申明形式
export function TheDecoratorName(args) { ... }
使用方式
@TheDecoratorName(args)
乍一看和java中的注解一样,但实际上这个更为强大,不仅能提供元数据,还能对类或方法的属性行为做修改装饰,实现切面的效果,ppspider中很多功能都是通过装饰器来提供的
接下来介绍一下实际开发中会使用到的装饰器
export function Launcher(appConfig: AppConfig)
申明整个爬虫系统的启动入口
其参数类型为
export type AppConfig = {
workplace: string; // 系统的工作目录
queueCache?: string; // 运行状态保存文件的路径,默认为 workplace + "/queueCache.json"
dbUrl?: string; // 数据库配置,支持 nedb 或 mongodb;少量数据用 nedb,url格式为:nedb://本地nedb存储文件夹;若应用要长期执行,生成数据量大,建议使用 mongodb,url格式为:mongodb://username:password@host:port/dbName;默认:"nedb://" + appInfo.workplace + "/nedb"
tasks: any[]; // 任务类
dataUis?: any[]; // 需要引入的DataUi
workerFactorys: WorkerFactory<any>[]; // 工厂类实例
webUiPort?: 9000 | number; // UI管理界面的web服务器端口,默认9000
logger?: LoggerSetting; // 日志配置
}
export function OnStart(config: OnStartConfig)
用于声明一个在爬虫系统启动时执行一次的子任务;后续可以在管理界面上点击该任务名后面的重新执行的按钮即可让该任务重新执行一次
参数说明
export type OnStartConfig = {
urls: string | string[]; // 要抓取链接
running?: boolean; // 系统启动后该队列是否处于工作状态
parallel?: ParallelConfig; // 任务并行数配置
exeInterval?: number; // 两个任务的执行间隔时间
exeIntervalJitter?: number; // 在 exeInterval 基础上增加一个随机的抖动,这个值为左右抖动最大半径,默认为 exeIntervalJitter * 0.25
timeout?: number; // 任务超时时间,单位:毫秒,默认:300000ms(5分钟),负数表示永不超时
maxTry?: number; // 最大尝试次数,默认:3次,负数表示一直尝试
description?: string; // 任务描述
filterType?: Class_Filter; // 添加任务过滤器,默认是 BloonFilter;保存状态后,系统重启时,不会重复执行;如果希望重复执行,可以用 NoFilter
defaultDatas?: any; // 该类任务统一预设的job.datas内容
}
使用例子 @OnStart example
export function OnTime(config: OnTimeConfig) { ... }
用于声明一个在特定时刻周期性执行的任务,通过cron表达式设置执行时刻
参数说明(cron以外的属性说明参考 OnStartConfig)
export type OnTimeConfig = {
urls: string | string[];
cron: string; // cron表达式,描述了周期性执行的时刻;不清楚cron表达式的可以参考这里:http://cron.qqe2.com/
running?: boolean;
parallel?: ParallelConfig;
exeInterval?: number;
exeIntervalJitter?: number;
timeout?: number;
maxTry?: number;
description?: string;
defaultDatas?: any; // 该类任务统一预设的job.datas内容
}
使用例子 @OnTime example
这两个装饰器必须一起使用,@AddToQueue 将被装饰的方法的返回结果添加到队列中,@FromQueue 从队列中获取 Job 并执行
export function AddToQueue(queueConfigs: AddToQueueConfig | AddToQueueConfig[]) { ... }
export type AddToQueueConfig = {
// 队列名
name: string;
// 队列类型, 目前提供了 DefaultQueue(先进先出),DefaultPriorityQueue(优先级队列)
queueType?: QueueClass;
// 过滤器类型,目前提供了 NoFilter(不进行过滤),BloonFilter(布隆过滤器)
filterType?: FilterClass;
}
一个 @AddToQueue 可以配置一个或多个队列;可以在多个地方用 @AddToQueue 往同一个队列中添加 Job ,队列类型由第一次申明处的 queueType 决定,但每一处的 filterType 可以不一样
@AddToQueue 装饰的方法的返回结果必须符合 AddToQueueData 的形式
export type CanCastToJob = string | string[] | Job | Job[];
export type AddToQueueData = Promise<CanCastToJob | {
[queueName: string]: CanCastToJob
}>
当 @AddToQueue 配置了多个队列信息时,返回类型必须是
Promise<{
[queueName: string]: CanCastToJob
}>
格式的数据;可以使用 PuppeteerUtil.links 方法方便的获取想要的连接,这个方法的返回结果正好是 AddToQueueData 格式的,具体的用法后面会介绍
@FromQueue,从队列从获取任务执行,同一个队列只能定义一个FromQueue
export function FromQueue(config: FromQueueConfig) { ... }
export type FromQueueConfig = {
name: string; // 队列名
running?: boolean;
parallel?: ParallelConfig;
exeInterval?: number;
exeIntervalJitter?: number;
timeout?: number;
maxTry?: number;
description?: string;
defaultDatas?: any; // 该类任务统一预设的job.datas内容
}
使用例子 @AddToQueue @FromQueue example
export function JobOverride(queueName: string) { ... }
在将 job 添加到队列之前对 job 的信息进行重写修改
同一个队列只能设置一个 JobOverride
最常用的使用场景:很多时候多个带有额外参数或尾缀的url实际指向同一个页面, 这个时候可以提取出url中的唯一性标识,
并将其作为 job 的 key,用于重复性校验,避免重复抓取
实际上 OnStart, OnTime 两种类型的任务也是通过队列管理的,采用 DefaultQueue(NoFilter) 队列,队列的命名方式为
OnStart_ClassName_MethodName,所以也可以通过 JobOverride 对 job 进行修改
JobOverride example
export function Serializable(config?: SerializableConfig) { ... }
export function Transient() { ... }
@Serializable 用于标记在序列化和反序列化中,需要保留类信息的类,没有这个标记的类的实例在
序列化之后会丢失类的信息
@Transient 用于标记类成员,在序列化时忽略该字段。注意:类静态成员不参与序列化
这两个主要为关闭系统时保存运行状态提供支持,在实际使用的时候,如果有些类成员和运行状态没有直接关联,不需要序列化保存的
时候,一定要用 @Transient 来忽略该字段,可以减小序列化后文件的大小
example
export function RequestMapping(url: string, method: "" | "GET" | "POST" = "") {}
@RequestMapping 用于声明 HTTP rest 接口,提供远程动态添加任务的能力,返回抓取结果需要自行实现(例如异步url回调)
RequestMapping example
仿造 java spring @Bean @Autowired 的实现,提供实例依赖注入的功能
example
在控制界面定制动态界面的功能,为数据可视化、用户交互提供了扩展支持,通过 Angular 动态编译组件实现,简化了数据通信(主动请求数据和被动接受推送数据)
需要在 @Launcher appConfig.dataUis 中导入 DataUi 定义类
系统内置了一个DbHelperUi,引入这个DataUi后,在UI界面上添加一个名为“Db Helper”的tab页,可以辅助查询数据库中的数据
example 3 网页截图工具
支持超长网页截图
将 page 的窗口分辨率设置为 1920 * 1080
向 page 中注入 jquery,页面刷新或跳转后失效,所以这个方法必须在 page 加载完成之后调用
用于解析jsonp数据中的json
禁止或启用图片加载
用于监听接口的返回结果,可以设置监听次数
用于监听接口的返回结果,仅监听一次
下载图片
获取网页中的连接
获取满足 selector 的元素个数,不支持jQuery的selector
使用 jQuery(selector) 查找节点, 并为其中没有 id 的节点添加随机 id,
最后返回一个所有节点 id 的数组
使用场景:
Page 中所有涉及到使用selector查找节点的方法都是使用 document.querySelector / document.querySelectorAll
但是document.querySelector / document.querySelectorAll 不支持jQuery的高级selector
例如 “#someId a:eq(0)”, “#someId a:contains(‘next’)”
这时候可以通过这个方法为这些元素添加特殊的id,然后在通过 #specialId 去操作对应的节点
页面滚动到最底部
将cookie字符串解析为 SetCookie 数组,然后就可以通过 page.setCookie(…cookieArr) 来设置cookie
cookie字符串获取方式
通过 chrome -> 按下F12打开开发者面板 -> Application面板 -> Storage
得到的cookie字符串形式如下
PHPSESSID ifmn12345678 sm.ms / N/A 35
cid sasdasdada .sm.ms / 2037-12-31T23:55:55.900Z 27
为单个page设置动态的代理
用于记录打开一个页面过程中的请求情况 NetworkTracing example
目前提供了 nedb 和 mongodb 支持,分别通过 NedbDao, MongodbDao 进行封装
通过 @Launcher 的 dbUrl 参数设置数据库连接,之后便可以通过 appInfo.db 来操作数据库,具体可用的方法参考 src/common/db/DbDao
nedb 是一个 server-less 数据库,不需要额外安装服务端,数据会持久化到本地文件,url格式:nedb://nedbDirectoryPath,当存储数据量较大时,数据查询速度较慢,且每次重启应用需要加载数据,耗时较多,所以只适用于数据量较小的应用场景
mongodb 需要额外安装mongo服务端,url格式: mongodb://username:password@host:port/dbName,适用于数据量较大的场景
应用启动后,会自动创建一个 job collection,用于保存执行过程中的任务记录
通过 src/common/util/logger.ts 中定义的 logger.debug, logger.info, logger.warn, logger.error 方法输出日志
输出的日志中包含时间,等级,源文件位置这些额外信息
logger.debugValid && logger.debug("test debug");
logger.info("test info");
logger.warn("test warn");
logger.error("test error");
通过 Page.evaluate, Page.evaluateOnNewDocument, Page.evaluateHandle 执行的代码,
实际上是通过 chrome devtools protocol 运行在 chromium 中,这些代码我们称为注入代码
这些代码并不是直接由Nodejs执行,所以在IDEA中并不能直接调试
其他直接由Nodejs执行的代码是可以在IDEA中直接调试的
下面是注入代码的调试方式
在构造 PuppeteerWorkerFactory 时,设置参数 headless = false, devtools = true ,chromium 执行到 debugger 会自动断点
Inject js debug example
import {Launcher, PuppeteerWorkerFactory} from "ppspider";
import {TestTask} from "./tasks/TestTask";
@Launcher({
workplace: __dirname + "/workplace",
tasks: [
TestTask
],
workerFactorys: [
new PuppeteerWorkerFactory({
headless: false,
devtools: true
})
]
})
class App {
}
然后在要调试的注入代码前加一行 debugger;
import {Job, OnStart, PuppeteerWorkerFactory} from "ppspider";
import {Page} from "puppeteer";
export class TestTask {
@OnStart({
urls: "http://www.baidu.com",
workerFactory: PuppeteerWorkerFactory
})
async index(page: Page, job: Job) {
await page.goto(job.url());
const title = await page.evaluate(() => {
debugger;
const title = document.title;
console.log(title);
return title;
});
console.log(title);
}
}
另外,开发者在开发 DataUi 时,界面部分也是在浏览器中调试
http://www.runoob.com/jquery/jquery-syntax.html
通过 PuppeteerUtil.addJquery 将 jQuery 注入到 page 中后,就可以愉快地使用 jQuery 获取页面中想要的信息了
https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md
对 chrome devtools protocol 的封装
在抓取页面的时候,基本上都是在和 Page 打交道,所以 Page 相关的Api需要着重了解
https://github.com/louischatriot/nedb
基于内存和日志的serverless轻量级数据库,类Mongodb的查询方式
src/common/db/NedbDao.ts 对nedb的加载,数据压缩,基础查询做了进一步封装,方便用户继承使用
https://docs.mongodb.com/manual/reference/method/js-collection/
https://angular.io/
DataUi 是基于 angular 运行时动态编译的 Component,如果要编写复杂的 DataUi,有必要了解 Angular 的知识
https://antv.alipay.com/zh-cn/g2/3.x/demo/index.html
web ui中集成了 G2,方便在DataUi中实现数据可视化
https://v3.bootcss.com/css/
web ui中集成了 bootstrap, jquery,方便在DataUi中可以直接使用bootstrap和jquery编写ui界面
使用浏览器打开 http://localhost:9000
Queue 面板可以查看和管理整个系统中子任务的运行情况
Job 面板可以对所有子任务实例进行搜索,查看任务详情
引入 Page class,然后在回调函数的参数列表中声明一个 page: Page 参数即可。如果通过
import {Page} from "ppspider";
2020-12-07 v2.2.4-preview.1607350101966
2020-04-07 v2.2.3
2019-09-04 v2.2.3-preview.1578363288631
2019-09-04 v2.2.3-preview.1577332807380
2019-09-04 v2.2.3-preview.1574909694087
2019-09-04 v2.2.3-preview.1569208986875
2019-07-31 v2.2.2
2019-06-22 v2.2.1
通过 typescript 和 reflect-metadata 提供的反射机制,重写 @OnStart, @OnTime, @FromQueue 回调函数注入worker实例的方式;
移除了 @OnStart, @OnTime, @FromQueue 参数中的workerFactory属性,框架通过回调方法的参数类型判定 是否需要
传递 job 参数,是否需要传递 worker 实例(如果需要,传递哪一种 worker 实例),参数列表的顺序和个数也不再固定。
但也有限制,参数列表中,最多一个Job类型的参数,以及最多一个有对应 WorkerFactory 定义的 Worker 类型(目前仅提供了 Page,
注意是 ppspider 包中提供的 class Page,而不是 @types/puppeteer 中定义的 interface Page)。
因为这个更改,需要对一些代码进行升级,需要移除 @OnStart, @OnTime, @FromQueue 参数中的 workerFactory 属性,回调函数
参数列表中如果要用到 page: Page,需要将 import {Page} from “puppeteer” 改为 import {Page} from “ppspider”,其他除了
job: Job 的参数都删除掉。参数列表的顺序和名字都可以随意定义,如果回调方法中没有用到 job: Job,也可以把这个参数删掉。
修复 @AddToQueue 不和 @OnStart / @OnTime / @FromQueue 一起使用时失效的bug
2019-06-13 v2.1.11
2019-06-06 v2.1.10
2019-06-03 v2.1.9
2019-06-02 v2.1.8
2019-05-28 v2.1.6
2019-05-24 v2.1.3
2019-05-21 v2.1.2
2019-05-09 v2.0.5
2019-05-08 v2.0.4
2019-04-29 v2.0.3
2019-04-29 v2.0.2
2019-04-22 v2.0.1
2019-04-04 v2.0.0
appInfo.eventBus.emit(Events.QueueManager_InterruptJob, JOB_ID, "your interrupt reason");
2019-01-28 v0.1.22
2018-12-24 v0.1.21
2018-12-10 v0.1.20
2018-11-19 v0.1.19
2018-09-19 v0.1.18
2018-08-24 v0.1.17
2018-07-31 v0.1.16
2018-07-30 v0.1.15
2018-07-27 v0.1.14
2018-07-24 v0.1.13
2018-07-23 v0.1.12
2018-07-19 v0.1.11
2018-07-16 v0.1.8