优雅的使用 WebMagic 框架,爬取唐诗别苑网的诗人诗歌数据
同时在几种动态加载技术(HtmlUnit、PhantomJS、Selenium、JavaScriptEngine)中对比作选择
WebMagic 虽然差不多两年没有维护,但其本身是一个优秀的爬虫框架的实现,源码中有很多值得参考的地方,特别是对爬虫多线程的控制。另外,由于页面爬取到的是非结构化数据,所以数据保存到 MongoDB。
技术准备
- IDE:IntelliJ IDEA 2018.3.5
- JDK 版本:1.8.0_181
- 数据库:MongoDB 4.0.10
-
涉及技术:
- Webmagic 轻量级爬虫框架
- HtmlUnit 网页分析工具包,模拟浏览器运行
- PhantomJS
- JavaScriptEngine
- MongoDB ORM 框架 Morphia
- JUC:Java 线程池、线程协作、线程安全类
- 日志 log4j 1.7.25
- Java 反射
- 单例模式、工厂模式、代理模式
pom.xml 文件中的依赖非常简单,并没有使用到 Spring 系列的框架,所以有些地方自己编码实现了 Spring 提供的功能
项目结构
- biz 包:包括页面爬取逻辑的 Processor 类,爬虫结果保存的 Pipeline 类
- dao 包:数据获取层
- entity 包:实体类,映射保存在 MongoDB 的文档(Document)
- vo 包:值对象,简单的 Java 对象
- util 包:工具包,包括数据库连接类、爬虫辅助类
- common 包:项目相关通用类
- Main 类:程序入口
项目说明
根据需求将数据保存到 MongoDB 数据库,因此 在程序运行前必须设置好 resources/mongodb.properties
文件
最好保证 MongoDB 的版本是 4.0 以上。另外 MongoDB 的用户管理比较麻烦,过程大致如下:首先需要创建存储数据的数据库,如命名为 user_tangpoem,并存入随便一条数据(集合)使数据库有效化,然后创建一个
admin 数据库的 root 用户,继续创建一个可以读写应用数据库 user_tangpoem 的用户,然后修改 MongoDB 配置文件使其以安全认证模式启动。重启数据库,选择 admin 数据库(use admin)
用刚刚创建的用户(非 root 用户)使用 db.auth()进行登录,返回 1 说明验证成功,选择 user_tangpoem 数据库(use user_tangpoem),输入 show collections,如果看到最初创建数据库时的集合,则说明用户创建成功。
详细可参考 MongoDB4.0.0 远程连接及用户名密码认证登陆配置——windows
爬虫以多线程的方式运行,在 resources/spider.properties
文件中可以 设置线程数和线程睡眠时间,在设置好数据库配置的基础上,直接运行 Main.main(),爬虫就会开始爬取。
线程睡眠,是 WebMagic 框架源码中每线程爬取完一个 url 后必然经历的过程,但作者文档并没有对此进行说明,请根据实际情况调整
动态加载技术的选择
1. PhantomJS 和 Selenium
WebMagic 底层已经很好的使用了 HttpClient 加载静态页面,对于动态页面,也有 PhantomJSDownloader 和SeleniumDownloader两个常用的利用
浏览器内核模拟浏览器行为的实现,其中,PhantomJS 需要指定 phantomjs.exe 和进行爬取的 JS 文件,而 seleniumDownloader 需要指定 chromedriver.exe,需要自行下载对应操作系统的版本,
使用起来并不难,本项目不多作讨论。这里关键说明HtmlUnit
2. HtmlUnit
一款开源的 Java 页面分析工具, 读取页面后, 可以有效的使用 HtmlUnit 分析页面上的内容。使用 纯 Java 实现的 模拟浏览器,不需要指定外部文件。
虽然其对 JS 的支持并不完全,但总体而言 HtmlUnit 的内存消耗、CPU 消耗和效率都比 PhantomJS 和 Selenium 好,值得进行使用
本项目使用 2.25 版本的 HtmlUnit 并没有出现 JS 加在不成功的问题,但使用 2.3x 的版本会无法加载
3. JavaScriptEngine
- 既然要加载 JS,为何不直接提取 JS 代码,使用 Java 自带的 JS 引擎处理呢?
因为JavaScriptEngine 是有局限性的,最明显就是其不支持 jquery 的语法,因为 jquery 使用了浏览器内置的对象,而 JS 引擎本身是没有浏览器对象的
- 那还能使用 JS 引擎吗?
当然可以,只要分析过页面的加载逻辑,如果不涉及浏览器对象的使用,或者将 JS 逻辑进行转化,还是能够使用 JS 引擎的,但 牺牲了泛用性。本项目经分析后使用 JS 引擎加载
4. 横向对比
经过测试,三者比较如下
- 加载一个接口的效率:PhantomJS 约 13 秒,HtmlUnit 约 10 秒,JS 引擎约 6 秒
- 内存消耗、CPU 消耗:PhantomJS > HtmlUnit > JS 引擎
PhantomJS 使用外置的程序,所以 JVM 无法管理这部分的硬件资源,需要打开任务管理器
爬取过程
经过分析,爬取步骤分为 4 步:
- 爬取所有的诗人 id。调用一次接口即可获得所有的诗人 id,返回 JSON 格式数据,接口地址为:http://poem.studentsystem.org…
- 爬取所有的诗人信息。根据上一步的诗人 id 逐一爬取对应的诗人详细信息,一共有 2529 条数据,则接口调用 2529 次,返回 JSON 格式数据,接口地址为:http://poem.studentsystem.org…{id}
- 爬取所有的诗歌信息。根据上一步的诗人信息获取所有的诗歌 id,然后逐一调用接口获取诗歌详细信息,一共有约 48000 条数据,则接口调用 48000 次,返回 html 页面,需要模拟浏览器动态执行 JS,接口地址为:http://poem.studentsystem.org…{id}
- 由于动态执行 JS 可会能超时,因此最后要处理未成功加载完毕的诗歌信息,从数据库中读取这类数据,再次构成 url 调用接口爬取,直到所有数据都完整。这类数据约占 1%,则接口调用约 480 次
显然,如上描述,采用的是宽度优先遍历,所以当执行到第 3 步时,才会有数据入库
优化后使用 Java8 的 nashorn JS 引擎执行 JS 代码,不需要动态加载 JS,所以不会出现 4 的问题
耗时估计
根据爬取过程分析,忽略程序启动时间和调用获取诗人 id 接口的时间
在开启 8 线程的并发模式下(使用 HtmlUnit 进行动态加载):
- 调用获取诗人信息的接口,每次需要 5 秒(5 秒是线程内置的睡眠时间,可设置)
- 调用获取诗歌信息的接口,每次需要 10 秒(包含了上述的 5 秒)
一共需要:2529 / 8 5 + 48000 (1 + 0.01)/ 8 * 10 ≈ 62596 秒 ≈ 1043 分钟 ≈ 17.4 小时
上述数据是在本地测试中得到的,配置为 win10 8G i5-4210M 4 核
优化后,用 JS 引擎取代模拟浏览器动态加载 JS,获取诗歌信息的耗时明显缩短,由 10 秒缩短到 6 秒左右,因此重新计算耗时如下:
2529 / 8 5 + 48000 / 8 6 ≈ 37185 秒 ≈ 620 分钟 ≈ 10.3 小时
性能评估
当使用模拟浏览器动态加载 JS 时 ,观察 JVM 的使用情况,发现爬取诗歌阶段频繁发生 Minor GC(新生代 GC),差不多 10 秒一次,如下图所示,
后判明是多线程模拟浏览器加载页面行为非常的耗内存(参考同时打开 8 个浏览器加载网页),对象频繁创建,频繁消耗,
建议运行时通过 -Xms -Xmx 把 JVM 内存设置得大些,至少 1G,然后把新生代的比例设大,如-Xms2048M -Xmx2048M -XX:+UseParallelOldGC -XX:NewRatio=1
后来,用 JS 引擎取代模拟浏览器动态加载 JS,不仅速度得到明显提升,而且内存的消耗大幅度降低,Minor GC 平均 1 分钟发生一次,如下图所示,
最后附上 GitHub 项目地址:https://github.com/Kanarienvogels/spider-tangpoem