0x00 前言

在 浅谈动态爬虫与去重 中,分享了动态爬虫中触发事件、监控节点变动、URL去重等的实现方法。在接近一年的线上运行与迭代更新过程中,处理了很多bug,也遇到一些有趣的漏抓案例。

本文将详细分析几个有代表性的案例,希望能对各位的coding大业有所帮助。

 

0x01 一个都不能少

上图为被抓取页面的源码。两个 <a> 标签点击后会分别跳转到 /test4.php 和 /test5.php(抓取时页面已锁定,实际不会跳转,但可以监控到跳转的目标URL)。

但爬虫并未抓取到 http://localhost/test4.php。检查下phantomjs模块的具体调用日志:

可以看到 id=test4 节点对应的事件确实被触发了,而且还把锚点的变化都记录下来了。但就是没抓到 /test4.php 跳转的请求。

原本以为是因为锚点的问题,锚点阻止了后续的事件执行(一个不负责任的脑洞)。但经过一番调试,发现是触发事件的时候出问题了。

window.location.href 重复执行的时候,浏览器只会执行后面的一个,比如这段代码,可以粘贴到 Console 执行下,页面会跳转到 /456。

解决方案如下图,由于 Javascript 的异步非阻塞的特性,还加了个闭包来实现 sleep。

如果不想这么做的话,也可以通过 Hook location对象来解决。

一句话总结:触发事件一定要有时间间隔!

0x02 猝不及防的关闭

<script type="text/javascript" language="javascript">
function BtnPsw_onclick() {
    window.open("../Login/UpdatePass")
} 
</script>
<form name="form1" method="post" action="" id="form1">
    用户名:<input name="UserName" type="text" id="UserName" class="logintext"><br/>
    密码: <input name="Password" type="password" id="Password" class="logintext"><br/>
    <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="1222">
    <input name="LoginButton" type="button" id="LoginButton" value="登 录" class="lbotton">
    <input name="login" type="button" value="退 出" class="lbotton" onclick="window.close()">
    <input name="UpdatePwd" type="button" value="修改密码" class="lbotton" onclick="BtnPsw_onclick()">
</form>

phantomjs解析的时候,超时严重导致漏抓。通过伟大的注释调试法,可以发现问题在这行代码里:

<input name="login" type="button" value="退 出" class="lbotton" onclick="window.close()">

动态分析时会主动去执行行内绑定的代码,即:window.close()。关闭了页面之后,PhantomJS后续绑定的事件都会失效,比如page.evaluate、page.onCallback、phantom.exit。没有执行exit函数,一直阻塞导致触发python的超时——狗带。

修复方案,在执行关闭页面的时候,PhantomJS的onClosing事件可以收到通知,示例代码如下:

还可以通过Hook来解决这个问题:

0x03 永远触发不完的事件

案例URL: https://lvyou.baidu.com/main/event/rank

动态分析超时导致没有结果返回。动态爬虫里触发行内绑定事件的代码如下:

逻辑是遍历所有的节点的所有属性,执行以on开头的属性值,即 onclick=alert(1) 这种。

但是抓取上面案例的时候,发现一直没有返回结果,使用伟大的print调试法打印了触发的具体内容后,发现页面一直在不停的触发同一个事件。

仔细看下页面源码:

登录应该是用JSONP来实现的,每次点击登录都会生成一个script标签,而且这个标签恰好还插入在了登录标签前面。

遍历数组的过程中,也在不断扩展这个数组。这就是问题的关键。

那应该怎么解决呢?用了个不太优雅的方法来实现JS深拷贝:

0x04 Hook是个哲学问题

<a onclick=show()>test</a>
<script type="text/javascript">
    function show(){
        window.showModalDialog("another_page.html");
    }
</script>

如上为漏抓页面的部分代码。爬虫进程超时,没有返回任何数据。

window.showModalDialog 是早期浏览器使用比较频繁的函数,用来弹出一个新页面,并且是阻塞执行的(所以造成爬虫超时被强行杀进程)。后来被 window.open 函数替代。替换的原因有:

1. showModalDialog 没有导航栏,无法进行后退、前进、收藏等操作

2. showModalDialog debug非常复杂(只能用alert调试法 2333)

3. 名字又长又难记(迷之猜测)

下图为正常打开的页面与 showModalDialog 打开的页面比较:

目前Chrome最新版已经不支持这个函数了,但Firefox、Safari、IE仍然支持。毫无意外的 PhantomJS 也支持。解决方案很简单,直接 Hook 函数就可以了:

这样的话,加上最开始就被 Hook 的 alert/prompt/confirm,现在已经 Hook 了四个可能会引起阻塞的函数了,是不是还有其他隐藏的存在呢?

写个脚本来检查下:

var page = require('webpage').create();
page.onConsoleMessage = function(msg) {
    console.log('> ' + msg );
    return true;
};
page.open("http://127.0.0.1:8082", "GET", "", function (status) {
    console.log(status);
    page.evaluateAsync(function(){
        for(var i in window){
            try {
                if (typeof eval("window." + i) != "function") {
                    continue
                }
            }catch (e){
            }
            // if(i in {"showModalDialog": "1"}){
            //     continue
            // }
            try{
                console.log(i)
                eval("(function(){" + i + "();})()");
            }
            catch (e){
                // console.log(e)
            }
        }
    }, 10)
}); 

用 PhantomJS 加载任意页面,然后遍历 window 对象。首先出现阻塞卡顿的函数是 showModalDialog,再次运行脚本,跳过 showModalDialog 函数,然后….

顺畅的运行完成,说好的 alert/prompt/confirm 函数导致的阻塞呢?

复制脚本到浏览器中运行,倒是成功复现了 alert/prompt/confirm/print 导致的阻塞:

分析原因,应该是PhantomJS在封装onAlert、onPrompt、onConfirm接口的时候就对这几个可能产生阻塞的函数做了处理。

同样的原理,可以套用在其他的动态解析器上。举个栗子,在Chrome Headless里需要 Hook 哪些接口,你现在知道了吗?

0x05 总结

虽然在 Chrome Headless 出来之后,PhantomJS 变得索然无味。但是同样都基于Webkit内核,所遇到的问题和解决方案也大多相通,不必拘泥于形式。

如果有动态分析和爬虫方面的问题/想法,欢迎微博私信我 @吃瓜群众-Fr1day