共计 4750 个字符,预计需要花费 12 分钟才能阅读完成。
之前在学校曾经用过 request+xpath 的方法做过一些爬虫脚本来玩,从 ios 正式转前端之后,出于兴趣,我对爬虫和反爬虫又做了一些了解,并且做了一些爬虫攻防的实践。
我们在爬取网站的时候,都会遵守 robots 协议,在爬取数据的过程中,尽量不对服务器造成压力。但并不是所有人都这样,网络上仍然会有大量的恶意爬虫。对于网络维护者来说,爬虫的肆意横行不仅给服务器造成极大的压力,还意味着自己的网站资料泄露,甚至是自己刻意隐藏在网站的隐私的内容也会泄露,这也就是反爬虫技术存在的意义。
下面开始我的攻防实践。
开始
先从最基本的 requests 开始。requests 是一常用的 http 请求库,它使用 python 语言编写,可以方便地发送 http 请求,以及方便地处理响应结果。这是一段抓取豆瓣电影内容的代码。
import requests
from lxml import etree
url = 'https://movie.douban.com/subject/1292052/'
data = requests.get(url).text
s=etree.HTML(data)
film=s.xpath('//*[@id="content"]/h1/span[1]/text()')
print(film)
代码的运行结果,会输出
[‘ 肖申克的救赎 The Shawshank Redemption’]
这就是最简单的完整的爬虫操作,通过代码发送网络请求,然后解析返回内容,分析页面元素,得到自己需要的东西。
这样的爬虫防起来也很容易。使用抓包工具看一下刚才发送的请求,再对比一下浏览器发送的正常请求。可以看到,两者的请求头差别非常大,尤其 requests 请求头中的 user-agent,赫然写着 python-requests。这就等于是告诉服务端,这条请求不是真人发的。服务端只需要对请求头进行一下判断,就可以防御这一种的爬虫。
当然 requests 也不是这么没用的,它也支持伪造请求头。以 user-agent 为例,对刚才的代码进行修改,就可以很容易地在请求头中加入你想要加的字段,伪装成真实的请求,干扰服务端的判断。
import requests
from lxml import etree
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = {'User-Agent' : user_agent}
url = 'https://movie.douban.com/subject/1292052/'
data = requests.get(url,headers=headers).text
s=etree.HTML(data)
film=s.xpath('//*[@id="content"]/h1/span[1]/text()')
print(film)
提高
现阶段,就网络请求的内容上来说,爬虫脚本已经和真人一样了,那么服务器就要从别的角度来进行防御。
有两个思路,第一个,分析爬虫脚本的行为模式来进行识别和防御。
爬虫脚本通常会很频繁的进行网络请求,比如要爬取豆瓣排行榜 top100 的电影,就会连续发送 100 个网络请求。针对这种行为模式,服务端就可以对访问的 IP 进行统计,如果单个 IP 短时间内访问超过设定的阈值,就给予封锁。这确实可以防御一批爬虫,但是也容易误伤正常用户,并且爬虫脚本也可以绕过去。
这时候的爬虫脚本要做的就是 ip 代理,每隔几次请求就切换一下 ip,防止请求次数超过服务端设的阈值。设置代理的代码也非常简单。
import requests
proxies = {"http" : "http://111.155.124.78:8123" # 代理 ip}
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = {'User-Agent' : user_agent}
url = 'https://movie.douban.com/subject/1292052/'
res = requests.get(url = http_url, headers = headers, proxies = proxies)
第二个思路,通过做一些只有真人能做的操作来识别爬虫脚本。最典型的就是以 12306 为代表的验证码操作。
增加验证码是一个既古老又相当有效果的方法,能够让很多爬虫望风而逃。当然这也不是万无一失的。经过多年的发展,用计算机视觉进行一些图像识别已经不是什么新鲜事,训练神经网络的门槛也越来越低,并且有许多开源的计算机视觉库可以免费使用。例如可以在 python 中引入的 tesseract,只要一行命令就能进行验证码的识别。
import pytesseract
from PIL import Image
...
#get identifying code img
...
im=Image.open('code.png')
result = pytesseract.image_to_string(im)
再专业一点的话,还可以加上一些图像预处理的操作,比如降噪和二值化,提高验证码的识别准确率。当然要是验证码原本的干扰线, 噪点都比较多,甚至还出现了人类肉眼都难以辨别的验证码(12306),计算机识别的准确度也会相应下降一些。但这种方法对于真实的人类用户来说实在是太不友好了,属于是杀敌一千自损八百的做法。
进阶
验证码的方法虽然防爬效果好,但是对于真人实在是不够友好,开发人员在优化验证操作的方面也下了很多工夫。如今,很多的人机验证操作已经不再需要输入验证码,有些只要一下点击就可以完成,有些甚至不需要任何操作,在用户不知道的情况下就能完成验证。这里其实包含了不同的隐形验证方法。
有些隐形验证采用了基于 JavaScript 的验证手段。这种方法主要是在响应数据页面之前,先返回一段带有 JavaScript 代码的页面,用于验证访问者有无 JavaScript 的执行环境,以确定使用的是不是浏览器。例如淘宝、快代理这样的网站。通常情况下,这段 JS 代码执行后,会发送一个带参数 key 的请求,后台通过判断 key 的值来决定是响应真实的页面,还是响应伪造或错误的页面。因为 key 参数是动态生成的,每次都不一样,难以分析出其生成方法,使得无法构造对应的 http 请求。
有些则更加高级一些,通过检测出用户的浏览习惯,比如用户常用 IP 或者鼠标移动情况等,然后自行判断人机操作。这样就用一次点击取代了繁琐的验证码,而且实际效果还更好。
对于这类的反爬手段,就轮到 selenium 这个神器登场了。selenium 是一个测试用的库,可以调用浏览器内核,也就是说可以打开一个真的浏览器,并且可以手动进行操作。那就完美可以完美应对上述两种隐形验证手段。
selenium 的使用也很简单,可以直接对页面元素进行操作。配合根据页面元素等待页面加载完成的时延操作,基本上把人浏览页面的过程整个模拟了一遍。而且因为 selenium 会打开一个浏览器,所以如果有点击的验证操作,一般这种操作也就在开始的登录页会有,人来点一下就是了。
from selenium import webdriver
browser = webdriver.Chrome()
browser.get("url")
#获得 dom 节点
node = browser.find_elements_by_id("id")
nodes = browser.find_elements_by_css_selector("css-selector")
nodelist = browser.find_elements_by_class_name("class-name")
#操作 dom 元素
browser.find_element_by_xpath('xpath-to-dom').send_keys('password')
browser.find_element_by_xpath('xpath-to-dom').click()
#等待页面加载
locator = (By.CLASS_NAME, 'page-content')
try:
WebDriverWait(driver, 10, 0.5).until(EC.presence_of_element_located(locator))
finally:
driver.close()
这么看起来仿佛 selenium 就是无解的了,实际上并不是。较新的智能人机验证已经把 selenium 列入了针对目标中,使得即使手动点击进行人机验证也会失败。这是怎么做的呢?事实上,这是对于浏览器头做了一次检测。如果打开 selenium 的浏览器控制台输入 window.navigator.webdriver
,返回值会是“true”。 而在正常打开的浏览器中输入这段命令,返回的会是“undefined”。在这里,我找到了关于 webdriver 的描述:navigator.webdriver
)。可以看到,webdriver 属性就是用来表示用户代理是否被自动化控制,也就是这个属性暴露了 selenium 的存在,人机验证就无法通过。而且,这个属性还是只读的,那么就不能直接修改。当然硬要改也不是不行,通过修改目标属性的 get 方法,达到属性修改的目的。这时的 webdriver 属性就是 undefined 了,然后再进行智能人机验证,就可以通过了。但这是治标不治本的,此时如果浏览器打开了新的窗口,或者点击链接进入新的页面,我们会发现,webdriver 又变回了 true。当然,在每次打开新页面后都输入这段命令也可以,不过事实上,虽然点击验证可以被绕过去,但如果直接在页面中加入检测 webdriver 的 JS 代码,一打开页面就执行,那么在你改 webdriver 之前,网站已经知道你到底是不是爬虫脚本了。
更多
道高一尺,魔高一丈。事实上即使这样的反爬手段,也还是可以绕过去。在启动 Chromedriver 之前,为 Chrome 开启实验性功能参数 excludeSwitches,它的值为[‘enable-automation’],像这样
from selenium.webdriver import Chrome
from selenium.webdriver import ChromeOptions
option = ChromeOptions()
option.add_experimental_option("excludeSwitches", ["enable-automation"])
driver = Chrome(options=option)
driver.get('url')
这时候,不管怎么打开新页面,webdriver 都会是 undefined。对于这个级别的爬虫脚本,还不知道要怎么防御,检测的成本太高了。
不过,事实上,换个思路,还有一些有趣的反爬方法。比如猫眼电影的实时票房和起点中文网,在浏览器里能看到内容,但是打开网页代码一看,全变成了方块。这就是一种很好地反爬方法,简单地说就是后端搭一套字体生成接口,随机生成一个字体,然后返回这个字体文件,以及各个数字的 unicode 对应关系,前端页面进行数据填充,就可以隐藏敏感数据。
还有些充分利用了 css 进行的反爬,脑洞更大。搞两套数据,显示的时候用 css 定位将真实的覆盖假的。或者搞一些干扰字符,显示的时候将 opacity 设为 0 进行隐藏。甚至还有设置一个背景,让它和显示的内容拼接在一起,成为真正要展示的内容。这些都是非常有趣的反爬手段。
不过对于前端来说,毕竟所有的数据和代码,都给到了客户端,爬虫脚本总是能想出办法来爬到数据,各种反爬的手段,也就是加大爬数据的难度而已。主要还是要自觉,拒绝恶意爬虫。