vlambda博客
学习文章列表

小墨是如何做一个分布式爬虫框架的

小墨的爬虫框架定位是什么?对小墨来说,是抓取自己感兴趣的数据,并非无限制爬取整个互联网等。所以属于聚焦性质的爬虫。也就是知道要爬取哪些站点的什么页面。

小墨简单构思了下爬取的过程,大致如下:


1.    指定具体网页,爬取这个网页的数据=下载这个网页->提取网页中指定元素值->输出到指定介质。

2.     指定具体的一个站点,爬取这个站点内符合规则的所有网页的数据->下载这个站点的入口网页->提取网页中符合规则的URL->依次批量下载这些URL网页->提取网页中指定元素值->输出到指定介质。

小墨是如何做一个分布式爬虫框架的

3.    指定的这个站点需要交互式输入才能得到期望得到的符合规则的网页。例如需要登录,需要输入表单查询

小墨是如何做一个分布式爬虫框架的

4.    根据第三方的OpenAPI爬取数据,例如github API,知乎API等。此时一个接口调用等同于一个网页下载。

小墨是如何做一个分布式爬虫框架的


这样一看,一个简单爬虫框架的骨架已经出来了。一个具体爬虫动作涉及四个部分:任务队列,下载器,解析器,输出管道,这是一个流水线,因此再来个执行器将这四个部分组装起来。


小墨是如何做一个分布式爬虫框架的

上述大致则为:

  • 下载器接口:包含内置两个实现类,http和浏览器模拟器,

  • 解析器接口:其实现类由外部定制实现

  • 输出管道接口:其实现类由外部定制实现。

  • 任务队列类:保证最基本的先进先出即可

  • Class执行器类:包含上述四个成员变量。并将任务获取->下载->解析->输出组织起来。


此时可以基于此骨架,着手实现了。But在实现层面了,还是有几个细节不清楚。

1.    浏览器模拟操作器,这是啥东西我清楚了,相当于程序模拟人的操作,完成交互式输入。但是咋实现?答:此时可以 google、baidu、bing了。基本出来就是Selenium、Phantomjs等的各种文章。这属于自动化测试领域的页面自动化测试方面的东西。当你了解完,写个几十行代码demo试下效果,基本上很可能就确定用Selenium就可以满足我们的要求了。


2.    任务队列我知道,但是啥方式实现呢?

答:由此疑问,则是需要结合这个爬虫框架的定位来看。我这个爬虫框架最终只作为lib库被集成使用?或者是我们要作为独立进程使用?如果是前者,则你根本不知道人家使用方的部署特点,即使是我们自己要作为独立进程,那也要考虑是单体?集群?因此既然是框架,既然你不清楚最后会咋样,整成接口总是不会错的。流出来可以扩展具体的实现嘛。


3.    我把这个实现了,但是我咋测试这个东西呢?我只想当做黑盒测试?

答:所以从可测试性角度看,任何的我们给的接口都需要内置默认一种的实现。以此满足我们黑盒测试的需要,同时对于扩展方给出一种实现的参考范例。这样来看,我们把任务队列提供一种最基本的队列实现,

输出管道就内置提供一种最基本的标准输出吧(stdout)。至于解析器,html提取满足规范的结构体,json转换成满足规范的结构体,而规范是啥取决于业务方,暂且就先给出固定的输入例子,定制实现解析器测试吧。


现在小墨就开始按照上面骨架来实现了,如下图,定义了几个接口间调用的结构体对象。定义接口,实现简易任务队列,两个下载器,一个控制台输出器。一个组装执行器。

小墨是如何做一个分布式爬虫框架的

看着似乎很舒服。小墨比较熟悉java,就用java很快写完了,写完也很有成就感后,用了多线程技术,LinkBlockQueue,Synchronized和ReenTrantLock,Jsoup,HttpClient,也掌握了selunium这个东东,很有成就感,于是打算爬个豆瓣的数据试试。

