资讯动态

Selenium反检测进阶:undetected_chromedriver无痕自动化实战

发布时间:2026/10/5 6:59:02 来源:尧图企业网站定制
从一次深夜崩溃说起我的爬虫在登录页面前反复被“语义验证”拦下。换IP、清缓存、伪装UA用户代理标识折腾一晚上最后打开抓包工具一看对方明明给了200响应可页面内容永远停在“点击验证”的滑块上。那时候我就意识到普通的Selenium方案走到头了。后来换成undetected_chromedriver配合正确的等待和交互节奏同一个脚本在同样的IP下直接通过连滑块都没弹出。这篇文章想把这段从“被识别”到“无痕化”的完整过程写清楚。想解决什么问题就是当你用Selenium做自动化时网站通过浏览器指纹、CDP特征、行为模型等方式把非真人流量识别出来的问题。适合两类读者一类是正在被反爬虫机制卡住的Python爬虫开发者另一类是做自动化测试、希望测试环境更贴近真实浏览器的工程师。我会从检测原理、undetected_chromedriver的底层机制、环境搭建、交互模拟到真实拦截案例的排查链路一条条拆开讲尽量让没有任何前置经验的读者也能照着落地。1. 为什么普通Selenium会被网站一眼识破1.1 浏览器指纹里的“机器人特征”普通Selenium被识别几乎不是因为你代码写得不好而是因为它在浏览器底层暴露了一个无法隐藏的“身份标记”——navigator.webdriver属性。这个属性在默认的ChromeDriver里总是返回true而正常用户使用Chrome浏览器时这个值应该是不存在或者是false的。网站端的反爬虫JS脚本根本不需要多复杂只要执行一行if (navigator.webdriver) { // 标记为机器人 }就能把绝大多数自动化流量筛掉。你可能会想那我用execute_script把navigator.webdriver直接改成false不就行了问题的关键是浏览器有Object.defineProperty的保护机制直接在页面上下文中修改这个属性很多情况下会触发更高级的检测逻辑比如检查属性描述符中configurable是否为true。反爬脚本会遍历所有属性描述符发现异常直接升级拦截策略。这也是为什么各种“硬改UA、硬改webdriver”的土办法在最开始有效后面越来越不管用的根本原因。1.2 网站反爬虫的检测维度不止一个即使你解决了webdriver标志普通Selenium依然活不过风险评估原因在于检测维度是立体多维的。我梳理过比较常见的检测维度整理成了一张表检测维度检测方式普通Selenium的表现浏览器原生属性检查navigator.webdriver、window.chrome等webdriver为truewindow.chrome对象可能缺失或异常CDPChrome DevTools Protocol特征检测Runtime.enable、Page.enable等协议调用痕迹存在明显的自动化协议调用特征浏览器指纹检测Canvas指纹、WebGL渲染参数、字体列表、时区等与真实浏览器环境不一致或指纹过于新鲜无浏览历史、无扩展行为模型分析鼠标轨迹、键盘输入间隔、点击坐标分布鼠标事件缺失输入间隔均匀坐标过于精确TLS/网络指纹服务器端分析TLS握手包、HTTP头顺序、Ja3指纹与浏览器真实网络栈特征不符服务器端的JA3/TLS指纹检测是最容易被忽略的因为它在应用层完全看不出来但服务端拿到TCP层的握手特征后可以和真实的Chrome浏览器网络栈做对比。普通的ChromeDriver走的是系统网络协议栈而真实Chrome有自己内置的网络栈处理逻辑两者在指纹上存在可检测的差异。这意味着有些时候即使浏览器端页面正常渲染请求到了服务端依然可能在风控引擎那里被直接打回。1.3 为什么普通Selenium方案越写越复杂却依然容易失效很多团队在普通Selenium的基础上叠加了各种“反检测插件”——有修改navigator属性的、有注入JS的、有打乱点击坐标的。我能理解这种思路毕竟在一套熟悉的工具链上打补丁比更换方案成本低。但这类补丁方案有一个共同的致命弱点检测逻辑一旦更新补丁代码可能立马失效而且你并不知道是哪一行不生效了。更关键的是普通Selenium本身是“显式自动化”——它通过ChromeDriver与浏览器通信而ChromeDriver是基于Chrome DevTools Protocol实现的。CDP协议在设计之初是为调试服务的不是为“隐藏自动化痕迹”服务的。只要网站拿到了CDP的连接信息它就能分析出你使用了自动化控制。而补丁方案只能解决浏览器侧的特征修改解决不了协议层的暴露。这也是我后来彻底转向undetected_chromedriver的直接原因只有把底层特征问题解决掉上层代码才有意义。2. undetected_chromedriver到底动了什么手脚2.1 自动patch ChromeDriver的过程undetected_chromedriver的核心不是增加了多少“隐身”参数而是它会在启动时自动对ChromeDriver做patch处理。这个patch动作包括几个关键步骤它会定位你系统中的Chrome浏览器版本下载或复用对应版本的ChromeDriver二进制文件。在启动ChromeDriver之前它会修改ChromeDriver内部的某些默认参数和JS特征注入逻辑让它生成的浏览器实例不再暴露navigator.webdriver等自动化标记。它还会替换一些关键的JS文件或注入预运行脚本在页面加载的早期阶段就完成底层特征的伪装。这个过程中最重要的是时序控制。普通的浏览器初始化过程里navigator.webdriver属性在页面脚本执行之前就已经被设定了。undetected_chromedriver的patch机制会在浏览器启动的最早期、甚至在加载任何网页之前就把这些特征清理掉从而避免网站在任何生命周期阶段看到自动化标志。2.2 和普通ChromeDriver的核心差异抛开玄学直接看它们在行为上的差异。普通ChromeDriver启动的浏览器你可以直接在开发者工具里运行navigator.webdriver返回的是true。而undetected_chromedriver启动的浏览器这个属性是完全不存在或为false的。第二点差异在WebDriver的“活动状态”上。普通Selenium在自动化期间document的readyState和异步任务的执行进度可能被风控脚本实时监测。undetected_chromedriver则在驱动层面做了噪音处理让浏览器的响应模式更接近人工操作。第三点是对window.chrome、navigator.plugins、navigator.languages等常规浏览器对象的重建。真实Chrome浏览器中这些对象都有非常具体的结构和顺序。undetected_chromedriver在patch时会模拟这些原生结构而不是像普通Selenium那样直接沿用可能被简化的默认配置。有一个很直观的对比我用普通ChromeDriver和一个完全正常Chrome浏览器的指纹做对比常规方案的指纹相似度大概在70%-80%之间而undetected_chromedriver可以做到95%以上剩余的差异主要来自实际显示器分辨率、系统字体这类环境因素反而是真实用户也会存在的正常差异。3. 环境搭建与首次运行让浏览器以“真人”模式打开3.1 安装undetected_chromedriver前的准备在做任何安装之前我建议先把环境梳理清楚。undetected_chromedriver对Python版本没有特别严格的要求3.7以上基本都可以但它很依赖本机的Chrome浏览器版本必须保证Chrome安装正常并且允许通过命令行方式启动。安装不复杂直接用pippip install undetected-chromedriver selenium如果官方源速度不理想可以用国内镜像pip install undetected-chromedriver selenium -i https://pypi.tuna.tsinghua.edu.cn/simple安装过程可能会自动下载对应版本的ChromeDriver如果下载速度慢或者失败你可以在第一次运行后去用户目录下找到.undetected_chromedriver文件夹手动放置对应版本的chromedriver它会识别并加载。需要特别提醒一下Python环境的问题这也是我刚开始踩过的坑。如果你电脑上同时装了多个Python版本确保当前终端的Python解释器和pip是同一个环境否则会出现pip install成功但脚本import不到模块的情况。最简单的验证方法是python -c import undetected_chromedriver; print(undetected_chromedriver.__version__)能正常输出版本号就说明环境没问题。3.2 第一个“无痕”脚本应该怎么写安装完成之后第一个脚本不要急着写复杂逻辑先跑一个最基础的功能测试。我们直接打开一个能检测浏览器特征的页面看看它在反爬JS面前的表现import time import undetected_chromedriver as uc # 创建浏览器实例不需要额外传driver路径 driver uc.Chrome() driver.get(https://example.com) # 等待页面完全渲染 time.sleep(5) # 检查关键特征属性 webdriver_flag driver.execute_script(return navigator.webdriver) print(navigator.webdriver:, webdriver_flag) # 查看userAgent user_agent driver.execute_script(return navigator.userAgent) print(User-Agent:, user_agent) driver.quit()如果一切正常navigator.webdriver应该返回None或者undefined而不是True。注意这里不要直接在Python里用driver.execute_script(return navigator.webdriver)来判断所有情况因为有些反爬页面会在DOMContentLoaded后动态改写这个属性。更稳妥的测试方式是直接在页面上下文中检查属性描述符Object.getOwnPropertyDescriptor(navigator, webdriver)正常情况下返回undefined而普通Selenium返回的是{get: ƒ, set: undefined, enumerable: true, configurable: true}。这一步能帮你确认patch是否真正生效。3.3 无头模式的是与非很多人在跑通有头模式后急着把浏览器设成无头模式想让脚本在服务器上跑。undetected_chromedriver虽然支持headless参数但我强烈建议不要刚上手就开无头模式。原因是无头模式下浏览器渲染路径和真实Chrome有差异比如navigator.webdriver的隐藏虽然能实现但WebGL渲染结果、音频上下文、Canvas指纹、字体枚举等方面和有头模式存在可检测的区别。看看常用的无头模式启动方式options uc.ChromeOptions() options.headless True driver uc.Chrome(optionsoptions)如果你确实需要在服务器上无头运行我建议先在有头模式下把业务逻辑调试通过确认每一步交互都没问题后再切换到无头模式做一次稳定性回归测试。如果无头模式下被拦截优先考虑改用Xvfb这类虚拟显示方案而不是硬调无头参数。我曾在一台CentOS服务器上跑过无头模式同样的代码在有头模式能通过无头模式却总在第二跳被拦截。后来查了一堆资料才发现问题出在navigator.plugins和languages的序列化差异上改为虚拟显示方案后问题才彻底解决。这算是无痕自动化绕不开的一个经验。4. 真人操作模拟的核心细节等待、点击、输入、滚动4.1 等待策略隐式等待是灾难固定等待也不是最优解Selenium的等待策略在普通自动化里可能只是影响稳定性在无痕自动化里直接决定“像不像真人”。很多初学者喜欢用time.sleep(5)这种无脑等待看起来简单实际上有两个问题一是固定等待无法应对网络波动页面没加载完或者提前加载完都会造成后续操作的不确定性二是固定间隔的等待时间如果被服务端记录会形成完美的均匀分布特征这在行为模型中很突兀。推荐的做法是把“条件等待”和“随机微延时”结合。条件等待保证操作时机正确随机微延时让时间轴看起来自然。我一般会封装一个simple_wait函数import random import time from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_and_click(driver, by, selector, timeout10): element WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((by, selector)) ) # 模拟人工点击前的短暂停顿 time.sleep(random.uniform(0.3, 1.0)) element.click()这里的random.uniform(0.3, 1.0)不是随便加的。真实用户在点击前会有认知、移动鼠标、定位、点击这一系列动作耗时通常在几百毫秒到一秒多。无论你多么熟练地操作鼠标这个微延时都是存在的。我在实际项目中习惯再加一层随机漂移让点击时间完全无法形成统计规律。4.2 鼠标轨迹瞬间移动是最大的破绽普通的element.click()是在坐标瞬间跳转后直接点击这在实际浏览器中是几乎不可能发生的。真实用户的鼠标移动轨迹通常是带弧度的曲线中间会有停顿和微调。在风控系统中鼠标轨迹数据是行为模型的输入项之一完美的直线运动轨迹很容易被判定为机器生成。这里可以用Selenium的ActionChains来模拟轨迹移动。但坦白讲ActionChains的move_to_element依然是直线插值方式如果网站风控严格还应该引入Bezier曲线插值算法。我写过一个简化版的曲线移动函数import numpy as np def bezier_curve(start, end, control, steps20): t np.linspace(0, 1, steps) points (1 - t) ** 2 * start 2 * (1 - t) * t * control t ** 2 * end return [(x, y) for x, y in points.astype(int)] def human_move(driver, element): action ActionChains(driver) start_x random.randint(300, 500) start_y random.randint(300, 500) end_x element.location[x] element.size[width] // 2 end_y element.location[y] element.size[height] // 2 control (random.randint(max(0, min(start_x, end_x) - 100), max(start_x, end_x) 100), random.randint(max(0, min(start_y, end_y) - 100), max(start_y, end_y) 100)) for x, y in bezier_curve((start_x, start_y), (end_x, end_y), control): action.move_by_offset(x - start_x, y - start_y) start_x, start_y x, y time.sleep(random.uniform(0.01, 0.03)) action.click().perform()这个函数会生成一条带弧度的贝塞尔曲线轨迹鼠标的移动中途会有微小的速度变化比直线插值真实得多。不过要注意曲线插值本身也会消耗性能在数据量大的批量任务里需要权衡不是所有场景都必须上曲线。4.3 输入节奏别让“打字”变成“机器打字”Selenium的send_keys在不做任何处理时会把整串文本一次性推送到输入框。真实用户打字则有明显的按键间隔长短字母的输入时长不同偶尔还会打错再删除重来。如果网站会记录键盘事件的时间分布一次性的send_keys特征极其明显。核心的解决思路是把整串输入拆成“逐字符输入 随机间隔”并且时而在中间插入一个停顿或删除重输import random import string def human_type(driver, element, text): element.click() time.sleep(random.uniform(0.2, 0.6)) for char in text: element.send_keys(char) # 英文和数字间隔更短中文模拟时稍慢 if char in string.ascii_letters or char in string.digits: time.sleep(random.uniform(0.03, 0.12)) elif char : time.sleep(random.uniform(0.05, 0.2)) else: time.sleep(random.uniform(0.08, 0.25)) # 偶尔模拟打错删除 if random.random() 0.02 and len(text) 6: element.send_keys(Keys.BACKSPACE) time.sleep(random.uniform(0.05, 0.1)) element.send_keys(char) time.sleep(random.uniform(0.03, 0.1))这里的随机间隔设置得很保守不能太快也不能太慢。太快会变成均匀分布太慢则容易触发会话超时。总体原则是让输入时间长度与实际人为输入的时间分布相匹配——一段20个字符的英文文本真实用户输入时间大概在2到4秒之间你的代码总耗时也控制在这个区间。4.4 页面滚动与下拉框处理的真实经验真实用户浏览页面时会滚动页面、暂停浏览、返回、再看。这个行为模式在无痕自动化里非常关键尤其是长页面场景。我的做法是在页面加载后先随机滚动到页面中部等待几秒再滚动到目标元素附近最后才执行点击或输入。滚动的速度可以用ActionChains的scroll_by_amount控制或者直接执行window.scrollTo配合随机间隔。记住不要一进页面就立刻滚动到底部这种“直奔答案式”的滚动很容易被行为模型识别。再单独说说下拉框。很多人的第一反应是用Select类但Select只对原生select标签有效对于热词里提到的divulli组合出来的“伪下拉框”Select完全失效。处理这类元素我的建议是点击展开这个div下拉框然后根据里面的li文本内容找到目标元素后用human_move的方式点击。如果下拉框里的选项是模糊搜索出来的那还需要在点击后等待li列表渲染完整。纯ulli结构的好处是每个选项都是独立元素可以精确点击。如果是带搜索框的伪下拉框还需要先往搜索框里逐字输入关键词等待下拉结果刷新然后再点击目标项。4.5 文件上传这类“反自动化”交互怎么破文件上传是很多自动化脚本的噩梦。普通的上传交互通常有两种解法一种是直接send_keys(file_path)到input typefile元素另一种是通过pyautogui模拟操作系统的文件选择对话框。send_keys方式虽然快但webdriver属性伪装如果不够彻底仍然会被网站检测到。而且如果网站使用了FileReader来读取文件内容还可能会在浏览器端检查文件大小、类型、修改时间等元数据。我推荐用原生input直接赋值加事件触发的方案。先用execute_script获取到文件输入框元素然后把文件路径塞进去并触发change事件。具体代码如下file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) driver.execute_script(arguments[0].style.display block;, file_input) file_input.send_keys(/path/to/file.pdf) driver.execute_script(arguments[0].dispatchEvent(new Event(change, {bubbles: true})))这里关键点是触发change事件来模拟用户选择完文件后的逻辑而不是直接依赖Selenium内部的自动事件。有些场景还需要在文件选择后模拟一段“检查文件、等待预览”的延时再点击确认上传按钮。如果网站前端做了额外的文件类型校验还会有一个校验过程。不要一上来就猛点给渲染留出时间。5. 实战中踩过的坑完整排查链路5.1 案例1出现Google二次验证或人机验证页面有时候脚本跑得好好的突然页面就跳到了验证码页面。这种问题最让人崩溃因为不是必现而是概率性出现。我的排查思路一般是这样第一步先确认是不是账号或IP的问题。换一个IP、换一个没有操作痕迹的账号如果验证码不再出现说明是账户/IP被标记了而不是浏览器特征问题。第二步如果换IP换账号后依然出现那就检查浏览器指纹。打开目标网站前先访问一个指纹检测页面对比本地正常Chrome和undetected_chromedriver的CPU核心数、内存大小、Canvas哈希值。如果差异明显就看是不是操作系统位数、时区、语言配置不一致导致的。第三步如果指纹一致那就要怀疑是执行节奏了。很可能是脚本操作速度远快于真人——比如页面刚加载完就点击人类需要思考的时间被压缩了。在关键操作前加入随机延时再看看概率是否降了下来。在实际项目中我有一次排查到最终发现问题是Chrome不是以用户代理模式启动的--user-agent参数被我手动固定了导致和浏览器版本型号对不上。去掉UA覆盖参数后验证码出现概率从30%直接降到了不到1%。5.2 案例2服务端检测到了自动化连接特征这种问题很隐蔽表现是页面正常打开、内容和真人看到的一样但某些需要登录的接口直接返回异样状态码。最终抓包看到的原因是服务端在请求头里检测到了Accept-Encoding或Connection等头部的不正常组合。普通Chrome的Connection头通常是keep-alive请求头顺序和标准HTTP库有差异。undetected_chromedriver虽然隐藏了webdriver属性但在某些网络环境下如果使用了系统代理或HTTP库改造过请求还是可能漏出马脚。排查链路先关掉所有代理、广告拦截插件纯净启动浏览器去访问目标接口如果正常说明受扩展影响如果依旧异常就打开chrome://net-export记录网络日志对比真实Chrome的连接参数。很多时候这种问题不是因为自动化框架而是因为网络中间层如公司防火墙安全软件修改了TLS指纹。5.3 案例3Cloudflare这类风控服务的JS挑战Cloudflare的JS挑战基本上是最难啃的那一关它会在页面加载初期执行大量JavaScript来验证浏览器环境的完整性。如果脚本速度太快执行时机不对就很容易被判定为可疑流量。应对这种风控除了undetected_chromedriver之外还要注意保持合理的等待时间尤其是等待浏览器执行挑战脚本后再进行下一步操作。我实现过一个简化的通过CF挑战的流程用uc.Chrome()启动浏览器。等待3到5秒先让页面自行渲染。用条件等待检查页面上是否出现iframe[src*challenge]或#challenge-running等标志。如果出现了挑战不要操作鼠标让页面自己完成挑战。CF挑战很多是基于浏览器环境自动通过的如果你去点击元素反而更容易触发二次验证。持续轮询等待直到检测到目标元素出现。这段代码的关键在于“不要干预”的思路。很多时候风控脚本需要一定的执行时间你的干预动作反而会让它觉得异常。另外如果挑战持续超过20秒仍未通过大概率是IP信誉太低换IP比重启浏览器更有效。5.4 踩坑后的教训不要一上来就认为是框架的问题排查这类反检测问题有一个很基础但又容易忽略的认知在正常的自动化开发中你遇到“页面没加载出来”“元素找不到”“点击无效”时你不会第一时间怀疑是webdriver属性被检测了对吗但在无痕自动化这个场景里很多看似普通的页面元素找不到问题、控制台报错、网络请求被重置底层原因真的可能就在浏览器特征上。所以我建议每个项目启动前先做一个基础测试用一个已知可靠的指纹检测网站比如https://bot.sannysoft.com或https://detect.headless.dev跑一次特征扫描把所有异常项记录下来。然后在每次脚本运行前先跑一次扫描对比异常项的变化。这样排查问题时就有一个基准不会每次都被各种表象绕得团团转。6. 边界思考反检测技术的能力边界和合规风险6.1 它能解决什么解决不了什么先把丑话说在前面undetected_chromedriver不是“免死金牌”。它解决的是浏览器客户端的特征暴露问题但解决不了风控系统中完全基于“信誉数据”的判定逻辑。如果你的IP已经被多次用于恶意访问对方风控完全可以不看任何浏览器特征直接对IP进行拦截或鉴权。这种情况下你换一百个浏览器驱动也没用。另外现代风控系统开始大量引入设备指纹库和关联分析。设备指纹会在第一次访问时生成第二次访问时会和之前的指纹进行关联。如果每次启动的浏览器指纹都被改了比如关闭了某些指纹随机化的默认行为那几次访问之间无法形成一致的设备档案反而会被风控系统认为是可疑设备。所以我不建议频繁清理浏览器配置文件或者频繁更换伪造的指纹信息。保持浏览器的指纹一致性比单纯隐藏webdriver属性更重要。6.2 合规提醒自动化技术不能用来做坏事写到这里我想认真说一下边界问题。undetected_chromedriver的本意是让自动化测试更接近真实用户、让合法爬虫更容易完成数据采集。但这项技术同样可以被用来刷接口、抢资源、绕过付费墙这些用途都有明确的法律风险。像爬虫采集如果目标网站明确禁止爬虫或者采集的数据涉及个人隐私不管技术多高明你都可能面临法律纠纷。在自动化方式上请务必控制合理频率设置延时不要对目标服务器造成不必要的负担。把自己的行为控制在“在线人数多一点”这个程度对大家都好。工具是中性的用工具的人要有判断力。能进能退知道什么时候该收手才是真正成熟的工程师。这篇内容围绕undetected_chromedriver和Selenium的无痕自动化从检测原理讲到实操代码再到案例排查链路基本上覆盖了从入门到进阶的完整过程。最后再分享一个我个人的习惯每次写完一段自动化代码都会先用Chrome的隐身模式跑一遍用肉眼观察整个页面生命周期看是否有任何异常的跳转、弹窗或请求重置。因为代码里的细节浏览器控制台里往往比日志更诚实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