做Selenium爬虫的人,基本都遇到过这类场景:脚本刚跑起来,页面正常打开,数据也加载了一部分,然后突然弹出一个验证框,或者网页直接返回一片空白,再看控制台,一行“拒绝访问”的报错躺在那里。我刚开始接触爬虫那几年,总觉得是代码哪里写错了,后来才发现,根本不是代码的问题——是网站的反爬系统已经认出你是一个浏览器自动化程序,而不是一个真人用户。
这个识别过程,其实没有你想的那么玄乎。反爬系统会从浏览器特征、运行指纹、操作行为等多个维度同时打分,一旦“机器人痕迹”太多,就把你标记出来,轻则弹验证码,重则直接拉黑IP。Selenium虽然是目前最主流的浏览器自动化工具,可它天生带着不少能被识别的“出厂设置”,所以才需要后天的伪装和调整。
这篇文章我会用一套组合拳,把我在真实项目中验证过的6个关键动作完整拆解出来。每一个动作都会讲清楚“为什么有效”,给出可以直接复制的Python代码,也会把我踩过的坑一起说出来。内容适合两类人看:一类是刚开始写爬虫、经常被验证码拦下来的新手;另一类是已经在用Selenium做数据采集,想提升稳定性和采集效率的老手。这套方案不能保证100%绕过所有检测,但至少可以让你从“每次都被识别”进阶到“大多数时候看起来都像个正常人”。
1. 反爬系统到底是怎么认出Selenium的
想做好规避,先得搞清楚对面的“侦查武器”是什么。反爬系统识别Selenium,并不是靠某一个单独的特征,而是综合多项线索来下定论。我把它们归纳成三个层面。
1.1 navigator.webdriver:最扎眼的“真身标志”
浏览器本身有几个JavaScript属性是专门给自动化测试工具用的,其中最关键的就是navigator.webdriver。普通的Chrome浏览器,这个值是undefined;而Selenium驱动的Chrome,这个值往往是true。反爬系统只要跑一行JS代码,就能立刻判断出你是不是自动化程序。
这个字段是Selenium被识别的头号原因。它之所以存在,是因为WebDriver规范要求自动化会话对页面公开自己的身份,方便开发者调试。但站在爬虫的角度来说,这个“身份信息”就是我们必须清除的破绽。
除了navigator.webdriver之外,还有一些衍生痕迹,比如window.cdc_开头的变量名(Chromium Driver相关的内部标识)、Chrome插件列表里多出来的扩展项。这些都是早期Selenium版本留下的“指纹”,虽然现在的反爬系统不一定每个都查,但能隐藏就隐藏,尽量把暴露面降到最低。
1.2 浏览器指纹:多维度交叉验证
就算你清掉了navigator.webdriver,反爬系统还会通过浏览器指纹来继续判断。指纹的意思,就是浏览器在很多细节上的参数组合,比如:
- 屏幕分辨率和可视区域大小(
screen.width、window.innerWidth) - 操作系统标识(
navigator.platform、navigator.userAgent) - 语言和时区(
navigator.language、Intl.DateTimeFormat().resolvedOptions().timeZone) - Canvas绘图指纹(通过绘制特定图形后提取像素数据)
- WebGL渲染信息(显卡型号、渲染参数)
- 字体列表、音频处理参数等等
真实用户打开Chrome,这些参数之间是高度自洽的。比如一个Windows系统用户,通常屏幕分辨率会是1920x1080或2560x1440,UA里会带Windows NT 10.0;但如果我们让Selenium在Linux服务器上跑,又不改UA,就会出现“操作系统是Linux,屏幕尺寸却和手机一样”这种明显矛盾,反爬系统一查就露馅。
1.3 行为节奏:机器人还是真人
浏览器特征之外,反爬系统还会观察你的操作行为。真人浏览网页的时候,鼠标会有加速度、会有小范围抖动,滚动页面是一段一段的,点击按钮之前会停顿一两秒来“思考”。但Selenium默认的click()方法,鼠标是瞬间跳跃到目标位置,点击间隔异常规律,滚动条直接跳到指定位置。
一套成熟的反爬系统,会把行为数据采集下来,用机器学习模型做分类判断。当出现“鼠标移动速度极快、点击前无停留、页面滚动由脚本控制”这些特征时,即便浏览器指纹伪装得再像,行为模型也会给你打一个较低的“人类分数”。所以后面我要讲的6个动作里,行为模拟和指纹伪装是密不可分的两半。
2. 动作一:用CDP启动注入,先干掉navigator.webdriver这个“卧底标志”
这是基础中的基础。如果navigator.webdriver还是true,后面再怎么做指纹伪装都是白搭。我第一次做规避处理时,就是先从这里下手的。
2.1 理解CDP:浏览器的“远程控制协议”
CDP的全称是Chrome DevTools Protocol,是Chrome提供给开发者工具的调试协议。Selenium的execute_cdp_cmd方法可以让我们在浏览器启动之后,直接向浏览器底层发送命令,其中有一个命令叫Page.addScriptToEvaluateOnNewDocument。
这个命令的作用是:在浏览器加载任何一个新页面之前,先执行一段你指定好的JavaScript。你可以理解为“页面刚开始加载时,先往里面打一针预防针”,让页面在执行自身脚本之前,就认为navigator.webdriver根本不存在。
2.2 动手实践:三行代码清除webdriver标志
下面这段代码,是我在项目里一直沿用的最小清除方案:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # 去掉“Chrome正在被自动化软件控制”的提示条 options.add_experimental_option("excludeSwitches", ["enable-automation"]) # 不加载自动化扩展 options.add_experimental_option("useAutomationExtension", False) # 禁用Blink引擎的自动化控制特征 options.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome(options=options) # 核心动作:在每一个新文档加载之前,重写navigator.webdriver属性 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """ }) # 跳转到目标页面 driver.get("https://example.com") # 验证一下:输出应该是 undefined print(driver.execute_script("return typeof navigator.webdriver"))这段脚本里最关键的是Object.defineProperty这行代码。它重新定义了navigator.webdriver这个属性的getter方法,让页面在读取时永远得到undefined。注意,这里我用了get: () => undefined而不是get: () => false,因为有些网站会判断值是否存在,undefined更贴近正常浏览器的表现。
2.3 常见误区:执行时机比代码本身更重要
有一个坑我必须要提醒:driver.execute_cdp_cmd必须在driver.get()跳转页面之前执行。因为Page.addScriptToEvaluateOnNewDocument只对“之后加载的文档”生效,如果先进入了目标页面,再执行这段命令,当前页面已经加载完了,里面的检测脚本早就跑完了,你之后再注入就来不及了。
另外要注意的是,excludeSwitches里的enable-automation选项,在部分Chrome版本上已经不再生效了。别管网上那些老教程怎么说的,实际测试发现--disable-blink-features=AutomationControlled这个参数在很多新版本Chrome上,直接写进options里的效果更稳定。我的建议是两者都写上,多一层保险总归没坏处。
3. 动作二:把浏览器指纹伪装得“严丝合缝”
清除navigator.webdriver只是第一步,接下来要做的是把浏览器的指纹整体调整到和真人一致。这里说的“一致”不只是某个参数改对了,而是所有参数组合起来要自洽,不能互相矛盾。
3.1 一份必须自洽的指纹清单
我给你整理了一份我在实战中必查的指纹清单。每次启动浏览器之前,我都会把这些参数过一遍,确保它们之间没有逻辑冲突:
| 指纹项 | 取值示例 | 自洽性要求 |
|---|---|---|
| 操作系统 | Windows 10 | 与UA里的Windows NT 10.0对应 |
| User-Agent | Chrome 120.0 Windows版本 | 不能出现“Mac UA + Windows平台”的组合 |
| 屏幕分辨率 | 1920x1080 | 注意,--window-size设置的是可视区域,不等于完整屏幕分辨率,有时候需要额外设置 |
| 设备缩放比 | 1 | 高清屏可能出现1.25或1.5,要和分辨率匹配 |
| 语言 | zh-CN, zh;q=0.9 | 与浏览器界面语言、页面Content-Language一致 |
| 时区 | Asia/Shanghai | 与操作系统的时区设置一致,频繁切换目标站点地区时要小心 |
| Canvas指纹 | 默认(不篡改) | 不建议手动改动,容易弄巧成拙 |
| WebGL渲染器 | 真实显卡或常见软件渲染 | 如果禁用WebGL,反爬系统反而会怀疑 |
你可能会问,为什么时区也这么重要?因为浏览器时区信息是从操作系统读取的。如果服务器部署在美国,目标网站是国内站,反爬系统发现你的浏览器时区是America/New_York,但页面语言和UA都是中文,这个组合在正常用户里很少见,就会提高你的异常分。
3.2 用Options参数给浏览器重新“塑身”
在Selenium里,调整指纹主要靠Options参数。下面是我常用的一个“塑身”配置:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # 隐藏自动化痕迹(动作一的加强版) options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) # 指纹塑造 options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") options.add_argument("--window-size=1920,1080") options.add_argument("--lang=zh-CN") options.add_argument("--force-device-scale-factor=1") options.add_argument("--timezone=Asia/Shanghai") # 这里需要把操作系统的时区也尽量保持一致 # 在Linux服务器上,可以通过环境变量 TZ=Asia/Shanghai 来设置 driver = webdriver.Chrome(options=options) # 验证关键指纹 print(driver.execute_script("return navigator.userAgent")) print(driver.execute_script("return screen.width + 'x' + screen.height")) print(driver.execute_script("return new Date().toString().match(/\\(([^)]+)\\)/)[1]"))这里有一个细节:--user-agent虽然改了UA,但navigator.platform这个属性不会跟着变。在Windows上它通常是Win32,在Linux上是Linux x86_64。如果UA显示Windows但navigator.platform是Linux,那就是一个互相矛盾的破绽。所以对于Linux服务器跑Selenium的情况,我会考虑用--user-agent的同时,再用CDP注入修改navigator.platform:
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'platform', { get: () => 'Win32' }); """ })这个方法可以顺便把navigator.language这类属性一起统一改掉,比依赖浏览器启动参数更灵活。
3.3 如何验证指纹是否有效
很多人改完参数就直接开跑,结果被拦截了还不知道为什么。我建议在正式采集之前,先打开目标网站,手动执行一段JS脚本,把关键指纹全部打印出来看一遍:
console.log({ userAgent: navigator.userAgent, platform: navigator.platform, language: navigator.language, webdriver: navigator.webdriver, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, screen: screen.width + 'x' + screen.height, outer: window.outerWidth + 'x' + window.outerHeight, inner: window.innerWidth + 'x' + window.innerHeight });执行之后逐项检查,尤其是webdriver必须是undefined,timezone必须正确。我遇到过很多次,改了一大堆参数,结果一查发现outerWidth比innerWidth只差十几像素,说明浏览器带滚动条,和窗口尺寸参数不匹配,反爬系统靠这个也能判断出异常。
4. 动作三:隐藏Chrome的自动化痕迹,别让控制台属性出卖你
除了navigator.webdriver,Chrome浏览器在自动化模式下还有一些“身份标记”藏在浏览器内部。这些标记普通人看不到,但反爬系统的检测脚本一跑就能发现。
4.1 关闭“正在受自动化软件控制”提示条
当你用Selenium启动Chrome的时候,浏览器顶部会显示一条黄色的提示条:“Chrome正在受到自动测试软件的控制。”这是默认开启的自动化提示,也是直觉上最容易被发现的破绽。
对应的处理方式是:
options.add_experimental_option("excludeSwitches", ["enable-automation"])这个选项会把Chrome的自动化控制开关从启动参数里排除掉。加上这行之后,那个黄色提示条就不会出现了。注意,这里说的是“排除自动化开关”,和后面要讲的--disable-blink-features是两个不同层面的操作,但两者往往一起用。
4.2 禁用Blink引擎上的AutomationControlled特性
Chrome的内核叫做Blink,它会根据是否处于自动化控制状态,暴露一些接口或者改变一些行为。AutomationControlled就是由Blink层暴露出来的一个特征。禁用它的写法很简单:
options.add_argument("--disable-blink-features=AutomationControlled")这个参数的作用是关闭Blink引擎上的自动化控制标记,让页面脚本在遍历浏览器特性时,看不到和自动化有关的痕迹。实测下来,这个参数对于部分检测能力较强的网站依然有效,是清单里的常青树。
4.3 处理残留的CDP注入变量
有些是在浏览器已经启动之后,页面能够检测出来的内部变量。比如说,部分旧版本的Selenium/ChromeDriver会在window上挂一个cdc_开头的属性。这个属性是ChromiumDriver的缩写,但正常浏览器里根本没有这个名字的变量。
这个残留变量在较新的ChromeDriver版本中已经越来越罕见了,但如果你还在用老版本,我建议升级一下驱动版本。有一个简单的排查方法:在目标页面里执行Object.keys(window).filter(k => k.startsWith('cdc_')),如果返回了结果,说明你的浏览器环境里还有这种特征,需要考虑切换驱动版本或做进一步注入清理。
这里我要特别说一句:网上有很多教程会教你用正则替换Chromedriver里的cdc_关键字,以此来欺骗检测。这种方式在原理上是可行的,但会破坏Chromedriver的签名和完整性,一旦网站升级检测策略,可能适得其反。我的建议是,优先用组件升级和配置调整来解决,不要一上来就动二进制文件。
5. 动作四:像人一样移动鼠标、滚动页面,告别“机械式操作”
指纹和特征都伪装完之后,还有一个环节很多人会忽略:操作行为。Selenium默认的click()和send_keys()太“直来直去”了,反爬系统看你的行为轨迹,一眼就能识别出机器人。
5.1 Selenium原生操作为什么会被行为识别
真人点击一个按钮,鼠标不会凭空瞬移到按钮中心。你的手部会带动鼠标,先快速移动到按钮附近,然后慢速微调,最后落点可能在按钮中心,也可能偏左几个像素。整个移动过程是有加速、减速、甚至小幅过冲的曲线。
而Selenium默认的click(),本质上是给浏览器发一个指令:把鼠标指针放到元素中心,然后按下。页面里记录的鼠标事件,是一个从某个位置瞬间跳到目标位置、立即点击的过程。检测系统只要计算一下鼠标移动速度,就会发现这个速度远远超过人类极限,于是直接标记为自动化操作。
5.2 用ActionChains模拟鼠标轨迹
我之前在采集过程中,写了一个模拟鼠标移动的辅助函数,核心思路是把一次鼠标移动拆成多个小段,每段带随机偏移和随机间隔:
import random import time from selenium.webdriver.common.action_chains import ActionChains def human_mouse_move(driver, target_element): """把鼠标从当前位置移动到目标元素上,模拟人类轨迹""" action = ActionChains(driver) # 先获取目标元素的位置和尺寸 target_x = target_element.location['x'] + target_element.size['width'] / 2 target_y = target_element.location['y'] + target_element.size['height'] / 2 # 获取当前鼠标位置 current_x, current_y = 0, 0 # 这里需要根据场景实际初始化 # 计算移动总距离 distance_x = target_x - current_x distance_y = target_y - current_y # 将移动拆成随机数量的步骤,模拟加速和减速 steps = random.randint(10, 20) for i in range(steps): progress = (i + 1) / steps # 先快后慢,模拟人的移动特性 eased_progress = progress ** 1.5 move_x = int(current_x + distance_x * eased_progress) move_y = int(current_y + distance_y * eased_progress) action.move_by_offset(move_x - current_x, move_y - current_y) action.perform() time.sleep(random.uniform(0.01, 0.05)) current_x, current_y = move_x, move_y # 最后移动到目标中心,再额外加一点随机偏移 jitter_x = random.randint(-3, 3) jitter_y = random.randint(-3, 3) action.move_by_offset(jitter_x, jitter_y).perform() time.sleep(random.uniform(0.1, 0.3)) target_element.click()这段代码里面,progress ** 1.5是用幂函数做的缓动效果,让鼠标在开始阶段移动快、接近目标时移动慢,符合人手运动的基本规律。当然,这个函数只是提供了一个基础框架,不同网站的检测维度不一样,你可能还需要根据实际情况调节步长和间隔。
5.3 滚动和输入的节奏也要“人化”
除了鼠标移动,滚动也是行为检测的高频观察点。真人看网页,是看一段滚一段,中间会有停顿,有时候还会往回调一点。而Selenium脚本如果用driver.execute_script("window.scrollTo(0, document.body.scrollHeight)"),就是一秒不到直接拉到底,毫无悬念。
我习惯把滚动拆成多次,每次滚动距离随机,之间的间隔也随机:
def human_scroll(driver, max_scroll): current_position = 0 while current_position < max_scroll: # 每次滚动随机一段距离 scroll_step = random.randint(200, 600) current_position += scroll_step current_position = min(current_position, max_scroll) driver.execute_script(f"window.scrollTo(0, {current_position});") # 随机停一下,有时候往回滚一点 time.sleep(random.uniform(0.3, 1.0)) if random.random() < 0.2: back_step = random.randint(50, 150) current_position = max(0, current_position - back_step) driver.execute_script(f"window.scrollTo(0, {current_position});") time.sleep(random.uniform(0.2, 0.5))输入文字也一样。真人在表单里填内容,是一个字一个字敲出来的,速度和按键间隔有波动。而send_keys()一次性把所有文本灌进去,速度极快。对于比较严格的站点,我建议用逐字输入再配合随机延迟的方式:
def human_input(element, text): for char in text: element.send_keys(char) time.sleep(random.uniform(0.05, 0.2))这三个函数组合起来,你的Selenium才能从“机器人行为模式”切换成“真人行为模式”。
6. 动作五:启动参数和服务选项的工程化配置
前面几个动作侧重于“伪装”,动作五则更偏向工程部署层面的稳定性。指纹、行为都优化了,但如果浏览器本身运行在一个被反爬系统认为“不常规”的环境里,一样会露馅。
6.1 常用启动参数对照表
我把Selenium启动Chrome时常用的参数整理了一份对照表,你可以直接拿来参考:
| 参数 | 作用 | 备注 |
|---|---|---|
--disable-blink-features=AutomationControlled | 关闭自动化控制特征 | 动作三已经讲过,必加 |
--disable-gpu | 禁用GPU加速 | 服务器环境没有GPU时建议启用,避免渲染异常 |
--no-sandbox | 关闭沙盒模式 | 在Docker或容器环境中必须加 |
--disable-dev-shm-usage | 禁用/dev/shm共享内存 | 容器环境内存受限时建议加 |
--window-size=1920,1080 | 设置浏览器窗口大小 | 直接影响window.outerWidth等参数 |
--lang=zh-CN | 设置浏览器界面语言 | 指纹自洽需要 |
--disable-extensions | 禁用浏览器扩展 | 减少指纹特征点 |
--disable-notifications | 禁用通知弹窗 | 避免网站活动弹窗干扰脚本 |
--incognito | 隐身模式 | 部分网站对隐身模式有策略,谨慎使用 |
--user-data-dir=路径 | 指定用户数据目录 | 多实例隔离、提升稳定性常用 |
6.2 无头模式的取舍与优化
无头模式(headless)是爬虫场景里的重灾区。options.add_argument("--headless")虽然能让浏览器在后台运行,不弹出窗口,更省资源,但无头模式下的Chrome很多特征和正常模式差别很大,比如:
- UA字符串里默认带
HeadlessChrome字样,这就是一个明晃晃的标志 - WebGL、Canvas的渲染结果可能和正常模式不一致
- 字体列表、系统API的暴露程度都不同
所以我的建议是:优先尝试有头模式。如果你的服务器有图形环境,或者你只是在个人电脑上跑程序,直接去掉--headless参数就好。如果确实因为服务器环境必须用无头模式,那你至少要做两件事:
第一件事,把UA改成正常Chrome的UA,去掉HeadlessChrome字样;第二件事,加上--window-size设置一个常见的屏幕分辨率,减少无头模式默认尺寸带来的暴露风险。
# 一个相对稳妥的有头模式配置模板 options = Options() options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") options.add_argument("--window-size=1920,1080") options.add_argument("--lang=zh-CN") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage")6.3 注意浏览器版本和Driver版本的匹配
这个属于工程层面最容易被忽略的细节,但雷区极大。Chrome升级是很频繁的,ChromeDriver如果跟不上Chrome的版本,启动时会直接报错“session not created: This version of ChromeDriver only supports Chrome version xxx”。
遇到这种问题别慌,去ChromeDriver的官方下载页找到和当前Chrome版本完全一致的驱动,替换掉即可。版本匹配这件事没有捷径,你只能老老实实看两个版本号,保持一致。我在采集环境里都会用一个自动化小脚本来定期检查Chrome版本,和本地Driver版本做对比,一旦出现大版本差异就自动提示我更新。
7. 动作六:频率控制、调度策略和验证码的实战应对
前面的动作都是“单次访问时不让网站认出你”,但数据采集是一个长期过程。就算你伪装得再好,如果请求频率太高,行为节奏再像人也白搭。频率策略和调度能力,决定了整个采集任务能持续多久。
7.1 请求频率:宁可慢,不可死
我发现很多爬虫新手容易掉进一个陷阱:以为爬得快等于效率高。实际上,对大部分中等体量的站点来说,控制频率才是效率的根本保障。一次采集任务哪怕要跑10个小时,只要你没被封,就是一次成功;但如果跑半小时就被封IP,之前的所有投入都清零了。
频率控制的做法很简单,在每次请求之间加一个随机延时:
import random import time def random_sleep(base=3, variance=2): """随机延迟,让请求间隔更像真人操作""" time.sleep(max(0.1, base + random.uniform(0, variance)))这个函数里base和variance的值要根据目标站点的访问压力来调整。对于比较敏感的站点,我通常会把间隔放到5到10秒;对于自身流量很大的平台,可以适当缩小到1到3秒。原则就是:你不确定的时候,先慢后快,逐步试探。
7.2 封IP后的延迟退避策略
即便你频率控制得再好,也有被反爬策略盯上的可能。一旦发现请求开始出现异常(比如频繁出现验证码、页面返回特定错误码),最忌讳的做法是继续用同一个IP硬冲。这时候应该启动延迟退避策略:
第1次被封:暂停5分钟 第2次被封:暂停30分钟 第3次被封:暂停2小时等于是给网站一个“冷却期”,等风控阈值降下来再恢复采集。这个策略我实测下来非常有效,能极大减少因为连续点击被拉黑的情况。
7.3 面对验证码:优先用行为引导,而不是硬破解
验证码是规避检测绕不过去的一环。滑块验证码、点选验证码、数字验证码,都已经成为主流网站的标配。我的观点是:遇到验证码,第一步想的不应该是“用什么平台去打码”,而是“为什么我触发了验证码”。如果前面几个动作都做到位了,大部分场景你根本碰不到验证码。
如果你确实需要处理滑块验证码,那么在写代码时,要注意模拟“拖拽轨迹”的连贯性,而不是简单地从A点拖到B点。很多失败的案例,都是因为拖动速度过于均匀,真实用户的拖动应该是一段慢、中间快、最后再慢下来的过程。如果你要用第三方打码服务,也尽量选带轨迹模拟能力的,这样可以提高通过率。
8. 常见问题与排查技巧实录
最后这部分,我把这些年做Selenium采集时遇到的典型问题整理成一个排查手册。很多问题看着复杂,实际上都有固定的处理套路。
8.1 改了navigator.webdriver还是被检测
这种情况我遇到过很多次。排查思路是这样的:先检查是不是JS注入顺序错了,确认execute_cdp_cmd确实在driver.get()之前执行;然后手动打印navigator.webdriver的值,如果显示undefined,说明第一层没问题,问题大概率出在别的指纹上。
常见的“别的指纹”包括:Canvas指纹异常(页面脚本能画出和正常浏览器不一样的像素)、WebGL渲染器信息暴露(服务器没有GPU会导致渲染器名称是SwiftShader)、以及浏览器插件列表为空(正常用户多少会装几个插件,空列表反而不正常)。针对这些,我的建议是不要一次性追求所有指纹都像真人,先把最容易被发现的三个搞定:UA、屏幕尺寸、时区,稳扎稳打。
8.2 无头模式一开就被识别
如果你设置--headless之后,发现目标网站立刻返回异常,我基本可以断定是UA里带了HeadlessChrome。排查方法是:
// 在页面中执行,查看UA是否包含Headless navigator.userAgent.includes("Headless")如果是这个原因,手动指定一个不带Headless标识的UA即可。如果UA没问题,再查WebGL的渲染器名称,无头模式下通常是SwiftShader,这也是一个强力识别点。为了绕开这个坑,我一般会在无头模式下启动之前,查一下目标网站用到的JS库(比如是不是用了fingerprintjs),然后针对性调整相关指纹。
8.3 IP被封,怎么判断该等还是该换
封IP之后,很多人第一反应是换代理继续跑。但如果是同一台机器、同一个浏览器指纹,只是换了一个IP,反爬系统照样能通过指纹把你关联起来。所以我的建议是:先确认封禁范围。如果你换一个普通浏览器访问同一个目标网站,发现完全正常,说明IP级别的封禁居多,这时可以等待冷却或更换IP;如果换浏览器后也异常,那就要回头检查指纹和代码逻辑了。
另外,不管用什么通道换IP,都要确保新IP的归属地和你的指纹参数一致。比如你的浏览器时区设置为Asia/Shanghai,但新IP的归属地显示在欧洲,这几个信息组合起来就会非常可疑。所以每次更换IP之后,我都会重新跑一遍上面提到过的指纹验证脚本,确保整体参数依然自洽。
8.4 一个简单的自检清单,开跑前过一遍
我每次上线新的采集脚本,都会过一遍下面这份自检清单,全部通过才放行:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| webdriver属性 | 页面执行JS | undefined |
| UA | 页面执行navigator.userAgent | 正常Chrome UA,无Headless字样 |
| 屏幕尺寸 | screen.width/height | 和你设置的窗口尺寸一致 |
| 时区 | Intl.DateTimeFormat().resolvedOptions().timeZone | 和目标地区一致 |
| 平台 | navigator.platform | 和UA里的系统一致 |
| 行为速度 | 观察鼠标轨迹间隔 | 有加速减速,非匀速瞬移 |
| 请求间隔 | 日志记录 | 每次间隔随机,非固定值 |
这份清单的检查过程本身很花时间,但带来的收益是实实在在的稳定。我宁可多花10分钟做自检,也不愿意跑了一下午才发现从头到尾都在被反爬系统“看戏”。
讲到这里,6个动作就全部拆完了。我在实际项目中最大的体会是,规避检测不是某一个技巧的比拼,而是一个系统工程。从浏览器启动参数、指纹伪装、CDP注入,到操作行为、频率控制,每一个环节都必须做到位,单点突破的意义不大。另外也想提醒一句:这套方法和代码,请只在你有合法数据使用权的站点上使用,尊重目标网站的robots协议和服务条款。反爬与爬虫的博弈永远在动态变化,今天有效的参数,明天可能就会被新的检测策略覆盖,保持学习和迭代的心态比记住任何一个固定配置都更重要。最后送你一个小技巧:把指纹验证脚本保存成一个单独的JS文件,每次换配置后先跑一遍,再开始采集,你会少踩很多无意义的坑。