Selenium被识别?用mitmproxy拦截替换JS绕过navigator.webdriver检测

发布时间:2026/10/9 16:30:58
Selenium被识别?用mitmproxy拦截替换JS绕过navigator.webdriver检测
简介这份PDF资料聚焦selenium配合chromedriver在爬虫实战中被目标站点识别并反爬的典型问题面向已有一定Python爬虫基础、正在使用或准备使用selenium的开发者。内容源于爬取某夕夕商城时的真实遭遇原本稳定的seleniumchromedrivermitmproxy方案数日后突然跳转登录页作者通过同机浏览器对比、Fiddler抓包比对请求头逐步排除IP与请求头因素最终定位到服务端对webdriver特征的检测并给出借助mitmproxy拦截响应、替换JS中webdriver关键字的解决思路同时提及修改webdriver参数等国外方案的实测效果。资源包共1个PDF文件约50KB篇幅精炼适合作为反爬排查与绕过思路的速查参考。目前已有7380人学习下载对遭遇同类检测的爬虫开发者具有较高的实战参考价值。1. 当 selenium 被识别一次爬虫翻车的复盘用 selenium chromedriver 跑自动化最让人头疼的不是元素定位不到而是某天脚本突然被目标站点识别直接跳登录页。我最近就遇到一次同一台机器手动打开浏览器访问完全正常但 chromedriver 驱动的实例一请求就被重定向到登录界面。抓包对比发现请求头一模一样IP 也没变问题出在浏览器环境本身——服务端通过 JavaScript 检测到了navigator.webdriver这个属性。这个属性在正常浏览器里是undefined而在 selenium 驱动的 Chrome 里是true。很多电商、社交类站点会把这个值作为反爬信号一旦命中就触发验证或跳转。这篇文章面向正在用 selenium 做数据采集、又不想被反爬卡住的工程师从检测原理讲到 mitmproxy 拦截替换的完整落地步骤最后给出几个我踩过的坑和验证方法。2. 反爬检测的底层逻辑为什么 chromedriver 会暴露2.1 服务端到底在检测什么要解决问题先得知道对方在看什么。selenium 驱动 Chrome 时会在浏览器启动参数里注入一系列标志这些标志会改变浏览器对象的默认状态。最常见的检测点有三个第一个是navigator.webdriver。W3C WebDriver 规范要求浏览器暴露这个只读属性selenium 控制的 Chrome 会把它设为true。正常用户手动打开的 Chrome这个属性是undefined。服务端一行if (navigator.webdriver) { location.href /login }就能把 selenium 流量筛出来。第二个是window.chrome对象。正常 Chrome 有window.chrome.runtime、window.chrome.loadTimes等但 chromedriver 启动的实例里这些可能缺失或行为异常。有些站点会检查window.chrome是否存在以及其子对象是否完整。第三个是 CDPChrome DevTools Protocol的痕迹。chromedriver 通过 CDP 与浏览器通信某些 CDP 命令会在页面留下可检测的副作用比如Runtime.enable会导致console.debug被重写。不过这类检测相对少见主要出现在对抗强度较高的站点。回到我遇到的情况目标站点在common_pdd这个 JS 文件里直接写了webdriver关键字的判断逻辑。fiddler 全局搜索响应体能清楚看到类似if (navigator.webdriver) { ... }的代码。这就是黑匣子被打开的时刻——不是玄学是明明白白的特征检测。2.2 为什么改 chromedriver 参数经常不生效网上流传很多方法比如给 ChromeOptions 加excludeSwitches、useAutomationExtension或者用add_argument(--disable-blink-featuresAutomationControlled)。这些方法在部分场景下有用但在我这次遇到的站点上全部失效。原因在于这些参数改的是浏览器启动时的行为但navigator.webdriver这个属性是 CDP 层面注入的只要 chromedriver 连接着它就会存在。你改启动参数改不掉已经建立的 CDP 会话特征。另一个常见误区是只改 User-Agent。UA 里确实可能带HeadlessChrome字样但这次抓包对比发现请求头完全一致说明对方根本没在 UA 层面做文章。所以血泪经验是先抓包定位检测点再决定改什么不要盲目套网上的参数模板。2.3 mitmproxy 拦截替换的思路既然检测逻辑写在 JS 里而 JS 是服务端返回的那就有操作空间。mitmproxy 作为中间人代理可以拦截响应体把 JS 里的webdriver关键字替换成别的字符串。这样浏览器拿到的 JS 里检测代码变成了if (navigator.userAgent) { ... }而navigator.userAgent是正常存在的字符串判断结果自然为假跳转逻辑就不会触发。这个方法的优势在于不需要改浏览器、不需要改 chromedriver、不需要对抗 CDP 特征只改服务端下发的 JS 内容。缺点是只对“检测逻辑写在 JS 里”的站点有效如果对方在服务端根据 TLS 指纹或请求时序判断那就无能为力了。但根据我的测试大部分中小型站点的反爬还是前端 JS 检测为主这招命中率很高。3. 用 mitmproxy 拦截替换从配置到跑通3.1 环境准备与代理链路搭建先确认三个组件版本兼容。selenium 用 4.xchromedriver 版本必须和本地 Chrome 主版本号一致mitmproxy 用 10.x 以上。安装命令如下pip install selenium mitmproxychromedriver 的下载和版本对应关系这里不展开只提醒一点Chrome 自动更新后 chromedriver 经常忘记换导致SessionNotCreatedException这是最常见的翻车点之一。代理链路是selenium 控制的 Chrome → mitmproxy 监听端口 → 目标站点。Chrome 需要设置代理指向 mitmproxy同时忽略证书错误因为 mitmproxy 要解密 HTTPS。启动 mitmproxy 的命令mitmproxy -p 8080 -s replace_webdriver.py --set block_globalfalse-p 8080指定监听端口-s加载自定义脚本block_globalfalse允许非本机连接如果 Chrome 和 mitmproxy 不在同一台机器上需要这个。首次运行 mitmproxy 后需要把它的 CA 证书安装到系统信任区否则 Chrome 会报证书错误。Linux 下把~/.mitmproxy/mitmproxy-ca-cert.pem导入系统证书库Windows 下双击安装到“受信任的根证书颁发机构”。3.2 编写替换脚本关键代码与参数说明替换脚本的核心逻辑是拦截响应判断 URL 是否命中目标 JS 文件然后做字符串替换。下面是我实际用的脚本# replace_webdriver.py from mitmproxy import http def response(flow: http.HTTPFlow) - None: # 只处理目标站点的 JS 文件避免全局替换影响其他请求 if /_next/static/js/common_pdd in flow.request.url: # 替换响应体中的 webdriver 关键字 if webdriver in flow.response.text: flow.response.text flow.response.text.replace( webdriver, userAgent ) # 打印日志确认替换发生 print(f[替换成功] {flow.request.url})逐行说明response函数是 mitmproxy 的钩子每个响应都会经过它。flow.request.url拿到请求地址用in判断是否包含目标 JS 路径。flow.response.text是解码后的响应体文本直接做replace。替换后的字符串userAgent是随意选的只要不是webdriver且是正常存在的属性即可。最后打印日志方便确认脚本是否生效。这里有个参数细节flow.response.text只对文本类型响应有效如果响应是 gzip 压缩的mitmproxy 会自动解压所以不用手动处理Content-Encoding。但如果 JS 文件是二进制流或分块传输可能需要用flow.response.content做字节级替换。我这次遇到的是普通文本 JS用.text就够了。3.3 selenium 侧配置让 Chrome 走代理mitmproxy 跑起来后selenium 启动 Chrome 时要指定代理。代码from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() # 设置代理指向 mitmproxy options.add_argument(--proxy-serverhttp://127.0.0.1:8080) # 忽略证书错误因为 mitmproxy 的证书可能不被完全信任 options.add_argument(--ignore-certificate-errors) # 可选禁用自动化控制提示条 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) driver.get(https://目标站点.com)参数说明--proxy-server必须和 mitmproxy 监听地址一致。--ignore-certificate-errors是必须的否则 Chrome 会拦截 mitmproxy 的证书导致页面加载失败。excludeSwitches和useAutomationExtension这两个参数对navigator.webdriver无效但能去掉地址栏下方的“Chrome 正受到自动测试软件控制”提示条减少视觉层面的暴露。启动后观察 mitmproxy 终端是否打印[替换成功]。如果打印了说明 JS 替换生效。此时再访问目标站点应该不会再跳登录页。如果没打印检查 URL 匹配条件是否写对——不同站点的 JS 路径不同需要根据实际抓包结果调整。3.4 验证替换是否生效的三种方法第一种看 mitmproxy 日志。替换成功会打印 URL这是最直接的信号。第二种在 Chrome 开发者工具里搜webdriver。打开 F12在 Sources 面板全局搜索webdriver如果替换成功应该搜不到原始关键字只能搜到userAgent。第三种在 Console 里执行navigator.webdriver。如果返回undefined或false说明浏览器层面的属性也被处理了但注意mitmproxy 替换的是 JS 文件内容不改变浏览器属性本身所以这个方法只在部分场景下有效。更可靠的验证是看页面是否还跳登录页以及数据是否能正常加载。4. 避坑与排查那些让我重跑三遍的细节4.1 替换后页面白屏或 JS 报错现象替换webdriver后页面加载出来是白屏Console 报SyntaxError或ReferenceError。原因webdriver这个字符串可能出现在 JS 的变量名、函数名或字符串字面量里。比如var webdriver require(webdriver)你把变量名也替换了导致后续引用找不到。或者webdriver出现在某个对象的属性名里替换后对象结构被破坏。解决不要全局替换先定位webdriver出现的上下文。在 mitmproxy 脚本里加判断只替换navigator.webdriver这种特定模式flow.response.text flow.response.text.replace( navigator.webdriver, navigator.userAgent )这样只改检测语句本身不影响其他代码。如果站点用了压缩混淆navigator.webdriver可能被拆成navigator[webdriver]或navigator[webdriver]需要把几种写法都覆盖。4.2 mitmproxy 证书不被信任导致请求失败现象Chrome 提示NET::ERR_CERT_AUTHORITY_INVALID页面完全打不开。原因mitmproxy 的 CA 证书没有安装到系统信任区或者 Chrome 使用了独立的证书存储比如某些 Linux 发行版的 snap 版 Chrome。解决确认证书安装路径正确。Linux 下用certutil导入到~/.pki/nssdb命令是certutil -d sql:$HOME/.pki/nssdb -A -t C,, -n mitmproxy -i ~/.mitmproxy/mitmproxy-ca-cert.pem。Windows 下安装到“受信任的根证书颁发机构”后重启 Chrome。如果用的是 snap 版 Chrome证书路径不同建议换用 apt 或 deb 安装的版本。4.3 替换脚本不生效但日志显示命中现象mitmproxy 打印了替换日志但页面依然跳登录页。原因目标站点的检测逻辑不止一处或者 JS 文件有多个版本比如 CDN 缓存、不同地区返回不同文件。你替换了common_pdd但实际生效的是另一个文件。解决用 fiddler 或 Chrome 开发者工具抓全部 JS 请求逐个搜索webdriver。把所有包含关键字的 JS 路径都加到替换脚本的匹配条件里。另外注意有些站点会把检测逻辑内联在 HTML 的script标签里这种情况需要拦截 HTML 响应做替换而不是 JS 文件。4.4 chromedriver 版本与 Chrome 不匹配现象SessionNotCreatedException: This version of ChromeDriver only supports Chrome version XX。原因Chrome 自动更新后版本号变了chromedriver 还是旧版。解决每次 Chrome 更新后去 chromedriver 下载页找对应版本。可以写个脚本自动检测本地 Chrome 版本并下载匹配的 chromedriver但最简单的方法是关闭 Chrome 自动更新或者用固定版本的 Chrome 做爬虫专用环境。4.5 替换后仍被检测CDP 层面的对抗现象JS 替换成功navigator.webdriver也搜不到了但访问几次后还是被限制。原因对方可能还在检测 CDP 的其他特征比如Runtime.enable导致的console.debug重写、Page.addScriptToEvaluateOnNewDocument注入的脚本痕迹等。这类检测属于高级对抗mitmproxy 替换 JS 解决不了。解决这种情况需要更底层的方案比如用 undetected-chromedriver 或 playwright 的 stealth 模式或者直接放弃浏览器自动化改用请求库逆向接口。但根据我的经验大部分中小型站点不会做到这个程度mitmproxy 替换已经能覆盖八成场景。5. 进阶技巧把替换逻辑做成可配置的通用方案上面写的脚本是硬编码 URL 和替换词换个站点就要改代码。实际项目中我一般会做成配置化把匹配规则和替换对放在外部文件里mitmproxy 启动时加载。这样同一套代理链路可以服务多个爬虫任务。配置文件用 JSON{ rules: [ { url_pattern: /_next/static/js/common_pdd, replace_from: navigator.webdriver, replace_to: navigator.userAgent }, { url_pattern: /static/js/anti_bot, replace_from: webdriver, replace_to: webDriver } ] }脚本改成读取配置import json from mitmproxy import http with open(rules.json, r) as f: config json.load(f) def response(flow: http.HTTPFlow) - None: for rule in config[rules]: if rule[url_pattern] in flow.request.url: if rule[replace_from] in flow.response.text: flow.response.text flow.response.text.replace( rule[replace_from], rule[replace_to] ) print(f[命中规则] {rule[url_pattern]} - {flow.request.url})这样新增站点只需改 JSON不用动 Python 代码。参数说明url_pattern用子串匹配够用且简单如果要做正则匹配把in换成re.search即可。replace_from和replace_to支持任意字符串对但注意替换后的词不能是 JS 保留字或已存在的关键属性否则可能引发新问题。还有一个技巧在替换前先判断响应类型。有些请求返回的是图片或二进制流flow.response.text会解码失败。加一层判断if text in flow.response.headers.get(Content-Type, ): # 执行替换这样避免对非文本响应做无意义操作。验证配置化方案是否生效我习惯用一个小脚本批量请求几个已知会触发检测的页面统计跳转登录页的比例。替换前跑一遍替换后再跑一遍对比成功率。这个习惯是从那次翻车之后养成的——每次改完代理规则都强制走一遍验证流程确认数据能稳定拿到再放到生产环境跑。希望这些经验帮到你少走几个我踩过的坑。本文还有配套的精品资源点击获取