在抓取对方网站、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 某旅游网调试界面
代理服务器
代理服务器是爬虫工具的基本武器,既可以隐藏真实的访问来源,又可以绕过 大部分网站都会有的 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 拨号代理也可以自行搭建,方法是购买具有 ADSL 拨号网络的服务器资源,使用脚本定时拨号,等待一段时间后挂断,从而获得不断变化的 IP 地址。
构建自己的代理池
网络上存在着大量的代理列表可以免费获取,虽然有效性通常少于 10%,但基
于庞大的数量(通常每日可获得上万个),也会有近千个代理可以用。在 GitHub 上 有很多抓取这类代理的项目,但质量良莠不齐,很难满足需要。经过对比后,我选 择了 ProxyBroker 这个项目。
ProxyBroker 是一个开源项目,可以从多个源异步查找公共代理并同时检查它们 的有效性。它比较小巧,代码不复杂且易于扩展,不依赖于 Redis 等第三方依赖,非 常专注地做好了抓取代理这件事。
特点:
(1)查询可用代理
使用下面的命令可查询到前10个美国的高匿名代理,并且支持HTTP和HTTPS。
$ proxybroker find --types HTTP HTTPS --lvl High --countries US –strict -l 10
(2)抓取列表并输出到文件中
使用下面的命令可查询前 10 个美国的高匿名代理到 proxies.txt 文件中,但是不 执行代理种类和连通性的检查。
$ proxybroker grab --countries US --limit 10 --outfile ./proxies.txt $ cat proxies.txt
(3)作为代理服务器使用
ProxyBroker 可以作为代理服务器使用。在这种模式下可以很方便地进行 IP 地址 的自动切换,对应用程序透明,对于一些既有的应用程序来说,使用代理服务器来 隐藏身份十分方便。
用法:在一个终端窗口中启动代理服务器。
$ proxybroker serve --host 127.0.0.1 --port 8888 --types HTTP HTTPS –lvl High Server started at http://127.0.0.1:8888
在另一个终端窗口中,使用这个代理地址访问 ifconfig.co,即可得到你的代理服 务器地址,而不是你的 IP 地址。
当前网络的 IP 地址:
$ curl ifconfig.co 202.56.38.130
使用高匿名代理后的 IP 地址:
$ curl -x http://localhost:8888 ifconfig.co 191.103.88.21
更多的命令及选项可以通过执行 proxybroker --help 获取。
(4)扩展
若命令行提供的功能并不符合我们的需求,可以对其核心进行扩展以满足我们 的需求。下面这个例子来自 ProxyBroker 官方代码,目的是显示找到的代理的详细信息:
import asyncio from proxybroker import Broker async def show(proxies): while True: proxy = await proxies.get() if proxy is None: break print('Found proxy: %s' % proxy) proxies = asyncio.Queue() broker = Broker(proxies) tasks = asyncio.gather( broker.find(types=['HTTP', 'HTTPS'], limit=10), show(proxies)) loop = asyncio.get_event_loop() loop.run_until_complete(tasks) $ python3 proxy-broker.py Found proxy:Found proxy: Found proxy: Found proxy: Found proxy: Found proxy: Found proxy: Found proxy: Found proxy: Found proxy:
更多的例子可以参考 ProxyBroker 官方文档。
(5)构建自己的代理列表池
我们想构造一个代理池,它仅包含一系列不断刷新的高匿名代理,以方便客户端的使用。这个代理池仅仅提供代理服务器的地址,并不需要处理额外的事情,客 户端拿到这些代理服务器地址后,需要对这个列表按照自己的需求进行处理。例如, 对代理进行筛选,对代理服务器的有效性进行评估,对代理服务器进行质量排序, 定时刷新代理列表,等等。
某些代理池软件设计得较为复杂,将代理的筛选、评价逻辑放到了代理池内部 进行处理,暴露给客户端的好像是使用一个代理地址,虽然这在一定程度上简化了 客户端的逻辑,但由于各个客户端对代理的使用不尽相同,因此往往限制了客户端 以佳的方式来使用代理列表。
当然,简化的代理池也存在一些优点和弊端:
在设计上,通过爬虫的方式获取的代理失效得都比较快,因此我们可以将 ProxyBroker 获取的代理服务器地址源源不断地放到 Redis 缓存中,以提供一个含有大量代理地址的列表。首先,对于每个代理,我们需要设置一天的有效期(或者更短),以便能够自动清除过期的代理。其次,我们需要提供一个简单的 HTTP 代理服务器,以便能够为应用程序提供一个代理服务器列表的访问入口。
通过 ProxyBroker 获取代理:
#Proxy-pool-gather.py import asyncio import datetime import logging from proxybroker import Broker import redis r = redis.Redis(host='localhost', encoding="UTF-8", decode_responses=True) expire_time_s = 60 * 60 * 24 #一天后过期 async def save(proxies): while True: proxy = await proxies.get() if proxy is None: break if "HTTP" not in proxy.types: continue if "High" == proxy.types["HTTP"]: print(proxy) row = '%s://%s:%d' % ("http", proxy.host, proxy.port) r.set(row, 0, ex=expire_time_s) while True: proxies = asyncio.Queue() broker = Broker(proxies, timeout=2, max_tries=2, grab_timeout=3600) tasks = asyncio.gather(broker.find(types=['HTTP', 'HTTPS']),save(proxies)) loop = asyncio.get_event_loop() loop.run_until_complete(tasks)
HTTP 服务器展示代理列表:
#Proxy-http-server.py from flask import Flask from flask_restful import Resource, Api import redis app = Flask(__name__) api = Api(app) r = redis.Redis(host='localhost', encoding="UTF-8", decode_responses=True) class Proxy(Resource): def get(self): return r.keys("*") api.add_resource(Proxy, '/proxy.json') if __name__ == '__main__': app.run(host="0.0.0.0", port=8000)
在一个终端中运行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 变量中注册这个类的实例即可:
class Kuaidaili(Provider): domain = "kuaidaili.com" async def _pipe(self): urls = ["http://www.kuaidaili.com/free/inha/%d" % n for n in range(1, 21)] urls += ["http://www.kuaidaili.com/free/outha/%d" % n for n in range(1, 21)] await self._find_on_pages(urls) PROVIDERS = [ ...... Kuaidaili(), ]
添加了国内的代理后,再将代理服务器部署到国外的服务器上,一般能够获取 大约一万条的代理资源信息。