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

2016-12-06 09:15:29 +08:00
 gouchaoer

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

上篇: https://www.v2ex.com/t/324309

我博客原文:《最好的语言 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 问题),当然你也可以开 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 上,只是码业务的话用别人做的基础组件经常觉得这么漂亮的工作别人都做好了,我去做的话肯定做不了这么好,所以缺乏去重复别人工作的动力也讨厌造轮子。不过转念一想,基础组件的存在就是为业务为需求服务的,所以在业务中沉淀技术,寻找有可能性的需求,说不定哪天能做出很漂亮的工作?

10029 次点击
所在节点    PHP
32 条回复
gouchaoer
2016-12-08 22:43:36 +08:00
@daydaygo

```
FROM daocloud.io/php:7-fpm
MAINTAINER gouchaoer <gou_chao@icekredit.com>

RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak && \
echo "deb http://mirrors.163.com/debian/ jessie main non-free contrib" >/etc/apt/sources.list && \
echo "deb http://mirrors.163.com/debian/ jessie-proposed-updates main non-free contrib" >>/etc/apt/sources.list && \
echo "deb-src http://mirrors.163.com/debian/ jessie main non-free contrib" >>/etc/apt/sources.list && \
echo "deb-src http://mirrors.163.com/debian/ jessie-proposed-updates main non-free contrib" >>/etc/apt/sources.list

RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get --yes install \
net-tools \
htop \
iftop \
nano \
libicu-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng12-dev \
libicu-dev \
libssl-dev \
libevent-dev


RUN docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install gd

RUN docker-php-ext-install exif intl mcrypt sockets pdo_mysql
RUN pecl install mongodb
RUN echo "extension=mongodb.so" >> /usr/local/etc/php/conf.d/mongodb.ini
RUN pecl install event
RUN echo "extension=event.so" >> /usr/local/etc/php/conf.d/event.ini
RUN docker-php-ext-install opcache
RUN pecl install apcu
RUN echo "extension=apcu.so" >> /usr/local/etc/php/conf.d/apcu.ini

RUN apt-get --yes install git cron

COPY . /app_src

RUN mkdir -p /root/.ssh
RUN chmod 0700 /root/.ssh
COPY id_rsa /root/.ssh
RUN chmod 0700 /root/.ssh/id_rsa

RUN crontab /app_src/mycron

COPY php.ini /usr/local/etc/php
COPY www.conf /usr/local/etc/php-fpm.d
COPY php-fpm.conf /usr/local/etc
CMD cron && php-fpm
```
这是一个 php-fpm 的 docker-file , php-cli 一样可以用。。。因为我开发环境没用 docker (其实用 docker 更好,但是我懒得改),把 php 和 php-pm 的一些配置加进去了你自己删改吧。。。

cron 的地方可以自己实现自动化开 php-cli 的。。。
gouchaoer
2016-12-08 22:46:10 +08:00
这样个镜像 6 、 700m ,没有新的阉割镜像小,不过软件比较齐全,也是 php 官方的系统,所以不折腾,小规模用比较合适。。。
gouchaoer
2016-12-08 22:55:08 +08:00
对了,要跑 selenium 的话,把基础镜像装上 jre ,然后一个 container 把入口改成那个 jar 的应用,一个 container 改成 master 命令行应用,源码目录 copy 到 host 的 /app 下面(我镜像里最初放到 /app_src 下纯粹为了初始方便,第一次把 /app-src 复制到 host 的 /app 下后,以后走 git 更新源码了), master 的 container 就 link 到 jar 应用就完了, 2 个 container 源码目录都 host 的 /ap 下。。。。

手机打字乱
gouchaoer
2016-12-08 22:59:41 +08:00
也可以不用另开一个 master 容器直接通过 cron 来开 php-cli ,查看运行状态进入容器查看,不过看不到 master 输出,所以可以从定向 cron 开的 php-cli 的输出,类似:* * * * * cd /app && /usr/local/bin/php console/yii app/cron >> /app/console/runtime/cron.log 2>&1
lxglife
2017-06-15 10:43:41 +08:00
楼主,请问爬虫的时候遇到分页有什么好的解决方案?请指教。
HYSS
2017-08-31 20:40:19 +08:00
请教下 如果是用 phantomjs 如何获取 phantomjs 环境中网页的 cookie 信息
damon1
2018-03-13 14:59:56 +08:00
楼主大大,我想问一下 phantomjs 如何解决缓存占用过高的问题,我用 java+selenium —— phantomjs 爬数据,中间有一个翻页过程,用 ajax 实现的。翻页 1000 多次之后,单个 phantomjs 浏览器占用内存高达 2.3g ,关闭当前窗口也无效,只能 driver.quit 退出当前浏览器才能释放内存,但是我需要保持页面在线状态又不能退出浏览器。求指教
gouchaoer
2018-03-13 15:14:03 +08:00
@damon1 我 centos6 下也观测到了这个问题,目前嫌麻烦还不想转 chrome headless,无解这个。。。内存太大了就退出重来吧
damon1
2018-03-13 15:38:41 +08:00
谢谢,现在 phantomjs 没有手动释放当前页面缓存的方法吗?因为要短信接收验证码才能进入的网站,重来很麻烦,想做到登陆一下,刷新保持在线。现在就剩内存成了最大的问题
gouchaoer
2018-03-13 15:47:04 +08:00
@damon1 这个简单啊,你把 cookie 都读出来保存起来,新开的 phantomjs 再 setccokie 进去就 ok 了。。。不过 phantomjs 的 setcookie 接口有一些 bug,不过可以在某种程度上回避,具体你遇到了去 issue 看吧,我一时找不到了
damon1
2018-03-13 15:52:30 +08:00
@gouchaoer 谢谢,我尝试一下
mingyun
2018-10-06 09:24:47 +08:00
学习了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/325540

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX