最好的语言PHP + 最好的前端测试框架Selenium = 最好的爬虫(上)

入职冰鉴科技做爬虫开发已经半年多了,陆续开发维护了几个爬虫以后终于在web端爬虫这一块有了登堂入室的感觉。中间踩了许多坑,也对爬虫的许多细节有了自己的认识,所以今天希望能分享一些爬虫经验。虽然爬虫的很多东西不好说太细,因为说太细了别人马上有针对性的反爬虫了,而且很多技巧业界没用通用的解决方案(别人就算做出来了也不太愿意分享),都是我自己慢慢摸索出来的。但是我认为适当的业界/友商之间的技术交流是必要的,不能闭门造车,我也渴望能和业界/友商有更多私下的深入交流,大家多切磋才能进步嘛。最近我在研究app反编译爬虫相关的,所以对这块特别感兴趣。

为什么是PHP

其实就目前业界来说,python下的爬虫轮子是最多的,我厂大多数同学都用python搞爬虫。我由于原来搞web后端用PHP比较多,对PHP下的生态和第三方库啥的如数家珍,厂里对使用的语言也不做强制要求,所以我就用最拿手的PHP开搞了。有同学可能会觉得PHP下爬虫轮子似乎不多,甚至有部分做惯了PHP后台的同学在需要完成爬虫任务时也拿起了python,难道PHP就不适合搞爬虫么?我认为恰恰相反,PHP在web领域积累了大量成熟的第三方库,而且其强大的内容处理能力使之在需要琐碎处理的爬虫任务中如鱼得水。爬虫从运行时间上大致可以分为两种:1、实时的爬虫:一个请求来了我就开一个爬虫去爬取结果,一般情况下这种爬虫直接对外提供API; 2、长期爬虫:这种爬虫一般会一直运行或者定期运行,把数据更新入库。一般来说这2种爬虫都需要比较频繁的维护更新,PHP作为一门部署简单的脚本语言,可以实施热更新爬虫代码,非常方便。

使用第三方库

用PHP搞爬虫请利用好composer下的第三方库。PHP在web领域积累了大量成熟的第三方库,基本上你想得到的库都能在github上都能找到,如果你不用第三方库的话,那么你就等于放弃了PHP在web领域的巨大优势。爬虫相关的PHP第三方库我用的比较多的有:
1、Guzzle:功能很完善的httpclient,带异步并发功能,别的脚本语言找不到这么好的httpclient
2、Goutte:对symfony的dom-crawler和css-selector的简单封装,你也可以直接用symfony的css-selector来抽取html的dom元素
3、symfony/process:symfony出品的php开进程的库(封装的proc_open),兼容windows,要知道pcntl扩展不支持windows的
4、php-webdriver:Facebook官方维护的selenium的php客户端
前段时间有一个《我用爬虫一天时间“偷了”知乎一百万用户,只为证明PHP是世界上最好的语言》,这个repo很受关注也一直在维护。我也研究了一下他的代码,质量很高,但是有一个缺点就是没有使用现有的第三方库而选择自己封装。我们应该把精力花在爬虫业务上而不是去从新造轮子,我平时直接无脑的使用现有的composer下的各种第三方库。我从今年4月份入职到现在8个月时间只写了3个爬虫(除了爬虫业务外,基于redis的分布式爬虫调度、单机多爬虫并发、报警+监控+参数控制、selenium多浏览器匹配+特性定制、代理策略定制and so on)一套下来,所有代码都加起来只有6000行PHP代码。已经有现成的成熟稳定的第三方库不用,自己造轮子是得不偿失的。

多线程、多进程和异步

爬虫不能不说到并发,爬虫作为一个IO密集型而不是CPU密集型的任务,一个好的并发的爬虫应该满足:1、尽量可能高的下载带宽(下载带宽越高,爬的数据越多);2、尽可能小的CPU消耗和尽可能小的内存消耗。
多线程似乎是实现并发的不错的方式,经常有人说“PHP没有多线程”让广大PHPer直不起腰。作为web后端的时候PHP没法使用多线程,但是作为命令行运行的话PHP是支持多线程的。我们知道PHP分为线程安全(ZTS)和非线程安全版本(NTS),后者其实是为了兼容win下IIS的ISAPI,这也就逼着PHP下的扩展基本上都提供的线程安全和非线程安全版本。也就是说从理论上来说命令行的PHP多线程是真的多线程,没有像py或者ruby那样的全局锁(实际上同一时刻只有一个线程在跑),但是实际上PHP命令行多线程不太稳定(毕竟它的多线程不是为php-cli设计的),所以我建议命令行应用还是使用多进程来做并发。
而异步也是实现并发的重要方法,爬虫需要并发的大多数情况是我想是同时去爬多个url,这种情况无须使用多进程/多线程,直接在单进程中使用异步就可以了。比如PHP的Guzzle异步支持非常好用,Guzzle默认异步是包装的curl的curl_multi的几个函数来做的,如果你想用性能更好的异步事件库可以设置Guzzle的adapter为react-guzzle-psr7(当然了你得安装Event之类的异步pecl扩展)。我个人试用下来觉得Guzzle默认的异步就够用了,单进程并发几十上百的http请求跑满小水管那是不成问题的,cpu和内存消耗还很小。总之,把php的多进程和异步合起来用,实现良好的并发不是问题。

