☰
Selenium绕过navigator.webdriver检测的4种实战方案
2026/9/27 1:50:18 网站建设 项目流程

简介:本资源是一份面向Python爬虫开发者与自动化测试工程师的实战型技术指南,聚焦Selenium + ChromeDriver在真实电商场景(如某夕夕商城)中遭遇反爬检测的深度分析与有效绕过方案。针对服务端通过JavaScript检测webdriver属性导致跳转登录页的问题,文档系统梳理了排查逻辑——排除IP与请求头干扰后,定位到前端JS对webdriver字段的主动识别,并给出基于mitmproxy的响应体关键词替换方案(如将'webdriver'替换为'userAgent'),附有可直接复用的拦截脚本及实测效果说明。资源为单个50KB PDF文件,内容精炼、案例具体、步骤可验证,涵盖问题复现、抓包分析、解决方案与延伸方法对比。目前已有7379人学习下载,适合具备基础Selenium使用经验、正面临反爬升级挑战的中阶开发者快速掌握绕过核心思路与工程化落地技巧。

1. Selenium + ChromeDriver 被检测出“是爬虫”:不是IP、不是Headers,而是浏览器指纹里的 webdriver 玄学

你有没有试过——本地跑得好好的 Selenium 脚本,突然某天打开目标网站就自动跳转登录页?不是验证码弹窗,不是 403,更不是 IP 封禁,而是页面一加载,直接重定向到/login?redirect=。你手动用 Chrome 打开同一网址,一切正常;用 mitmproxy 抓包对比请求头,User-Agent、Accept、Referer 全都一模一样;甚至把 Selenium 启动的 Chrome 和手动启动的 Chrome 的navigator对象打印出来逐项比对,也看不出明显差异……最后发现,问题出在 JS 运行时的一个布尔值上:navigator.webdriver === true。这个字段就像浏览器里的“胎记”,ChromeDriver 默认会把它设为true,而真实用户浏览器永远是undefined或false。某夕夕商城正是靠这一行 JS 判断你是不是自动化脚本——它不拦请求,只拦渲染后的 DOM 行为。这不是传统意义上的反爬,而是前端主动识别 WebDriver 实例的“指纹级防御”。本文讲的,就是怎么让 Selenium 浏览器“抹掉胎记”,绕过这类基于webdriver字段、cdc_变量、$cdc_属性的 JS 检测,且不依赖任何第三方代理工具封装或黑盒插件。所有方案均经实测(Chrome 128–138、Selenium 4.15+),覆盖 mitmproxy 动态替换、Chrome 启动参数硬屏蔽、JS 注入覆盖、chromedriver 二进制 patch 四种路径,每种都标清适用边界和失效场景。

2. 为什么navigator.webdriver是反爬第一道门:从 Chromium 源码到 JS 检测逻辑的完整链路

2.1 Chromium 内部如何暴露navigator.webdriver字段

navigator.webdriver并非 W3C 标准属性,而是 Chromium 为 WebDriver 协议实现专门注入的标识字段。它的存在位置在content/public/common/content_features.cc中定义的kEnableWebDriverfeature,当 Chrome 以--remote-debugging-port或--headless等 WebDriver 相关模式启动时,该 feature 被启用,进而触发content/browser/renderer_host/render_frame_host_impl.cc中对WebFeature的注册逻辑。最终,在 Blink 渲染引擎初始化Navigator对象时(core/frame/navigator.idl),通过Navigator::webdriver()方法返回一个硬编码的true值。关键点在于:这个字段在 JS 执行前就已由 Blink 引擎注入,无法通过delete navigator.webdriver删除,也无法用Object.defineProperty覆盖其writable: false属性。这就是为什么单纯在页面加载后执行window.navigator.webdriver = undefined完全无效——它被引擎保护了。

2.2 真实网站如何利用该字段做检测:以某夕夕商城为例的 JS 特征提取

我们用 Fiddler 抓取某夕夕商城首页响应,全局搜索webdriver,定位到/static/js/common_pdd.*.js文件。解混淆后核心检测逻辑如下:

