首页 / 资讯中心 / 文章详情

Selenium + Chromedriver 反爬破解:环境伪装与行为模拟实战

Selenium + Chromedriver 反爬破解:环境伪装与行为模拟实战 ★ FEATURED ARTICLE
简介这份PDF资料聚焦selenium配合chromedriver在爬虫实战中被目标站点识别并拦截的典型问题面向已有一定Python爬虫基础、正遭遇反爬困扰的开发者。内容以爬取某夕夕商城为真实场景记录了从正常刷取到突然跳转登录页的排查过程通过同机浏览器对比、抓包分析请求头等方式逐步定位到webdriver特征被服务端JS检测这一关键原因并给出借助mitmproxy拦截响应、替换JS中webdriver关键字的可行解决思路同时提及修改webdriver参数等国外方案的实测效果。资源包内含1个PDF文件大小约50KB篇幅精炼但信息密度较高适合作为反爬排查的参考笔记。目前已有7380人学习下载读者可从中获得一套完整的检测定位与绕过思路理解selenium被识别的底层逻辑并迁移应用到其他同类反爬场景中。1. 详解 selenium chromedriver 被反爬的解决方法为什么你的浏览器一启动就露馅做过网页自动化采集的人大多经历过这个场景本地跑得好好的 selenium chromedriver 脚本换到目标站点上页面要么直接返回一个空白验证页要么加载出来的内容跟浏览器里手动打开完全不一样日志里连个像样的报错都没有。这不是脚本写错了而是浏览器在启动的那一刻就已经被对方识别出来了。selenium chromedriver 被反爬的解决方法核心要解决的就是「怎么让自动化浏览器看起来像一个真人打开的浏览器」。这套方案适合两类人一类是刚接触 selenium、被验证页卡住不知道从哪下手的新手另一类是已经加了 UA、加了等待但依然被拦、想搞清楚检测点到底在哪的熟手。下面按「检测原理 → 环境伪装 → 行为伪装 → 排错 → 进阶验证」的顺序把这条链路拆开讲透每一步都给到能直接抄的代码和参数。2. 先搞清楚对方在检测什么chromedriver 的四个暴露面在动手改代码之前得先知道对面到底在看什么。很多人一上来就无脑加--user-agent结果发现没用就是因为没定位到真正的检测点。selenium 驱动 chromedriver 启动的 Chrome和手动双击打开的 Chrome在页面 JavaScript 眼里是有本质区别的。这些区别分布在四个层面从易到难依次是浏览器指纹属性、驱动进程特征、网络请求特征、行为特征。理解这四个层面后面的每一步伪装才有针对性而不是碰运气。2.1 navigator.webdriver 与一串被改写的浏览器属性最经典也最容易被检测的就是navigator.webdriver。用 selenium 启动的 Chrome这个值默认是true而正常浏览器是false或undefined。目标站点只要一行if (navigator.webdriver) { ... }就能把你拦下来。除了它还有一批属性在自动化模式下会露馅navigator.plugins长度异常、navigator.languages为空、window.chrome对象缺失或结构不对、navigator.permissions查询结果不一致。这些属性单看一个可能不算致命但组合起来就形成了一个很稳定的自动化指纹。检测方通常不会只查一个属性而是把十几个属性打包成一个指纹向量跟正常浏览器的基线做比对。所以伪装也要成体系地做改一个navigator.webdriver只是入门。下面这段是在页面加载前注入脚本、批量修正这些属性的最小示例用的是 Chrome DevTools Protocol 的Page.addScriptToEvaluateOnNewDocument它能在页面任何脚本执行之前生效比加载完再改要可靠得多。from selenium import webdriver from selenium.webdriver.chrome.options import Options # 需要在页面脚本执行前注入的伪装脚本 STEALTH_JS // 抹掉 webdriver 标记 Object.defineProperty(navigator, webdriver, {get: () undefined}); // 补上正常的插件数量正常浏览器一般有 PDF 查看器等 Object.defineProperty(navigator, plugins, {get: () [1, 2, 3, 4, 5]}); // 补上语言列表 Object.defineProperty(navigator, languages, {get: () [zh-CN, zh, en]}); // 补上 window.chrome正常 Chrome 一定有这个对象 window.chrome { runtime: {} }; // 修正 permissions 查询避免 headless 下返回不一致 const originalQuery window.navigator.permissions.query; window.navigator.permissions.query (parameters) ( parameters.name notifications ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); options Options() driver webdriver.Chrome(optionsoptions) # 关键在每次新文档创建时都注入而不是只注入一次 driver.execute_cdp_cmd( Page.addScriptToEvaluateOnNewDocument, {source: STEALTH_JS} ) driver.get(https://example.com)这段代码的逻辑是execute_cdp_cmd直接调用 Chrome 的调试协议把伪装脚本注册成「每个新文档创建时自动执行」。参数上Page.addScriptToEvaluateOnNewDocument的source就是那段 JS 字符串它会在页面自身的任何 JS 之前跑所以目标站点读到的navigator.webdriver已经是undefined。注意plugins这里用[1,2,3,4,5]只是占位真实场景里更稳妥的做法是读取一个正常浏览器的真实值再回填否则长度对了、内容不对一样可能被识破。2.2 chromedriver 进程与 CDP 端口留下的痕迹属性伪装做完还有一层更底层的暴露面chromedriver 本身。selenium 的工作模式是「Python 进程 → chromedriver 进程 → Chrome 进程」中间多了一个 chromedriver。这个进程名、它监听的调试端口、以及 Chrome 启动时带的一堆--enable-automation之类的命令行参数都可能被检测。比如 Chrome 启动参数里如果带着--enable-automation页面里某些行为会不一样再比如调试端口如果暴露在固定端口上扫描一下就能发现。应对思路有两个方向。一是尽量削减自动化痕迹把--enable-automation这类参数关掉用excludeSwitches排除掉二是干脆不用 chromedriver 这条链路改用直接连 CDP 的方式启动浏览器减少中间层。前者改动小、上手快后者更彻底但代码要重写。对大多数采集场景先把参数清理干净就能挡掉一批初级检测。options Options() # 去掉「Chrome 正受到自动测试软件的控制」提示条和对应标记 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 关闭一些在自动化下容易露馅的特性 options.add_argument(--disable-blink-featuresAutomationControlled) # 指定一个独立的用户数据目录避免和日常浏览器配置冲突 options.add_argument(--user-data-dir/tmp/chrome-profile-01)excludeSwitches里的enable-automation是重点它去掉的是那条显眼的提示条和背后的标记useAutomationExtension设为False是关掉旧版自动化扩展--disable-blink-featuresAutomationControlled则是从 Blink 渲染引擎层面关掉自动化控制相关的特性开关。--user-data-dir建议每次任务用独立目录避免多个实例抢同一个配置目录导致启动失败也方便隔离 cookie。2.3 请求头、TLS 指纹与加载时序的破绽就算浏览器属性全伪装好了网络层还是可能露馅。目标站点服务端能看到的东西包括请求头顺序、Accept-Language是否和navigator.languages一致、有没有sec-ch-ua系列头、TLS 握手的指纹JA3 之类。selenium 默认发出的请求头和真实 Chrome 是有差异的尤其是sec-ch-ua这类客户端提示头版本对不上就很可疑。另一个常被忽略的是加载时序。自动化脚本往往get完立刻就开始抓元素而真人会有一个自然的停顿和滚动。有些站点会记录「从页面加载到第一次交互」的时间差太短就判定为机器。所以除了改请求头还要在关键节点插入符合人类节奏的等待而不是清一色的time.sleep(1)。检测维度正常浏览器表现自动化默认表现处理方向请求头顺序固定且符合版本顺序可能不同用真实抓包结果对齐sec-ch-ua与 Chrome 版本一致可能缺失或过时手动设置或升级内核首次交互延迟数百毫秒以上常常接近 0加入随机等待Accept-Language与 navigator 一致可能不一致两处统一设置这张表里的四个维度前两个属于「静态可对齐」后两个属于「动态要模拟」。静态的用配置解决动态的用行为脚本解决别混在一起调否则出了问题很难定位是哪一层导致的。3. 环境伪装落地从启动参数到指纹注入的完整配置上一章讲的是「检测什么」这一章讲「怎么改」。环境伪装的目标是让自动化浏览器在静态特征上尽量贴近真实浏览器。这里给一套我常用的完整配置从启动参数、指纹注入到请求头对齐按顺序拼起来就能跑。需要说明的是没有任何一套配置能通吃所有站点具体参数要根据目标站点的检测强度微调但骨架是通用的。3.1 一套可直接复用的 ChromeOptions 配置先看启动参数。下面这份配置覆盖了窗口尺寸、语言、图片加载策略、自动化标记清理等常见项。窗口尺寸建议设成常见分辨率别用默认的 800x600那个尺寸本身就很少见。from selenium import webdriver from selenium.webdriver.chrome.options import Options def build_options(profile_dir: str) - Options: options Options() # 常见桌面分辨率避免默认小窗口引起怀疑 options.add_argument(--window-size1920,1080) # 语言和时区要和后续请求头保持一致 options.add_argument(--langzh-CN) options.add_argument(--timezoneAsia/Shanghai) # 清理自动化标记 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) options.add_argument(--disable-blink-featuresAutomationControlled) # 独立配置目录隔离 cookie 和缓存 options.add_argument(f--user-data-dir{profile_dir}) # 可选无头模式但无头更容易被检测非必要不开 # options.add_argument(--headlessnew) return options参数逐个说--window-size影响window.innerWidth/innerHeight很多指纹脚本会读这两个值--lang要和后面请求头里的Accept-Language对齐不一致是常见破绽--timezone影响Intl.DateTimeFormat的时区输出和 IP 归属地不匹配也会被记一笔--user-data-dir用独立目录好处是每次任务环境干净坏处是首次访问没有历史 cookie某些站点会因此判定为新设备这个要权衡。无头模式我一般不开--headlessnew虽然比老版无头好很多但仍有若干属性差异除非跑在无显示环境的服务器上否则优先用有头模式。3.2 用 CDP 注入指纹并统一请求头启动参数解决的是进程层指纹注入解决的是页面层。把 2.1 里的伪装脚本扩展一下再加上请求头对齐。请求头这块selenium 本身不直接暴露「设置任意请求头」的接口常见做法是用 CDP 的Network.setExtraHTTPHeaders或者用代理中间层。这里用 CDP 的方式简单直接。def apply_stealth(driver): # 页面脚本执行前注入指纹修正 driver.execute_cdp_cmd( Page.addScriptToEvaluateOnNewDocument, {source: STEALTH_JS} ) # 统一请求头注意 sec-ch-ua 要和实际内核版本匹配 headers { Accept-Language: zh-CN,zh;q0.9,en;q0.8, sec-ch-ua: Chromium;v120, Not(A:Brand;v24, sec-ch-ua-mobile: ?0, sec-ch-ua-platform: Windows, } driver.execute_cdp_cmd(Network.enable, {}) driver.execute_cdp_cmd(Network.setExtraHTTPHeaders, {headers: headers}) driver webdriver.Chrome(optionsbuild_options(/tmp/chrome-profile-01)) apply_stealth(driver) driver.get(https://example.com)逻辑上Page.addScriptToEvaluateOnNewDocument管页面内可见的属性Network.setExtraHTTPHeaders管服务端可见的请求头两者配合才能把「页面里读到的」和「服务端收到的」对齐。参数上要特别注意sec-ch-ua里的版本号它必须和你实际用的 Chrome 大版本一致写错了反而更可疑——一个声称是 120 的内核却发出 118 的客户端提示头等于自报家门。Network.enable要先调用否则setExtraHTTPHeaders不生效这个顺序坑过不少人。3.3 用真实浏览器配置反推参数而不是凭感觉填上面这些参数值最忌讳的就是凭感觉填。我一般的做法是先用一台正常浏览器访问目标站点通过页面控制台把关键指纹读出来再把这些真实值回填到伪装脚本里。比如navigator.plugins的真实内容、navigator.hardwareConcurrency的核数、screen.colorDepth的位数这些都能在控制台一行行读出来。// 在正常浏览器控制台执行把结果抄回伪装脚本 console.log(JSON.stringify({ webdriver: navigator.webdriver, plugins: Array.from(navigator.plugins).map(p p.name), languages: navigator.languages, hardwareConcurrency: navigator.hardwareConcurrency, colorDepth: screen.colorDepth, chromeKeys: Object.keys(window.chrome || {}) }, null, 2));把这段输出保存下来作为你伪装脚本的「基线」。每次目标站点更新检测策略就重新采一次基线做对比。这个习惯能省掉大量瞎猜的时间。需要提醒的是不同机器、不同系统上这些值本来就有差异所以基线最好来自和目标环境接近的机器别拿一台 Mac 的值去伪装 Windows 环境。4. 行为伪装让操作节奏不像机器环境伪装做到位能过掉大部分静态检测但还有一类检测是看行为的。真人操作有停顿、有滚动、有鼠标移动轨迹而脚本往往是「定位元素 → 点击 → 立刻下一步」节奏过于规整。这一章讲怎么把行为做得自然一些。核心原则是不要追求「快」要追求「像」。很多脚本被拦不是因为技术不行而是因为太快了快到不像人。4.1 随机等待与滚动把固定 sleep 换掉time.sleep(2)这种固定等待在检测方眼里就是规律性极强的信号。改成随机区间并且把等待拆散到多个动作之间效果会好很多。下面这个辅助函数把「等待」和「滚动」打包模拟真人浏览时的停顿。import random import time from selenium.webdriver.common.action_chains import ActionChains def human_pause(driver, low0.8, high2.5): 随机停顿模拟阅读节奏 time.sleep(random.uniform(low, high)) def human_scroll(driver, steps3): 分步滚动每步之间停顿避免一次性跳到底 for _ in range(steps): delta random.randint(200, 600) driver.execute_script(fwindow.scrollBy(0, {delta});) time.sleep(random.uniform(0.3, 1.2)) # 偶尔回滚一点更像真人 if random.random() 0.3: driver.execute_script(window.scrollBy(0, -120);) time.sleep(random.uniform(0.2, 0.6))human_pause的low和high是等待区间的上下限单位秒建议根据页面内容量调整内容多的页面区间拉大。human_scroll里每步滚动距离随机步与步之间停顿最后有 30% 概率回滚一点——真人浏览时确实会来回看。这些细节单看很小但累积起来能显著降低行为特征的规律性。注意别把区间设得太夸张一次停顿十几秒反而拖慢效率得不偿失。4.2 鼠标轨迹与点击位置偏移点击也是重灾区。element.click()是直接命中元素中心而真人点击会落在元素范围内的随机位置鼠标移动也有轨迹。用 ActionChains 可以模拟得更自然一些。def human_click(driver, element): 带偏移和轨迹的点击 actions ActionChains(driver) # 先移动到元素附近再移动到元素上形成两段轨迹 actions.move_to_element_with_offset(element, random.randint(-5, 5), random.randint(-5, 5)) actions.pause(random.uniform(0.1, 0.4)) actions.move_to_element(element) actions.pause(random.uniform(0.05, 0.2)) actions.click() actions.perform()move_to_element_with_offset的偏移量别超过元素本身尺寸否则会点到元素外面。两段移动加两次随机暂停是为了制造「先靠近再精确点击」的轨迹感。pause的参数是秒别设太长。这套动作比直接click()慢但换来的是更低的被拦概率在关键操作比如登录、提交上值得用。4.3 页面加载策略与资源拦截的取舍driver.get()默认等页面load事件但很多站点资源多、加载慢脚本容易卡住。可以改成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 # 设置页面加载策略为 eagerDOM 就绪即返回 options.page_load_strategy eager # 用显式等待替代固定 sleep等目标元素出现 element WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, .content)) )page_load_strategy可选normal、eager、noneeager是折中方案。WebDriverWait的超时设 15 秒是个经验值太短容易误判超时太长出问题时排查慢。presence_of_element_located只等元素出现在 DOM 里如果还要等它可点击换成element_to_be_clickable。资源拦截这块如果确实要拦建议只拦字体和媒体图片尽量放行因为图片请求是页面正常加载的一部分。5. 避坑与排查被拦时到底该看哪里前面几章把该做的都做了但实际跑起来还是可能被拦。这一章列几个我踩过的坑按「现象 → 原因 → 解决」写方便对照排查。排查的核心思路是先确认是哪一层被检测再针对性处理别一上来就大改配置。5.1 现象加了伪装脚本还是被识别原因通常是注入时机不对。如果用driver.execute_script在get之后注入页面自身的检测脚本早就跑完了改也白改。另一个可能是伪装脚本本身有语法错误静默失败了。解决确认用的是Page.addScriptToEvaluateOnNewDocument并且在get之前调用注入后在控制台手动读一次navigator.webdriver验证是否生效。5.2 现象本地能跑服务器上必被拦原因多半是环境差异。服务器上常用无头模式无头模式的指纹和正常浏览器差异更大另外服务器的 IP 段、时区、语言设置也可能和伪装值不匹配。解决优先用有头模式加虚拟显示把时区、语言、sec-ch-ua全部对齐到目标地区检查 IP 归属地和时区是否矛盾。5.3 现象请求头改了但服务端还是返回验证页原因是请求头没真正生效或者顺序不对。Network.setExtraHTTPHeaders必须在Network.enable之后调用且要在导航之前。另外某些头比如User-Agent用 CDP 设置可能被 Chrome 自身覆盖。解决用抓包工具确认实际发出的请求头而不是只看代码里写了什么User-Agent建议通过启动参数--user-agent设置更稳。5.4 现象脚本跑一会儿就被封换 IP 也没用原因是行为特征太规律或者 cookie 被关联。固定间隔、固定滚动距离、固定点击位置跑久了必然被聚类。解决把所有等待、滚动、偏移都改成随机每个任务用独立的--user-data-dir控制单 IP 的请求频率别把并发拉满。5.5 现象元素定位不到报超时原因不一定是被反爬可能是页面结构变了或加载策略问题。先手动打开页面确认元素选择器是否还有效再检查page_load_strategy和显式等待条件。解决用eager策略配合WebDriverWait选择器尽量用稳定的属性别依赖会变的 class 名。提示排查时养成「先复现、再定位、后修改」的习惯每次只改一个变量否则改完不知道是哪个改动起的作用。6. 进阶验证怎么确认伪装真的生效了伪装做完不能靠「没被拦」就认为成功了因为目标站点可能只是暂时没触发检测。更靠谱的做法是主动验证。我一般会用一个自建的检测页把常见检测点全列出来跑一遍看哪些还露馅。下面这个检测脚本可以在目标站点上执行也可以在本地起一个页面执行输出一份「指纹体检报告」。// 在自动化浏览器里执行输出当前环境的检测结果 (function () { const report { webdriver: navigator.webdriver, pluginsCount: navigator.plugins.length, languages: navigator.languages.join(,), hasChrome: typeof window.chrome ! undefined, hardwareConcurrency: navigator.hardwareConcurrency, colorDepth: screen.colorDepth, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, ua: navigator.userAgent, }; console.log(JSON.stringify(report, null, 2)); return report; })();把这份报告和 3.3 里采的正常浏览器基线逐项对比差异项就是还没处理干净的暴露面。重点看webdriver是否为undefined、pluginsCount是否和基线接近、timezone是否和 IP 归属地一致、ua里的版本是否和sec-ch-ua对得上。这套对比方法比盲目试参数高效得多。再进一步可以做一个「回归检测」把伪装配置固定下来每天定时跑一次检测页记录各项值的变化。一旦某项突然变了比如 Chrome 自动升级导致ua和sec-ch-ua版本错位就能第一时间发现。这个习惯帮我省过好几次「昨天还好好的今天全挂」的排查时间。最后说个我自己的教训早期我总想着一步到位把所有能加的伪装全加上结果配置越来越复杂出了问题根本不知道是哪一项导致的。后来改成「最小可用配置 按需叠加」先保证基础项webdriver、请求头、时区对齐被拦了再针对性加反而更稳。伪装这件事够用就好堆太多参数本身就是一种特征。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站