| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
MickiBrinkm
8年前发布

浅谈动态爬虫与去重

   <p style="text-align:center"><strong><img src="https://simg.open-open.com/show/2ca86a17f6fa0fa63b8646a282ce217c.png"> </strong></p>    <p><strong>0x01 简介</strong></p>    <p>随着Web 2.0的发展,页面中的AJAX也越来越多。由于传统爬虫依靠静态分析,不能准确的抓取到页面中的AJAX请求以及动态更新的内容,已经越来越不能满足需求。基于动态解析的Web 2.0爬虫应运而生,通过浏览器内核解析页面源码,模拟用户操作,能有效解决上述问题。本文将详细分析利用PhantomJS + Python 编写爬虫并进行去重的思路。</p>    <p><strong>0x02 PhantomJS</strong></p>    <p>PhantomJS 是无界面的 Webkit 解析器,提供了 JavaScript API 。由于去除了可视化界面,速度比一般 Webkit 浏览器要快很多。同时提供了很多监控和事件接口,可以方便的操作 DOM 节点,模拟用户操作等。</p>    <p>接下来我们通过一个简单的例子,来展示下动态爬虫和传统爬虫的区别。目标:加载一个页面,并且获取其中所有的 <strong><a></strong> 标签。</p>    <pre>  <code class="language-python">// example.js  var page = require('webpage').create();  page.onAlert = function (message) {      console.log(message);      return true;  };  page.onCallback = function() {      page.evaluate(function(){          atags = document.getElementsByTagName("a");          for(var i=0;i<atags.length;i++){              if (atags[i].getAttribute("href")){                  alert(atags[i].getAttribute("href"));              }          }      })      phantom.exit()  };  page.open("http://named.cn/.mine", "get", "", function (status) {      page.evaluateAsync(function(){          if (typeof window.callPhantom === 'function') {              window.callPhantom();          }      }, 10000)  });</code></pre>    <p>抓取结果如下:</p>    <pre>  <code class="language-python">/.mine  /cmd2/controls/signin  /cmd2/controls/getcode  /download.html  /.blog  /.mine  /.at  /~发现推荐.findbbs  /help.html  /江南水乡.bbs/7313348  /摄情男女.bbs/7313242  /欢乐一家亲.bbs/7313356  /深夜食堂.bbs/7313168  /家有熊孩子.bbs/7313321  /乐淘亲子营.bbs/7313320  .../*省略*/...  /婚礼记.bbs/7277165  /不知道的事情.bbs/7277164  /不知道的事情.bbs/7277162  /婚礼记.bbs/7277160  /不知道的事情.bbs/7277016  http://www.miitbeian.gov.cn/  /cmd2/controls/mailpost/内容举报  download.html</code></pre>    <p>静态抓取的代码如下:</p>    <pre>  <code class="language-python">import requests  import pyquery  res = requests.get("http://named.cn/.mine")  count = 0  pq = pyquery.PyQuery(res.content)  for i in pq("a"):      print "[%d]: %s" % (count, pq(i).attr("href"))</code></pre>    <p>抓取结果为空。</p>    <p>从上述的例子中,我们可以明显看出动态分析比静态分析抓取到了更多的结果。产生差别的原因,是页面中的数据加载来自于AJAX请求,所有的 <strong><a></strong> 标签都是动态更新到页面中的。静态分析对这种情况无能为力,而基于浏览器内核的动态分析,可以轻松的处理这些情况。</p>    <p>但也可以明显看出动态分析的缺点:系统资源占用较多,且占用时间较长,还会有一些莫名其妙的坑,编写起来也更复杂更花时间(需要对前端编程有一定的了解)。</p>    <p>当然除了 PhantomJS 还有一些其他的动态解析器,比如同样基于 Webkit 内核的 PyQt(PhantomJS的最新版本也是基于pyqt来实现)、基于 PhantomJS 封装的 CasperJS、基于的 Firefox Gecko 内核的SlimerJS等。由于并没有一个统一的标准,各个动态解析器的API实现程度也参差不齐,也会有各种各样的坑,并没有一个 “最佳” 的解决方案。</p>    <p><strong>0x03 触发事件及页面监听</strong></p>    <p>上面的例子,介绍了爬虫中常见的一个场景:在页面加载完成后,通过AJAX加载数据。但现实中的场景,往往会更复杂,需要与用户进行交互后才会触发,比如在点击了某个按钮后跳转到某个页面、滚动到页面尾部后加载下一页的数据等。我们需要新的解决方案,去模拟正常用户的操作。那么,应该如何将用户交互抽象为代码?</p>    <p>用户操作的本质,实际上是触发了绑定在DOM节点的事件。所以模拟用户操作的问题,可以简化为触发节点事件。事件执行的结果也是多种多样的,但对于爬虫来说,我们需要关注的结果只有两种:1. 是否添加了新的节点( <strong><a></strong> 、 <strong><iframe></strong> 等等) 2. 是否发起了新的请求(包括AJAX请求、跳转等)。简化后,我们需要解决的问题有:</p>    <p>1. 如何获取绑定事件?</p>    <p>2. 如何触发事件?</p>    <p>3. 如何获取事件触发的结果?</p>    <p>最后我们的解决方案如下:</p>    <p>1. 如何获取绑定事件?JavaScript中绑定事件,都会调用 <strong>addEventListener</strong> 函数。在页面里的代码执行前( <a href="/misc/goto?guid=4959734130782565039" rel="nofollow,noindex"> onInitialized | PhantomJS </a> ),hook addEventListener函数,就可以捕获到哪些DOM节点绑定了事件。</p>    <pre>  <code class="language-python">_addEventListener = Element.prototype.addEventListener;  Element.prototype.addEventListener = function(a,b,c) {      EVENT_LIST.push({"event": event, "element": this})      _addEventListener.apply(this, arguments);  };</code></pre>    <p>2. 如何触发事件?JavaScript中提供了 <strong>dispatchEvent</strong> 函数,可以触发指定DOM节点的指定事件,也就是上一个问题中,我们收集的 <strong>EVENT_LIST</strong> 。</p>    <pre>  <code class="language-python">for(var i in EVENT_LIST){      var evt = document.createEvent('CustomEvent');      evt.initCustomEvent(EVENT_LIST[i]["event"], true, true, null);      EVENT_LIST[i]["element"].dispatchEvent(evt);  }</code></pre>    <p>除了通过addEventListener绑定事件,还有一些inline-script,是无法通过hook addEventListener来获取的。比如:</p>    <pre>  <code class="language-python"><div id="test" onclick="alert('hello')"></div></code></pre>    <p>解决方法是遍历节点,执行所有的onxxxx属性的值。</p>    <pre>  <code class="language-python">function trigger_inline(){      var nodes = document.all;      for (var i = 0; i < nodes.length; i++) {          var attrs = nodes[i].attributes;          for (var j = 0; j < attrs.length; j++) {              attr_name = attrs[j].nodeName;              attr_value = attrs[j].nodeValue;              if (attr_name.substr(0, 2) == "on") {                  console.log(attrs[j].nodeName + ' : ' + attr_value);                  eval(attr_value);              }              if (attr_name in {"src": 1, "href": 1} && attrs[j].nodeValue.substr(0, 11) == "javascript:") {                  console.log(attrs[j].nodeName + ' : ' + attr_value);                  eval(attr_value.substr(11));              }          }      }  }</code></pre>    <p>3. 如何获取事件触发的结果?HTML5中的 <strong>MutationObserver</strong> 方法,可以检查页面中的DOM是否发生变化。但是PhantomJS并不支持(摊手 Support for Mutation Observers ),解决方案是监听了 <strong>DOMNodeInserted</strong> 事件。AJAX请求的捕获,解决方案有两种: onResourceRequested 可以捕获非主框架的请求,但需要通过正则表达式匹配筛选出有效请求;hook <strong>XMLHttpRequest.open</strong> 和 <strong>XMLHttpRequest.send</strong> 可以准确的捕获请求内容。</p>    <pre>  <code class="language-python">document.addEventListener('DOMNodeInserted', function(e) {      var node = e.target;      if(node.src || node.href){          LINKS_RESULT.push(node.src || node.href);      }  }, true);  _open = XMLHttpRequest.prototype.open  XMLHttpRequest.prototype.open = function (method, url) {      if (!this._url) {          this._url = url;          this._method = method;      }      _open.apply(this, arguments);  };  _send = XMLHttpRequest.prototype.send  XMLHttpRequest.prototype.send = function (data) {      window.$Result$.add_ajax(this._url, this._method, data);      _send.apply(this, arguments);  };</code></pre>    <p>整理一下,在页面加载前,需要hook三个接口: <strong>addEventListener</strong> 、 <strong>XMLHttpRequest.open</strong> 、 <strong>XMLHttpRequest.send</strong> 。页面加载完之后,需要获取所有的 <strong><a></strong> 、 <strong><iframe></strong> 、 <strong><form></strong> 标签,开启页面DOM节点监听,并且触发所有的事件。最后输出结果。</p>    <p>在实现了动态爬取的基本功能后,还有一些可以提升爬虫的稳定性和效率的小tips:自动填写表单(应对某些情况下参数为空导致表单无法提交)、禁止非必要资源的加载(jpg、png、css、mp4等)、页面加载完成后禁止跳转(防止因为触发事件导致的跳转)、hook会导致页面阻塞的函数(alert、prompt)、触发事件向下冒泡(解决一些不标准的前端代码绑定的DOM节点太宽泛导致的问题,但实测非常影响效率)等。</p>    <p><strong>0x04 去重</strong></p>    <p>去重是爬虫中最核心,也是最影响爬虫效率和结果的部分。去重过于粗放,在遇到页面比较多的网站时爬不完。过于严格的话,爬取的结果太少,也会影响后续扫描的效果。</p>    <p>去重一般分为两个部分:任务队列的去重、结果队列的去重。这两种去重的区别在于,在爬虫运行过程中,任务队列是一直变动的(增加 & 减少),而结果队列是不断的增加的。对任务队列的去重,要在扫描过程中重复的进行的,即某个页面爬取完成后,获取的结果加入任务队列进行下一次爬虫任务之前,需要做一次去重(或者每完成一个深度的爬虫任务,进行一次去重),而结果队列是在所有的任务结束后进行去重,不影响爬虫运行效率,只影响最后的结果输出。这两种去重可以使用相同的去重策略,也可以使用不同的策略(对任务队列的去重,可以根据当前的任务量,进行去重力度的调整)。</p>    <p>我们将爬虫的功能和需求程度逐一列出来: </p>    <p>1. 基础: 非抓取目标站点的URL </p>    <p>2. 基础: 完全重复的URL & 参数打乱但实际上仍然重复的URL </p>    <p>3. 温饱: 分析参数,去除遍历型的,exp: <strong>page.php?id=1</strong> 、 <strong>page.php?id=2</strong> 等 </p>    <p>4. 温饱: 支持伪静态URL去重 </p>    <p>5. 小康: 奇形怪状URL的去重,exp: <strong>test.php?a=1?b=2?from=233</strong> 、 <strong>test.php?a=1?b=3?from=test </strong></p>    <p>6. 小康: 根据当前的任务量,动态调整去重力度</p>    <p>前两个基础需求实现起来比较简单,将域名、参数列表提取出来进行对比就可以了,一次循环解决问题。</p>    <p>第三个需求,需要匹配参数值,比如: int、hash、中文、URL编码等。需要注意的是,不可以直接用匹配的方式处理英文的参数值。如:</p>    <pre>  <code class="language-python">http://test.com/index.php?m=home&c=test&a=view  http://test.com/index.php?m=home&c=test&a=add</code></pre>    <p>其中m、c、a参数分别代表了不同的module、controller、action,属于“ 功能型参数 ”,需要保留。功能性参数的值在大多数情况下是字母(有意义的单词),有些情况下也会是数字或者数字字母的混合。那么,应该如何做策略?</p>    <p>这个问题目前的解决方案也比较粗暴,对全部是字母的参数值,不做处理,对数字字母混合的参数值,根据任务量的多少进行“ 弹力去重 ”(详见需求6)。举个实际的例子:</p>    <pre>  <code class="language-python"># 去重处理前:  http://test.com/index.php?m=home&c=test&id=3  http://test.com/index.php?m=home&c=test&type=friday  http://test.com/index.php?m=home&c=test&type=464730bbd7fb2016c880ffd597f2808f  http://test.com/index.php?m=home&c=test&type=b59c67bf196a4758191e42f76670ceba  # 处理过程:  {"m": "home", "c": "test", "id":"{{int}}"}  {"m": "home", "c": "test", "id":"{{int}}"}  {"m": "home", "c": "test", "type":"friday"}  {"m": "home", "c": "test", "type":"{{hash}}"}  {"m": "home", "c": "test", "type":"{{hash}}"}  # 去重结果:  http://test.com/index.php?m=home&c=test&id=2  http://test.com/index.php?m=home&c=test&type=friday  http://test.com/index.php?m=home&c=test&type=464730bbd7fb2016c880ffd597f2808f</code></pre>    <p>第四个需求,支持伪静态去重。首先要定义对路径去重的策略,我们把路径用/分隔开,扔到处理参数值的函数中去处理(符合规则的替换为指定字符串、不符合规则的原样返回),然后再用替换过的URL做去重处理就可以了。当然还有一些伪静态长这样:</p>    <pre>  <code class="language-python">htttp://xxx.com/?index_1_test_233  htttp://xxx.com/?index_1_new_456</code></pre>    <p>再按照上述的去重策略就过于粗略,应该怎么处理呢?继续往下看。</p>    <p>第五个需求,奇形怪状的URL。目前已有的去重策略都是通过分析替换参数值、路径名来实现的,但是这种奇奇怪怪的URL根本不按套路出牌,只能采用非常的方法:在参数、路径进行拆分处理前,替换掉一些干扰字符。举个实例:</p>    <pre>  <code class="language-python"># 处理前  http://test.com/test.php?id=12?from=te?user=233  http://test.com/test.php?id=12?from=te?user=233_abc    # 替换后  http://test.com/test.php?id={{int}}?from=te?user={{int}}  http://test.com/test.php?id={{int}}?from=te?user={{mix_str}}</code></pre>    <p>第六个需求,根据当前的任务量,自动调整去重策略。在有些情况下,上述的各种去重套路都不好用,比如:</p>    <pre>  <code class="language-python">http://test.com/user.php?name=test  http://test.com/user.php?name=今天是阴天  http://test.com/user.php?name=bbbbb  ...</code></pre>    <p>当用户名为自定义,且有成千上万个用户的时候,上述的去重策略就都失效了。问题出在哪里?</p>    <p>需求三的解决方案似乎过于粗略了,直接把所有的纯英文字符串放过了,但是也没有更好的解决方案。只能针对这种特殊情况,再加一次循环,先找到出现次数过多的参数,再针对这些特定的参数进行强制去重。新的策略是这样的:第一次循环只进行预处理,分析当前的参数列表,并计数。第二遍,根据参数列表的计数值判断当前参数是否需要强制去重。举个实例:</p>    <pre>  <code class="language-python">http://test.com/index.php?name=friday&m=read  http://test.com/index.php?name=test&m=read  http://test.com/index.php?name=2333&m=read     # 第一轮遍历结果  {      md5(name+m):{count:3, "name":["friday","test","{{int}}"], "m": ["read"]},  }</code></pre>    <p>当参数列表相同的URL数量大于某个特定值,且某个参数的值的个数大于某个特定值的时候,强制对该参数进行去重,即将全英文字符串替换为 <strong>{{str}}</strong> 。</p>    <p>上述方法实现起来稍微有点儿绕,还有个粗暴点儿的解决方案:不去检测具体参数,只判断当前任务队列里的任务数是否超过某个值,一旦超过,就启动强制去重(只要参数列表或根路径相同,直接去掉,可能会误杀很多伪静态)。</p>    <p>在实现了上述的六个需求后,一个简洁有效的去重脚本就完成了,流程图如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7fefcd6ce48a0e4631ed0818fec3d542.png"></p>    <p><strong>0x05 对比</strong></p>    <p>为了测试动态爬虫(以下简称KSpider)的基本功能和效率,选取了同样是基于动态分析的WVS扫描器的爬虫(以下简称WVSSpider)来对比。</p>    <p>首先测试基本抓取功能。 AISec漏洞扫描器测试平台 提供了几个demo,爬取结果如下:</p>    <pre>  <code class="language-python"># 注: WVSSpider无法设置爬虫深度及线程数,针对path相同的url会进行聚合处理,生成SiteFile。  WVSSpider # wvs_console.exe /Crawl http://demo.aisec.cn/demo/aisec/ /SaveLogs /SaveFolder C:\Users\xxx\Desktop /ExportXML   Request Count: 31, SiteFile Count: 11, Time Count: 23  KSpider # python crawler.py http://demo.aisec.cn/demo/aisec/ {"depth": 5, "thread_count": 5}   Request Count: 23, Result Count: 18, Time Cost: 33  KSpider Basic # python crawler.py http://demo.aisec.cn/demo/aisec/ {"depth": 5, "thread_count": 5, "type": "basic"}   Request Count: 11,  Result Count: 8, Time Cost: 1</code></pre>    <p>前两个扫描都抓取到了5个关键的请求,包括:</p>    <pre>  <code class="language-python">基础<a>标签: http://demo.aisec.cn/demo/aisec/html_link.php?id=2  JS自动解析: http://demo.aisec.cn/demo/aisec/js_link.php?id=2&msg=abc  JS自动解析 + FORM表单: http://demo.aisec.cn/demo/aisec/post_link.php  JS自动解析 + AJAX请求: http://demo.aisec.cn/demo/aisec/ajax_link.php?id=1&t=0.04278885293751955  事件触发 + DOM改变: http://demo.aisec.cn/demo/aisec/click_link.php?id=2</code></pre>    <p>静态分析的扫描速度很快,但只扫出了上述5个请求中的第一个。通过表单分析抓取到了第三个POST请求,但是由于 <strong><form></strong> 表单中的 <strong><input></strong> 标签是由JavaScript动态生成(代码如下),所以没有抓取到请求的具体参数。</p>    <pre>  <code class="language-python"><form method="post" name="form1" enctype="multipart/form-data" action="post_link.php">  <script>  document.write('<input type="text" name="i'+'d" size="30" value=1><br>');  document.write('<input type="text" name="m'+'sg" size="30" value="abc">');  </script>  <input type="submit" value="提交" name="B1">  </form></code></pre>    <p>接下来是爬虫的效率测试,抓取目标是 百度贴吧 。结果如下:</p>    <pre>  <code class="language-python">WVSSpider # wvs_console.exe /Crawl https://tieba.baidu.com /SaveLogs /SaveFolder C:\Users\xxx\Desktop /ExportXML   Request Count: 201, SiteFile Count: 101, Time Count: 220  KSpider # python crawler.py https://tieba.baidu.com {"depth": 5, "thread_count": 10}   Request Count: 410, Result_length: 535, Time_Cost: 339</code></pre>    <p>可以看到,随着网站复杂度的上升,WVS爬虫的请求数增长相对平稳,而KSpider在线程数为10的情况下,在6分钟内也完成了爬取任务,表现正常。</p>    <p>在分析过程中,虽然 WVSSpider 速度很快,整体效率非常高,但也有一些缺点:爬取深度无法指定、无法跨平台工作、对于伪静态形式的URL去重效果较差(如下图所示的SiteFile共有43个,占比42%)、爬虫结果中有部分URL分割结果(如: <strong>https://tieba.baidu.com/home/main?un=111</strong> 会分割成两个SiteFile, <strong>/home</strong> 和 <strong>/home/main</strong> ,所以实际扫描到的URL数量比结果要少)等。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/458340200e65404c4c4f80617478a9b7.png"></p>    <p>由于目标网站URL较多,覆盖率比较难测算,我们用脚本简单对比了 WVSSpider 和 KSpider 抓取的结果,除去静态资源,KSpider 覆盖了98%的 WVSSpider 抓取结果(即 WVSSpider 抓取结果里,有98%的结果同样被 KSpider 抓到),而 WVSSpider 仅覆盖了38%的 KSpider 抓取结果。</p>    <p>0x06 总结</p>    <p>除了以上提到的去重和动态解析,还有一些小tips,如fuzz常见路径、从robots.txt中提取信息、爬取过程中进行敏感信息识别、生成网站信息画像等,对爬虫的覆盖率及后续的扫描任务会有帮助。</p>    <p>本文详细的介绍了在动态爬虫的实现过程中,可能会遇到的问题以及解决方案。优秀的代码不会一蹴而就,需要持续的优化调整,后期会考虑开源,欢迎沟通交流。</p>    <p>参考资料</p>    <p><a href="/misc/goto?guid=4959734130877188560" rel="nofollow,noindex">让人欢喜让我忧的phantomjs  </a></p>    <p><a href="/misc/goto?guid=4959734130960120676" rel="nofollow,noindex">盘点selenium phantomJS使用的坑  </a></p>    <p><a href="/misc/goto?guid=4959734131045855101" rel="nofollow,noindex">SuperSpider——打造功能强大的爬虫利器  </a></p>    <p><a href="/misc/goto?guid=4959734131127156151" rel="nofollow,noindex">XSS dynamic detection using PhantomJs </a></p>    <p style="text-align:center"> </p>    <p> </p>    <p> </p>    <p>来自:http://bobao.360.cn/learning/detail/3391.html</p>    <p> </p>    
 本文由用户 MickiBrinkm 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1484051072289.html
PhantomJS Ajax 网络爬虫