关于爬虫框架

开箱即用封装好的爬虫框架不是银弹。我一开始也研究了java和py下的一些比较著名的框架,企图先把这些框架学会然后把自己的爬虫任务整合进去,后来发现这么做很困难。诚然用爬虫框架基本上改两行就可以跑起来了,对简单的爬虫任务来说很不错。但是用别人封装好的框架会导致爬虫的定制性变差(要知道爬虫是需要灵活处理各种情况的),而我们知道爬虫的本质就是开着httpclient取回html然后dom抽取数据就完了(并发的话再加个多进程管理),就这么简单的任务为了尽可能满足所有人需要被封装成了一个复杂系统的框架,并不一定适合所有的情况。有一次v2ex上也有人出来质疑说我直接用requests也很简单啊,scrapy的优势在哪里呢?我的理解是爬虫框架的优势就在于把爬虫的并发调度都做了,而我们直接单进程来写爬虫的话只能是一个单进程爬虫没有并发调度。其实爬虫的多进程并发调度没那么复杂,也不需要搞太复杂,我说说我的php爬虫是怎么做并发调度的(python下一回事)。

爬虫多进程调度

我的PHP爬虫多进程调度比较简单粗暴,爬虫分为管理爬虫进程的Master进程和负责具体爬取业务的worker进程,而redis负责对爬虫进行控制以及显示爬虫的状态。
基础模板1

比如我有一个爬取A站点的爬虫任务,我开发好爬虫Worker A以后,我可以在redis中设置在服务器Node1上我开2个Worker A来爬,而Node1上的master1进程会定期去redis中读取控制参数,如果发现Node1上的Worker A进程不足2个的话就会新开Worker A进程补充。当然了,控制参数需要包含哪些你可以自己定制,比如我就定制了每个节点的Worker上限、使用的代理策略、是否禁止加载图片、浏览器特性定制等等。Master进程新开Worker进程有2种方式,一种是通过类exec(比如在Master进程中proc_open(‘php Worker.php balabala’, $descriptorspec, $pipes)这样)调用来开一个新的命令行php的Worker进程,另外就是通过fork机制来做。我采用了类exec调用的方法(其实是symfony/process库,它封装的proc_open函数来开的进程)来开Worker进程(如果要传命令行参数给Worker进程注意使用base64编码一下,因为命令行可能会过滤某些参数),这么做的好处就是解耦。需要注意的是,现在Worker进程都是Master进程的子进程,所以Master进程退出的话所有Worker进程也会退出,所以Master进程注意异常的catch,尤其是redis、数据库和别的有网络io的地方。如果你希望Worker进程damonize的话请按这篇文章的方法来(php下也是一样的,不过不兼容windows)。
我不建议Master进程通过IPC机制对Worker进程进行控制,因为这么做一下子就让Master进程和Worker进程耦合起来了,Master进程应该只是简单的负责开Worker进程而已。对Worker进程的控制可以通过Redis来完成,也就是说Worker进程每隔一段时间(可以是完成了一次http请求,或者每隔几秒)可以去Redis读一次控制参数(如果需要的话,也可以到汇报一下自己状态,参数比较多的话用好redis的pipeline),在实践中这种方法工作的很好。
我的PHP爬虫中都采用了这个简单粗暴的方案,我认为它的好处有4个:
1、支持分布式且依赖简单,参数控制+状态汇报直接通过单一的redis节点。我推荐你用一个好的redis的GUI工具来管理redis,redis的5种数据结构用来做爬虫参数控制+爬虫状态显示非常方便
2、Master进程和Worker进程解耦,而且可以解决爬虫较多发生的内存泄漏问题(Worker进程跑完直接退出),也可以热更新代码
3、实时爬虫可以通过Master进程抢占push到redis list中的请求来做,而长期任务的爬虫在Worker进程意外退出后Master进程立刻补充,能适应各种爬虫任务
4、开发爬虫只用去写Worker进程就ok了,开发方便,不用关心调度问题
缺点当然就是这一套机制都需要你自己写,高度可定制性的代价就是自己动手。

总结

把我的PHP下爬虫经验的几个方面拿出来讲了一下,由于篇幅有限Selenium相关的经验就留到下次再说了。

以上

最好的语言PHP + 最好的前端测试框架Selenium = 最好的爬虫(上)》有10个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注