ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Selenium应对反爬全攻略:指纹伪装与行为模拟实战

Selenium应对反爬全攻略:指纹伪装与行为模拟实战 做采集项目的人十有八九都会碰上反爬。我年初帮一个自建系统采集历年天气数据每天从某气象站点抓温度、湿度、风速数据量不大但对方的风控一天比一天严格。最初用 requests 直接拿 API后来封 IP换了 selenium 模拟浏览器结果还是被识别登录页面频弹验证码滑块拖了十几次过不去日志里各种WebDriverException。折腾了三个礼拜才稳定下来今天就把这套应对策略完整拆开聊。这篇文章不聊怎么攻击网站重点是你用 selenium 采集数据时怎么最大程度降低被反爬机制识别和限制的概率包括常见检测点、指纹伪装、人类行为模拟、代理轮换、验证码处理以及哪些场景其实根本不该用 selenium。适合正在做数据采集、爬虫调试、自动化测试或者被验证码和风控折磨得不行的朋友。内容偏 Python 生态但思路对任何语言都通用。1. 反爬机制到底在“反”什么1.1 别把反爬想得太神秘它就是在判断“你是不是真人”所有反爬机制本质上都是同一个问题后台看到一个请求它要推断这个请求是人发出来的还是程序发出来的。所谓“反爬”手段就是围绕这个判断建立的一系列特征采集。最常见的是请求频率。一个人再无聊也不可能每秒点两次“下一页”程序却可以轻松做到。所以第一道防线几乎都是频率限制比如同一 IP 一段时间内最多 N 次请求超出就返回 429、验证码或者直接封 IP。其次是请求头。正常浏览器会带一整套User-Agent、Accept-Language、Referer、Sec-Fetch-*之类的头信息顺序和值都有规律。requests 默认的python-requests/2.31.0一眼假所以新手拿 requests 抓那些防御严的站第一步就被识别了。然后是行为链路。真人访问页面会先在输入框点一下再输入字符字符是一个一个上屏的中间有思考停顿鼠标移动路径是曲线滚动页面也有加速度。程序往往是瞬间填充表单、瞬间点击、瞬间滚到底。这些行为细微但可量化现代化风控系统已经能通过前端埋点全量采集。再往后是浏览器指纹。Canvas 绘制、WebGL 渲染、字体列表、屏幕分辨率、时区、语言、CPU 核心数、触控支持等等组合起来可以形成一个非常高精度的指纹。你用 selenium 启动的裸 Chrome 和正常用户打开的真实 Chrome指纹上有明显差异。最后是验证码、滑块、设备指纹联动。当你被多次判定为“可疑”风控会给你出验证题通过才放行。这一层本质上是把最终判断权交给“人机验证服务”所以很多采集项目最终都会卡在验证码这一关。1.2 动手前先做两件事否则后面全是坑第一件事确认数据获取是否合规。我在接天气数据这个项目时先查了目标站点的 robots.txt确认哪些路径允许抓取哪些不允许。另外数据如果涉及个人信息、版权内容、商业机密采集和使用都有法律风险。这一块不是空话你辛辛苦苦把数据采下来结果不能商用那才是最大的坑。第二件事明确采集规模。你是想采一次还是每天增量采集数据量是几十条还是几十万条如果只是一次性拿几百条数据最好优先找公开 API 或者别人整理好的数据集犯不着跟反爬死磕。真要天天采、大量采才需要下面的整套工程方案。1.3 selenium 不是万能钥匙但有些场景不得不选它selenium 最大的价值是能执行 JavaScript处理动态渲染页面。很多现代网站数据是通过异步接口加载的直接 requests 请求 HTML 拿不到数据得分析 XHR 接口、处理 token、签名。如果你不想逆向 JS或者对方加密签名的成本太高selenium 就是最快的路子。但代价也很明显慢、重、容易被识别。一个 selenium 实例占用几百 MB 内存页面渲染一秒你采集一千条数据可能要一小时。所以在项目设计时我一般会把数据源分成几类有开放 API 的用 requests有 JSON 接口但带简单签名的用 requests 签名模拟只有纯浏览器渲染或接口加密复杂的才上 selenium。如果你已经决定用 selenium那接下来的问题就是它到底哪里露馅了。2. selenium 为什么会被一眼识破2.1 浏览器里藏着的“我是自动化工具”标志selenium 启动的 Chrome 和普通 Chrome 其实是有区别的。最典型的是navigator.webdriver这个属性。在正常浏览器里它是undefined或者false而 selenium 驱动的浏览器里它往往是true。很多检测脚本只要读这一行就能把你筛掉。除了 webdriver还有其他差异。比如window.chrome对象缺失、navigator.plugins列表为空、navigator.languages不完整、PermissionsAPI 行为异常还有 Chrome DevTools Protocol 的连接痕迹。这些特征单独看很难说明问题但组合起来风控引擎用一个简单的 JS 检测脚本就能给浏览器打“自动化”标签。我自己踩过的坑是一开始只改了User-Agent觉得可以蒙混过关结果对方的 JS 里同时检测了navigator.webdriver和window.chrome我直接撞枪口。记住反爬检测是多点联合判断单点优化没有用。2.2 浏览器指纹差别到底有多大如果只是加一个webdriver标志隐藏那就太天真了。现代风控会采集更细的指纹。我举例说明Canvas 指纹。网站会在页面上绘制一段文字或图形然后读取像素信息。不同浏览器、不同显卡驱动、不同系统渲染结果都有细微差异。selenium 驱动的 Chrome 和正常 Chrome 如果版本不同、字体缺失绘制结果就会不一样后台就能判断“这个浏览器指纹很少见”。WebGL 指纹也一样。显卡渲染三维图形的参数、扩展名、渲染结果都可以被采集。Linux 服务器上装的无头 Chrome没有真实 GPUWebGL 信息会异常。字体指纹。正常 Windows 系统有几十种字体而很多 Docker 容器里的 Chrome 只有几款基础字体。通过document.fonts.check()检查字体是否存在就能判断你是不是在真实用户环境里。音频指纹、时区指纹、语言指纹……采集项多到你数不过来。但要注意普通站点不一定全用只有那些抗采集需求强的站点才会集成重型风控 SDK。所以应对策略也应该按等级来不要一上来就搞最重的。2.3 行为信息的缺失同样致命除了静态指纹动态行为也是重要维度。真人打开页面后鼠标会小幅移动滚动是间歇性的会在某一段停下来读内容再继续往下滚。而 selenium 的页面操作是离散的定位一个元素点击定位另一个元素点击。如果网站埋了 mouse move 事件监听你的鼠标坐标可能从头到尾都是固定值或者完全没有任何 mousemove 记录。这种“行为空窗”一样会被后台标记。还有页面加载速度。真人从输入网址到点击按钮至少有几秒间隔但脚本往往页面刚加载完就立刻操作。如果你用隐式等待加固定等待时间太短后台通过指标能发现“这个用户动作快得不正常”。我后来养成的习惯是所有页面跳转之间加入随机睡眠模拟阅读时间鼠标移动用曲线轨迹而非直线跳转滚动用增量方式而不是window.scrollTo(0, document.body.scrollHeight)一步到位。这套操作虽然简单但对降低风险真有很大帮助。3. 实操一套完整的 selenium 反“反爬”方案3.1 先换驱动用 undetected-chromedriver 替代裸 selenium如果说只做一件事来降低 selenium 的识别概率那我首选换undetected-chromedriver。这个库对 Chrome 做了大量补丁能隐藏navigator.webdriver修正部分指纹特征让 selenium 驱动的浏览器看起来更像正常用户。安装很简单pip install undetected-chromedriver基本用法import undetected_chromedriver as uc driver uc.Chrome( version_main120, # 可选指定Chrome主版本 headlessFalse, # 不建议无头模式无头特征太明显 ) driver.get(https://example.com)我第一次用它的时候之前必出的滑块验证直接不出了首页加载时间也正常。当然它不是万能的undetected-chromedriver没有封装的场景或者网站检测太强还是可能被识别。但作为基础替换性价比极高。注意如果遇到启动报错多半是 Chrome 版本和驱动版本不匹配。先运行chrome://version查看浏览器版本然后在代码里指定version_main。另外尽量保持 Chrome 自动更新关闭用固定版本部署否则线上浏览器一升级驱动就得跟着调。3.2 手动伪装请求头与浏览器启动参数undetected-chromedriver解决了一部分问题但还不够。我一般还会手动设置启动参数让它更接近正常用户环境。import undetected_chromedriver as uc from fake_useragent import UserAgent ua UserAgent() options uc.ChromeOptions() options.add_argument(fuser-agent{ua.random}) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_argument(--langzh-CN) options.add_argument(--window-size1920,1080) options.add_argument(--disable-infobars) options.add_argument(--no-first-run) options.add_argument(--no-default-browser-check) driver uc.Chrome(optionsoptions)这里有几个参数背后的原因--disable-blink-featuresAutomationControlled可以关掉 Blink 内核的自动化控制标记能去掉一部分 webdriver 痕迹。--langzh-CN是为了让浏览器的语言环境跟目标站点用户一致避免出现中文站点却一嘴英文浏览器这种异常。window-size设置成常见分辨率1920x1080 是最稳的太小或太大都会让指纹显得独特。--no-first-run和--no-default-browser-check是去掉首次启动的引导页既能提速又能减少特征变化。fake_useragent可以根据统计概率返回真实的浏览器 UA比自己手写字符串要靠谱得多。当然如果你只针对几个站点也可以从 Chrome 开发者工具里复制一个真实的User-Agent写死。3.3 页面加载策略与等待时间调优采集效率和安全是一对矛盾。页面加载得太快容易被识别太慢又影响效率。我在项目里采用两级等待策略。第一级是页面加载阶段设置page_load_timeout为 30 秒超过就重试。第二级是元素等待显式等待目标元素出现超时 10 秒。不要用time.sleep(10)这种固定等待速度慢不说行为模式还容易被算法识别出“每次间隔都一样”。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By driver.set_page_load_timeout(30) try: wait WebDriverWait(driver, 10, poll_frequency0.5) wait.until(EC.presence_of_element_located((By.ID, dataTable))) except Exception: driver.refresh()等待之外还要加随机延迟。我封装了一个函数import random import time def random_pause(min_sec0.8, max_sec2.5): time.sleep(random.uniform(min_sec, max_sec))所有页面切换之间调用这个函数间隔模拟真人阅读。这个习惯在长周期采集里非常关键能显著降低平均请求频率波动。3.4 鼠标轨迹和滚动行为模拟鼠标轨迹模拟是很多人忽略的细节。ActionChains默认的操作是瞬间从 A 点移动到 B 点没有中间过程。真实用户鼠标是曲线运动还有微小偏移。我实现过一个简单的贝塞尔曲线鼠标移动函数import numpy as np from selenium.webdriver.common.action_chains import ActionChains def human_move(driver, element): actions ActionChains(driver) # numpy 生成从当前位置到目标位置的曲线路径 # 这里不展开数学细节核心思路是分成多步小移动 actions.move_to_element(element) actions.perform() random_pause(0.3, 1.0)其实更简单的方式是使用ActionChains的move_by_offset分多步移动每步之间加随机停顿。很多开源项目已经实现了人类鼠标轨迹算法可以直接拿过来改。只要记住目标不是完美模拟而是不要留下“零移动记录”这种致命特征。滚动行为也重要。我写了个分段滚动函数def human_scroll(driver): current 0 target driver.execute_script(return document.body.scrollHeight) while current target: step random.randint(200, 500) current step driver.execute_script(fwindow.scrollBy(0, {step})) random_pause(0.2, 0.8)这样页面内容是慢慢出现的跟人眼看着内容往下翻的节奏差不多。如果目标页面是列表可以先滚动到页面底部让所有懒加载内容都渲染出来再开始定位元素能少踩很多“元素找不到”的坑。3.5 代理与 IP 轮换的正确姿势IP 被封是最常见的反爬手段。如果你的采集目标有严格频控单一 IP 一天几万次请求那不管怎么伪装浏览器IP 先暴露了。这种情况下需要代理池。我在天气数据项目里用的是自建代理池提前准备了一批住宅代理和机房代理按权重分配。住宅代理价格贵但质量好机房代理便宜但容易被识别适合低强度场景。关键不光是换 IP还要控制每个 IP 的请求频率。我给每个代理设置了一个配额比如一个 IP 每 10 秒最多 2 个请求超过就换下一个。前端看起来就是一个普通用户在多台电脑上分散访问。代码层面selenium 的代理设置很简单options.add_argument(--proxy-serverhttp://127.0.0.1:7890)但生产环境里我不推荐直接写死代理。更好的做法是用mitmproxy或者自研一个本地网关动态给每个浏览器进程分配独立代理。这样可以在不重启浏览器的情况下切换出口 IP。注意现在很多高质量站点会检测代理 IP 的匿名度、历史行为。如果你的代理 IP 之前被用来刷流量可能一上来就是高危标记。所以选代理供应商很重要宁可贵一点也别买那种已经被用到烂的共享代理。3.6 验证码和滑块先想怎么不触发再想怎么处理验证码是采集项目里最耗精力的一环。我做了多年采集最大的心得是最好的验证码处理方式是“不触发”。只要你的浏览器指纹和访问行为足够像真人验证码出现的频率会大幅下降。如果还是触发了分两种情况。滑块验证。很多网站的滑块并没有做过多的轨迹校验你可以用 selenium 模拟拖拽但要注意轨迹不能是一条直线。我常用的方式是分两到三段移动先在滑块上按住移动一段距离暂停再继续移动最后快速滑到目标位置。目标距离可以通过计算图片缺口得到涉及 OpenCV 的边缘检测。拼图验证。这类验证码更复杂需要识别缺口位置再模拟拖拽。如果频次不高手工过一遍也花不了多少时间。如果频次很高建议评估是不是该降低采集频率或者换数据源。图形验证码。基础 OCR 可以用 ddddocr识别率很高。但面对复杂扭曲、干扰线多的验证码OCR 就不太稳了。这时商业化打码平台是选项之一但要注意成本和使用合规边界。我在项目里坚持一条原则验证码出现频率超过总请求数的 5%就停下来检查是不是指纹或行为模拟出问题了。继续硬刚是浪费时间。4. 常见问题与排查技巧4.1 高频问题速查表现象可能原因解决思路启动浏览器后页面显示“您使用的浏览器异常”裸 selenium 被 webdriver 检测换 undetected-chromedriver检查启动参数登录时频繁弹滑块行为模拟不够自然或者 IP 异常加随机延迟、模拟鼠标轨迹换干净代理第一次能访问刷新后无法访问浏览器指纹被持久化标记清理缓存、Cookie换浏览器模式页面加载慢等待超时网络代理慢或被限速检查代理质量调整等待策略降低并发元素定位不到偶尔有偶尔没有懒加载页面数据未完全渲染先滚动到底再显式等待元素出现采集到的数据有缺失或错乱页面结构动态变化或接口返回不完整增加解析校验记录日志失败重试这张表是我在实际项目里总结的不能说覆盖所有情况但排查时能帮你快速缩小范围。4.2 一条靠谱的排查顺序遇到被反爬的时候别瞎调。我的排查顺序是先看浏览器手动访问是否正常。如果手动访问都进不去说明网站本身挂了或者你本地 IP 被封跟代码无关。然后开 Chrome 开发者工具对比正常浏览器和 selenium 浏览器的 Network 面板看请求头和响应头差异。很多风控会在响应头里带上标记比如Set-Cookie里出现特殊的校验字段。接着看控制台有没有报错。打开浏览器的 Console如果出现类似navigator.webdriver is true的提示说明检测脚本确实在起作用。这时优先换 undetected-chromedriver再检查启动参数。最后看时序。把采集日志打出来记录每个请求的耗时、间隔、状态码、验证码出现次数。统计下来如果某个 IP 段的验证码率明显偏高那就是代理池质量的问题如果浏览器打开后验证码率也高那就是指纹的问题。4.3 采集完别急数据清洗和落库才是后半场热词里有句话叫“数据治理要先采集再清洗”太对了。我今年辅助的好几个采集项目数据采下来之后往往乱七八糟日期格式不统一、字段缺失、重复记录、异常值。如果直接拿去做分析结论全是错的。我通常会在采集脚本里加一个清洗层至少做三件事。去重。用数据里唯一的 ID比如天气数据的日期加站点编号作为主键。数据库里插入时用INSERT ... ON DUPLICATE KEY UPDATE天然去重。格式统一。日期全部转成 ISO 格式数值型字段转成 float空值统一填NULL不要留各种五花八门的空字符串。异常值校验。拿温度来说如果某个值超过历史极值要么标记为可疑要么丢弃。这个逻辑可以在采集之后单独跑一个校验任务也可以用简单的范围判断在写入前完成。存储层面小数据量用 SQLite 就行大数据量上 MySQL/PostgreSQL。采集脚本做到幂等重复执行不会产生脏数据。5. 更稳的方案什么时候应该放弃 selenium5.1 selenium 不是银弹对比 requests 和 Playwright 再选型如果目标网站有公开 API并且你只是拿数据展示用那用 requests 就够了轻量、快速、不易触发反爬。selenium 反而是“杀鸡用牛刀”页面渲染开销大还多了很多被检测的特征。如果页面是动态渲染但又不想碰 JS 逆向playwright 也是不错的选择。Playwright 的自动等待机制比 selenium 顺手还有内置的add_init_script可以方便地在每个页面加载前注入 JS隐藏自动化特征的方式也更优雅。我在一个新项目里已经用 Playwright 替代了大部分 selenium 场景。但对一些老系统、依赖特定浏览器插件的场景selenium 依然有它的位置。我的建议是不要把技术栈锁死多备几套方案根据站点反爬强度灵活切换。5.2 分布式采集不是越高并发越好很多人一上来就想搞分布式觉得速度快、效率高。但分布式采集有一个大坑如果多个节点的 IP 段、浏览器指纹、行为模式都差不多风控系统很容易把它们关联起来识别成同一个团伙。一旦被识别封禁范围可能是整个 IP 段。我在项目里反而是“克制派”。宁可单机跑控制请求频率把采集周期拉长也不盲目加节点。如果非要分布式每个节点的浏览器指纹尽量差异化比如不同的字体、不同的分辨率、不同的 UA。这听起来复杂但对长期稳定采集非常重要。5.3 工程化必备监控、告警、重试最后聊点工程化的经验因为采集程序不是写完就万事大吉它是要长期跑的服务。监控方面我每个采集任务都会输出结构化日志包括时间戳、请求 URL、状态码、耗时、验证码标识。日志汇总到本地文件或日志系统每天扫一遍看有没有异常趋势。告警方面当连续失败次数超过阈值或者验证码出现率飙升触发告警。常见的实现是写一个定时脚本检查日志里的失败比例超过 20% 就往企业微信或钉钉发消息。我还在任务里加了“看门狗”如果某个页面 3 分钟没完成自动重启浏览器。重试方面请求失败要区分是网络问题还是反爬问题。网络问题直接重试反爬问题就切换代理等待时间再试。重试次数要有限制否则会陷入死循环。我一般最多重试 3 次3 次都失败就跳过最后统一补采。这一套流程搭起来之后我那个天气数据项目已经稳定跑了四个月被封次数屈指可数。日常维护就是偶尔看看日志切切代理别让验证码率超标。如果你正在被反爬折磨建议先别急着堆各种黑科技照着这篇文章把指纹伪装和行为模拟做扎实九成的坑都能避开。数据采集是个持久战稳才是第一位的。
RELATED READING

延伸阅读

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