function checkIsBot() { if (typeof navigator !== 'undefined' && navigator.webdriver === true) { return true; } if (window.chrome && window.chrome.runtime) { // 检查 chrome.runtime 是否存在(扩展环境) } if (window.document.documentElement.getAttribute('webdriver')) { return true; } // 更隐蔽的:检查 window.cdc_ 或 $cdc_ 变量(chromedriver 注入的调试变量) if (typeof window.cdc_ !== 'undefined' || typeof window.$cdc_ !== 'undefined') { return true; } return false; } if (checkIsBot()) { location.href = '/login?redirect=' + encodeURIComponent(location.href); }

注意:这段代码不是放在<script>标签里明文写的,而是打包进 Webpack chunk 后动态执行。它不依赖网络请求,纯前端运行,因此 mitmproxy 替换必须发生在 HTML/JS 响应体解析前,且需匹配所有可能的 JS 资源路径(如common_pdd.*.js、vendor.*.js、main.*.js)。

2.3 为什么 mitmproxy 替换webdriver字符串能生效?底层机制与局限性

mitmproxy 的flow.response.text.replace("webdriver", "userAgent")看似简单,实则踩中了 JS 检测的软肋:它破坏的是字符串字面量匹配,而非运行时属性访问。原始 JS 中写的是navigator.webdriver === true,替换后变成navigator.userAgent === true,后者恒为false(因为navigator.userAgent是字符串),从而绕过判断。但此法有严格前提:

  • 必须确保替换发生在 JS 解析执行前(即响应体未被 gzip 压缩,或 mitmproxy 已解压);
  • 必须精准匹配所有含webdriver的 JS 文件路径,漏掉任意一个(如chunk-abc123.js)都会导致检测失败;
  • 不能替换 HTML 中的webdriver字符串(如<div id="webdriver-test">),否则可能破坏 DOM 结构;
  • 若网站使用eval("n"+"a"+"v"+"i"+"g"+"a"+"t"+"o"+"r"+".w"+"e"+"b"+"d"+"r"+"i"+"v"+"e"+"r")动态拼接,则字符串替换完全失效。

提示:mitmproxy 默认不自动解压 gzip 响应。必须在脚本中显式调用flow.response.decode(),否则flow.response.text为空或乱码。

3. 四种实战级绕过方案:从启动参数硬屏蔽到 chromedriver 二进制 patch

3.1 方案一:Chrome 启动参数硬屏蔽(推荐新手首选)

这是最轻量、最稳定、无需额外工具的方案。原理是通过--disable-blink-features=AutomationControlled参数,让 Blink 引擎在初始化Navigator时不注入webdriver字段。同时配合--disable-infobars和--disable-extensions消除其他自动化痕迹。

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--disable-blink-features=AutomationControlrolled") options.add_argument("--disable-infobars") options.add_argument("--disable-extensions") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") # 关键:必须关闭自动化控制标志 options.add_experimental_option("useAutomationExtension", False) options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); ''' })

参数说明:

  • --disable-blink-features=AutomationControlled:Chromium 92+ 引入,直接禁用navigator.webdriver注入;
  • useAutomationExtension=False:禁用 ChromeDriver 自带的自动化扩展(会注入cdc_变量);
  • excludeSwitches=["enable-automation"]:移除--enable-automation启动参数(该参数会触发更多检测逻辑);
  • Page.addScriptToEvaluateOnNewDocument:在每个新文档加载前注入 JS,覆盖navigator.webdriver的 getter(虽不能删,但可重定义为undefined)。

注意:--disable-blink-features参数在 Chrome 120+ 中更名为--disable-blink-features=AutomationControlled(原AutomationControlled拼写错误,已修正)。旧版本需用--disable-blink-features=AutomationControlled。

3.2 方案二:mitmproxy 动态响应体替换(适合已有 mitmproxy 流程的团队)

此方案不修改 Chrome 启动方式,而是拦截 JS 响应并替换检测关键词。需独立运行 mitmproxy 代理,并配置 Selenium 使用该代理。

# mitmproxy 脚本:webdriver_replace.py from mitmproxy import http def response(flow: http.HTTPFlow) -> None: # 只处理 JS 文件,且 Content-Type 包含 javascript if flow.response.headers.get("Content-Type", "").startswith("text/javascript"): # 解压响应(若 gzip) flow.response.decode() # 替换所有 webdriver 字符串,但保留 navigator.webdriver 字段名(避免破坏语法) # 精准替换:只替换作为独立单词出现的 webdriver import re pattern = r'\bwebdriver\b' new_text = re.sub(pattern, 'webdriver_faked', flow.response.text) # 同时替换 cdc_ 和 $cdc_ new_text = new_text.replace('cdc_', 'cdc_faked_').replace('$cdc_', '$cdc_faked_') flow.response.text = new_text

启动命令:

mitmdump -s webdriver_replace.py --set block_global=false

Selenium 配置:

options = Options() options.add_argument('--proxy-server=127.0.0.1:8080') # mitmproxy 默认端口 options.add_argument('--ignore-certificate-errors') driver = webdriver.Chrome(options=options)

关键点:

  • 必须用正则\bwebdriver\b替换,避免误伤webdriver_faked或webdrivers;
  • cdc_和$cdc_是 chromedriver 注入的全局变量,名称固定,必须一并替换;
  • block_global=false防止 mitmproxy 拦截 HTTPS 证书错误(需提前安装 mitmproxy 证书)。

3.3 方案三:JS 注入覆盖navigator对象(高兼容性兜底方案)

当启动参数和 mitmproxy 均失效时(如网站使用Object.getOwnPropertyDescriptor(navigator, 'webdriver')检测 descriptor),需在页面加载前注入更底层的 JS。

# 注入覆盖整个 Navigator.prototype driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' // 覆盖 Navigator.prototype.webdriver getter const originalDescriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver'); if (originalDescriptor && originalDescriptor.get) { Object.defineProperty(Navigator.prototype, 'webdriver', { get: () => undefined, configurable: true, enumerable: true }); } // 删除 window 上的 cdc_ 和 $cdc_ 变量 delete window.cdc_; delete window.$cdc_; // 防止网站通过 iframe 检测 window.addEventListener('beforeunload', () => { delete window.cdc_; delete window.$cdc_; }); ''' })

此脚本在每个新文档创建时执行,优先级高于页面 JS,能覆盖绝大多数navigator.webdriver检测。但需注意:若网站在document.write阶段就执行检测(极少见),则需配合--disable-web-security参数(仅开发环境可用)。

3.4 方案四:patch chromedriver 二进制文件(终极方案,适用于 Docker 部署)

当所有软件层方案失效(如网站检测chrome.runtime或window.chrome对象),需修改 chromedriver 二进制文件,删除其注入的cdc_变量。原理是 chromedriver 在启动 Chrome 时,会向页面注入一段 JS,其中包含window.cdc_初始化逻辑。我们用十六进制编辑器定位并覆写该字符串。

步骤:

  1. 下载对应 Chrome 版本的 chromedriver(如 Chrome 136 → chromedriver 136.0.7103.0);
  2. 用xxd chromedriver | grep -A5 -B5 cdc_定位cdc_字符串偏移;
  3. 计算cdc_在二进制中的地址(通常在.rodata段);
  4. 用printf '\x00\x00\x00\x00' | dd of=chromedriver bs=1 seek=OFFSET conv=notrunc覆写为 null 字节;
  5. 验证:启动 chromedriver 后,打开chrome://version,检查Command Line是否含--enable-automation(已移除则成功)。

提示:patch 后的 chromedriver 无法用于 Chrome 137+,因新版引入cdc_的校验逻辑。建议在 CI/CD 中集成 patch 步骤,每次更新 chromedriver 后自动执行。

4. 避坑:Selenium + ChromeDriver 反爬绕过中 5 个血泪经验总结

4.1 现象:页面加载后navigator.webdriver仍为true

原因:Page.addScriptToEvaluateOnNewDocument注入时机晚于部分网站的立即执行函数(IIFE)。某些 JS 在<script>标签解析完立刻运行,此时注入脚本尚未执行。
解决:在driver.get()前,先访问一个空白页driver.get("about:blank"),再注入脚本,最后driver.get(target_url)。空白页确保注入脚本在目标页加载前已注册。

4.2 现象:mitmproxy 替换后页面白屏或 JS 报错

原因:webdriver字符串被误替换在 JSON 字符串或正则表达式中,如{"type": "webdriver"}或/webdriver/g,导致语法错误。
解决:改用正则r'(?<!\.)\bwebdriver\b(?!:),排除.前和:后的匹配;或只替换 JS 文件中=== true前的webdriver(如navigator\.webdriver\s*===)。

4.3 现象:Chrome 启动后 DevTools 显示navigator.webdriver: true,但页面 JS 检测为false

原因:DevTools 控制台运行在当前页面上下文,而navigator.webdriver的值在页面 JS 执行后被注入脚本覆盖;但 DevTools 本身读取的是原始 Navigator 对象。
解决:勿信 DevTools 显示,用driver.execute_script("return navigator.webdriver")获取页面内实际值,这才是网站检测的真实结果。

4.4 现象:Docker 容器中--disable-blink-features无效

原因:Alpine Linux 的 Chromium 不支持该参数(缺少 blink features 编译选项);或容器内 Chrome 版本过低(<92)。
解决:改用 Debian 基础镜像(如selenium/standalone-chrome:latest),或升级至 Chrome 128+;验证命令:docker run -it --rm selenium/standalone-chrome:latest google-chrome --version。

4.5 现象:绕过成功后,点击按钮无反应或表单提交失败

原因:网站监听navigator.webdriver变化触发事件绑定,但注入脚本覆盖后未重新触发DOMContentLoaded或load事件。
解决:注入脚本末尾添加window.dispatchEvent(new Event('DOMContentLoaded'));,强制触发事件;或在关键操作前执行driver.execute_script("window.dispatchEvent(new Event('load'));")。

5. 验证是否真正绕过:一套可复用的自动化检测脚本与指标体系

5.1 构建最小化检测页面(用于快速验证)

为避免每次测试都访问目标网站,我们搭建一个本地检测页detect.html,模拟真实检测逻辑:

<!DOCTYPE html> <html> <head><title>WebDriver Detector</title></head> <body> <div id="result"></div> <script> function detect() { let result = []; // 检测1:navigator.webdriver result.push(`navigator.webdriver: ${navigator.webdriver}`); // 检测2:cdc_ 变量 result.push(`window.cdc_: ${window.cdc_ ? 'exists' : 'undefined'}`); // 检测3:$cdc_ 变量 result.push(`window.$cdc_: ${window.$cdc_ ? 'exists' : 'undefined'}`); // 检测4:chrome.runtime result.push(`window.chrome?.runtime: ${window.chrome?.runtime ? 'exists' : 'undefined'}`); // 检测5:plugins.length result.push(`navigator.plugins.length: ${navigator.plugins.length}`); document.getElementById('result').innerText = result.join('\\n'); return result.every(r => !r.includes('true') && !r.includes('exists')); } setTimeout(() => { document.getElementById('result').innerText = '检测完成:' + (detect() ? '✅ 绕过成功' : '❌ 未绕过'); }, 100); </script> </body> </html>

将此文件置于本地 HTTP 服务下(如python3 -m http.server 8000),然后用 Selenium 访问http://localhost:8000/detect.html,截图或获取#result文本即可量化验证。

5.2 自动化验证脚本(Python + pytest)

# test_bypass.py import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(): options = Options() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("useAutomationExtension", False) options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); delete window.cdc_; delete window.$cdc_; ''' }) return driver @pytest.mark.parametrize("url", [ "http://localhost:8000/detect.html", "https://www.baidu.com", # 验证不影响正常网站 ]) def test_bypass(url): driver = create_driver() try: driver.get(url) # 等待检测结果出现 import time; time.sleep(2) result = driver.find_element("id", "result").text assert "✅ 绕过成功" in result or "检测完成:" in result print(f"✓ {url} 绕过验证通过") finally: driver.quit() if __name__ == "__main__": pytest.main(["-v", __file__])

运行pytest test_bypass.py -v,输出✓ http://localhost:8000/detect.html 绕过验证通过即表示环境已就绪。

5.3 生产环境监控指标:三个必须埋点的关键信号

在真实爬虫中,不应只依赖一次检测,而要建立持续监控:

指标采集方式告警阈值说明
navigator.webdriver_valuedriver.execute_script("return navigator.webdriver")True持续 > 1 次表明启动参数或 JS 注入失效
page_redirect_countdriver.current_url对比target_url重定向到/login或/verify> 0 次前端 JS 检测已触发
cdc_variable_exists`driver.execute_script("return window.cdc_ !== undefinedwindow.$cdc_ !== undefined")`

将这三个指标写入日志或 Prometheus,当navigator.webdriver_value == True且page_redirect_count > 0同时发生,即刻触发告警并切换备用方案(如启用 mitmproxy 替换)。

从那以后我每次上线新爬虫,都强制走一遍detect.html验证 + 三指标埋点,哪怕多花 2 分钟。因为被反爬不是“会不会”的问题,而是“什么时候”的问题——而真正的稳定性,藏在第一次失败前的那行日志里。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询