abbshr / abbshr.github.io

人们往往接受流行,不是因为想要与众不同,而是因为害怕与众不同
http://digitalpie.cf
444 stars 44 forks source link

前端hack技术之——crawl攻防战 #27

Open abbshr opened 9 years ago

abbshr commented 9 years ago

爬虫常被看作是一个边缘hack技术。通过别人的前端页面获取数据似乎为人所不耻,但是不管人们怎么评价它,爬虫的作用就摆在那里,有用没用全取决于你的目的。

为什么在互联网这片浩瀚的海洋里,我仍然旧事重提?起因与我最近做的事有关。

如果说让你爬取一个网页,你会怎么做?

OK,你很可能会说这很简单:“拿一个HTTPClient请求一下那个URL不就行了”。

对于普通网页,可能很顺利的就拿下了,那么对于如下几种情况,该怎么办呢:

我把以上情景大致分为两类:

拒绝访问的一般就是在Web server上做了爬虫防范措施。当有客户端请求数据时,对其进行检测,如果判断为浏览器,则允许与自己通信,否则就可以用各种方法为难客户端,比如:close这个socket(切断连接),返回非浏览器客户端不懂的redirect 302或非法访问等等。

至于如何断定该客户端就是浏览器或一定不是浏览器,有好多方法,打开network流量监视器,可以看到每个请求的详细内容,服务器可以检查: RefererHostUser-Agent,因为这些是浏览器最具代表性的字段。

一般我们把这几个字段也一同提交过去大多数服务器就能被搞定了,但随着技术的进步,防范措施也在不断改进。

但对于最后一种情况,你可能会很头痛并且百思不得其解,这也是我在实际操作中遇到的问题。

我在爬取一个看似再正常不过的页面时,爬取工具竟然crash了!排除网络问题,我又详细的看了几遍浏览器中的请求头,以及是否有明显的redirect痕迹。结果很遗憾:network控制台仅显示了一个请求,响应码为200!: 2014-10-27 12 41 33

这个问题很蹊跷,先不管爬虫怎么崩的,把浏览器的所有请求头发过去试试再说。结果令我意外的是爬虫再次崩溃了!我再次检查了请求字段是否写错,然后排除了请求头的问题。

既然不是请求头的问题,那这个服务器究竟是靠什么判断客户端类型的呢?这个我当时真的没想明白。

这下几乎头绪全没了。我真想到了302 redirect,但控制台里完全没有这个请求啊,有的只是个200。

而后我又拿wget将这个页面下载了下来,打开一看,大吃一惊,竟然完完整整的下载下来了!

看wget的log,在请求的阶段,确实存在一个目标地址临时转移,也就是说确实有返回302这么一个过程。继续看,发现location竟然是自己。

到这里我终于明白爬虫为什么crash了,原来这货在往死循环里跳!

但问题仍为解决,如果说真的是redirect loop,那浏览器也应该请求失败才对啊,况且我把请求头都加进去了,按理来说应该伪装成了浏览器。但却失败了。

我想了好久,302 location为自己这肯定没错了,问题一定出在请求信息上。既然浏览器没进入死循环,那么说明一定在某个条件下停止了,并且绝对不是在第一次请求时就得到页面!在这一系列请求中,好多字段都是不变量,能让条件改变的,应该只有双方交互用的cookies了。但之前我已经说过,把所有字段都添加进去了,当然也包括cookies,为什么就失败了?因为cookie在这个循环中是变量!而服务器是在至少第二次循环请求中通过检测上一次为客户端设置好的cookies来判断客户端类型的:只有正确携带上一次设置的cookie的客户端才是真正的浏览器!我突然觉得这个想法非常靠谱,因为也只有这样了。

为了验证我的想法,我手写了一个简单的爬虫,首次请求仅传入了一个URL,然后进入递归函数,并附带浏览器请求头信息,同时把上一次服务器响应的cookies一并写入请求头,再去发起下一次请求。

这次跑爬虫时,果然不出我所料,网页的内容正确显示出来了,并且是经过了两次请求。

兴奋过后,反过来想想,这个防范构思的确巧妙,巧妙利用了redirect loop和cookie变量的特点,覆盖了流量检测工具对请求的识别(redirect loop导致仅显示了最后一个成功的请求,让人以为这仅仅是一次普通的请求,并不存在跳转),并借助“询问”的方式,检查对方能否携带正确的cookie(也就是能否明白上次给你的东西),使爬虫左右为难。

所以,解决这类问题的关键就是:无论何时,不管是否有redirect,请求头中都要附带标志性字段和变量cookie。

接下来谈谈允许访问时的爬取失败情况。

这类例子很常见。尤其是在动态加载的网页里,大部分内容是由script标签和Ajax技术异步加载,这样我们直接抓取网页必然会导致失败。通常的对策是,先分析这个页面的所有流量去向,把异步加载的那些URL全提取出来,然后再遍历这些URL做循环请求,这样反倒方便了不少,毕竟这些URL是以JSON API形式返回的,格式已经是很清晰的了,根本不用HTML解析器做麻烦的节点分析了。

但这仅仅是异步加载中的冰山一角,可能在你分析的第一步就进行不下去了:流量监测里找不到异步请求的API。

没用Ajax,也没静态加载,看起来很玄乎哈?但仔细看流量分析表里的script,他们都是异步请求的,看来数据都应该与他们有关,并且既然没有后续的数据请求了,那就说明我们所需的数据全部包含在那些异步加载的script里。那么该如何获取这些数据呢?

这时就需要一双犀利的眸子了。。。可能是在脚本的一个变量里存储着,这是我们最期望的。

对于脚本中的字符串,怎么高效处理?用match正则匹配?NONONO,那样太麻烦,eval在这里才派上大用场了,把这些脚本字符串统统传给eval

这里顺便说个题外话,关于eval,大家虽然知道他强大,却也惧其三分,原因在于eval载全局环境下执行代码导致结果是不可预测的,这个例子中就完美的证实了这一点,不过细节我就不说了。

回到正题,假设我们并没有在某些显示变量中发现要找的数据,而是藏在函数参数中或函数作用域内。

仍然采用高效的办法:把字符串中的所有函数,对象在外部重新声明一下,然后在数据出现的那个函数体里手动构造全局变量,指向参数或者局部变量,等eval调用完毕,全局变量里存放的就是抓取的数据了。期间涉及到的技术内幕之一是可能需要对字符串中的重叠变量进行替换,replace是个不错的选择。

下面该处理爬虫返回的数据了,对于JSON数据或者JSONP返回的数据很好办,我们主要是解析“爬”下来的东西,也就是一大堆混乱的HTML代码,各类编程语言都有HTML/XML节点解析库,比如说JavaScript中的cheerio,有了这样的工具干起活来也相对容易。

以上就是这两天的技术总结。关于爬虫还有好多话题待挖掘,这篇工作日志里就不详谈了。

pengpengzx commented 6 years ago

强强强