爬虫实战:从数据到产品
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.4 爬虫和反爬虫的斗争

1.常见的方法

在抓取对方网站、APP应用的相关数据时,经常会遇到一系列的方法阻止爬虫。一方面是为了保证服务的质量,另一方面是保护数据不被获取。常见的一些反爬虫和反反爬虫的手段如下。

(1)IP限制

IP限制是很常见的一种反爬虫的方式。服务端在一定时间内统计IP地址的访问次数,当次数、频率达到一定阈值时返回错误码或者拒绝服务。这种方式比较直接简单,但在IPv4资源越来越不足的情况下,很多用户共享一个IP出口,典型的如“长城宽带”等共享型的 ISP。另外手机网络中的 IP 地址也是会经常变化的,如果对这些IP地址进行阻断,则会将大量的正常用户阻止在外。

对于大多数不需要登录就可以进行访问的网站,通常也只能使用IP地址进行限制。比如“Freelancer网站”,大量的公开数据可以被访问,但同一个IP地址的访问是有一定的限制的。针对IP地址限制非常有效的方式是,使用大量的“高匿名”代理资源。这些代理资源可以对源IP地址进行隐藏,从而让对方服务器看起来是多个IP地址进行访问。另一种限制方式是,根据业务需要,对国内、国外的IP地址进行单独处理,进而对国外的高匿名代理进行阻断,例如使用海外的IP地址访问“天眼查网站”则无法访问。

(2)验证码

验证码是一种非常常见的反爬虫方式。服务提供方在IP地址访问次数达到一定数量后,可以返回验证码让用户进行验证。这种限制在不需要登录的网页界面比较常见,它需要结合用户的 cookie 或者生成一个特殊标识对用户进行唯一性判断,以防止同一个IP地址访问频率过高。验证码的存在形式非常多,有简单的数字验证码、字母数字验证码、字符图形验证码,网站也可以用极验验证码等基于用户行为的验证码。针对简单验证码,可以使用打码平台进行破解。这种平台通过脚本上传验证的图片,由打码公司雇用的人工进行识别。针对极验验证等更复杂的验证码,可以尝试模拟用户的行为绕过去,但通常比较烦琐,难度较大。谷歌所用的验证码更为复杂,通常是用户端结合云端进行手工打码,但会带来整体成本较高的问题。

要想绕过这些验证码的限制,一种思路是在出现验证码之前放弃访问,更换 IP地址。ADSL 拨号代理提供了这种可能性。ADSL 通过拨号的方式上网,需要输入ADSL账号和密码,每次拨号就更换一个IP地址。不同地域的IP地址分布在多个地址段,如果 IP 地址都能使用,则意味着 IP 地址量级可达千万。如果我们将 ADSL主机作为代理,每隔一段时间主机拨号一次(换一个 IP),这样可以有效防止 IP 地址被封禁。这种情况下,IP 地址的有效时限通常很短,通常在1分钟以下。结合大量的 ADSL 拨号代理可以达到并行获取大量数据的可能。如果网站使用了一些特殊的唯一性的标识,则很容易被对方网站识别到,从而改进反爬虫策略,面对这种情况,单独切换IP地址也会无效。遇到这种情况,必须要搞清楚标识的生成方式,进而模拟真实用户的访问。

(3)登录限制

登录限制是一种更加有效的保护数据的方式。网站或者APP可以展示一些基础的数据,当需要访问比较重要或者更多的数据时则要求用户必须登录。例如,在天眼查网站中,如果想要查看更多的信息,则必须用账号登录;“知乎”则是必须在登录后才能看到更多的信息。登录后,结合用户的唯一标识,可以进行计数,当访问频度、数量达到一定阈值后即可判断为爬虫行为,从而进行拦截。针对“登录限制”的方法,可以使用大量的账号进行登录,但成本通常比较高。

针对微信小程序,可以使用wx.login()方法,这种方式不需要用户的介入,因而不伤害用户的体验。小程序调用后会获取用户的唯一标识,后端可以根据这个唯一标识进行反爬虫的判断。

(4)数据伪装

在网页上,我们可以监听流量,然后模拟用户的正常请求。mitmproxy等工具可以监听特定网址的访问(通常是 API 的地址),然后将需要的数据存储下来。基于Chrome Headless的工具也可以监听到流量并进行解析。在这种情况下,某些网站会对数据进行一些伪装来增加复杂度。例如,在某网站上展示的价格为945元,在DOM树中是以CSS进行了一些伪装。要想得到正确的数值,必须对CSS的规则进行一些计算才行,某网站上展示的价格如图1-1所示。

