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, ): # 执行替换这样避免对非文本响应做无意义操作。验证配置化方案是否生效我习惯用一个小脚本批量请求几个已知会触发检测的页面统计跳转登录页的比例。替换前跑一遍替换后再跑一遍对比成功率。这个习惯是从那次翻车之后养成的——每次改完代理规则都强制走一遍验证流程确认数据能稳定拿到再放到生产环境跑。希望这些经验帮到你少走几个我踩过的坑。本文还有配套的精品资源点击获取

相关新闻

C# WinForm流程图编辑器实战:坐标变换、撤销命令与XML持久化

C# WinForm流程图编辑器实战:坐标变换、撤销命令与XML持久化

简介:这是一份面向 C# WinForms 开发者的流程图设计器完整源码,围绕工具箱建元、画布缩放、图元拖拽缩放、连线跟随、操作撤销及属性调节等高频需求,提供可直接运行的示例与可扩展架构。资源包共 94 个文件,以 53 个 cs 源码文件为…

2026/10/9 16:29:42 阅读更多 →
数据库原理课程资源包使用指南:复习策略与课设避坑全拆解

数据库原理课程资源包使用指南:复习策略与课设避坑全拆解

简介:这是一份面向天津大学数据库应用(原理)课程学习者的完整复习与实战资料包,集合了历年试卷、课程大作业和实验报告,适合正在修读该课程的学生以及需要系统梳理数据库知识、备考或完成课设的读者。压缩包共182个文件…

2026/10/9 16:29:42 阅读更多 →
数据库系统原理课程设计实战:从选题到答辩的完整路线

数据库系统原理课程设计实战:从选题到答辩的完整路线

简介:这份数据库系统原理课程设计报告面向计算机相关专业学生与数据库初学者,围绕一家批发企业的信息化管理需求展开,完整呈现从需求分析到数据库运行维护的设计流程。报告以商品、订单、零售商、供应商和商品类型五类核心信息为主线&#xf…

2026/10/9 16:29:42 阅读更多 →

最新新闻

SQL Server误删数据恢复实战:事务日志与ApexSQL Log解析

SQL Server误删数据恢复实战:事务日志与ApexSQL Log解析

简介:ApexSql Log是一款面向SQL Server 2005/2008/2012的数据库日志恢复工具,专门解决因误删、误改或事务日志异常导致的数据丢失问题,适合数据库管理员、开发人员以及运维人员作为应急修复手段。该绿色破解版压缩包约11.13MB,包含…

2026/10/9 17:32:46 阅读更多 →
MOBA的M是什么意思?从五个字母拆解多人在线竞技游戏的核心机制

MOBA的M是什么意思?从五个字母拆解多人在线竞技游戏的核心机制

1. 从五个字母说起:MOBA到底是个什么东西第一次听到“MOBA”这个词,很多人脑子里冒出来的第一个问题就是:这五个字母到底代表什么?尤其是那个打头的“M”,看着眼熟,但真要说出个所以然来,又有点…

2026/10/9 17:32:46 阅读更多 →
大模型训练全流程实战:从预训练到PPO对齐的完整指南与TaoToken接入

大模型训练全流程实战:从预训练到PPO对齐的完整指南与TaoToken接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 17:32:46 阅读更多 →
SAP数据归档SARA实战:从配置到避坑,解决数据库膨胀与性能下降

SAP数据归档SARA实战:从配置到避坑,解决数据库膨胀与性能下降

简介:这份资源聚焦SAP数据归档工具SARA的实操讲解,面向SAP运维、BASIS顾问及需要优化数据库性能的IT管理员。内容围绕采购订单归档展开,涵盖创建PO、Write阶段维护归档变式、Deletion阶段标记与删除、SM37作业状态监控,以及归档对…

2026/10/9 17:32:46 阅读更多 →
牛津词典结构化数据:Excel+SQL版英语词典落地指南

牛津词典结构化数据:Excel+SQL版英语词典落地指南

简介:本资源是《牛津英语词典》的结构化电子版,专为语言学习者、数据处理初学者及Python/Java等开发者设计,解决传统PDF词典难以检索、编辑与集成开发的问题。压缩包含2个核心文件:words.xls(Excel格式词典&#xff0c…

2026/10/9 17:32:46 阅读更多 →
JSP+Servlet手写成绩管理系统:从MVC架构到数据库设计与部署避坑指南

JSP+Servlet手写成绩管理系统:从MVC架构到数据库设计与部署避坑指南

简介:一套基于JSP与Servlet的Web成绩管理系统课程设计源码,面向计算机专业学生与Java Web初学者,覆盖学生、教师、管理员三类角色的完整业务闭环。学生端支持个人信息维护与成绩查询,教师端提供成绩录入、修改、查看与分析&#x…

2026/10/9 17:31:45 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →