Python Selenium 反检测指南:如何绕过网站的自动化检测?

你用 Selenium 写了个脚本,本地跑得好好的。换个网站,直接被拦了。不是代码写错了,是网站知道你在用 Selenium。

如果你还没用过 Selenium,先看上一篇 👉 《Python Selenium 自动化入门》


网站怎么发现你的?

navigator.webdriver

最蠢也最有效的检测。Selenium 启动的 Chrome 会把 navigator.webdriver 设成 true,正常浏览器是 undefined。网站检测就一行:

if (navigator.webdriver) {
    // 拜拜了您嘞
}

ChromeDriver 的痕迹

ChromeDriver 启动时会往浏览器里塞一些内部变量,比如 document.$cdc_ 开头的东西。正常浏览器里不会有。

浏览器指纹

正常用户的指纹是自洽的。Windows 用户的 UA 里有 "Windows NT",navigator.platform 是 "Win32",插件列表不为空,屏幕分辨率是 1920×1080 这种常见值。而 Selenium 默认启动的浏览器呢?无头模式下 navigator.plugins 是空的,platform 可能和 UA 对不上。一眼假。

行为分析

鼠标瞬移到按钮上点击,每次操作间隔一模一样,从来不滚动页面。人不会这样操作,机器会。前三种是静态检测,技术上能绕过。最后一种是行为检测,得靠模拟真实操作,难度最高。


三种方案,怎么选

方案一:stealth.min.js 注入

来自 puppeteer-extra-plugin-stealth(7.2k+ ⭐),16 个反检测模块,覆盖 navigator.webdriverchrome.runtime、插件列表、语言、权限、窗口尺寸等几乎所有静态检测点。通过 CDP 注入:

with open("stealth.min.js", "r") as f:
    stealth_js = f.read()

driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": stealth_js})

覆盖全,可控性强,和原版 Selenium 完全兼容。缺点是需要自己生成 stealth.min.js 文件(下一节讲怎么生成)。

方案二:undetected-chromedriver

undetected-chromedriver 走了另一条路。它直接改 ChromeDriver 的二进制文件,把 $cdc_ 这些特征字符串抹掉。

import undetected_chromedriver as uc
driver = uc.Chrome()

两行代码,不用管什么 stealth.js。但它只处理 ChromeDriver 层的特征,浏览器指纹不管。

方案三:SeleniumBase UC 模式

SeleniumBase 在 undetected-chromedriver 基础上包了一层,反检测能力更强一些。

from seleniumbase import SB
with SB(uc=True) as sb:
    sb.driver.uc_open_with_reconnect("https://example.com", 5)

但 API 换了一套,不是标准的 Selenium 写法。

对比

stealth.jsundetected-chromedriverSeleniumBase UC
覆盖检测点✅ 16 个模块⚠️ 仅 ChromeDriver 特征✅ 较全面
自定义灵活度
和标准 Selenium 兼容⚠️ API 有差异⚠️ 自己的 API
适合需要精细控制的项目快速验证中等复杂度

我们的 Rainyun-Qiandao 签到项目 选了方案一。因为要给每个账号生成独立指纹,这个需求只有 stealth.js + 手动注入能做到。


stealth.min.js 怎么来的?

这个文件大概 180KB,包含 16 个反检测脚本。不是手写的。stealth 插件本来是给 Puppeteer 用的,浏览器打开页面时,插件通过 page.evaluateOnNewDocument() 注入反检测脚本。而 extract-stealth-evasions 这个工具做了一件事:evaluateOnNewDocument 替换成一个假的,只录不跑。

// 猴子补丁:把"执行"换成"录音"
page.__proto__.evaluateOnNewDocument = function(func, args) {
  scripts += '(' + func.toString() + ')(' + JSON.stringify(args) + ');\n'
}

流程:启动 Puppeteer + stealth 插件 → 替换 evaluateOnNewDocument → 打开空白页触发插件 → 16 个脚本全被假方法截获写进 scripts 变量 → 关浏览器(白开了,就为触发插件) → Terser 压缩 → 输出 stealth.min.js。一行命令生成:

npx extract-stealth-evasions

生成出来的是一段纯 JS,虽然从 Puppeteer 生态里提取的,但通过 CDP 塞给 Selenium 驱动的 Chrome 一样能跑。


指纹伪装

抹掉 navigator.webdriver 只是第一步。10 个自动化任务用同一套指纹跑,网站一样能把你关联起来。我们在签到项目里做了账号专属指纹:基于账号 ID 生成确定性的参数,同一账号每次跑指纹一致,不同账号各不相同。

user_agent = get_random_user_agent(account_id)
options.add_argument(f"--user-agent={user_agent}")

fingerprint_js = generate_fingerprint_script(current_user)
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": fingerprint_js})

伪装最重要的一点是自洽。UA 写的是 Windows,navigator.platform 返回 Linux,比不伪装还可疑。要一起动的参数:

参数要求
User-Agent和 platform、版本号对得上
navigator.platform和 UA 里的系统匹配
屏幕分辨率别用奇怪的数值
navigator.languages和地区一致
CPU 核心数、内存别填 0

几个容易踩的坑

stealth.js 注入了还是被检测。 大概率是在 driver.get() 之后才注入的。Page.addScriptToEvaluateOnNewDocument 要在访问目标网页之前调用,不然第一个页面加载时脚本还没生效。

无头模式更容易被检测。 确实。--headless=new 比旧版好,但不完美。如果过不去,先用有头模式跑一次,确认是无头的问题还是别的原因。

指纹做得再好,IP 不行也白搭。 同一个 IP 短时间大量请求,什么指纹都救不了你。多账号场景建议一号一 IP。

每次都重新登录容易触发风控。 把 Cookie 存下来复用,能省很多事。


这些技术在我们的开源项目里全部有落地:stealth.js 注入、账号指纹、Cookie 缓存、代理 IP 轮换 👉 Rainyun-Qiandao:雨云全自动签到

想了解验证码是怎么自动破解的? 👉 《雨云签到验证码怎么破?2026最新自动识别方案》


⚠️ 免责声明:本文介绍的反检测技术仅供学习浏览器自动化和网络安全攻防原理。请勿将其用于绕过网站安全措施进行未授权操作,使用时请遵守目标网站的服务条款。