
平常做爬虫和自动化测试的朋友估计都遇到过这种尴尬明明就是用 Selenium 打开浏览器、点几个按钮、填个表单网站却像长了眼睛一样直接弹出一个验证页面或者干脆嘲讽式地提示“检测到异常访问”。更离谱的是有的网站连页面内容都不加载就给你一个空白页加一段 JavaScript 的指纹检查脚本。说白了你被网站识别成机器人了。老实说单纯用 Selenium 做自动化在几年前还挺好用的但现在主流网站的反爬体系早就升级了好几轮。它们会在浏览器层面做各种标记检测比如navigator.webdriver是否为true、浏览器启动参数里有没有自动化相关的 flag、甚至通过 CDPChrome DevTools Protocol协议特征来判断几乎一抓一个准。常规 Selenium 脚本在这些检测面前几乎没有还手之力。而undetected_chromedriver这个库就是专门解决这个问题的。它能直接替代 Selenium 的 webdriver从底层 patch ChromeDriver 和浏览器启动过程让浏览器实例看起来更像一个真实用户手动打开的 Chrome。再配合 Selenium 的 API写出来的自动化脚本隐蔽性会高很多。这篇文章我会从原理到实战把 undetected_chromedriver Selenium 这套无痕自动化方案讲清楚包括环境搭建、核心代码、踩坑记录、常见问题排查希望能帮你少走弯路。1. 为什么你的 Selenium 请求一眼就被识破了先理解一个基础问题网站凭什么判断你是机器人很多人以为网站是靠 IP 频率限制来判断的但更常见、也更先触发的其实是浏览器侧的指纹检测。1.1 navigator.webdriver 是第一个暴露点如果你打开任何一个带反爬的网站在浏览器控制台输入navigator.webdriver正常手动打开的 Chrome 会返回undefined或false而通过 Selenium 启动的 Chrome这个值几乎必然是真。浏览器厂商和反爬厂商约定俗成用这个字段标识自动化环境Selenium 默认是不处理它的。from selenium import webdriver options webdriver.ChromeOptions() options.add_argument(--disable-blink-featuresAutomationControlled) driver webdriver.Chrome(optionsoptions) print(driver.execute_script(return navigator.webdriver))以上代码里--disable-blink-featuresAutomationControlled这个参数能去掉一部分 Chrome 的自动化特征但实践下来你会发现navigator.webdriver仍然可能返回true。因为 ChromeDriver 本身会在浏览器启动后注入一堆自动化相关的属性到 window 对象里光靠一个命令行参数根本挡不全。1.2 WebDriver 特征不仅限于 navigator.webdriver网站的反爬脚本远不止查一个字段。它们还会遍历window上的其他属性比如window.cdc_开头的变量ChromeDriver 注入的特征document.$cdc_asdjflasutopfhvcZLmcfl_这类随机生成的属性window.chrome对象里的loadTimes、csi方法是否与真实浏览器一致navigator.plugins和navigator.languages是否匹配permissionsAPI 查询结果是否被修改过这些检测点分散在浏览器环境的各个角落。哪怕你解决了navigator.webdriver其它特征也可能把你揪出来。所以现在做无痕自动化单纯靠chrome_options加参数是不够的我们需要从更底层去处理。2. undetected_chromedriver 的核心原理undetected_chromedriver社区里通常简称为uc是一个 Python 库它的定位不是替代 Selenium而是作为 Selenium 的一个增强版驱动层。它做了几件非常关键的事帮你把自动化痕迹擦干净。2.1 它重新编译并 patch 了 ChromeDriver普通 ChromeDriver 是官方提供的里面带了很多自动化检测标识尤其是启动握手阶段与 Chrome 进程的通信内容反爬脚本通过 CDP 协议读取一下就能发现异常。uc在运行时会动态下载或引用对应版本的 ChromeDriver 源码并在内存里 patch 掉那些暴露自动化身份的字符串和交互逻辑。简单来说它让浏览器认为当前驱动是“真实用户操作”而不是 Selenium 发起的控制指令。2.2 它处理了浏览器启动参数Chrome 在被自动化启动时通常会有以下特征参数--enable-automation --remote-debugging-port --user-data-dir其中--enable-automation是最明显的它会直接导致浏览器显示“Chrome 正受到自动测试软件控制”的提示条并且让navigator.webdriver变成true。uc会移除这些参数同时保留真正的调试端口和用户数据目录以便 Selenium 指令能正常下发。2.3 它打了一遍浏览器指纹“补丁”浏览器启动后uc会执行一段预先打包好的 JavaScript去抹平常见的指纹差异。比如重新定义navigator.webdriver的getter确保任何脚本读取都返回undefined比如补齐window.chrome对象的方法再比如覆盖navigator.permissions的query行为让Notification等权限查询结果更像正常浏览器。这些补丁不保证覆盖所有场景但至少覆盖了 90% 以上常规站点的检测逻辑。这也是为什么uc在社区里口碑比较好的原因——它做的是系统级的修复而不是像以前那样靠人肉堆代码去对抗。3. 环境准备装好工具链写代码之前先把环境理清楚。建议直接用 Python 3.8 以上的版本Windows、macOS、Linux 都支持。3.1 安装 Python 依赖包undetected_chromedriver和selenium都是 pip 包安装命令如下pip install undetected-chromedriver selenium注意这里装的是undetected-chromedriver中间是连字符导入库时用import undetected_chromedriver as uc下划线。不少新手在这里踩坑报ModuleNotFoundError: No module named undetected_chromedriver就是因为安装名和导入名没对应上。3.2 确认本机 Chrome 版本uc的机制是自动匹配本机安装的 Chrome 版本并下载对应的 ChromeDriver。所以你需要确保 Chrome 浏览器本身能正常打开且版本不要太旧。可以在地址栏输入chrome://version查看具体的版本号。如果 Chrome 版本太老某些反爬网站反而更容易认出来而且新版的 Chrome DevTools Protocol 特性也支持不了。3.3 Linux 服务器上可能需要的额外依赖如果你是部署在 Linux 服务器上跑除了 Chrome 本体还得安装一堆运行库。不然浏览器起不来或者启动后白屏。常见的问题包括缺少libnss3、libatk、libgbm等。这里给一段 Debian/Ubuntu 系常用的安装命令sudo apt update sudo apt install -y unzip xvfb libxi6 libgconf-2-4 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2如果你的服务器没有桌面环境可以用xvfb-run来跑虚拟屏幕xvfb-run -a python3 your_script.py4. 第一个无痕自动化脚本从零到能跑起来这个部分直接上代码。先跑通一个最简的版本确认 undetected_chromedriver 能正常启动并访问目标站点然后再逐步增加细节。4.1 最简写法import undetected_chromedriver as uc # 直接创建浏览器对象 driver uc.Chrome() # 访问一个能检测浏览器指纹的网站 driver.get(https://example.com) print(driver.title) # 检查 navigator.webdriver result driver.execute_script(return navigator.webdriver) print(navigator.webdriver is:, result) driver.quit()如果你运行后navigator.webdriver打印出来是None或undefined说明 uc 的基础 patch 生效了。如果是True大概率是你本地 Chrome 版本和 uc 自动匹配的 ChromeDriver 版本不对或者有别的插件干扰需要继续排查。4.2 配置用户数据目录保持登录状态很多场景下我们不想每次启动浏览器都重新登录账号比如维护电商后台、抓取需要登录才能看到的订单数据。每次都走登录流程很痛苦而且频繁登录反而更容易触发风控。解决方案是复用 Chrome 的用户数据目录。import undetected_chromedriver as uc user_data_dir /path/to/your/user/data options uc.ChromeOptions() options.add_argument(f--user-data-dir{user_data_dir}) driver uc.Chrome(optionsoptions) driver.get(https://example.com/login) input(登录完成后按回车继续...) driver.quit()第一次运行时浏览器会以配置的目录作为数据目录启动你手动登录一次。后续再启动登录态会保留。注意这里说的是“登录态”不是 Cookie 的单文件复制而是整个浏览器 profile 存档包括 localStorage、IndexedDB、Service Worker 等这些都会被复用真实性更高。4.3 等待策略不要用固定 sleep很多 Selenium 脚本的坏习惯是到处time.sleep(3)、time.sleep(5)。这种做法不仅慢而且在网络波动大的时候容易出错。更专业的做法是显式等待也就是等某个元素出现或某个条件满足再做下一步。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待登录框出现最多等 10 秒 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, username)) )使用显式等待的好处有两个一是稳定性高不会因为页面加载慢而误报二是行为更接近真实用户一个页面还没加载完就疯狂点击按钮本身就容易被序列判定为异常。5. 处理登录、翻页、内容提取的完整流程无痕自动化的最终目的通常不是打开一个页面然后退出而是完成一连串操作。下面用一个模拟场景讲清楚登录一个需要认证的网站按关键词搜索内容翻页并把结果保存下来。5.1 真实案例登录并抓取搜索结果import undetected_chromedriver as uc from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time import csv user_data_dir /tmp/my_profile options uc.ChromeOptions() options.add_argument(f--user-data-dir{user_data_dir}) driver uc.Chrome(optionsoptions) try: # 1. 打开登录页 driver.get(https://example.com/login) WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.NAME, account)) ) # 2. 填写账号密码 driver.find_element(By.NAME, account).send_keys(your_username) driver.find_element(By.NAME, password).send_keys(your_password) # 3. 勾选“记住我”之类的复选框如果有 try: remember_me driver.find_element(By.CSS_SELECTOR, input[typecheckbox]) if not remember_me.is_selected(): remember_me.click() except Exception: pass # 4. 点击登录按钮 driver.find_element(By.XPATH, //button[contains(text(), 登录)]).click() # 5. 等待登录完成跳转回首页 WebDriverWait(driver, 15).until( EC.url_changes(https://example.com/login) ) print(登录成功当前URL:, driver.current_url) # 6. 搜索关键词 search_box WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.NAME, keyword)) ) search_box.clear() search_box.send_keys(自动化测试) search_box.submit() # 7. 等待搜索结果列表加载 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, .result-item)) ) # 8. 翻页抓取这里直接抓前3页 results [] for page in range(3): items driver.find_elements(By.CSS_SELECTOR, .result-item) for item in items: title_elem item.find_element(By.CSS_SELECTOR, .title) link_elem item.find_element(By.CSS_SELECTOR, a) results.append([title_elem.text, link_elem.get_attribute(href)]) print(f第 {page 1} 页共抓取 {len(items)} 条数据) # 点击下一页 try: next_btn driver.find_element(By.CSS_SELECTOR, a.next) next_btn.click() # 等待下一页内容加载 WebDriverWait(driver, 10).until( EC.staleness_of(items[0]) # 等第一个元素过期代表页面刷新了 ) except Exception: print(没有下一页了停止翻页) break # 9. 写入 CSV 文件 with open(search_results.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([标题, 链接]) writer.writerows(results) print(抓取完成共保存, len(results), 条数据) finally: driver.quit()这段代码是典型的 Selenium 操作流但有几个细节值得拿出来单独说第一登录之后等待跳转用的是url_changes而不是presence_of_element_located因为登录成功的标志是 URL 变化而不是某个元素可见。如果你非要等一个可能不存在的“欢迎语”反而容易超时。第二勾选“记住我”的操作用了try/except包起来。因为不是所有网站的登录页都有这个选项如果找不到复选框脚本不能因此崩溃。这种容错处理在实际爬虫里非常重要。第三翻页后等待页面刷新的方式是EC.staleness_of(items[0])。这个技巧很实用上一页的第一个元素在 DOM 里挂掉了说明页面已经跳转到了下一页。比固定sleep(2)靠谱得多也比等待某个新元素出现更通用因为你可能不知道下一页的典型元素是什么。5.2 下拉框和非原生下拉框的处理搜索结果页面经常需要选择筛选条件这里就涉及下拉框定位。HTML 原生的select可以直接用Select类处理from selenium.webdriver.support.ui import Select select_elem driver.find_element(By.TAG_NAME, select) select Select(select_elem) select.select_by_visible_text(最新发布)但更麻烦的是网站用divulli组合模拟的下拉框。这种下拉框本质上是一组普通元素只是通过点击和样式控制来模拟展开/收起。处理思路也很简单先点击触发下拉框展开再找到对应的选项点击。# 点击模拟下拉框容器让它展开 dropdown_toggle driver.find_element(By.CSS_SELECTOR, .dropdown-trigger) dropdown_toggle.click() # 等待选项列表渲染 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, .dropdown-menu li)) ) # 选择包含“最新发布”文本的 li target_option driver.find_element( By.XPATH, //li[contains(text(), 最新发布)] ) target_option.click()这里最容易踩的坑是选项列表虽然渲染了但元素被遮挡导致click()时提示ElementClickInterceptedException。解决方案一般是先scroll_into_view把目标元素滚动到可视区域再点击driver.execute_script(arguments[0].scrollIntoView();, target_option) time.sleep(0.3) target_option.click()6. 更深一层的反检测策略应对行为分析和浏览器指纹undetected_chromedriver解决了浏览器自动化标记的问题但它不是万能的。真正精细的反爬系统会结合行为分析、IP 信誉、Cookie 一致性等多个维度综合打分。如果打分超过阈值照样会要求验证甚至封号。6.1 模拟真实用户操作习惯真实用户操作浏览器有几个特点鼠标有移动轨迹、键盘有输入间隔、同一时间只干一件事。而 Selenium 脚本的特征是瞬间点击、批量输入、毫无停顿。为了让行为更像真人可以引入随机延迟和鼠标轨迹模拟import random import time def human_delay(min_sec0.5, max_sec1.5): time.sleep(random.uniform(min_sec, max_sec)) def human_click(driver, element): 模拟人类点击前的小动作 driver.execute_script( arguments[0].scrollIntoView({block: center});, element ) human_delay(0.2, 0.8) # 移动鼠标到元素中心 from selenium.webdriver.common.action_chains import ActionChains ActionChains(driver).move_to_element(element).pause(0.2).click().perform() human_delay(0.3, 1.0)注意human_delay里的时间要落在正常的“人”的反应区间。不要每次都是精确到小数点后一位的固定值也不要时间跨度太大否则会显得刻意。6.2 防止 IP 级别风控不管浏览器指纹隐藏得多好如果你的请求 IP 在短时间内高频访问一个站点IP 本身就会成为一个风险信号。常用的缓解手段有控制请求频率不要多线程并发打同一个站随机轮换 User-Agent但注意要和真实浏览器版本匹配设置合理的 Cookie 持久化不要每次清空在uc里设置 User-Agent 也是可行的但有个前提--user-agent这个参数在 uc 里会和它的 patch 逻辑冲突最好在启动后通过 CDP 设置driver.execute_cdp_cmd(Emulation.setUserAgentOverride, { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 })6.3 处理验证码就算你做得再好遇到必要的验证码还是绕不过去。这里想提醒一句不要试图暴力破解验证码尤其是图文点选、滑块、行为验证这类非要硬刚只会得不偿失。比较务实的思路是降低操作频率让验证码尽量少出现预留人工介入的入口比如检测到验证码时发个通知对于滑块验证适当用轨迹模拟库生成人类滑动的轨迹滑块验证的轨迹模拟不是简单地从 A 点移到 B 点而是需要模拟“先快后慢、中间可能有停顿、最后又往回调整一点”的过程。这类小技巧网上有现成库但真正用好还是需要多试。7. 常见问题与排错实战我实际使用 uc 的过程中遇到过不少问题这里挑几个高频的和大家一起看看。7.1 ChromeDriver 版本自动匹配失败uc 默认会自动下载匹配本机 Chrome 版本的 ChromeDriver。但有些环境里它下载失败报错信息类似Could not find chromedriver in default paths or via download这时候你需要手动指定 ChromeDriver 的路径。先访问 Chrome for Testing 网站下载对应版本的 ChromeDriver然后在代码里指定import undetected_chromedriver as uc driver uc.Chrome(driver_executable_path/path/to/chromedriver)这个方法也适用于公司内网、离线环境。7.2 启动后白屏或直接闪退这个问题在 Linux 服务器上特别常见。如果启动浏览器后窗口一闪而过或者长时间停留在空白页优先排查以下三项缺少系统依赖库用前面提到的apt install命令装齐没有设置--no-sandbox和--disable-dev-shm-usage容器环境常见用户数据目录权限不对导致浏览器无法写入第三项容易被忽略。如果你在 root 用户下运行Chrome 出于安全考虑会拒绝启动。需要给 user-data-dir 指定一个普通用户可写的路径或者加上--no-sandbox有安全风险建议仅在隔离环境用。options uc.ChromeOptions() options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage)7.3 被检测后出现无限验证循环如果你发现用 uc 访问某些网站还是会出现验证码先别急着怀疑库不行。大概率是以下原因你的本机 IP 已经被标记了换电脑也是一样你复用了有污点的用户数据目录早期用普通 Selenium 跑过你的行为特征太明显了比如脚本里用了大量time.sleep(1)每个操作的间隔都是 1 秒整排查思路是先换一个全新的 user-data-dir用最慢、最简单的方式访问一次看能不能通过检测。如果还是不行再考虑是否是 IP 层面的问题。7.4driver.quit()挂起这个问题在 Windows 上偶尔出现。driver.quit()会尝试关闭所有 Chrome 进程但如果某些弹窗或页面崩溃导致进程卡死quit()就会一直阻塞。经验做法是给quit()加一个超时控制或者在finally里手动杀进程import signal def timeout_handler(signum, frame): raise TimeoutError(quit 超时手动处理) signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(10) try: driver.quit() except TimeoutError: # 手动杀残余进程 import subprocess subprocess.run([pkill, -f, chromedriver]) subprocess.run([pkill, -f, chrome]) finally: signal.alarm(0)这种方案在 Linux 下好用Windows 下没有SIGALRM可以换用子进程超时方案或者直接调用os.system(taskkill /f /im chrome.exe)。7.5 常见问题速查表问题现象可能原因排查方向ModuleNotFoundError安装了库名和导入名不一致用pip install undetected-chromedriver导入用import undetected_chromedriver as ucnavigator.webdriver为Trueuc 版本过旧或浏览器版本太新升级库版本确认 Chrome 版本在支持范围内浏览器启动白屏缺少系统依赖或沙箱权限问题安装依赖库加--no-sandbox --disable-dev-shm-usage页面加载后自动跳转验证页行为特征过于明显或 IP 已被标记降低操作频率更换 user-data-dir保持登录态element not interactable元素被遮挡或尚未渲染完成用显式等待 scrollIntoView后再操作登录状态无法保存user-data-dir 路径太长或权限不对使用独立目录避免特殊字符和空格8. 写自动化脚本时的一些个人体会写到这里这篇关于 undetected_chromedriver Selenium 无痕自动化的核心内容基本都覆盖了。最后想分享几个实际操作层面的心得。第一别把 uc 当成万能钥匙。它解决的是浏览器自动化标记被识别的问题不代表你可以在任何网站上为所欲为。如果一个网站明显有严格的反爬策略或者在你访问前就已经限制了新账号的权限那再好的指纹伪装也没用。合理的自动化策略永远是“低调、低频、模拟真实用户”。第二永远保留一个人工介入的开关。我在实际脚本里会加一个MANUAL_MODE环境变量当值为true时遇到验证码或异常页面就停住让我接管浏览器手动处理处理完再继续。这个做法在长期运行的采集任务里极其有用。import os MANUAL_MODE os.getenv(MANUAL_MODE, false).lower() true def handle_captcha_if_present(driver): if captcha in driver.current_url.lower() or verify in driver.current_url.lower(): print(检测到验证码进入人工处理模式) if MANUAL_MODE: input(人工处理完成后按回车继续...) else: raise RuntimeError(需要人工处理验证码)第三用户数据目录就是你的资产。一个养了很久的、登录状态正常、访问历史真实的浏览器 profile比任何代码技巧都值钱。不要在自动化脚本里用完就清理 profile尽量与手动浏览器分开、独立维护让它慢慢“暖”起来。这样后续自动化操作的异常率会明显下降。第四日志和监控一定要做好。无痕自动化不是跑一次就完事它需要长期维护。建议脚本里把每一步的关键操作写进日志包括当前 URL、页面标题、操作耗时以及可能的异常输出。这样等到某天网站前端改版导致脚本中断你查看日志就能快速定位是哪一步挂了而不用重新盯着浏览器调试半天。最后再补充一个很多人问过的细节uc 和普通 selenium 代码能不能混用答案是大部分 API 兼容你可以把 uc.Chrome() 当成一个 webdriver.Chrome 的替代品find_element、WebDriverWait、ActionChains 这些用法都一样。所以如果你已经积累了不少 selenium 的模板代码迁移成本很低。真正需要重新适应的是把它跑起来之后的那些版本兼容、环境配置、行为调优问题希望这篇内容能帮你把那些坑提前避过去。