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

在上篇中,我主要讲了用PHP写爬虫时的一些经验,在下篇中我会对Selenium进行展开,把我总结的Selenium技巧和一些坑的处理方法介绍给大家。

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

为什么是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下安装扩展的时候,去pecl下载dll的时候注意区分线程安全和非线程安全版本,而且扩展之间依赖关系,比如你要安装php_event.dll扩展,你会发现它依赖php_sockets.dll扩展,而且你在php.ini中必须把php_event.dll放在php_sockets.dll的后面。
PHP下的Selenium的客户端php-webdriver由Facebook维护,在composer添加依赖安装即可,需要注意的是PHP下需要一个Selenium官方的java的命令行的应用(负责管理浏览器和分发来自Selenium客户端的命令,也就是所谓的Selenium RC)。Selenium在今年8月推出了Selenium3.x大版本,我依然在使用2.x版本也暂时无升级打算,所以如果你需要一些3.x支持的新特性的话可以试试3.x版本。如果使用2.x版本的话,安装好java运行环境,然后下载官方build好的最新selenium-server-standalone-2.53.1.jar,启动参数请读官方文档(目前我试过chrome、firefox和phantomjs都是没问题的),需要注意的是官方的2.x版本的代码(官方repo称之为leg-rc分支)有个比较重要的bug没有修导致selenium-server-standalone-2.53.1.jar存在bug,后面我会讲这个bug。这一切都搞定后,就可以按照php-webdriver官方github的example.php例子跑个hello world了,然而php-webdriver并没有那种一步一步教初学者入门的文档(它的API也只是类自动生成的),很多特性需要使用者看源码或者去读Selenium官方文档。我个人觉得php-webdriver虽然文档缺乏,但写得很漂亮,大量使用类来克服脚本语言弱类型的缺点,写起来像用java那样鲁棒又不失脚本语言的速度,Facebook不愧是PHP的大厂。

开发Selenium的模式

我不会给大家长篇累牍的介绍Selenium的API或者贴上几块爬xxx页面的example代码,因为这些东西官方文档(Selenium官网被墙了)/源码里面就有贴出来真的没啥意思。所以这里主要讲Selenium的运用模式,大家熟悉了Selenium的API了觉得如果爬虫干不下去了,可以试试直接转web前端测试(笑)。
一旦采用了Selenium就意味着必须开个浏览器,而速度就成了一个很大的问题,这也是很多人比较关心的。一个解决思路就是并发,我可以开很多爬虫进程来驱动很多浏览器,然而这种模式有个缺点是对CPU、内存和带宽的消耗特别大,毕竟用Selenium就意味着开始拼成本了。为了降低对CPU和内存的消耗,phantomjs等似乎是一个很不错的选择,我在windows下的经验是每个phantomjs进程内存消耗90M左右,i7的CPU单机并发可以跑到100个phantomjs进程,所以说内存和CPU都是可以接受的。另外请务必打开phantomjs的静态资源缓存(缓存图片、js和css等静态资源,参数看phantomjs官网文档),或者干脆禁止加载图片,做完这些以后每次http请求基本只会下载html和一些ajax动态请求而已,加快速度的同时非常节省带宽。当然了phantomjs并发有一个神坑,如果你给phantomjs设置了–disk-cache=true并且有并发,由于所有的phantomjs进程实例会使用同一个系统默认的缓存目录,所以时间久了以后会导致缓存文件会被破坏(并发越多重现越容易)。此时phantomjs的表现很诡异:访问别的url可以正常访问,但是访问一直在爬的站点url就会在GET的时候卡住(我猜测静态缓存文件是根据url的hash来存储的),此时CPU占用100%,你知道当初发现这个状况后很难怀疑是phantomjs自己的问题,觉得肯定是目标网站用了黑科技。用fiddler抓包发现tcp没有建立连接,后来用wireshark抓包发现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的学杂费欠着学校,我们可爱的辅导员经常很和蔼的关心我聊些答辩啥啥啥之类的,我没办法就找到我厂打算打点杂在毕业前挣点学杂费。打了一个星期杂以后,有一个比较难的爬虫没人搞我就接手了,当时也没怎么正式搞过爬虫,于是凭着一点技术直觉选定了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上,只是码业务的话用别人做的基础组件经常觉得这么漂亮的工作别人都做好了,我去做的话肯定做不了这么好,所以缺乏去重复别人工作的动力也讨厌造轮子。不过转念一想,基础组件的存在就是为业务为需求服务的,所以在业务中沉淀技术,寻找有可能性的需求,说不定哪天能做出很漂亮的工作?

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

  1. 请教 $driver->get(‘http://xxx/’); 访问一个页面,页面中有很多ajax请求,(前后端分离的模式) 。 现想获取所有ajax的返回值,应该怎么写? 谢谢。

    1. 这很困难,思路估计只有2种,第一是在代理层抓ajax包(比如fiddler),第二就是各个浏览器自己的开发者工具hook请求完成的Listener;这2个方案都不太好用

  2. 我在抓取一个网站上的数据 这个网站使用thinkphp模板生成的 每次模板只返回两条数据 有400多万条 我用php的curl抓了800多条后出现 couldn`t connect to host 时不时会有验证码 请教下这种情况要怎么处理

发表评论

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