在识别网络抓取工具方面,javascript 是迄今为止最强大的工具,因为它允许在客户端机器上执行任意代码。该代码可以访问大量独特的数据点,这些数据点可用于构建客户端指纹,甚至可以立即识别网络爬虫控制的浏览器。 在本文中,我们将了解如何使用 javascript 通过指纹识别网络抓取工具。我们将介绍常见的指纹识别技术和由无头浏览器使用引起的指纹泄漏。我们还将了解如何在使用 Selenium、Playwright 或 Puppeteer 等浏览器自动化工具包的网络抓取工具中防止和修补这些泄漏。
浏览器指纹识别如何工作?
浏览器中的 Javascript 可以访问数以千计的不同环境细节,例如 javascript 运行时变量、显示能力(例如分辨率和颜色足迹等)。所有这些信息都可以用来识别和阻止网络抓取工具,所以让我们来看看指纹识别是如何工作的,以及我们作为网络抓取工具开发人员如何避免它。 在本文中,我们将介绍两种不同的 javascript 用法概念来识别网络抓取工具:
- 当 javascript 环境可用于识别某物是人还是机器人时,机器人指纹泄漏。
- 当使用 javascript 环境创建用于跟踪用户的唯一身份时,身份指纹识别。在网络抓取中,这主要意味着如果我们的抓取器被跟踪,它可以在建立太多不自然的连接后被识别出来。换句话说,如果 ID 1234 正在以非人类的速度浏览网页,则服务器可以自信地推断出客户端不是人类。
这两个概念密切相关,但隐藏机器人身份更为重要,因为这是阻止由 Playwright/Puppeteer/Selenium 提供支持的爬虫的一种非常常见的方法。
浏览器自动化泄漏
指纹识别技术可以强大到足以立即识别网络抓取工具。不幸的是,许多 web 浏览器自动化工具将有关自身的信息泄露给 javascript 执行上下文——这意味着 javascript 可以很容易地告诉浏览器是由程序而不是人控制的。这应该是我们指纹强化的第一步——我们需要覆盖我们的抓取环境留下的痕迹。 Scraper 控制的浏览器通常包含额外的 javascript 环境信息,表明浏览器在没有 GUI 元素(又名无头)的情况下运行或在不常见的操作系统(例如 Linux)上运行 例如,当涉及到 Selenium、Playwright 或 Puppeteer 等浏览器自动化工具时,最常见的泄漏是navigator.webdriver
自动浏览器将navigator.webdriver
值设置为异常值的 泄漏true
:
在上方,我们看到受控浏览器已navigator.webdriver
设置为true
自然浏览器始终将其设置为的位置false
。任何网站都可以读取这些变量,从而非常容易地识别机器人!
我们可以通过并排启动真实的自动浏览器并探索 javascript 控制台(大多数浏览器中的 F12)来轻松探索这些漏洞。
如何修补指纹泄漏
为了防止通过 javascript 变量泄漏数据,我们可以使用 javascript 为浏览器的 javascript 环境修补假值:
// change navigator.webdriver getter to always return value `false`: Object.defineProperty(navigator, 'webdriver', {get: () => false})
这个简短的脚本将navigator.webdriver
值重新定义为始终返回false
– 这修复了我们的漏洞! 为了在我们的浏览器自动化工具中修补这些漏洞,我们可以利用页面启动脚本功能: Playwright (Python)
from playwright.sync_api import Page page: Page script = "Object.defineProperty(navigator, 'webdriver', {get: () => false})" page.add_init_script(script)
Selenium (Python)
from selenium.webdriver import Chrome driver: Chrome script = "Object.defineProperty(navigator, 'webdriver', {get: () => false})" driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": script})
Puppeteer (Javascript)
const script = "Object.defineProperty(navigator, 'webdriver', {get: () => false})" page.evaluateOnNewDocument(script)
上面的代码会将我们的navigator.webdriver
修复脚本附加到页面/浏览器启动过程,这将在整个抓取会话中堵住这个漏洞! 现在我们知道如何堵住这些漏洞,让我们来看看如何找到它们。
哪些浏览器泄漏最少?
每种浏览器类型都有不同的泄漏向量,必须单独处理,因此不幸的是,我们不能全面应用所有相同的防御规则——我们必须单独处理每种浏览器类型。 一般来说,Chrome 比 Firefox 漏洞更多,因此加固 Firefox 更容易。话虽这么说,Chrome 在网络自动化领域的存在时间要长得多,因此随着公共资源的增多,它更容易使用。Chrome 真的更糟还是我们更了解它? 另一件需要注意的事情是,Chrome 的市场份额要高得多,因此,如果我们的目标是大规模融入,我们会坚持使用它。 我们将探索 Chrome 和 Firefox 浏览器的 javascript 环境,但主要是,一旦我们学会了如何正确识别泄漏并修补它们,我们就可以强化任何浏览器以用于网络抓取!
强化浏览器
Javascript 暴露了大量关于客户端的信息,并且将所有这些信息驯服到完美将需要数千小时的工作。话虽如此,并非所有信息都是平等的,我们可以非常自信地填补最大的漏洞! 并非所有泄漏都以二元方式处理(无论您是否是机器人),但有些泄漏肯定是。反机器人保护系统希望避免误报,网络空间的多样性足以提供一些喘息的空间。话虽这么说,堵住最大的漏洞对于任何试图访问受保护网站的网络抓取工具来说都是至关重要的。
泄漏检测工具
有许多在线工具可以分析 Web 浏览器中最常见的泄漏和指纹值:
- https://bot.sannysoft.com/
- http://arh.antoinevastel.com/bots/areyouheadless
- https://antoinevastel.com/bots/
- https://github.com/paulirish/headless-cat-n-mouse
- https://abrahamjuliot.github.io/creepjs/
值得注意的是,这些工具都不是完美的,并且由于网络浏览器环境随着每个新浏览器的发布而不断发展和变化,我们应该自己确认所有这些数据。 为此,让我们使用一个小的测试脚本,让我们将我们的自动浏览器与真实浏览器进行比较: checkplaywright.py – 比较 Playwright 浏览器的脚本
#!/usr/bin/env python3 # check-playwright.py import sys from playwright.sync_api import sync_playwright, Page, Browser, BrowserType def run(browser: str, headless: str, script: str, url=None): headless = 'headless' in headless.lower() with sync_playwright() as pw: browser_type: BrowserType = getattr(pw, browser) browser: Browser = browser_type.launch(headless=headless) page: Page = browser.new_page(viewport={"width": 1920, "height": 1080}) if url: page.goto(url) result = page.evaluate(script) return result if __name__ == "__main__": print(run(*sys.argv[1:]))
checkselenium.py – 比较 Selenium 浏览器的脚本
#!/usr/bin/env python3 # check-selenium.py import sys from checkselenium import run as run_selenium from checkplaywright import run as run_playwright def run(script:str, url=None): data = {} for toolkit, tookit_script in [('selenium', run_selenium), ('playwright', run_playwright)]: for browser in ['chromium', 'firefox']: for head in ['headless', 'headful']: data[f'{toolkit:<10}:{head:<8}:{browser:<8}:{url or ""}'.strip(':')] = tookit_script(browser, head, script, url) return data if __name__ == "__main__": for query, result in run(*sys.argv[1:]).items(): print(f"{query}: {result}")
checkall.py – 比较 Selenium 和 Playwright 浏览器的脚本
import sys from checkselenium import run as run_selenium from checkplaywright import run as run_playwright def run(script:str, url=None): data = {} for toolkit, tookit_script in [('selenium', run_selenium), ('playwright', run_playwright)]: for browser in ['chromium', 'firefox']: for head in ['headless', 'headful']: data[f'{toolkit:<10}:{head:<8}:{browser:<8}:{url or ""}'.strip(':')] = tookit_script(browser, head, script, url) return data if __name__ == "__main__": for query, result in run(*sys.argv[1:]).items(): print(f"{query}: {result}")
在这里,我们有两个微型 pythons 脚本,用于将 javascript 执行值与 Selenium 或 Playwright 进行比较。使用这些脚本和真实浏览器的 javscript 控制台(大多数网络浏览器中的 F12 键)我们可以快速比较不同的环境值:
$ python checkall.py "navigator.webdriver" selenium :headless:chromium: True selenium :headful :chromium: True selenium :headless:firefox : True selenium :headful :firefox : True playwright:headless:chromium: True playwright:headful :chromium: True playwright:headless:firefox : False playwright:headful :firefox : False
如您所见,Selenium 和 Playwright 都在 Chrome 上navigator.webdriver
设置了变量True
,而在两个本机浏览器上都是False
。对于 Firefox,我们看到 Playwright 确实具有正确的值,而 Selenium 仍然失败。 使用此设置,我们可以快速比较和调试我们的 web 抓取环境以了解 javascript 环境差异,这有助于我们识别常见的身份泄漏。现在我们有了正确的工具,让我们看一下用于识别网络抓取工具的一些最常见的漏洞。
常见泄漏
其中许多漏洞都是众所周知的,许多浏览器自动化库都有现有的工具来处理它们:
- puppeteer-stealth – Puppeteer 的插件
- playwright-stealth – 剧作家的插件
- selenium-stealth – Selenium 的插件
- headless-cat-and-mouse – 探索来自客户端和服务器端的漏洞
不幸的是,由于 Web 浏览器变化迅速,开源库往往落后,因为需要大量志愿者努力才能跟上。指纹泄漏的知识对于业内人士来说是一个宝贵的商业秘密,因此公开提供最佳解决方案的动力很小。 话虽这么说,我们有我们的工具,我们必须从某个地方开始。让我们来看看一些众所周知的泄漏类型和泄漏,以及我们如何堵塞它们。 为此,很好的起点是探索puppeteer-stealth插件,虽然它在某些地方过时且不准确,但仍然包含许多我们可以学习的重要技术和策略。
浏览器功能
识别和指纹浏览器的常用方法是测试它们的功能。 最广为人知的例子是Chrome 上的navigator.plugins和navigator.mimetypes变量,虽然已弃用,但仍作为硬编码值存在于所有版本的 Chrome 浏览器中。 这两个变量表示浏览器使用的插件和支持的文档类型。由于无头浏览器不支持视觉细节,因此此变量解析为空数组:
$ python checkall.py "[navigator.plugins.length, navigator.mimeTypes.length]" selenium :headless:chromium: [0, 0] selenium :headful :chromium: [5, 2] selenium :headless:firefox : [0, 0] selenium :headful :firefox : [0, 0] playwright:headless:chromium: [0, 0] playwright:headful :chromium: [5, 2] playwright:headless:firefox : [0, 0] playwright:headful :firefox : [0, 0]
因此,当使用无头浏览器进行网络抓取时,我们希望确保我们为这些值模仿有头浏览器。 这些对象非常大,因此请参阅此 puppeteer-stealth github 存储库以了解如何正确模拟此浏览器功能。
HTTP 和 HTTPS 中的不同行为
浏览器 javascript 环境可能会根据当前连接是否使用 SSL 进行保护而有所不同。 最广为人知的例子是Chrome 浏览器上的Notification.permission变量:
虽然 Firefox 始终将此值设为“默认值”,但 Chrome 仅对不安全的网站将其设为“拒绝”。不幸的是,浏览器自动化工具包无法匹配这种行为,因此很容易识别爬虫:
# check secure connections - result should always be "default" $ python checkall.py "Notification.permission" https://httpbin.org/headers selenium :headless:chromium:https://httpbin.org/headers: denied # ^ ❌ should be "default" selenium :headful :chromium:https://httpbin.org/headers: default selenium :headless:firefox :https://httpbin.org/headers: default selenium :headful :firefox :https://httpbin.org/headers: default playwright:headless:chromium:https://httpbin.org/headers: denied # ^ ❌ should be "default" playwright:headful :chromium:https://httpbin.org/headers: default playwright:headless:firefox :https://httpbin.org/headers: default playwright:headful :firefox :https://httpbin.org/headers: default # check unsecure connections - result should be "denied" for chromium and "default" for firefox $ python checkall.py "Notification.permission" http://httpbin.org/headers selenium :headless:chromium:http://httpbin.org/headers: denied selenium :headful :chromium:http://httpbin.org/headers: denied selenium :headless:firefox :http://httpbin.org/headers: default selenium :headful :firefox :http://httpbin.org/headers: default playwright:headless:chromium:http://httpbin.org/headers: denied playwright:headful :chromium:http://httpbin.org/headers: denied playwright:headless:firefox :http://httpbin.org/headers: default playwright:headful :firefox :http://httpbin.org/headers: default # all are ✅ here!
我们可以看到 Selenium 和 Playwright 在使用 headless Chrome 时都失败了。因此,如果我们运行的是无头 Chrome,我们必须在抓取 SSL 安全网站时进行修补Notification.permission
以返回:default
const isSecure = document.location.protocol.startsWith('https') if (isSecure){ Object.defineProperty(Notification, 'permission', {get: () => 'default'}) }
提示:有时值得将此值设置为 以granted
提高我们对大多数用户已启用通知的网站的指纹评级。
浏览器特定的 JS 对象
另一个常见的指纹区域是浏览器特定的 javascript 对象。 最广为人知的例子是Chrome
Chrome 浏览器上的对象。该对象由 Chrome 扩展程序使用,浏览器的无头版本不包含它,这意味着我们需要自己手动重新创建它。 指纹的这一部分很古老并且很好理解,所以我们建议参考 puppeteer stealth 插件的chrome.* evasions(从chrome.app
一个开始)。Chrome
本质上,我们想要重新创建我们在浏览器的 headful 版本中看到的相同对象,因为它是一个静态对象,我们可以复制所有内容。
浏览器进程标志
浏览器是非常复杂的软件套件,为了应对这种复杂性,每个浏览器都可以通过启动标志进行定制。不幸的是,浏览器自动化工具通常会添加额外的启动标志,这可能会导致浏览器出现异常行为,从而导致身份泄露。让我们来看看其中的一些。 要找到我们的浏览器使用的默认标志,我们需要启用调试日志,它将打印浏览器启动命令: Playwright – 启用调试日志 可以通过 DEBUG 环境变量启用 Playwright 的调试日志:
$ export DEBUG="pw*" $ python checkplaywright.py chromium headless "" <...> pw:browser <launching> /home/dex/.cache/ms-playwright/chromium-939194/chrome-linux/chrome --disable-background-networking --enable-features=NetworkService,NetworkServiceInProcess --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,AcceptCHFrame --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-sync --force-color-profile=srgb --metrics-recording-only --no-first-run --enable-automation --password-store=basic --use-mock-keychain --no-service-autorun --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-7lVx0j --remote-debugging-pipe --no-startup-window +0ms <...>
Selenium – 启用调试日志 (Python) Selenium 的调试日志必须通过本机日志管理器启用。在 Python 的情况下,它是logging
模块:
selenium_logger = logging.getLogger('selenium.webdriver.remote.remote_connection') selenium_logger.setLevel(logging.DEBUG) logging.basicConfig(level=logging.DEBUG) # results in logs like: # DEBUG:selenium.webdriver.remote.remote_connection:POST http://127.0.0.1:52799/session {"capabilities": {"firstMatch": [{}], "alwaysMatch": {"browserName": "chrome", "platformName": "any", "goog:chromeOptions": {"extensions": [], "args": ["--headless"]}}}, "desiredCapabilities": {"browserName": "chrome", "version": "", "platform": "ANY", "goog:chromeOptions": {"extensions": [], "args": ["--headless"]}}}
Puppeteer – 显示默认参数 大多数这些标志都是无害的,实际上可以提高浏览器的性能。但是,有些不是:
--disable-extensions
– 禁用使浏览器看起来不自然的浏览器扩展。--disable-default-apps
– 启动新选项卡时禁用默认浏览器应用程序的安装。--disable-component-extensions-with-background-pages
– 禁用使用背景页面的默认浏览器扩展。
所以,我们至少应该从摆脱这些开始: Playwright – 使用 ignore_default_args 选项忽略默认参数
# playwright has a handy `ignore_default_args` argument: browser: Browser = chromium.launch( ignore_default_args=[ '--disable-extensions', '--disable-default-apps', '--disable-component-extensions-with-background-pages' ] )
Selenium – 使用实验性 excludeSwitches 选项禁用标志
from selenium import webdriver chromeOptions = webdriver.ChromeOptions() chromeOptions.add_experimental_option( 'excludeSwitches', [ 'disable-extensions', 'disable-default-apps', 'disable-component-extensions-with-background-pages', ]) chromeDriver = webdriver.Chrome(chrome_options=chromeOptions)
Puppeteer – 使用 ignoreDefaultArgs 选项忽略默认参数
const browser = await puppeteer.launch({ ignoreDefaultArgs: [ '--disable-extensions', '--disable-default-apps', '--disable-component-extensions-with-background-pages' ] })
用户代理身份
我们在相关文章中详细介绍了标头,因此其中大部分内容也适用于此处: 当谈到 javascript 时,我们要确保标头值与浏览器功能相匹配。因此,如果我们在基于 Linux 的网络爬虫上使用基于 Windows 的用户代理标头,我们需要修改 javascript 命名空间(如navigator.platform
etc)以反映正确的操作系统。 我们还希望使用具有相同浏览器版本的用户代理字符串,因为每个浏览器版本都有独特的功能,可以从 javascript 环境中确定。换句话说,如果我们的用户代理说它是 Chrome 94 而我们的浏览器在 Chrome 99 上,javascript 指纹可以看到我们有一些 Chrome 94 中不可用的功能,所以我们很可能在我们的用户代理字符串上撒谎 -一面巨大的红旗! 大多数自动化工具允许您通过一些选项配置用户代理字符串,但是这些选项只会更改标题而不是剩余的 javascript 空间,因此应该避免使用它们。相反,一种处理此问题的快捷方式是Network.setUserAgentOverride Chrome 开发者协议 (CDP) 命令,它不仅会更新 User-Agent 标头,还会更新许多与之相关的 javascript 详细信息。
<code class="language-python">USER_AGENT = { # usual user agent string "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", "platform": "Win32", "acceptLanguage": "en-US, en", "userAgentMetadata": { # ensure the order of this array matches real browser! "brands": [ # at the time of writing this is always == 99 {"brand": " Not A;Brand", "version": "99"}, # ensure that the versions here match ones from User-Agent string {"brand": "Chromium", "version": "74"}, {"brand": "Google Chrome", "version": "74"}, ], "fullVersion": "74.0.3729.169", "platform": "Windows", "platformVersion": "10.0", "architecture": "x86", "model": "", "mobile": False, }, } cdp.send('Network.setUserAgentOverride', USER_AGENT)
在这里,我们正在设置 Windows 10 的整个用户代理配置文件,而不仅仅是用户代理标头。 Playwright – 如何发送 CDP 信号?
# we can access CDP connection through page context: USER_AGENT = {} cdp = page.context.new_cdp_session(page) cdp_response = cdp.send('Network.setUserAgentOverride', USER_AGENT)
Selenium – 如何发送 CDP 信号?
# Selenium allows CDP communication directly through Browser object USER_AGENT = {} browser.execute_cdp_cmd("Network.setUserAgentOverride", USER_AGENT)
Puppeteer – 如何发送 CDP 信号?
// we can access CDP connection through page target: const USER_AGENT = {} const client = await page.target().createCDPSession(); await client.send("Network.setUserAgentOverride", USER_AGENT);
防指纹识别
使用强化浏览器,我们可以避免即时识别,但是我们的网络抓取工具仍然可以被阻止,因为反机器人服务收集有关我们的连接模式的数据并将它们与唯一的指纹 ID 相关联。 Javascript 指纹可以提取有关连接浏览器的数千个详细信息,例如渲染和音频功能、硬件详细信息、使用的字体等。 为了进一步理解这一点,让我们参考一个流行的开源指纹分析工具creepjs:

在这里,由 javascript 公开的数千个标识符详细信息。幸运的是,我们不需要随机化一切。只需更改一些细节,我们就可以使我们的指纹足够独特,从而避免被发现。补充一点,详细的指纹识别是一项资源密集型操作,大多数网站都无法让用户在那里等待 2 秒来加载内容。 所以,我们应该从随机化基础开始:
- 标头——我们已经介绍了最重要的用户代理字符串,但我们还可以更改一些细节,如语言标头、编码功能等。
- 视口 – 虽然 1920×1080 是最受欢迎的分辨率,但它并不是唯一的分辨率。我们可以很容易地随机选择最流行的分辨率,并且可以更进一步,因为许多自然用户不使用全屏浏览器。有关更多信息,请参阅creepjs 的屏幕测试。
- Locale、Timezone、geolocation——另一种将随机性引入浏览器指纹的简单方法。请注意,如果您使用代理,则应坚持使用代理地理定位。
我们进一步扩展我们的网络抓取环境,更重要的配置文件随机化。话虽如此,只有在我们完全强化我们的浏览器后,才值得深入研究这个领域。
概 括
在本文中,我们介绍了复杂的 JavaScript 指纹识别世界。 我们首先学习如何通过插入无头浏览器或自动化系统(如 Selenium、Playwright 或 Puppeteer)留下的各种 javascript 漏洞来加强我们的浏览器以避免即时检测。然而,仅仅堵住漏洞并不足以大规模地抓取受到良好保护的目标,因此我们还研究了如何检查和随机化非关键指纹元素,从长远来看,这些元素会阻止网络抓取工具的识别。 综上所述,javascript 指纹识别是网络抓取中最大和最复杂的主题之一。它随着每个 Web 浏览器迭代而不断发展和变化,因此它是抽象层的完美位置!