
先说一个自己踩过的场景。前阵子我帮朋友调一个Selenium脚本任务很简单定时从一个信息公开页面拉取列表数据本地跑了几十次都没问题但换到一台长期在线的主机上跑了不到半小时页面就开始弹滑块验证再往后干脆直接返回一个“访问过于频繁”的空白页。当时我第一反应是访问频率太快把每个请求之间又加了好几秒延时重跑之后照样被拦。反复试了很多次才反应过来真正的问题不是访问节奏而是这个Selenium实例本身带的“自动化特征”早就被对方识别了后面无论怎么放慢速度识别结果都不会变。这篇文章就想把这件事说透Selenium为什么会被检测网上流传的各种“加一段JS就能绕过”的脚本到底哪句靠谱、哪句纯属玄学以及一套我实际验证过、能落地到具体项目的规避思路。如果你正在学爬虫或者在做自动化测试时遇到“脚本好好的但目标系统就是不买账”的情况这篇应该能帮你少走不少弯路。1. 被反爬拦下之前先搞清楚网站到底在检测什么1.1 navigator.webdriver几乎所有反爬系统都会先看这个属性只要用Selenium打开一个页面在控制台里执行navigator.webdriver大概率会返回true。这个属性是W3C WebDriver规范里定义的标准字段目的很纯粹让网站知道自己正被自动化工具控制。正常的用户浏览器里navigator.webdriver的值是undefined。而Selenium、Puppeteer、Playwright这些自动化框架在建立浏览器会话时就会把这个标志置成true。对反爬系统来说这个判断零成本、极稳定、几乎没有误伤所以几乎是所有检测方案的“入门门槛”。反爬系统通常不会只查这一个点但它的优先级很高。很多网站的请求处理流程是这样的先读取navigator.webdriver如果发现是true直接进高风险流程如果不是再继续查其他指纹特征。所以规避检测的第一步就是把这个属性从页面环境里“擦掉”。不过这里要提前泼一盆冷水只改掉navigator.webdriver远远不够。真实的反爬系统很少单点判断它会把几十项特征汇总成一个分数超过阈值就触发验证码或封禁。所以在动手改代码之前先把自己环境里的“暴露面”完整看一遍。1.2 除了webdriver还有一堆指纹细节运行一个裸Selenium浏览器和打开一个真实的Chrome窗口在指纹层面差异很大。拿几个最常见的检测点来说User-Agent默认的Selenium窗口UA里如果走无头模式会有HeadlessChrome字样即使是有头模式也可能因为浏览器版本差异和你声称的系统平台对不上。navigator.plugins 和 navigator.languages真实Chrome的plugins列表里至少有几个内置插件项languages反映了操作系统的语言环境。而自动化浏览器里这些数据可能为空或不完整。window.chrome 对象普通Chrome窗口里存在chrome对象且包含runtime、loadTimes等属性部分自动化环境下这个对象缺失或为空。浏览器控制台日志和Console API行为某些反爬脚本会往控制台打印一个测试字符串如果执行环境被自动化工具污染行为会偏离预期。Canvas/WebGL 指纹正常浏览器的GPU渲染参数和自动化浏览器存在细微差异检测方可以通过canvas绘图结果生成一个哈希值来比对。这些特征单看任何一个都不致命但组合起来就很有辨识度。用一个表格说明一下常态差异检测项普通Chrome用户裸Selenium环境navigator.webdriverundefinedtrueUser-Agent正常Chrome字符串可能含HeadlessChrome或版本与平台矛盾navigator.plugins.length5左右0window.chrome对象完整可能undefined或残缺鼠标/键盘事件序列自然、有停顿机械、点击轨迹异常所以真正务实的思路不是“解决某一个问题”而是把整套环境的指纹协调一致让它看起来像一个真实、可信的浏览器会话。1.3 先学会查看自己环境的“暴露面”规避检测的第一步不是改代码而是先知道当前环境到底暴露了多少特征。我自己习惯的做法是在本地起一个简单的HTML检测页把所有常见检测项全部输出到页面上然后用Selenium打开逐项对照。检测页的JS逻辑大致是这样const result {}; result.userAgent navigator.userAgent; result.webdriver navigator.webdriver; result.languages JSON.stringify(navigator.languages); result.plugins Array.from(navigator.plugins).map(p p.name).join(, ); result.chrome typeof window.chrome; result.headless navigator.userAgent.indexOf(HeadlessChrome) 0; document.getElementById(result).textContent JSON.stringify(result, null, 2);拿到这份输出后你就能清楚看到UA是否正常webdriver是不是依然为trueplugins是否为空chrome对象到底存不存在。后面所有优化本质上都是在和这份清单“对答案”。2. 网上那些“加一段JS就能过检测”的脚本为什么很多不灵2.1 只改User-Agent最典型的伪规避网上很多教程一上来就教options.add_argument(user-agentxxx)好像只要把UA改成Chrome就万事大吉了。这个做法不能说完全没用但它的作用极其有限。反爬系统做身份判断时讲究的是“交叉验证”。比如你的UA说自己是Windows Chrome 120但navigator.platform返回的却是Linux或者navigator.plugins.length是0这些矛盾一出现检测方几乎立刻能断定这个环境是伪造的。说白了只改UA就像穿了一件别人的外套但裤子鞋子都还露着。真正的问题在于UA只是众多指纹里最容易改的一项真实浏览器里UA、platform、language、插件列表、渲染参数是相互关联的一套整体。只改其中一项反而制造了更多矛盾。2.2 页面加载完之后再改属性改了也白改还有一个流行错误是先把页面打开再用driver.execute_script去改属性driver.get(https://example.com) driver.execute_script(Object.defineProperty(navigator, webdriver, {get: () undefined}))这段代码的问题是执行时机太晚了。页面一旦开始加载HTML里的脚本就已经在读取navigator.webdriver你回头再改人家已经记下了原始值。而且很多反爬脚本在document_start阶段就把特征数据上报给了后端后面你本地改得再干净后端拿到的还是污染数据。正确的做法是在文档创建之前就把补丁装进去。Chrome提供了一条路径通过DevTools协议CDP注册一个“新文档创建即执行”的脚本这个脚本会在页面任何脚本运行之前把navigator.webdriver等属性改写掉。2.3 只改部分特征制造了一堆矛盾指纹还有一种情况是看到网上代码复制过来就跑也没理解它在干什么。比如只把navigator.webdriver改成undefined但UA还是原来的plugins还是空的chrome对象也还是残缺的。结果就是检测系统发现了个奇怪组合一个“不是自动化”的浏览器却长着一张远程机器常见配置的脸。这种半吊子状态比什么都不改更容易被标记。反爬系统对“尝试掩盖但没盖全”的请求往往给予更高风险评分因为正常用户不会做出这种操作。所以在规避检测这件事上一个朴素的真相是要么不做要做就做全套。下文第三部分给的就是一套“尽量做全套”的方案不一定覆盖所有盲区但至少能让你在绝大多数常见检测系统下通过率大幅提升。3. 一套能落地的规避方案3.1 先配好浏览器启动参数优化前先把地基打好这里是我整理的一组比较实用的Chrome启动参数from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) options.add_argument(--no-first-run) options.add_argument(--no-default-browser-check) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False)逐个解释一下--window-size1920,1080固定窗口尺寸避免无头或小窗环境下布局和字体渲染的差异。--disable-blink-featuresAutomationControlled这是最关键的一项它会关闭Chromium的自动化控制标记顺带消除navigator.webdriver的一部分明显特征。--disable-gpu在服务器场景下减少渲染异常但不是必须某些GPU环境反而需要让它正常工作。excludeSwitches[enable-automation]去掉Chrome右上角“Chrome正受到自动测试软件控制”的提示条也能减少一部分特征。useAutomationExtensionFalse禁用自动化扩展。这几项组合起来属于“基础防护”。单独靠它们不够但它们能帮你在后续CDP注入时少一些需要处理的干扰项。另外提醒一点无头模式能不用就不用。无头Chrome在UA、指纹、DOM行为上和有头模式存在不少可检测差异实测下来通过率远低于有头模式。如果服务器实在没有显示环境优先考虑安装xvfb这类虚拟显示方案而不是直接headlessTrue。3.2 核心用CDP在文档创建前注入反检测脚本接下来是整套方案的核心通过CDP的Page.addScriptToEvaluateOnNewDocument让反检测代码在页面每个新文档创建的瞬间就执行。代码示例js Object.defineProperty(navigator, webdriver, { get: () undefined }); Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5] }); Object.defineProperty(navigator, languages, { get: () [zh-CN, zh, en] }); window.chrome window.chrome || { runtime: {}, loadTimes: function() {}, csi: function() {} }; driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, {source: js})为什么这套代码比driver.execute_script强因为它注册的是一个“未来每个页面创建时都会执行的脚本”在页面任何业务逻辑运行之前就生效了。反爬脚本读到navigator.webdriver时看到的已经是被改写后的值。有一个细节值得展开navigator.plugins在真实浏览器里是一个PluginArray对象而不是普通数组。这里写一个长度5的数组目的是骗过“plugins为空”这类检测。更严谨的写法是构造一个带length和item方法的对象但实际使用中普通数组已经能通过大多数检测。如果你的目标网站会遍历插件名可以改成const fakePlugins [ {name: Chrome PDF Plugin, filename: internal-pdf-viewer}, {name: Chrome PDF Viewer, filename: mhjfbmdgcfjbbpaeojofohoefgiehjai}, {name: Native Client, filename: internal-nacl-plugin} ]; Object.defineProperty(navigator, plugins, { get: () fakePlugins });这段补丁只改写了几个最常见的检测点实际使用中可以根据目标站的检测方式增减。关键是理解机制而不是背代码。3.3 行为层让操作节奏像人指纹改完之后还有一个大项行为特征。哪怕浏览器指纹再像真人如果脚本的运行节奏像个机器人检测系统依然可以靠统计模型把你识别出来。举个最直观的例子真人打开页面后是先看内容、慢慢滚动再偶尔点击而自动化脚本通常是“秒开页面、立即定位元素、瞬间点击”。这个时间差本身就是强信号。我的做法通常有三条随机延时每次操作之间加random.uniform(0.5, 2.5)秒不要让间隔固定。模拟滚动不要用driver.execute_script(window.scrollTo(0, document.body.scrollHeight))一步到底而是分几次scrollTo每次滚动random.randint(300, 700)像素中间停顿。用ActionChains模拟鼠标轨迹点击按钮前先用move_by_offset移动到目标附近再加一点随机偏移然后点击。不过这里也要有取舍。如果你只是在做数据采集访问的页面结构固定、操作简单那么花大力气做完整的鼠标轨迹模拟性价比不高。很多反爬系统对“非核心业务页面”其实不会投入全套行为检测只有到了登录、支付等高价值页面行为模型才会比较严格。所以行为模拟要做但做到什么程度取决于目标页面本身的保护级别。3.4 selenium-stealth这类工具能不能用除了自己写补丁社区里还有一个常用的库叫selenium-stealth可以通过pip install selenium-stealth安装。它的原理和我上面写的CDP注入类似只不过把补丁封装成了一套现成函数甚至还处理了WebGL的厂商信息、console输出等更细的维度。使用示例from selenium_stealth import stealth stealth(driver, languages[zh-CN, zh, en], vendorGoogle Inc., platformWin32, webgl_vendorIntel Inc., rendererIntel Iris OpenGL Engine, fix_hairlineTrue)我自己的看法是这类工具适合快速验证但不适合直接拿到生产环境长期用。原因有两个。第一它的适配往往滞后于Chrome和chromedriver的版本迭代Chrome一升级可能就失效第二因为它太出名、太好用反爬系统只要检测出这个库的典型补丁痕迹就能反过来标记你。相比之下自写注入脚本更可控虽然一开始要花点时间调试但维护起来心里有底。如果你想用现成工具建议先用我前面说的检测页把它实际产生效果验证一遍再决定要不要长期依赖。4. 完整Demo从“被识别”到“通过检测”的实测记录4.1 准备一个可以复现的检测页为了不让测试依赖于某个外部站点我建议你本地建一个最小检测页。把下面的HTML保存成detect.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlewebdriver-detection-test/title /head body h1Detection Test/h1 pre idresult/pre script const res {}; res.userAgent navigator.userAgent; res.webdriver navigator.webdriver; res.languages JSON.stringify(navigator.languages); res.plugins Array.from(navigator.plugins).map(p p.name).join(, ); res.chrome typeof window.chrome; res.headless navigator.userAgent.indexOf(HeadlessChrome) 0; document.getElementById(result).textContent JSON.stringify(res, null, 2); /script /body /html然后用python -m http.server 8000在对应目录下起一个静态服务本地访问http://127.0.0.1:8000/detect.html就能看到检测结果。这个方案的优点是可复现、稳定而且你可以在自己控制的环境里反复测试不需要担心影响第三方站点。4.2 通过前后对比完整Python代码先看裸Selenium的效果from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options Options() options.add_argument(--window-size1920,1080) driver webdriver.Chrome(serviceService(ChromeDriverManager().install()), optionsoptions) driver.get(http://127.0.0.1:8000/detect.html) result driver.find_element(id, result).text print(result) driver.quit()输出的webdriver字段大概率是trueplugins为空chrome是undefined。再用优化后的方式import time import random from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options Options() options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) options.add_argument(--no-first-run) options.add_argument(--no-default-browser-check) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(serviceService(ChromeDriverManager().install()), optionsoptions) js Object.defineProperty(navigator, webdriver, { get: () undefined }); Object.defineProperty(navigator, plugins, { get: () [ {name: Chrome PDF Plugin, filename: internal-pdf-viewer}, {name: Chrome PDF Viewer, filename: mhjfbmdgcfjbbpaeojofohoefgiehjai}, {name: Native Client, filename: internal-nacl-plugin} ] }); Object.defineProperty(navigator, languages, { get: () [zh-CN, zh, en] }); window.chrome window.chrome || { runtime: {}, loadTimes: function() {}, csi: function() {} }; driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, {source: js}) driver.get(http://127.0.0.1:8000/detect.html) time.sleep(random.uniform(1, 2)) result driver.find_element(id, result).text print(result) driver.quit()这时输出应该符合预期webdriver是undefinedplugins有三个条目languages是[zh-CN,zh,en]chrome是object。到这一步常见的指纹型检测基本都能过关。4.3 反复被识别时的排查步骤如果你的脚本在加了这些补丁之后仍然会被识别大概率是下面几个原因Chrome版本与chromedriver版本不匹配。Selenium对版本很敏感版本不匹配会触发一些异常启动参数。用webdriver-manager可以减少这类问题但如果连这个都出现异常优先检查Chrome是否自动更新过。CDP注入顺序不对。execute_cdp_cmd必须在driver.get()之前调用一旦页面加载完成再注册脚本就只对后续跳转生效当前页面依然暴露。指纹内部矛盾。比如UA说是Windows但navigator.platform返回Mac或者你改了webdriver但navigator.plugins.length是0。这种交叉矛盾是反爬系统重点关注的信号。行为痕迹太明显。如果页面有行为级检测点击间隔、轨迹、滚动速度都会暴露你。这类问题的解决方式不是改指纹而是改操作节奏。排查时我习惯遵循这个流程先在本检测页确认指纹已干净再用目标页面访问并打开Chrome远程调试端口在chrome://version和DevTools里观察当前会话的UA、平台、插件信息是否一致。如果目标站的检测代码更严格还可以在无痕模式下对比正常运行和脚本运行的差异不断缩小变量范围。5. 真要放到生产环境前先想清楚边界和使用场景5.1 自动化测试场景规避检测是用来验证自家反爬强度的如果你是开发者写这套东西的初衷大概率是在做自动化测试。比如你自己搭了一个需要登录的管理后台写Selenium脚本做回归测试结果发现连自己的系统都把你当成攻击者拦了。这种情况下规避检测不是目的让CI流程能顺利跑完才是目的。换个视角看如果你维护的网站有反爬需求你其实可以主动用Selenium脚本模拟攻击者的手段去测试自己的防护策略是否会误伤内部测试机器人。我见过不少团队的反爬策略把自家测试账号都封了最后排查半天才发现是navigator.webdriver的锅。这类问题越早发现越好写一遍测试脚本往往能帮你提前排除掉很多反爬策略的误判逻辑。5.2 数据采集场景公开数据不等于可以无限索求如果你的目的是抓公开数据有一个原则需要反复强调公开可访问不等于可以无限量索取。一个有基本修养的爬虫项目至少应该做到以下几点先检查目标站点是否有robots.txt尊重对方声明的访问边界。控制并发和访问频率不要对目标服务器造成实质性压力。能调官方API优先用API没有API再考虑页面采集。采集到的数据不要二次售卖尤其是涉及个人隐私的数据。Selenium本身是一个重型工具每个页面都要拉起一个完整浏览器实例资源开销远高于直接发HTTP请求。如果你只是抓公开普通页面优先考虑requests配合解析库既轻量又不容易引发争议。Selenium更适合需要渲染JS、需模拟登录、或必须通过复杂点击流程才能拿到数据的场景。5.3 我的建议先看目标站点是否允许自动化访问在投入大量成本去“规避检测”之前先花五分钟判断一下这个站点是否值得你这样做。有些站点的条款明确禁止自动化访问这时你再去逆向它的检测逻辑风险敞口会大很多。还有一些内容需要登录、付费、或处于明确鉴权之后才能访问这种场景下强行绕过性质就完全变了。我的经验是把规避检测技术用在“自己拥有控制权”或“对公众开放且没有明确禁止自动化”的页面上永远是最稳妥的。其余的宁可多查查官方文档看看是否有更体面的接入方式。另外如果只是日常写脚本建议在本地保留一个像我上面那样的最小检测页每次Chrome升级之后先跑一遍确认当前脚本补丁是否失效。这个习惯能帮你省掉很多定位环境问题的时间。规避检测这件事本质上是在和浏览器的版本迭代赛跑谁掌握了自我检查的能力谁就能在这个对抗中少受折磨。