如果有人问你Python爬虫抓取技术的门道请叫他来看这篇文章

28次阅读

共计 4997 个字符,预计需要花费 13 分钟才能阅读完成。

web 是一个开放的平台,这也奠定了 web 从 90 年代初诞生直至今日将近 30 年来蓬勃的发展。然而,正所谓成也萧何败也萧何,开放的特性、搜索引擎以及简单易学的 html、css 技术使得 web 成为了互联网领域里最为流行和成熟的信息传播媒介;但如今作为商业化软件,web 这个平台上的内容信息的版权却毫无保证,因为相比软件客户端而言,你的网页中的内容可以被很低成本、很低的技术门槛实现出的一些抓取程序获取到,这也就是这一系列文章将要探讨的话题—— 网络爬虫

有很多人认为 web 应当始终遵循开放的精神,呈现在页面中的信息应当毫无保留地分享给整个互联网。然而我认为,在 IT 行业发展至今天,web 已经不再是当年那个和 pdf 一争高下的所谓 “超文本”信息载体 了,它已经是以一种 轻量级客户端软件 的意识形态的存在了。而商业软件发展到今天,web 也不得不面对知识产权保护的问题,试想如果原创的高质量内容得不到保护,抄袭和盗版横行网络世界,这其实对 web 生态的良性发展是不利的,也很难鼓励更多的优质原创内容的生产。

未授权的爬虫抓取程序是危害 web 原创内容生态的一大元凶,因此要保护网站的内容,首先就要考虑如何反爬虫。

从爬虫的攻防角度来讲

最简单的爬虫,是几乎所有服务端、客户端编程语言都支持的 http 请求,只要向目标页面的 url 发起一个 http get 请求,即可获得到浏览器加载这个页面时的完整 html 文档,这被我们称之为“同步页”。

作为防守的一方,服务端可以根据 http 请求头中的 User-Agent 来检查客户端是否是一个合法的浏览器程序,亦或是一个脚本编写的抓取程序,从而决定是否将真实的页面信息内容下发给你。

这当然是最小儿科的防御手段,爬虫作为进攻的一方,完全可以伪造 User-Agent 字段,甚至,只要你愿意,http 的 get 方法里,request header 的 ReferrerCookie 等等所有字段爬虫都可以轻而易举的伪造。

此时服务端可以利用浏览器 http 头指纹,根据你声明的自己的浏览器厂商和版本(来自 User-Agent),来鉴别你的 http header 中的各个字段是否符合该浏览器的特征,如不符合则作为爬虫程序对待。这个技术有一个典型的应用,就是 PhantomJS 1.x 版本中,由于其底层调用了 Qt 框架的网络库,因此 http 头里有明显的 Qt 框架网络请求的特征,可以被服务端直接识别并拦截。

除此之外,还有一种更加变态的服务端爬虫检测机制,就是对所有访问页面的 http 请求,在 http response 中种下一个 cookie token,然后在这个页面内异步执行的一些 ajax 接口里去校验来访请求是否含有 cookie token,将 token 回传回来则表明这是一个合法的浏览器来访,否则说明刚刚被下发了那个 token 的用户访问了页面 html 却没有访问 html 内执行 js 后调用的 ajax 请求,很有可能是一个爬虫程序。

如果你不携带 token 直接访问一个接口,这也就意味着你没请求过 html 页面直接向本应由页面内 ajax 访问的接口发起了网络请求,这也显然证明了你是一个可疑的爬虫。知名电商网站 Amazon 就是采用的这种防御策略。

以上则是基于服务端校验爬虫程序,可以玩出的一些套路手段。

基于客户端 js 运行时的检测

现代浏览器赋予了 JavaScript 强大的能力,因此我们可以把页面的所有核心内容都做成 js 异步请求 ajax 获取数据后渲染在页面中的,这显然提高了爬虫抓取内容的门槛。依靠这种方式,我们把对抓取与反抓取的对抗战场从服务端转移到了客户端浏览器中的 js 运行时,接下来说一说结合客户端 js 运行时的爬虫抓取技术。

刚刚谈到的各种服务端校验,对于普通的 python、java 语言编写的 http 抓取程序而言,具有一定的技术门槛,毕竟一个 web 应用对于未授权抓取者而言是黑盒的,很多东西需要一点一点去尝试,而花费大量人力物力开发好的一套抓取程序,web 站作为防守一方只要轻易调整一些策略,攻击者就需要再次花费同等的时间去修改爬虫抓取逻辑。

此时就需要使用 headless browser 了,这是什么技术呢?其实说白了就是,让程序可以操作浏览器去访问网页,这样编写爬虫的人可以通过调用浏览器暴露出来给程序调用的 api 去实现复杂的抓取业务逻辑。

其实近年来这已经不算是什么新鲜的技术了,从前有基于 webkit 内核的 PhantomJS,基于 Firefox 浏览器内核的 SlimerJS,甚至基于 IE 内核的 trifleJS,有兴趣可以看看这里和这里 是两个 headless browser 的收集列表。

这些 headless browser 程序实现的原理其实是把开源的一些浏览器内核 C ++ 代码加以改造和封装,实现一个简易的无 GUI 界面渲染的 browser 程序。但这些项目普遍存在的问题是,由于他们的代码基于 fork 官方 webkit 等内核的某一个版本的主干代码,因此无法跟进一些最新的 css 属性和 js 语法,并且存在一些兼容性的问题,不如真正的 release 版 GUI 浏览器运行得稳定。

这其中最为成熟、使用率最高的应该当属 PhantonJS 了,对这种爬虫的识别我之前曾写过一篇博客,这里不再赘述。PhantomJS 存在诸多问题,因为是单进程模型,没有必要的沙箱保护,浏览器内核的安全性较差。另外,该项目作者已经声明停止维护此项目了。

