最好的语言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负责对爬虫进行控制以及显示爬虫的状态。

比如我有一个爬取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了,开发方便,不用关心调度问题
缺点当然就是这一套机制都需要你自己写,高度可定制性的代价就是自己动手。

 

为什么是Selenium

在简单的爬虫中直接用httpclient就可以爬了,但是反爬虫比较厉害的情况下,有很多反爬虫的机制,比如:各种302跳转、js检测、种cookie、iframe、captcha等等。去逻辑分析这些机制成本太高了,就算分析出来了用httpclient模拟也会写一大堆,代码也很难重用,于是不得已只能增加成本上浏览器了。浏览器也得分有界面浏览器和无界面浏览器,无界面浏览器不用渲染自然CPU和内存消耗低适合爬虫,也有人在linux下用Xvfb来把有界面浏览器运行在虚拟屏幕上来降低的消耗。Selenium作为事实上的前端测试标准,其完整的API是为大量的前端测试需求而成熟,这是前端给我们爬虫工程师的馈赠。Selenium和phantomjs、HtmlUnit、ghost.py之类的headless浏览器(这些headless浏览器一般都提供了原生的API)不是一类东西,你可以把它理解成可以驱动包括phantomjs、HtmlUnit、Chrome、IE等主流浏览器的统一接口。至于原生的headless的API方案,比如你直接用原生的phantomjs完成稍微复杂一点的操作会很困难,因为你看不见也很难debug。实际上phantomjs、ghost.py、HtmlUnit之类的比较小众的原生API的headless方案在大多数场景下,是无法和Selenium相提并论的。由于Selenium统一了和浏览器交互的标准,客户端基本上包含了主流语言。也就是说你可以在你喜欢的编程语言下用Selenium在Chrome上开发好了爬虫,然后在生产环境直接把浏览器换成phantomjs就ok了,API提供统一的dom、js注入、cookie管理、事件等待、浏览器控制和输入等操作,完整且成熟。而且由于我用的是最好的语言,实际上用PHP的话选择似乎就只有Selenium没有别的选项了,所以Selenium还有个优势就是PHPer除了它没得选。

PHP下Selenium开发环境

虽然说Selenium本身用啥语言都行,考虑到我们的主题是用PHP来搞爬虫,所以这里把我的PHP下的Selenium开发环境的一些基本点介绍一下,方便刚入门的同学。
我以开发环境win7+XAMPP+eclipse(with PDT)举例,大家可以用自己喜欢的PHP开发套件,我只说一下其中一些注意点。新的项目我建议都用PHP7.0,基本上所有的第三方库和扩展都支持PHP7.0了。另外windows下安装扩展的时候,去phantomjs连tcp的连接请求都没发,最后才发现是phantomjs的缓存问题,也就是说你一旦指定了–disk-cache=true并且有并发,请一定给不同的phantomjs实例指定–disk-cache-path为不同的缓存目录。
另一个加速的方法自然就是在拿数据的时候,把cookie从phantomjs中取出来,然后用httpclient带上cookie去嗖嗖的取就ok了,不过很多时候请求参数的构造很复杂导致这种办法比较困难。然而Selenium强大的API提供了一个在浏览器中同步/异步注入js代码的功能,这个功能如果发挥想象力的话在很多时候可以克服请求参数构造复杂的问题并且速度还很快。

Selenium的一个神坑