1:引入这个爬虫框架包

2:创建电影这个结构体

3:定制豆瓣的processor,从接口给过来的Page中用jsoup解析html,解析成电影结构体。

4:初始化excutor,往excutor添加URL(注入任务),往excutor添加自己定制的processor,启动运行。控制台输出了爬取的热门电影信息。


写完看着似乎也不错,小墨就打算爬取知乎有趣的话题,爬取一些诗词等。写着写着小墨自己觉得有些无聊了,为啥每个页面都要写processor?不就是Html转换为自己的结构体么。于是小墨在琢磨这怎么可以省去processor,小墨因为使用了jsoup,自然比较熟悉了。每次都getElement啥的好烦。


Google、baidu,bing。很快发现了xpath这个东东,让Jsoup支持Xpath语法就行了呀。这样解析动作就变为了配置xpath选择规则了。变为了配置类似下面的东东://h1[@class=‘f3 text-gray text-normal lh-condensed’]/a[@class=text-bold]/@href。解析完了又咋没有结构体实例的情况下赋值呢?小墨很聪明,立即想到了java.lang.reflect.Field、Method等。可以通过反射做呀。那xpath选择规则要咋告诉我的框架?扩展的时候有结构体,结构体的属性就是通过xpath的规则取出来的,小墨很聪明,立即想到了java.lang.annotation下的注解。这下ok了。提供个内置的解析器,完成html解析为自己的结构体对象。具体Xpath选择器规则配置在结构体上,然后内置的解析器上根据结构体类名反射获取属性,属性获取上面配置的xpath选择器,然后反射赋值。


小墨很快写了个内置的processor完成了html提取满足规则的元素到指定的结构。不仅在属性上定义注解,也加了类注解,用于指定url域名,结构体对应的页面的xpath选择题。小墨很开心,这回又用了反射,注解机制。小墨现在真是越来越聪明了。现在小墨爬取网页数据只需要定义个结构体就完了。再也不用每个都写解析动作了。

小墨是如何做一个分布式爬虫框架的


有一天小墨要从某个网站爬取个想要的数据,自己要的这个数据分布在不同的网页中。以前都是自己期望的数据和网页一对一,现在变为了一对多了。小墨还发现有些是多对一呢。也就是一个网页包含了自己要的多种类型的数据,或者多个网页的数据组合一起才是自己要的。

对于数据和网页多对一,小墨调整了下内置的processor的实现,接收多class。然后扩展了processor和output的结构,在之前的Result对象中增加了多数组。对于数据和网页一对多:小墨又扩展了excutor和output。这样一个任务执行完毕后的输出就先不做销毁了,第二个任务的processor执行完毕后,可以将两个processor之后的Result合并输出到Sink端。为此小墨又扩展写了个Processor。目的用于after单个processor后,在sink之前执行合并或者拆分。

小墨很快就按照自己的思路写完了,刚才的问题一下子解决了,这下这个框架支持网页和数据一对一,一对多,多对一了。也不用小墨在写每个解析器了,只需要定义java结构体就行了,小墨越做越有成就感。


小墨开始思考:任务队列数据多了咋办?系统重启了咋办?每天爬取的任务中有重复了怎么办?怎么看到爬虫的执行过程?怎么看到每天爬取了多少url,多少数据,成功了多少,失败了多少,当前多少线程在爬?业务上可能数据要上报到消息队列、生成文件、写入数据库、rest上报给业务等。要是爬取的是图片、文档等字节流格式呢?


小墨真是成长了,开始考虑系统性东西了。于是

1:小墨考虑怎么提供个httpserver并restfullapi,了解了springboot,vert.x,restlet等后,基于springboot发布了自己的restful api。用于增删改查job,启停job。又在excutor中增加了每个job里面的task执行成功或失败的事件发布,又写了个订阅,用于统计执行次数,成功次数,失败次数,并提供了rest接口查询。