如今 Google Chrome 团队在 Chrome 59 release 版本中开放了 headless mode api,并开源了一个基于 Node.js 调用的 headless chromium dirver 库,我也为这个库贡献了一个 centos 环境的部署依赖安装列表。

Headless Chrome 可谓是 Headless Browser 中独树一帜的大杀器,由于其自身就是一个 chrome 浏览器,因此支持各种新的 css 渲染特性和 js 运行时语法。

基于这样的手段,爬虫作为进攻的一方可以绕过几乎所有服务端校验逻辑,但是这些爬虫在客户端的 js 运行时中依然存在着一些破绽,诸如:

基于 plugin 对象的检查

if(navigator.plugins.length === 0) {console.log('It may be Chrome headless');
}

基于 language 的检查

if(navigator.languages === '') {console.log('Chrome headless detected');
}

基于 webgl 的检查

var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl');

var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
var vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
var renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);

if(vendor == 'Brian Paul' && renderer == 'Mesa OffScreen') {console.log('Chrome headless detected');
}

基于浏览器 hairline 特性的检查

if(!Modernizr['hairline']) {console.log('It may be Chrome headless');
}

基于错误 img src 属性生成的 img 对象的检查

var body = document.getElementsByTagName('body')[0];
var image = document.createElement('img');
image.src = 'http://iloveponeydotcom32188.jg';
image.setAttribute('id', 'fakeimage');
body.appendChild(image);
image.onerror = function(){if(image.width == 0 && image.height == 0) {console.log('Chrome headless detected');
    }
}

基于以上的一些浏览器特性的判断,基本可以通杀市面上大多数 Headless Browser 程序。在这一层面上,实际上是将网页抓取的门槛提高,要求编写爬虫程序的开发者不得不修改浏览器内核的 C ++ 代码,重新编译一个浏览器,并且,以上几点特征是对浏览器内核的改动其实并不小,如果你曾尝试过编译 Blink 内核或 Gecko 内核你会明白这对于一个“脚本小子”来说有多难~

更进一步,我们还可以基于浏览器的 UserAgent 字段描述的浏览器品牌、版本型号信息,对 js 运行时、DOM 和 BOM 的各个原生对象的属性及方法进行检验,观察其特征是否符合该版本的浏览器所应具备的特征。

这种方式被称为 浏览器指纹检查 技术,依托于大型 web 站对各型号浏览器 api 信息的收集。而作为编写爬虫程序的进攻一方,则可以在 Headless Browser 运行时里预注入一些 js 逻辑,伪造浏览器的特征。

另外,在研究浏览器端利用 js api 进行 Robots Browser Detect 时,我们发现了一个有趣的小技巧,你可以把一个预注入的 js 函数,伪装成一个Native Function,来看看下面代码:

var fakeAlert = (function(){}).bind(null);
console.log(window.alert.toString()); // function alert() { [native code] }
console.log(fakeAlert.toString()); // function () { [native code] }

爬虫进攻方可能会预注入一些 js 方法,把原生的一些 api 外面包装一层 proxy function 作为 hook,然后再用这个假的 js api 去覆盖原生 api。如果防御者在对此做检查判断时是基于把函数 toString 之后对 [native code] 的检查,那么就会被绕过。所以需要更严格的检查,因为 bind(null) 伪造的方法,在 toString 之后是不带函数名的,因此你需要在 toString 之后检查函数名是否为空。

这个技巧有什么用呢?这里延伸一下,反抓取的防御者有一种 Robot Detect 的办法是在 js 运行时主动抛出一个 alert,文案可以写一些与业务逻辑相关的,正常的用户点确定按钮时必定会有一个 1s 甚至更长的延时,由于浏览器里alert 会阻塞 js 代码运行(实际上在 v8 里他会把这个 isolate 上下文以类似进程挂起的方式暂停执行),所以爬虫程序作为攻击者可以选择以上面的技巧在页面所有 js 运行以前预注入一段 js 代码,把 alertpromptconfirm 等弹窗方法全部 hook 伪造。如果防御者在弹窗代码之前先检验下自己调用的 alert 方法还是不是原生的,这条路就被封死了。

反爬虫的银弹

目前的反抓取、机器人检查手段,最可靠的还是 验证码 技术。但验证码并不意味着一定要强迫用户输入一连串字母数字,也有很多基于用户鼠标、触屏(移动端)等行为的 行为验证 技术,这其中最为成熟的当属 Google reCAPTCHA,基于机器学习的方式对用户与爬虫进行区分。

基于以上诸多对用户与爬虫的识别区分技术,网站的防御方最终要做的是 封禁 ip 地址 或是对这个 ip 的来访用户施以高强度的验证码策略。这样一来,进攻方不得不购买 ip 代理池来抓取网站信息内容,否则单个 ip 地址很容易被封导致无法抓取。抓取与反抓取的门槛被提高到了 ip 代理池 经济费用的层面。

机器人协议

除此之外,在爬虫抓取技术领域还有一个“白道”的手段,叫做 robots 协议。AllowDisallow 声明了对各个 UA 爬虫的抓取授权。

不过,这只是一个君子协议,虽具有法律效益,但只能够限制那些商业搜索引擎的蜘蛛程序,你无法对那些“野爬爱好者”加以限制。

写在最后

对网页内容的抓取与反制,注定是一个魔高一尺道高一丈的猫鼠游戏,你永远不可能以某一种技术彻底封死爬虫程序的路,你能做的只是提高攻击者的抓取成本,并对于未授权的抓取行为做到较为精确的获悉。

正文完
 0