ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

编译好的Chromedriver特征抹除与配套浏览器实战指南

编译好的Chromedriver特征抹除与配套浏览器实战指南 简介这是一份面向爬虫开发者与自动化测试人员的Chromedriver资源针对反爬检测场景提供已抹除自动化特征的Windows 10专用驱动并配套完整浏览器环境解决常规驱动易被识别、导致脚本失效的问题。压缩包共491个文件约56MB以json配置、png图标、js脚本、html页面、dll动态库及crx扩展等为主涵盖浏览器运行所需的各类资源与数据文件结构完整。使用前先安装配套浏览器再将chromedriver.exe放入Application目录即可完成部署。目前已有1800人学习下载适合需要稳定绕过特征检测、搭建可控浏览器环境的爬虫与自动化从业者参考使用。1. 编译好的Chromedriver特征已经被抹除配套浏览器这套组合到底在解决什么问题做过浏览器自动化的工程师大概率都遇到过这种场景本地跑得好好的脚本一上目标站点就返回空数据或者干脆弹出一个验证页。排查半天代码逻辑没问题请求头也没问题最后发现是navigator.webdriver这个属性暴露了身份。Chromedriver 作为 WebDriver 协议的标准实现本身会往浏览器里注入一批可被检测的痕迹这不是 bug是设计使然。标题里说的「编译好的 Chromedriver特征已经被抹除」本质上就是有人把 Chromedriver 的源码拉下来改掉了那些会暴露自动化身份的字符串和属性重新编译出一个二进制文件再配一个版本严格对齐的浏览器。这套组合的价值在于你不需要自己搭编译环境、不需要啃 Chromium 的构建文档拿到就能用。适合谁适合做数据采集、自动化测试、页面监控的从业者尤其是那些被反自动化策略卡住、又不想上重型方案的人。但要注意特征抹除和反检测是持续对抗的过程今天能过不代表下个月还能过这是心理预期。2. 特征抹除到底抹了什么从navigator.webdriver到 CDP 痕迹2.1 检测方最常盯的几个信号要理解「抹除」这件事得先知道对方在看什么。最常见的检测点有这么几类第一类是navigator.webdriver标准 WebDriver 模式下这个值是true正常浏览器是false或undefined第二类是 CDPChrome DevTools Protocol相关的痕迹比如window.cdc_adoQpoasnfa76pfcZLmcfl_这种随机前缀的变量是 Chromedriver 注入的第三类是权限查询的异常比如Notification.permission在自动化环境下返回denied而正常浏览器是default第四类是 User-Agent 里带HeadlessChrome字样。这些信号单独看都不致命但组合起来就是一个高置信度的自动化指纹。抹除特征的核心思路就是把这些信号逐个改掉让浏览器在 JS 层面看起来和真人操作的一致。2.2 源码级修改和运行时注入的区别市面上有两种做法。一种是运行时注入用Page.addScriptToEvaluateOnNewDocument在页面加载前执行一段 JS覆盖掉那些属性。这种做法的优点是灵活缺点是时序敏感——如果注入晚了检测脚本已经跑完了就白搭。另一种是源码级修改直接改 Chromedriver 的 C 源码把注入的变量名改掉、把navigator.webdriver的返回值改掉重新编译。标题里说的「编译好的」就是后者。源码级修改的优势是彻底因为它在二进制层面就不存在那些特征字符串了检测方拿不到任何可匹配的常量。缺点是编译门槛高Chromedriver 依赖 Chromium 的构建体系一个版本编译下来动辄几小时还得处理各种依赖。所以有人编译好了直接分享省掉了最痛苦的一步。2.3 配套浏览器为什么必须版本对齐这里有个血泪经验Chromedriver 和 Chrome 的版本必须严格对应大版本号差一个就可能连不上。你拿一个 120 版本的 driver 去驱动 119 的浏览器大概率报session not created错误。标题里强调「配套浏览器」就是因为抹除特征后的 driver 往往改了内部通信协议的一些细节只有配套的那个浏览器版本才能正常握手。我一般会这样做版本核对# 查看 Chromedriver 版本 ./chromedriver --version # 输出示例ChromeDriver 120.0.6099.109 (3419140ab665...) # 查看浏览器版本Linux 下 /path/to/chrome --version # 输出示例Chromium 120.0.6099.109两个版本号的主版本和次版本必须一致构建号最好也一致。如果构建号不同但主次版本相同通常还能用但遇到诡异问题时优先怀疑这里。2.4 一个最小可跑的验证脚本拿到这套组合后第一件事不是跑业务脚本而是验证特征是否真的被抹掉了。下面这段 Python 代码可以快速检查几个关键信号from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options # 指定配套的 driver 路径和浏览器路径 chrome_options Options() chrome_options.binary_location /path/to/配套浏览器/chrome service Service(executable_path/path/to/编译好的/chromedriver) driver webdriver.Chrome(serviceservice, optionschrome_options) # 逐项检查特征 checks { navigator.webdriver: driver.execute_script(return navigator.webdriver), cdc 变量: driver.execute_script( return Object.keys(window).filter(k k.includes(cdc_)).length ), Notification.permission: driver.execute_script( return Notification.permission ), UA: driver.execute_script(return navigator.userAgent), } for name, value in checks.items(): print(f{name}: {value}) driver.quit()逻辑说明navigator.webdriver期望返回False或Nonecdc 变量期望返回0Notification.permission期望返回defaultUA 里不应该出现HeadlessChrome。如果这几项都符合预期说明特征抹除是生效的。参数上binary_location必须指向配套浏览器的可执行文件executable_path指向编译好的 driver两个路径写错一个都会启动失败。3. 从零跑通一套自动化流程环境、参数与第一个页面3.1 环境准备和目录结构假设你拿到的是一个压缩包解压后应该包含 driver 二进制和浏览器目录。我习惯的目录结构是这样的auto_env/ ├── chromedriver # 编译好的 driver ├── chrome/ # 配套浏览器 │ └── chrome ├── scripts/ # 业务脚本 │ └── main.py └── user_data/ # 持久化用户目录user_data这个目录很关键。默认情况下每次启动都是全新的临时 profile没有 cookie、没有 localStorage这本身就是一种异常特征——真人不会每次访问都是空白的。指定一个持久化的用户目录可以让浏览器保留历史记录、cookie 和缓存更接近真实使用状态。3.2 启动参数怎么设才不露馅启动参数是另一个容易翻车的地方。很多人为了图快直接上--headless结果 UA 里带HeadlessChrome一秒被识别。如果确实需要无头模式用--headlessnew新版无头模式的 UA 和正常模式一致。下面是一组我常用的参数chrome_options Options() chrome_options.binary_location /path/to/chrome/chrome # 持久化用户目录保留 cookie 和缓存 chrome_options.add_argument(--user-data-dir/path/to/user_data) # 新版无头模式UA 不带 Headless 字样 chrome_options.add_argument(--headlessnew) # 禁用自动化提示条 chrome_options.add_argument(--disable-infobars) # 窗口尺寸避免默认的 800x600 异常值 chrome_options.add_argument(--window-size1920,1080) # 禁用 GPU服务器环境常用 chrome_options.add_argument(--disable-gpu) # 关键排除自动化开关 chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False)参数说明--user-data-dir指向持久化目录注意这个目录不能被多个实例同时占用否则会报锁冲突--headlessnew是 Chrome 112 之后的新无头模式老版本没有这个选项excludeSwitches和useAutomationExtension这两个实验性选项能去掉一部分自动化提示但注意它们对源码级抹除的 driver 来说可能是多余的加上也无害。3.3 页面加载策略和超时设置默认的加载策略是normal会等所有资源加载完遇到慢资源容易卡死。我一般改成eager等 DOM 就绪就返回后续用显式等待处理具体元素from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By driver webdriver.Chrome(serviceservice, optionschrome_options) # 页面加载策略改为 eager driver.set_page_load_timeout(30) driver.implicitly_wait(0) # 关掉隐式等待避免和显式等待冲突 driver.get(https://example.com) # 显式等待某个元素出现最多等 15 秒 try: element WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, #content)) ) print(element.text[:200]) except Exception as e: print(f等待超时: {e})逻辑说明set_page_load_timeout控制页面加载的总超时超过就抛异常implicitly_wait(0)关掉全局隐式等待因为隐式等待和显式等待混用会导致等待时间不可预测这是很多人踩过的坑WebDriverWait配合expected_conditions是更可控的等待方式超时时间按目标站点的实际响应速度调整一般 10 到 20 秒比较稳妥。3.4 验证页面是否真的「干净」跑通第一个页面后别急着上业务逻辑先做一轮特征自检。除了前面提到的navigator.webdriver和 cdc 变量还可以检查window.chrome对象是否存在、plugins数组是否为空、languages是否合理。正常浏览器的navigator.plugins至少有 PDF 相关的几个条目如果返回空数组就是一个明显的异常信号。# 检查 plugins 和 languages plugins_count driver.execute_script(return navigator.plugins.length) languages driver.execute_script(return navigator.languages) chrome_obj driver.execute_script(return typeof window.chrome) print(fplugins 数量: {plugins_count}) # 期望 0 print(flanguages: {languages}) # 期望 [zh-CN, zh, ...] print(fwindow.chrome: {chrome_obj}) # 期望 object如果plugins返回 0说明浏览器启动参数里可能带了--disable-plugins之类的选项或者配套浏览器本身被裁剪过。这种情况需要回到启动参数排查把禁用插件相关的选项去掉。4. 避坑指南特征抹除后仍然被识别的五种情况4.1 现象页面能打开但数据是空的原因目标站点用了 JS 挑战在页面加载后执行一段脚本采集浏览器指纹如果指纹异常就不渲染真实内容。特征抹除只解决了静态属性但指纹采集还会看 Canvas 渲染、WebGL 参数、字体列表、时区等。解决先确认是哪个环节被拦。打开开发者工具的 Network 面板看有没有一个返回 403 或 200 但内容为空的 XHR 请求。如果有把那个请求的 URL 和参数记下来单独用driver.execute_script复现看返回什么。常见做法是补上时区和语言的一致性——如果 IP 在国内但navigator.language是en-US这就是矛盾点。4.2 现象启动时报session not created原因driver 和浏览器版本不匹配或者 driver 没有可执行权限。解决先chmod x chromedriver给权限再核对版本号。如果版本号一致还报错检查是不是有多个 Chrome 进程残留占用了用户目录。Linux 下用pkill -f chrome清理Windows 下在任务管理器里结束所有 chrome 进程。另外--user-data-dir指向的目录如果被上一个实例锁住也会报这个错换个目录或者等几秒重试。4.3 现象跑一段时间后突然全部失败原因目标站点更新了检测规则或者你的 IP 被限流了。特征抹除不是一劳永逸的对抗是动态的。解决先换 IP 测试如果换 IP 后恢复说明是 IP 层面的限流需要控制请求频率。如果换 IP 也不行大概率是检测规则更新了需要重新检查特征。我一般会保留一个「基线检测脚本」每次出问题先跑一遍看哪个信号变了。这个脚本就是前面 2.4 节那段代码存下来定期跑。4.4 现象无头模式下正常有头模式反而失败原因有头模式下浏览器会加载真实的插件和扩展某些扩展会注入额外的变量反而增加了指纹维度。另外有头模式的窗口尺寸、屏幕分辨率如果和常见设备差异太大也会被标记。解决有头模式下把窗口尺寸设成常见分辨率比如 1920x1080 或 1366x768。如果不需要看到界面优先用--headlessnew。如果业务必须用有头模式考虑用虚拟显示Xvfb跑在服务器上这样既有头又不需要物理显示器。4.5 现象execute_script返回的结果和预期不符原因页面里有 iframe脚本执行在了顶层文档而不是目标 iframe 里。或者页面用了 CSP阻止了某些脚本执行。解决先driver.switch_to.frame()切到目标 iframe 再执行脚本。如果是 CSP 问题检查响应头里的Content-Security-Policy看是否限制了unsafe-eval。这种情况一般不影响正常的元素操作只影响execute_script可以改用driver.find_element加get_attribute的方式获取信息。5. 进阶技巧把特征抹除做成可复用的检测基线5.1 建立一套自动化的特征巡检脚本前面反复提到「基线检测」这里给一个完整实现。思路是把所有已知的检测点写成一个字典每次启动后自动跑一遍输出差异报告。这样当目标站点更新规则时你能第一时间知道是哪个信号出了问题而不是盲目地换 driver。import json from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options # 期望的基线值 BASELINE { webdriver: None, # navigator.webdriver 应为 None 或 False cdc_count: 0, # cdc 变量数量应为 0 plugins_count_min: 3, # plugins 至少 3 个 chrome_type: object, # window.chrome 应为 object ua_has_headless: False, # UA 不应含 Headless } def inspect(driver): result {} result[webdriver] driver.execute_script(return navigator.webdriver) result[cdc_count] driver.execute_script( return Object.keys(window).filter(k k.includes(cdc_)).length ) result[plugins_count] driver.execute_script( return navigator.plugins.length ) result[chrome_type] driver.execute_script( return typeof window.chrome ) result[ua] driver.execute_script(return navigator.userAgent) result[ua_has_headless] Headless in result[ua] return result def compare(result): issues [] if result[webdriver] not in (None, False): issues.append(fwebdriver 暴露: {result[webdriver]}) if result[cdc_count] 0: issues.append(f存在 {result[cdc_count]} 个 cdc 变量) if result[plugins_count] BASELINE[plugins_count_min]: issues.append(fplugins 数量异常: {result[plugins_count]}) if result[chrome_type] ! BASELINE[chrome_type]: issues.append(fwindow.chrome 类型异常: {result[chrome_type]}) if result[ua_has_headless]: issues.append(UA 含 Headless 字样) return issues if __name__ __main__: options Options() options.binary_location /path/to/chrome/chrome options.add_argument(--headlessnew) options.add_argument(--user-data-dir/path/to/user_data) service Service(executable_path/path/to/chromedriver) driver webdriver.Chrome(serviceservice, optionsoptions) driver.get(about:blank) result inspect(driver) issues compare(result) print(json.dumps(result, indent2, ensure_asciiFalse)) if issues: print(发现问题:) for i in issues: print(f - {i}) else: print(所有基线检查通过) driver.quit()逻辑说明inspect函数负责采集compare函数负责比对基线。基线值不是死的plugins_count_min可以根据实际浏览器调整webdriver的期望值在不同 driver 实现下可能是None也可能是False两个都算通过。这个脚本的价值在于可重复——每次换 driver、换浏览器、换目标站点先跑一遍心里有底。5.2 参数调优的几个经验值跑久了会积累一些经验值。窗口尺寸用 1920x1080 或 1366x768这两个是统计上最常见的分辨率page_load_timeout设 30 秒WebDriverWait设 15 秒这两个值覆盖了绝大多数正常站点请求间隔不要低于 2 秒同一域名下的并发不要超过 3 个。这些不是硬性规定但偏离太多就容易触发风控。还有一个容易忽略的点--user-data-dir目录要定期清理。跑久了里面会积累大量缓存和日志体积膨胀不说还可能因为某些状态文件损坏导致启动失败。我一般每周清一次只保留 cookie 和 localStorage 相关的文件。5.3 什么时候该放弃这套方案说句实在话特征抹除加配套浏览器这套方案适合的是中等强度的反自动化场景。如果目标站点上了商业级的风控系统比如要求通过复杂的 JS 挑战、采集几十个维度的指纹、还结合行为分析那这套方案的投入产出比就不高了。这时候要么上更重的方案要么换数据源。我自己的判断标准是如果基线检测全部通过、请求频率也控制住了但连续三天数据获取率低于 30%那就说明对抗升级了该考虑换思路了。死磕一套方案不如把精力花在找替代数据源上这是踩过几次坑之后的习惯。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进