大家直接看这个PR:https://github.com/SeleniumHQ/selenium/pull/2031 ,关于这个bug我还需要给大家讲一个故事。由于我之前花了很长时间在搞一个私人的兴趣项目,到今年4月份的时候发现还有接近2W的学杂费欠着学校,我们可爱的辅导员经常很和蔼的关心我聊些da辩啥啥啥之类的,我没办法就找到我厂打算打点杂在毕业前挣点学杂费。打了一个星期杂以后,有一个比较难的爬虫没人搞我就接手了,当时也没怎么正式搞过爬虫,于是凭着一点技术直觉选定了Selenium+phantomjs的技术栈,花了20多天把并发爬虫调度+爬虫业务这些东西打通了(其实后来队友直接用httpclient模拟也能搞定的样子)。结果发现phantomjs进程存在无法回收的问题,并发多了以后跑着跑着内存就炸了,这个问题没法解决一切无从谈起。我还尝试了C++写了个daemon检测无法回收的phantomjs进程来着,然而Selenium不对客户端暴露浏览器的进程号,导致做起来效果不太好。当时算是实习,干了1个月啥成果都没有我也比较郁闷。4月干完后就是五一,当时觉得干不下去了,然后没事逛github看到了4月29号开的那个PR,这尼玛不就是我遇到的bug么,而且这个bug在google code那边几年了前就被提出了,这么巧刚好在我卡住的时候被解决了?于是觉得要不把这个bug解决试试,于是用Selenium的repo自己build了一份已经fix好了的驱动,然后问题就解决了,之后什么都比较顺利了。磨合久了后比我小三岁的Leader可以在很短时间做出正确的判断给了我很深的印象,所以之后毁了杭州蘑菇街的三方留在冰鉴科技继续搞爬虫也是后话了。然后这个bug fix官方很不负责的只是merge进了master(对应后来的3.x),这个bug fix没有cherry-pick到2.x版本,于是官方发布的2.x版本的驱动依然有这个bug。这个repo很大下载下来要花很久,用2.x版本的同学觉得build麻烦的话可以用我build好的2.x版本的:http://pan.baidu.com/s/1kUQsBAZ ,你可以把它当做selenium-server-standalone-2.53.1.jar修复bug之后的版本。然后就是无限感叹,这个bug在selenium rc中才存在,如果你用来做爬虫时挂上代理时由于基本上代理不靠谱肯定会经常timeout然后触发这个bug,因为Selenium主要为前端测试存在所以这种bug没人关注也不奇怪,在php下用selenium做爬虫的并发场景下,这个bug没有解决前是不是就没人打通过呢。

一些可注意的地方

如果大家在php下用Selenium驱动phantomjs没有并发的话,其实可以完全抛弃selenium-server-standalone-2.53.1.jar这个服务端,因为phantomjs自带的ghostdriver自己就是个Selenium服务器端,调用方法见:https://github.com/MergEye/phpSelenium 。其实如果可以这么玩的话,那我php的并发爬虫进程中开一个phantomjs的子进程绑定一个该进程独有的端口,然后进程内部再用Selenium客户端去连接phantomjs子进程的端口,这样不仅可以绕开烦人的selenium-server-standalone.jar,还可以保证php进程退出后phantomjs子进程一定会退出。我这么测试了一下发现是可行的,但是有个问题就是i7下phantomjs的并发从100个降到了15个,所以这个方案适合无并发且不想单开一个java命令行应用的同学,因为它并发性能太差了。

由于phantomjs的依赖被静态编译进了二进制中(这一点做的非常好),在win/linux下使用时就是一个绿色版的二进制,非常方便。另外phantomjs的某些Selenium API存在一些bug,以及存在崩溃问题(实际上在CPU比较高的时候,别的浏览器也存在崩溃),这些问题我只能在业务上容错考虑进去,毕竟没有银弹。需要注意的一个Selenium的session对应一个浏览器实例,这些实例不是线程安全的,所以任何时候都只能有一个Selenium客户端在控制一个浏览器。

