今天就说说常见的一种反爬手段:「动态页面」。
大多数的网站页面都是经过 JS 处理后生成的结果。这些数据的来源有多种,可能是通过 Ajax 加载的,可能是包含在 HTML 文档中的,也可能是经过 JS 和特定算法计算后生成的。所以如果我们遇到这样的页面,直接利用 requests 等库来抓取原始页面,是无法获取到有效数据的。这时我们需要分析网页后台向服务端发送的 Ajax 请求,只要我们能找到 Ajax 接口的规律,就可以构造出对应的的请求,数据自然就能被轻松爬取到。但是,在很多情况下,Ajax 请求的接口通常会包含加密的参数,如 token、sign 等。
这个时候解决方法通常有两种:一种是深挖其中的逻辑,把其中加密参数的构造逻辑完全找出来,再用 Python 复现,构造 Ajax 请求;另外一种方法就是直接通过模拟浏览器的方式,绕过这个过程。由于第一种方法难度较高,在这里我们就先说说第二种方法,模拟浏览器爬取。
我们有多种工具可以选择用来模拟浏览器行为,在 PC 端网络爬虫这块,我们主要使用 Selenium 和 Pyppeteer 这两个自动化测试工具。
Selenium 可以做到可见即可爬。对于一些动态页面来说,此种抓取方式非常有效。具体用法请移步官方文档:https://selenium-python.readthedocs.io/index.html,这里就不做过多介绍了。
不过在这里可以提一下反屏蔽,现在很多网站都加上了对 Selenium 的检测。大多数情况下,检测基本原理就是检测当前浏览器窗口下的 window.navigator 对象是否包含 webdriver 这个属性。因为在正常使用浏览器的情况下,这个属性是 undefined,然而一旦我们使用了 Selenium,Selenium 会给 window.navigator 设置 webdriver 属性。
这时候我们可能会想到直接使用 JS 把这个 webdriver 属性置空,比如通过调用 execute_script 方法来执行如下代码:
Object.defineProperty(navigator, "webdriver", {get: () => undefined})
这行 JS 的确是可以把 webdriver 属性置空,但是 execute_script 调用这行 JS 语句实际上是在页面加载完毕之后才执行的,执行太晚了,网站早在最初页面渲染之前就已经对 webdriver 属性进行了检测,所以用这个方法并不能达到效果。
在 Selenium 中,我们可以通过 CDP 实现在每个页面刚加载的时候执行 JS 代码,执行的 CDP 的方法叫作 Page.addScriptToEvaluateOnNewDocument,然后传入上面的 JS 代码即可,这样我们就可以在每次页面加载之前将 webdriver 属性置空了。另外我们还可以加入几个选项来隐藏 WebDriver 提示条和自动化扩展信息,代码实现如下:
from selenium import webdriver
from selenium.webdriver import ChromeOptions
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
option.add_experimental_option('useAutomationExtension', False)
browser = webdriver.Chrome(options=option)
browser.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
})
browser.get('https://www.baidu.com')
对于大多数的情况,上面的方法可以实现 Selenium 反屏蔽。但对于一些特殊的网站,如果其有更多的 WebDriver 特征检测,可能需要具体排查。