/ PhantomJS

Selenium+PhantomJS的爬虫那些事儿

0x00

最近写爬虫分析灰色网站,要使用无头浏览器动态加载网页,使用selenium+PhantomJS, 自己研究的时候遇到了一些比较有意思的坑,和大家分享一下。

0x01

先说一下架构,在大规模爬取网页内容的时候,为了提高性能,降低存储和计算开销,单个PhantomJS进程往往需要连续处理大量的URL,那么针对单个PhantomJS进程在连续处理不同URL的时候,往往会出现一些意想不到的问题。

例如,由于我们需要通过

from selenium import webdriver
d = webdriver.PhantomJS()
d.set_page_load_timeout('10')
d.implicitly_wait('10')
d.get(url)
d.current_url
```这样的方式记录给予Phantomjs引擎的原始URL和PhantomJS动态加载后的URL,但是当单进程PhantomJS大量处理URL时,我们发现有许多原始URL和动态加载后的URL完全无法对应。这会是什么原因导致的呢?

就目前遇到的情况而言,大体分为两种情况导致:

* 网页内部存在onbeforeunload事件调用使得网页无法被正常关闭。
* 待访问资源不可用。

##### 0x02 onbeforeunload
我们来看一个比较有意思的网页,在爬取到该网页所在的URL之后,phantomjs”停止了工作“, 所有后续给phantomjs处理的URL 其调用current_url返回的URL值都是这个特殊网页的URL,以下是该网页内含部分HTML代码:
```html
<BODY onbeforeunload="return('你确定仔细阅读此文章了吗?')" style="margin:0px;"><iframe border="0" name="lantk"  width="0" height="0" allowtransparency="" scrollbars="yes" frameborder="0"></iframe>

形如<ELEMENT onbeforeunload="handler">的代码会注册一个事件,当浏览器引擎卸载当前HTML文档之前抛出一个对话框,用户可以确认是否他要离开这个网页。
对应在Javascript中,可相应的对该事件进行处理:

object.onbeforeunload = handler;
object.addEventListener("beforeunload", handler, useCapture);

事件是DOM事件的简称,根据w3.org DOM的文档 1.4.2 Complete list of event types:unload事件类型的实现将从环境中移除该网页文档自身和其附带的所有资源,包括图片、CSS、和Javascript脚本。在运行这样的事件之后文档将被卸载。

那DOM的设计者们设计onbeforeunload事件的本意是什么?onbeforeunload存在的意义在于,你无法通过javascript脚本形式对其进行处理,如果用户想要离开某个网页,那么你无法阻止他离开这个网页,你也无法在用户点击关闭网页(触发unload事件时)进行其他阻止用户离开的活动,为了安全性考虑,用户的自由是第一位的,网页编写者无法将用户”囚禁“在网页应用程序中。

在IE浏览器中,可以通过自定义字段来建立自定义消息,如上述代码中的return(‘你确定仔细阅读此文章了吗?',而在其他浏览器中,这个事件不会显示自定义的消息。

那么在我们的案例中,原因就在于phantomjs接收到URL并加载了网页DOM之后,受困于onbeforeunload事件,导致代码中没有抛出异常,然而引擎也永远停留在了当前页面,对于后续接收到的任务不予处理。

URL无法访问的其他情况

除了上述的情况导致phantomjs异常以外,还有一种由于URL无法访问导致current_url异常的情况,这种情况可以在如下三种子条件下触发:

  • DNS查询返回域名不存在,无论是本地hosts还是远程DNS服务器的返回都可以。
  • URL的无法返回正常HTTP响应。

单独通过phantomjs访问上述两种情况的URL,current_url将会返回about:blank, 然而如果同一phantomjs进程曾经处理过其他URL, 则由于上述两种情况,phantomjs driver没有真正去处理第二个URL。

我们可以把phantomjs的状态简单的视为一个状态机模型,由于在处理一系列URL时候状态是连续传递下去的,通过输入,会改变状态机的状态,而满足上述任意一个条件的输入,将导致phantomjs状态停留在原位,当用户简单将输入和输出进行对应的时候就会出现问题。

0x03 解决方案

针对onbeforeunload的问题,可以在获取网页源码后加入对使用onbeforeunload的网页使用phantomjs自身对网页中注入js脚本进行处理。然而这样会造成一定的性能下降。

针对URL无法访问的情况,有两种方案: 1.可以在phantomjs处理每个任务之前先进行DNS查询,对于无DNS解析记录的可以不用提交phantomjs处理。然而这种方案对于IP类URL没有很好的控制。
2。可以在phantomjs每个任务之间插入driver.get('http://about:blank')这样可以间接避免上一个URL的状态污染到下一个任务。

然而,这两种方法都是治标不治本,最根本解决方案应该是将每phantomjs会话之间的执行完全隔离,我们使用selenium连接phantomjs后端其实使用的是detro开发的ghostdriver,是一个Remote WebDriver Wire protocol的Phantomjs实现,根据Github上的链接,以及作者ghostdriver库github页首的简介,项目开发者由于生活的压力已经两年没有更新了。。你们说Github是不是应该开一个打赏机制?

一点小记,如果有错误欢迎指正。
(另:PhantomJS作者近日正式宣布停止维护phantomjs,目前最好的替代品是Chrome Headless)