图1-1 某网站上展示的价格

该网站使用特殊的字体对数据进行了伪装。例如,3400,对应显示的是 1400,如图1-2所示。如果能够找到所有的字体对应的关系,则可以逆向出正确的价格。

某电影网站使用特殊的字符进行数据隐藏,这种不可见的字符会增加复杂度,但还是可以通过对应的 UTF-8字符集找到对应关系,从而得到正确的值,如图1-3所示。

图1-2 3400显示为1400

图1-3 网站用特殊字符进行伪装

对于这种伪装,可以人工分析目标网站的前端代码,对CSS、JavaScript和字符进行分析,推导出计算公式。在这种情况下,使用爬虫必须要非常小心,因为很可能目标网站进行改版后,规则已经发生了变化,抓取到的数据便会无效。在爬虫程序的维护上可以增加一些数据有效性的检查,通过自动化或者人工的方式进行检查。例如,针对机票数据可以检查价格是否在一个合理的区间范围内,如果超出,则认为规则已经变化。更为复杂的方案是可以借助OCR技术,对所需要的区域进行识别,然后对比抓取到的结果。

(5)参数签名

设计良好的API通常都要对参数使用签名(sign)来驱避非法请求,常见于手机APP。APP通过加密算法对请求的参数进行运算,从而得到一个签名。这个签名通常和时间戳相关,并且在请求中附加上时间戳。在请求的参数固定的情况下,能够在一小段时间内生效。当请求发送到服务端后,服务端对参数、时间戳进行验证,比较签名是否一致。如果不一致,则判断为非法请求。这样做的好处是,可以保护请求,即便是被抓包,在很短时间内这个请求就会失效。获取APP端的加密算法一般较为困难,通常需要进行反编译才能获得加密算法。然而现阶段绝大多数APP已经被加壳(典型的如360加固、爱加密等),要进行反编译都很困难。另一种保护措施是,将加密算法放到原生代码中进行编译,通常这些代码是C或C++代码。由于原生代码相对于Java代码更难进行逆向工程,所以这给反编译又带来了更多的麻烦。

针对这种参数签名的方法,没有太好的途径能够来解决,在逆向反编译无果的情况下,可以试着找寻有没有其他的入口,例如,HTML5、微信小程序等。如果它们请求了相同的 API,则很有可能在源代码中包含了加密算法。幸运的是,基于JavaScript开发的应用非常容易逆向分析,能够很快地获取加密算法,从而绕过APP的保护机制。如果这些方法都不奏效,则可以考虑模拟用户操作应用,通过抓包的方式采集到流量中的信息。但这种方式效率较低,如果要发出多个并发的请求,往往需要多个设备同时进行。

(6)隐藏验证

更复杂的反爬虫的方式之一是,隐藏验证。例如,在网站的防护上,通过JavaScript请求一些特殊的网址,可以得到一些特定的令牌(token),这样每次请求时即可生成不同的令牌。甚至有些网站会在不可见的图片加上一些特殊的请求参数,从而识别是否是真正的浏览器用户。这种情况下,想直接获取 API 进行请求通常行不通或者非常困难,只能通过Chrome Headless等工具模拟用户的行为,从而规避这种情况。

(7)阻止调试

在分析某旅游网站时发现,一旦打开浏览器的控制台界面,就会无限触发浏览器的debugger指令。深入研究代码发现,该网站在一个名为leonid-tq-jq-v3-min.js中给所有的构造函数都加上了 debugger 这个关键字,导致任何对象的生成都会触发调试器。这样做的目的是阻止意外的脚本或程序进行跟踪调试,从而保护代码。这种情况下,可以构建一个修改过的js文件,去掉debugger关键字,使用mitmproxy转发流量并拦截leonid-tq-jq-v3-min.js,将改后的js文件返回给浏览器,从而绕过这个限制,某旅游网调试界面如图1-4所示。

图1-4 某旅游网调试界面

2.代理服务器

代理服务器是爬虫工具的基本武器,既可以隐藏真实的访问来源,又可以绕过大部分网站都会有的IP地址的访问频度的限制。常见的代理有HTTP代理和HTTPS代理两种,根据匿名程度的不同,可以将代理级别分为以下5种。