2:小墨又扩展了下队列实现,用redis和磁盘文件实现了个TaskQueue,又完善了去重判断。这样同一个job里面任务就不会重复了。任务也都外置持久化了,进程复位后又接着上次的任务继续爬取了。不过这样使得这个框架又依赖了redis这个外来的东西,小墨这个框架用java写的,就在想怎么可以在jvm上支持数据分布式?小墨google、baidu、bing了下,发现了Hazelcast这个东西、正好符合自己的期望,可以分布式发布订阅,缓存。又不引入额外的进程,一个jar包搞定。于是小墨又用Hazelcast实现了这个队列。


3:小墨扩展了excutor,使得可以同时往多个sink吐数据,紧接着实现了json文件的sink,jdbc的sink,restfull接口的sink,kafaka的sink,以及fastdfs和hdfs的sink用于保存、文档等字节流格式文件。


4:图片,文档等字节流格式的,小墨分析了主流的几种持久化方式,fastdfs,hdfs这两种分布式文件系统,又了解到大多数也会保存在云端对象存储,故又提供了fastdfs,hdfs的sink和几个aws的s3接口格式的对象存储。


5:小墨脑洞彻底打开了,不仅关注爬取数据,也关注爬下来的用途,例如文档,爬下来可能要被检索,所以小墨又做了pdf,doc格式的转换的processor,使其转换为html,又写了elasticsearch的sink。将整个个html文本保存在elasticsearch中。这样下游就可以利用elasticsearch检索了。

小墨是如何做一个分布式爬虫框架的

至此,小墨很有成就感,又熟悉了解了springboot,redis,fastdfs,对象存储,es,和Hazelcast。


至此一个相对完善的爬虫框架就出来了。对于扩展方来说,只需要写个结构体对象,并配置小墨提供的注解就行了,也具备扩展自己合适的下载器,解析器,输出器能力。


小墨是个追求极致的人,在想着如何让不会写java的人来基于爬虫框架写爬虫业务?换句话说怎么让爬虫插件方不用写java结构体对象?毕竟这些对象都只是个零时产物,最终输出的还不是json呀等结构数据。另外初始化excutor的过程怎么也可以不用java去写了。这样小墨想到了全部基于配置模板的方式,配置模板采用结构化良好的xml表示。也就是小墨把这个框架所有的能力均以这个xml版本的方式暴露了出来。这样业务方只需要编写这个xml,并在初始化的时候注入进来即可。这样小墨定义了模板ID,名称,下载器类型,Endpoint-mapping,Request-mapping,Response-mapping等。结构如下。

小墨是如何做一个分布式爬虫框架的

另外小墨又扩展了excutor,使得初始化任务的时候可以传入任务对应的模板ID。同时小墨利用模板引擎技术使得这个xml模板支持了简单逻辑(条件判断,循环,变量定义等)。


这样一来,对于业务方只需要写xml,也不用学习java了。最基本的只需要熟悉xpath语法,熟悉json结构就行了。小墨整完这些后,很满意。


小墨还想做很多:包含做几个页面,里面包含爬虫的job管理,以及task可视化,和job的作业监控。所以小墨又开始熟悉vue,element-ui这些东东,打算再做个可视化的东西。

小墨做的这个东西支持分布式部署运行,也具备良好的开放性(有完善的restfull的管理api,完成job的管理)和扩展性(任务队列,采集器,下载器,解析器均可自定义扩展) ,以及易用性(可以写少量硬编码,也可以基于xml模板方式语言无关的方式)。


回顾下看都涉及了解或使用了哪些知识点?

  1. Java基础知识中:反射,注解,多线程,队列。

  2. Java Web:SprintBoot,Vert.x Web

  3. 实践模式涉及:发布-订阅,组合,工厂,责任链。

  4. 分布式缓存:Redis,Hazelcast

  5. 分布式存储:elasticsearch

  6. 分布式文件系统:fastdfs,hdfs

  7. 其它:Selenium,Jsoup,xpath,Restfull,OpenOffice等