生产环境我实践过Windows和Linux(docker下),Windows下没啥好说的,docker下由于爬虫需要经常更新所以我建议通过git来做(更新一点源码就重新build docker镜像然后分发是不值得的)。我们在docker的系统中安装好git,然后每次更新代码exec/attach后进入源码目录git手动更新代码就好了(build镜像的时候把.git目录copy进去,因为.git目录下的东西是跨平台的,不过请务必注意[autocrlf问题](http://stackoverflow.com/questions/39700531/using-docker-for-win10-to-build-image-and-found-the-copy-command-changed-the-ne)),当然你也可以开cron定期更新。由于我的爬虫代码需要对源码目录有较多的写操作,而docker镜像的文件系统写消耗比较大,所以我采取了比较dirty的办法就是把源码挂载到主机目录上了(没用专门的数据卷纯粹觉得数据卷比较坑)。对了,用compose在生产环境build镜像是不对的。

我已经把php下selenium驱动phantomjs的并发方案都分享给大家了,更深入的东西实在是不好再说了,毕竟厂里养着我搞出来技巧也不能都分享了。如果有友商/同好有app反编译爬虫相关的经验的话,非常愿意私下里多交流互补一下(我博客下联系),我们目前这块打算积累起来。

提高生产效率的几点

还有几个我认为对提高效率比较重要的点:

1、把xdebug环境配置好。我把配置好xdebug环境放到了第一位是觉得这个可以大幅度提高开发效率,我们知道在php-fpm中的php进程是不能做长期爬虫的(就算使用ignore_user_abort(true);set_time_limit(0);之类的也是没法保证稳定的),所以我们要用php-cli来开发爬虫。然而就我接触的大部分同学都知道在web开发中使用xdebug调试功能来开发,但是不知道在php-cli中也是可以使用xdebug的,具体可以参考:http://stackoverflow.com/questions/1947395/ 。因为Selenium操作是一步接一步的,很多时候我们只有通过单步调试就可以非常愉快的找出bug,以及继续往下写逻辑。

2、开发时设置fiddler抓包。和python不同,PHP的默认http不走系统默认的代理(至少windows下是这样的),所以如果你在开发爬虫的时候就算开了fiddler也是没法抓到包的,所以你需要在你使用的httpclient中显式设置代理为本地fiddler的代理端口。这么做的好处就是开发的时候每一个http请求都在掌控之中,如果可以的话可以把浏览器也设置为fiddler代理抓包,可以大幅提高开发效率。

3、确保IDE的typehint可以用,使用第三方库的时候没typehint完全没法干活。另外许多php的web框架都带有为php-cli写的console应用脚手架,由于web框架本身把配置、路由和很多组件都包含进去了,如果在这些web框架的console应用脚手架上开发应该会省力很多。

最后谈谈我对爬虫工程师的理解

爬虫是任何内容提供商都需要面临的问题,据说在Web流量中有60%是由爬虫贡献的,当然了如果是搜索引擎爬虫的流量那肯定是收到欢迎的。然而非搜索引擎的爬虫带来的流量让网站的内容流失,而且消耗服务器带宽和CPU甚至影响正常访问,简直百害无一利,所以反爬虫基本上大家都在做。作为一个爬虫工程师(我回去翻了一下我当初offer的title的确是“高级爬虫开发工程师”,虽然有时候我也打打杂干点别的),我相信入这一行的同好们很快就可以体会到,我们和网站搞反爬虫的后端们是谁也离不开谁得关系。后端不反爬虫的话,那扒东西随便找个刚毕业的菜鸟三下五除二就搞完了,我要感谢辛苦反爬虫的后端们。没有我们的话,后端的KPI和竞争力不是也会减少么,其实我老本行就是PHP后端来着,自己和自己相互理解了(笑)。然而爬虫工程师比较悲催的是,这是一个市场需求比较小的业界,技术很难积淀(比如两个要价25K的简历,一个写着5年iOS经验,一个写着5年爬虫经验,你觉得后者是不是比较搞笑),也很难拿出来和人交流,大部分工作也是体力活没啥技术含量,给人比较low的印象。业界搞得很出名的梁斌,你能找出比梁博还出名的搞爬虫的么?我基本上一和人谈起来梁博马上就有人跳出来说梁博水(梁博自己也在微博自嘲过),这其实也反映了我前面所说的爬虫工程师的悲催。还有朋友总结说爬虫很难处于一个核心的业务,而且爬虫需要人长期维护(爬和反爬此消彼长,最后拼成本,对抗技术更新也比较快),比较累,而且觉得写爬虫好玩靠这个入门的新人一大堆,这块感觉很饱和。这些我觉得说得还是很有道理的,不过有一点我想补充的是简单的爬虫的确是体力活没啥技术含量,但是比较困难任务还是很难做的,因为业界没有通用的解决方案,你得自己摸索。我也很难把自己局限在“爬虫工程师”这么一个title上,只是码业务的话用别人做的基础组件经常觉得这么漂亮的工作别人都做好了,我去做的话肯定做不了这么好,所以缺乏去重复别人工作的动力也讨厌造轮子。不过转念一想,基础组件的存在就是为业务为需求服务的,所以在业务中沉淀技术,寻找有可能性的需求,说不定哪天能做出很漂亮的工作?

转载于:https://my.oschina.net/ppmeng/blog/800806

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值