简介:这份PDF资料聚焦selenium配合chromedriver在爬虫实战中被目标站点识别并反爬的典型问题,面向已有一定Python爬虫基础、正在使用或准备使用selenium的开发者。内容源于爬取某夕夕商城时的真实遭遇:原本稳定的selenium+chromedriver+mitmproxy方案数日后突然跳转登录页,作者通过同机浏览器对比、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是否存在以及其子对象是否完整。
第三个是 CDP(Chrome 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-features=AutomationControlled")。这些方法在部分场景下有用,但在我这次遇到的站点上全部失效。原因在于:这些参数改的是浏览器启动时的行为,但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.x,chromedriver 版本必须和本地 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_global=false-p 8080指定监听端口,-s加载自定义脚本,block_global=false允许非本机连接(如果 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-server=http://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(options=options) 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", ""): # 执行替换这样避免对非文本响应做无意义操作。
验证配置化方案是否生效,我习惯用一个小脚本批量请求几个已知会触发检测的页面,统计跳转登录页的比例。替换前跑一遍,替换后再跑一遍,对比成功率。这个习惯是从那次翻车之后养成的——每次改完代理规则,都强制走一遍验证流程,确认数据能稳定拿到,再放到生产环境跑。希望这些经验帮到你,少走几个我踩过的坑。
本文还有配套的精品资源,点击获取