(1)高匿名代理

高匿名代理会将数据包原封不动地转发,从服务端来看,就像是真的一个普通客户端在访问,而记录的IP地址是代理服务器的IP地址,可以对很好地隐藏访问源,所以这种代理为爬虫工具首选。

(2)普通匿名代理

普通匿名代理会在数据包上做一些改动,代理服务器通常会加入的 HTTP 头有HTTP_VIA和HTTP_X_FORWARDED_FOR两种。根据这些HTTP头,服务端可以发现这是一个代理服务器,并且可以追踪到客户端的真实IP地址。

(3)透明代理

透明代理不仅改动了数据包,还会告诉服务器客户端的真实IP地址,因此在抓取数据时应该避免使用这种代理服务器。

网上有一些免费代理列表网站会定期扫描互联网,从而获取一些代理服务器的信息,然后将这些信息公布出来。这些代理服务器的有效期可能比较短,也容易被滥用,质量通常较差,所以需要客户端自己筛选出可用的代理。

在代理的种类上,HTTP代理最多,HTTPS代理较少。在互联网倡导HTTPS的趋势下,单纯使用HTTP代理是无法访问HTTPS网址的。大部分往往网站会同时保留HTTPS和HTTP的访问,所以可以试着将HTTPS网址改为HTTP(协议),一个原则是,如果网站的HTTP可以用,则不要使用HTTPS。原因是HTTPS需要多次握手,速度比较慢,经过代理之后会显得更慢。HTTP则会快很多,而且代理服务器可选资源较多,HTTP代理列表如图1-5所示,HTTPS代理列表如图1-6所示。

图1-5 HTTP代理列表

图1-6 HTTPS代理列表

(4)洋葱代理

洋葱代理(The Onion Router,TOR)是用于访问匿名网络的软件,可以防止传输到互联网上的流量被其他人过滤、嗅探或分析。洋葱代理在国内无法使用,如果需要抓取国外的网站,可以在海外的服务器上搭建洋葱代理,通过它提供的 Socks5代理端口进行匿名访问。洋葱代理的IP地址可以进行受控的切换,从而得到不同的出口IP地址。但遗憾的是,洋葱代理要经过多层的加密和跳转,延迟时间很长,也不稳定,出口的IP地址也并不是随机地在全球出口选择,而是固定在一定的区间内,因而洋葱代理在多并发、高速的场合下并不适用。

(5)付费代理资源

如果能够做好代理的质量筛选,那么大部分场景下免费代理资源都是够用的。付费代理资源通常用在需要更为稳定的访问场合或者免费资源不够用的情况下。ADSL拨号代理可以提供大量的国内IP资源,还可以指定省份。ADSL拨号代理服务器可以每隔几秒钟就更换IP地址,所以服务器看到的是来自不同的IP地址的访问。由于使用该IP地址的时间不长,不大可能被服务器屏蔽,所以通常数据抓取质量比较稳定,能够持续使用。获得这些代理的方式有以下两种:

●代理列表。服务商会提供一个代理列表访问地址,每次可以提取一定数量的代理服务器。这些代理服务器通过 ADSL 代理获得,它们通常存活时间不长,根据服务商的不同,一般存活时间在两三分钟之内。客户端必须不断地刷新代理服务器列表以取得最新的代理列表数据。

●服务提供商会提供一个固定的访问地址和账号,通过访问这个固定的地址,可以得到不停更换的出口IP地址。代理商在服务期内会通过二次代理随机地将请求分发到不同的代理服务器上。这种方式对于客户端来说访问是透明的,适用于无法通过编程获得代理服务器列表的应用。

另外,ADSL 拨号代理也可以自行搭建,方法是购买具有 ADSL 拨号网络的服务器资源,使用脚本定时拨号,等待一段时间后挂断,从而获得不断变化的IP地址。

3.构建自己的代理池

网络上存在着大量的代理列表可以免费获取,虽然有效性通常少于10%,但基于庞大的数量(通常每日可获得上万个),也会有近千个代理可以用。在 GitHub 上有很多抓取这类代理的项目,但质量良莠不齐,很难满足需要。经过对比后,我选择了ProxyBroker这个项目。

