你是不是也遇到过这种情况Selenium写了一个爬虫本地调试时一切正常换到云服务器上跑了不到十分钟就开始返回验证码、强制登录、甚至直接把IP封掉。更离谱的是有些目标网站在Chrome手工访问时好好的只要由WebDriver驱动第一次打开页面就被识别。这其实就是“反爬虫检测”在起作用——而且它不是看你的爬虫多会写而是看你的浏览器行为像不像一个“真人”。我做了快十年的数据采集最初也踩过无数个坑。后来我总结出一个朴素的结论想让Selenium规避检测本质上不是“对抗”而是“伪装”。目标网站的JS脚本会从很多维度去判断当前浏览器是不是被自动化工具控制我们只要把这几个维度都对齐基本上就能稳定跑。这里面有六个动作是我每次搭建环境必做的也是我认为性价比最高的组合。这篇文章就把完整思路和可复现的代码全部分享出来给还在被检测折磨的朋友一个参考。1. 先搞清楚为什么Selenium一抓就死这跟反爬还不完全是同一回事很多人把“被封IP”和“被识别为Selenium”混在一起其实这是两套不同的检测逻辑。被封IP往往是因为请求频率太快、单IP并发太高属于服务端的流量治理而被识别为Selenium则是因为浏览器运行时暴露了太多自动化特征。我们要讨论的“规避检测”主要解决的是第二类问题——让网站觉得当前这个浏览器就是一个正常用户在访问而不是WebDriver在操作。1.1 navigator.webdriver 这个标记是第一个暴露点用过Selenium的人应该都有印象只要通过ChromeDriver启动浏览器在控制台执行window.navigator.webdriver返回的几乎都是true。这是一个由ChromeDriver在初始化时主动写入的全局标记目的就是告诉页面“当前浏览器受自动化工具控制”。绝大多数网站的反爬脚本第一件事就是检查它如果检测到就直接进入风控。我知道有人会直接执行driver.execute_script(window.navigator.webdriver undefined)来解决但这在实战中基本是无效的。因为页面的检测脚本通常在你的代码运行之前就已经执行完了而且很多新版的ChromeDriver会重新注入该属性页面刷新一次就会恢复原状。正确做法是在页面加载之前就屏蔽掉这个特征的注入而不是等页面加载完之后再去“擦除”。1.2 行为指纹你的动作太“稳定”了除了JS属性检测方还会收集鼠标移动轨迹、点击间隔、滚动速度、输入节奏等行为数据。真人操作永远是无规律的——鼠标会有微小的抖动和偏移输入文字的速度是忽快忽慢的滚动页面也不是匀速到底。而自动化脚本最常见的操作方式是打开页面后直接执行某个元素的点击事件输入框用send_keys一次性填入全部内容然后瞬间跳转下一个页面。这种“过于稳定”的行为模式在服务端的数据分析模型中非常显眼。我见过一个比较极端的检测案例某电商平台在登录环节统计同一个Session内“鼠标移动的路径长度”如果从页面加载到点击登录按钮之间没有任何鼠标移动记录直接就判定为机器人。这意味着即使所有JS属性都伪装好了只要行为不够自然同样会被识别。1.3 别忽略环境指纹无头模式、IP、时区还有一类检测是针对浏览器运行环境的。比如用--headless模式启动Chrome时User-Agent里往往会带上HeadlessChrome字样ChromeDriver默认的Accept-Language可能和目标网站的用户群体不一致有时候连时区、显卡渲染器、CPU核数、屏幕分辨率都会成为比对的线索。现代反爬系统通常采用“多特征加权打分”的策略任何一个单独的特征都不会触发风控但当十几个特征组合起来偏离正常用户画像时就会被判定为可疑。这就引出了规避检测的核心思路不要试图掩盖单个特征而是要让整套浏览器指纹形成一个自洽的、像真人的画像。2. 三个“藏身份”动作把自动化标记从根上抹掉这部分我按照实际操作的顺序来讲每个动作都是可以直接复制到代码里的。需要注意的是这三个动作解决的是“身份”层面的问题也就是让网站从技术特征上看不出你是自动化工具。2.1 动作一关掉Chrome的自动化开关Selenium启动Chrome时默认会带上一个叫enable-automation的开关这个开关会让Chrome在右上角显示“Chrome正在受到自动测试软件的控制”提示条同时在CDPChrome DevTools Protocol里暴露自动化状态。通过excludeSwitches可以把这层标识去掉配合disable-blink-featuresAutomationControlled可以进一步关闭Blink内核中的自动化控制特性。from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) driver.get(https://example.com) print(driver.execute_script(return window.navigator.webdriver))这段代码跑完以后navigator.webdriver的值应该会变成undefined。这里有一点要特别留意useAutomationExtension这个参数在Selenium 4.0以上的版本中已经被标记为即将废弃某些新版本的ChromeDriver已经不再接受它。如果你运行的时候控制台报错把它注释掉即可不影响主流程。我在最初尝试这个方案时踩过一个坑只加了excludeSwitches但忽略了disable-blink-features。结果Chrome 88之后的版本中navigator.webdriver还是返回true。后来查资料才知道从Chrome 88开始Blink内核增加了一个独立的自动化控制特性开关必须单独关闭。2.2 动作二用CDP在页面加载前注入伪装脚本仅仅关掉自动化开关还不够。现在很多反爬脚本不仅查navigator.webdriver还会查navigator.plugins、navigator.languages、window.chrome是否存在甚至检查Permissions API。这些属性在手动打开的Chrome里都有正常的默认值但在Selenium启动的浏览器里有的被置空有的直接被删除。正确的做法是利用Chrome DevTools Protocol的Page.addScriptToEvaluateOnNewDocument命令在浏览器加载任何页面之前注入一段初始化脚本把缺失的属性和值补齐。这段脚本会作用于后续每一次页面跳转和iframe加载比execute_script只作用于当前页面要可靠得多。import json from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) with open(stealth.min.js, r, encodingutf-8) as f: stealth_js f.read() driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: stealth_js }) driver.get(https://example.com)上面代码里我引用了stealth.min.js这是接下来动作三的重要内容。如果你只是自测也可以直接用一小段精简的注入脚本来理解这个过程Object.defineProperty(navigator, webdriver, { get: () undefined }); Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5] }); window.chrome { runtime: {} }; Object.defineProperty(navigator, languages, { get: () [zh-CN, zh, en], set: () {} });这段脚本的原理其实很好理解Object.defineProperty可以直接覆盖浏览器内置属性的getter方法让目标页面在读取navigator.webdriver时得到一个“不存在”的假象。但要注意仅仅伪造属性值是初级套路进阶的反爬脚本会检查属性的“描述符”——比如真实浏览器里的navigator.webdriver属性是只读且不可配置的如果你用Object.defineProperty重新定义它specificity或者configurable的描述符特征可能和新版Chrome默认的实现不一致。这就是为什么完整版的stealth脚本需要模拟几百行代码不只是覆盖几个属性。2.3 动作三直接用现成的stealth方案别自己从头造轮子如果你不想手动去补齐所有特征可以直接使用现成的selenium-stealth库。这个库相当于把所有已知的自动化特征包括WebDriver标记、Chrome对象、Permissions API、WebGL渲染器、动画帧特征等一次性修复并且会跟随新版本特征持续更新。安装和调用都很方便# pip install selenium-stealth from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium_stealth import stealth options Options() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) stealth(driver, languages[zh-CN, zh, en], vendorGoogle Inc., platformWin32, webgl_vendorIntel Inc., rendererIntel Iris OpenGL Engine, fix_hiderTrue) driver.get(https://example.com)这里我还想提一个备选方案undetected-chromedriver。这个库的思路和selenium-stealth不太一样它是直接对ChromeDriver的启动过程做了一层深层次的patch让WebDriver本身就不暴露出自动化特征。在应对某些检测比较严格的目标网站时它的成功率往往比普通Selenium搭配stealth脚本更高。但它的劣势也比较明显对Chrome和ChromeDriver的版本匹配要求比较苛刻升级浏览器时需要同步升级库版本否则可能启动失败。我个人的实操经验是普通的公开数据采集selenium-stealth已经足够了如果遇到那种开了WAF强校验、连页面都打不开的情况再考虑切换到undetected-chromedriver。这两个库不要混用否则脚本注入冲突反而会暴露。3. 两个“装人样”动作行为模拟不是慢是自然身份层面的伪装做完之后接下来要解决的是行为层面的问题。这部分最难的不是技术而是对于“自然度”的把握。模拟真人行为不是越慢越好也不是动作越复杂越好而是要符合人在真实浏览网页时的心理状态。3.1 鼠标轨迹别当“闪现侠”默认情况下Selenium的click()方法是触发浏览器底层的点击事件它并不产生物理鼠标的移动轨迹。但在有鼠标轨迹检测的网站上这种操作就相当于“瞬移”——一个真人要从页面右上角移动到左下角的登录按钮中间必然有连续的运动曲线而不是凭空触发点击。处理方式有两种一种是用ActionChains模拟移动from selenium.webdriver.common.action_chains import ActionChains import random element driver.find_element_by_id(login) actions ActionChains(driver) actions.move_to_element_with_offset(element, random.randint(1, 5), random.randint(1, 5)) actions.pause(random.uniform(0.1, 0.3)) actions.click() actions.perform()另一种更精细的方式是注入一段鼠标轨迹函数按照贝塞尔曲线逐步把鼠标移动到目标位置。我封装过一个简单的版本获取起点终点坐标后通过JavaScript循环调用MouseEvent每次移动2~3像素并加入随机抖动。实战里其实不需要追求太复杂的轨迹算法。绝大多数检测系统看重的是“有没有轨迹”以及“轨迹是否过于直线”而不关心你是不是用贝塞尔曲线。只要你有真实的移动过程、有抖动、有停顿就已经通过了大部分行为检测。3.2 输入节奏别把send_keys当粘贴板另一个明显特征是文本输入速度。send_keys(username)会把整个字符串一次性填入输入框这在浏览器的性能记录里是非常异常的——真人打一串10个字符的账号至少需要1到2秒期间每一个字符的输入间隔都有差异。推荐的做法是逐字发送并在每个字符之间加入随机延迟import time import random element driver.find_element_by_name(username) element.click() for char in test_user_2025: element.send_keys(char) time.sleep(random.uniform(0.05, 0.2))还有一个细节在很多真实场景下用户输入密码时偶尔会输入错误然后删除重新输入。有经验的爬虫工程师会在输入比较长的字段时以一定的概率比如5%模拟一次“输入到中间删除两个字符再继续”的操作。虽然这看起来是增加复杂度但它能明显降低行为轨迹的“机器味”。另外输入前的等待绝对不能省。打开登录页面后用户先是看了一眼页面、移动鼠标、定位到输入框、点击这至少需要200到500毫秒。你如果页面刚加载完就立刻点击输入框并开始输入从性能时间线上看就是一个异常点。3.3 随机延迟与滚动行为让整个浏览节奏“有人气”页面滚动也是行为检测的重要维度。真人打开一个长页面时很少会从头到尾匀速滚动到底通常是在中途停留有的段落看仔细些有的一晃而过然后在接近底部的位置停下来犹豫一会儿再继续。简单地随机执行几次scrollBy是不行的更接近真实的做法是分段滚动每段之间加入阅读时间import time import random height driver.execute_script(return document.body.scrollHeight) current 0 while current height: step random.randint(300, 700) driver.execute_script(window.scrollBy(0, arguments[0]);, step) current step time.sleep(random.uniform(0.3, 1.2))相对的是请求与页面切换之间的随机延迟。很多人喜欢在循环里加一个固定的time.sleep(1)这其实比不加延迟更危险——固定间隔的请求就像是节拍器一样有规律很容易被统计模型识别。正确做法是每次都随机落在一定区间内比如1到3秒之间的均匀分布或正太分布。4. 一个“改身份”动作把指纹从里到外都理顺身份特征和行为特征都搞定之后还有一个很容易被忽略的维度环境指纹的一致性。简单说你的浏览器不能同时既显示Windows系统、又说自己用的是Mac的User-Agent也不能在IP归属地是北京的时区时运行在东京的时区配置里。这些“内部矛盾”在不做检测的网站上看不出问题但一旦对方跑指纹比对脚本就会立刻暴露。4.1 UA、语言、时区先保住第一层伪造UA是每个爬虫工程师最早学会的技能但很多人只替换了User-Agent字符串却忽略了其他配套参数。一套完整的浏览器声明应该包含UA、Accept、Accept-Language、Sec-CH-UA这几个头它们之间必须互相吻合。例如UA里声明自己是Chrome 126但Sec-CH-UA返回的浏览器版本是Chrome 120反爬系统一眼就能看出UA是被篡改的。options.add_argument(--user-agent) options.add_argument(--langzh-CN,zh;q0.9,en;q0.8)时区可以通过CDP来覆盖driver.execute_cdp_cmd(Emulation.setTimezoneOverride, { timezoneId: Asia/Shanghai })时区的检测通常和Date对象、Intl.DateTimeFormat方法有关。如果目标网站是一个电商平台它还可能通过时区判断你不是目标国家的用户从而触发风控。4.2 WebGL与Canvas别让显卡“说话”WebGL指纹是绕过大多数检测的关键。每个设备的显卡型号、驱动版本、渲染器字符串都是不同的浏览器通过WebGLRenderingContext.getParameter可以取到这组信息。Selenium启动的Chrome和真实Chrome的渲染器字符串可能完全相同但因为启动时缺少GPU相关的初始化数据有些检测脚本会拿到空值。在这里我们可以在注入脚本中覆盖WebGL的vendor和rendererconst getParameter WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter function(parameter) { if (parameter 37445) return Intel Inc.; if (parameter 37446) return Intel Iris OpenGL Engine; return getParameter.call(this, parameter); };这段代码能让目标网站读取WebGL厂商信息时得到一个“常见显卡”的值。相比于显眼的“NVIDIA高端显卡”或“AMD Radeon专业卡”大众的Intel核显反而是最不容易触发异常的。Canvas指纹方面不建议主动去伪造像素数据因为不同系统、不同浏览器渲染同样Canvas内容时本来就有细微差异只要不做特殊处理一般检测系统很难只凭Canvas就断言你是机器人。4.3 IP与请求头的整体协调最后剩下的是IP层面的协调。如果你的目标网站在国内建议使用国内住宅IP如果是海外站点尽量使用目标国或相近地区的IP。为什么说这块很重要因为如果你用Selenium伪装了一套北京时区、中文语言、Windows系统的浏览器指纹结果出口IP却显示位于法兰克福这种矛盾本身就是最大的风险信号。国内合法合规的代理服务商有很多选择时优先看对方的IP池是否覆盖目标城市、是否支持会话保持、并发连接数是否足够。配置方式很简单在ChromeOptions中加上代理参数即可options.add_argument(--proxy-serverhttp://username:passwordip:port)需要注意不要为了“保险”而一次性设置过长的IP使用时间。爬虫圈常说“短效IP容易封长效IP被封就全完”根据我的实践一个IP在目标网站上的并发请求数控制在10个以内、单次存活时间不超过10分钟整体稳定性会好很多。5. 实操中的常见问题与排查技巧实录写到这里我把自己在实际操作中遇到过的几类经典问题整理一下。这些问题你在各种教程里未必看得到但几乎每一个爬虫工程师都会在某个时间点遇上。5.1 Chrome版本一升级伪装就失效这是最频繁的问题。很多人头一天跑得好好的第二天系统或者Chrome自动升级之后同样的代码navigator.webdriver又变成了true。原因是新版本的Chrome可能会新增或改变检测特征旧的伪装脚本没有覆盖到。我的处理习惯是在本地写一个检测页面语言上用Python启动浏览器后读取一组关键参数navigator.webdriver、navigator.plugins.length、window.chrome、navigator.languages.toString()、WebGL vendor每次升级完Chrome或者Driver后先跑一遍检测确认所有特征符合预期再上线爬虫。5.2 无头模式一上就被封怎么办这是最有意思的一个问题。有朋友说自己的爬虫在本地有头模式跑得好好的一到服务器上改用--headless模式立刻被识别。原因主要有两个一是旧的无头模式和有头模式在浏览器配置上存在差异比如JS引擎、GPU支持、可用字体都不一致二是无头模式下的请求头往往没有Sec-CH-UA这些客户端提示信息这种缺失本身就是最大的特征。如果服务器没有显示器但你又需要无头模式我推荐用--headlessnewChrome 109已经开始支持新无头模式并手动补全UA和客户端提示头。如果还是不放心可以使用Xvfb在Linux服务器上虚拟一个显示环境继续跑有头模式。后者虽然更吃资源但检测难度会大幅降低。5.3 检测到iframe和window.top跳转有个别网站会在页面里嵌入iframe然后用脚本检测window.self ! window.top如果发现当前窗口被嵌在iframe里就会触发反制逻辑。Selenium默认进入iframe后可能会被认为是在模拟嵌套浏览。解决思路是优先使用主框架直接访问避免在无必要时进入iframe。另外我遇到过一次比较特殊的反制页面脚本会在加载时检测用户是否在极短时间内访问了多个无关联页面。如果访问路径太乱——比如刚打开帮助中心下一秒就跳转到了用户中心——这也不像真人行为。这种问题没法靠伪装手段解决只能在业务逻辑上合理安排访问顺序。5.4 双栈请求requests拿cookie后Selenium打开还是会掉这种操作很常见先用requests请求目标网站用获取到的Cookie再启动Selenium浏览器会话以为这样能“继承登录态”。但这样做的结果是字体、安装列表、Canvas指纹、JS生成的特征等数据是断裂的——requests没有执行JS自然没有生成这些浏览器动态特征Selenium启动后反而出现了Cookie与指纹不匹配的情况。很多童鞋在这里反复排查都找不到原因其实就是两个会话的指纹不一致。如果必须共用Cookie建议让Selenium自行完成登录流程获取登录后的Cookie后再交给requests处理API请求。反过来也可以但一定要保证两端使用相同的UA和指纹配置。5.5 兼容Selenium 4与ChromeDriver新版的注意点最后提醒一个兼容层面的问题。Selenium 4把驱动管理逻辑改成了基于Selenium Manager的自动化方案启动时如果本机的ChromeDriver版本和Chrome不匹配它会自动下载对应版本。这本来是一个便利功能但如果你部署在服务器上服务器没有外网权限它就会启动失败。解决方法是手动下载匹配的ChromeDriver放在指定目录里并在代码中声明Service路径。还有一点换到ChromeDriver 110以上之后很多老的excludeSwitches参数用法需要调整。新版本对于自定义参数的校验更加严格遇到报错要多看Selenium的release notes百度上很多教程都已经过时了。5.6 排查速查表现象可能原因优先排查方向navigator.webdriver返回true自动化开关未关闭检查excludeSwitches和disable-blink-features请求头中带HeadlessChrome用了旧版无头模式改为--headlessnew或手动改UA页面加载后立即出现验证码IP指纹异常检查代理IP与浏览器时区、语言是否匹配频繁要求登录WebGL与Canvas指纹缺失检查stealth脚本是否完整注入有时成功有时失败行为特征波动不足增加随机延迟模拟鼠标移动与分段滚动使用了requests和Selenium共享Cookie失败指纹不一致统一两端的UA、TLS指纹、IP写在最后的个人体会这些诡计折腾下来我的感悟是没有一套放之四海而皆准的“反检测配置”。每个站点用的风控引擎不同同一引擎在不同行业、不同流量层级下的灵敏度也不一样。所谓规避检测本质上是把你能控制的所有特征统一起来让风险模型找不到一个足够强的高危信号。在我自己的实践中最稳妥的组合是excludeSwitches disable-blink-features selenium-stealth 行为模拟 高质量代理五个环节缺一不可。而且我会专门维护一个“浏览器探测页面”每次项目上线前先跑一遍把所有关键参数都打出来截图留档这样下次出问题时能快速定位是哪个维度出了问题。最后想认真说一句这篇文章的目标是解决正常数据采集中被误伤的问题不是鼓励你去突破那些明确设置了付费墙、需要登录协议才能访问的数据。很多大站的风控系统花了几千万迭代到今天的水平靠几个脚本就去攻破既有法律风险又有技术风险。如果你的数据源很重要先去查一查对方有没有官方API很多时候付费API比你交的代理费还便宜而且稳定性高一个数量级。