ProxyBroker是一个开源项目,可以从多个源异步查找公共代理并同时检查它们的有效性。它比较小巧,代码不复杂且易于扩展,不依赖于Redis等第三方依赖,非常专注地做好了抓取代理这件事。

特点:

●从大约50个来源中找到7000多个代理工作。

●支持协议:HTTP/HTTPS,Socks4/5;还支持CONNECT方法的80端口和23端口(SMTP)。

●代理可以按照匿名级别、响应时间、国家和DNSBL中的状态进行过滤。

●支持充当代理服务器,将传入的请求分发到外部代理,使用自动代理轮换。●检查所有代理是否支持cookie和Referer(如需要,还检查POST请求)。

●自动删除重复的代理。

●异步获取。

ProxyBroker支持命令行操作,可以作为一个单独的工具使用。

(1)查询可用代理

使用下面的命令可查询到前10个美国的高匿名代理,并且支持HTTP和HTTPS。

(2)抓取列表并输出到文件中

使用下面的命令可查询前10个美国的高匿名代理到proxies.txt文件中,但是不执行代理种类和连通性的检查。

(3)作为代理服务器使用

ProxyBroker可以作为代理服务器使用。在这种模式下可以很方便地进行IP地址的自动切换,对应用程序透明,对于一些既有的应用程序来说,使用代理服务器来隐藏身份十分方便。

用法:在一个终端窗口中启动代理服务器。

在另一个终端窗口中,使用这个代理地址访问ifconfig.co,即可得到你的代理服务器地址,而不是你的IP地址。

当前网络的IP地址:

使用高匿名代理后的IP地址:

更多的命令及选项可以通过执行proxybroker--help获取。

(4)扩展

若命令行提供的功能并不符合我们的需求,可以对其核心进行扩展以满足我们的需求。下面这个例子来自ProxyBroker官方代码,目的是显示找到的代理的详细信息:

更多的例子可以参考ProxyBroker官方文档。

(5)构建自己的代理列表池

我们想构造一个代理池,它仅包含一系列不断刷新的高匿名代理,以方便客户端的使用。这个代理池仅仅提供代理服务器的地址,并不需要处理额外的事情,客户端拿到这些代理服务器地址后,需要对这个列表按照自己的需求进行处理。例如,对代理进行筛选,对代理服务器的有效性进行评估,对代理服务器进行质量排序,定时刷新代理列表,等等。

某些代理池软件设计得较为复杂,将代理的筛选、评价逻辑放到了代理池内部进行处理,暴露给客户端的好像是使用一个代理地址,虽然这在一定程度上简化了客户端的逻辑,但由于各个客户端对代理的使用不尽相同,因此往往限制了客户端以最佳的方式来使用代理列表。

当然,简化的代理池也存在一些优点和弊端:

●客户端可能有重复的逻辑,但这种逻辑可以通过代码共享、包共享等方式消除。

●有些客户端无法修改源代码,无法植入代理使用的逻辑。

在设计上,通过爬虫的方式获取的代理失效得都比较快,因此我们可以将ProxyBroker获取的代理服务器地址源源不断地放到Redis缓存中,以提供一个含有大量代理地址的列表。首先,对于每个代理,我们需要设置一天的有效期(或者更短),以便能够自动清除过期的代理。其次,我们需要提供一个简单的HTTP代理服务器,以便能够为应用程序提供一个代理服务器列表的访问入口。

通过ProxyBroker获取代理:

HTTP服务器展示代理列表:

在一个终端中运行python3 proxy-pool-gather.py后可以看到代理已经开始抓取工作。在另一个终端中运行 python3 proxy-http-server.py,访问 http://localhost:8000/proxy.json会返回代理列表,如图1-7所示。

图1-7 代理列表

这时就已经建立好一个代理池供爬虫工具使用。

(6)增加国内的代理网站

ProxyBroker提供的代理网站,大多数来自国外的代理列表;在国内,有些网站因被屏蔽而获取不到代理资源。针对这种情况,可以把ProxyBroker部署到国外的服务器上以便于寻找代理资源。

增加代理列表网站的解析相对比较容易,在providers.py文件中提供了所有的代理列表网站的解析方法。以快代理为例,增加它的解析非常方便,只需增加一个类,并且在PROVIDERS变量中注册这个类的实例即可:

添加了国内的代理后,再将代理服务器部署到国外的服务器上,一般能够获取大约一万条的代理资源信息。