百度旋转图片验证码,应该有不少朋友在登录百度账号、转存网盘资源或者发帖时候遇到过。它跟常见的滑块验证不太一样,不是把碎片推到对应凹槽里,而是给你一张倾斜的图片,让你用鼠标或者手指把它转到水平位置。这个交互看起来很简单,背后涉及图像处理、角度回归、人机交互轨迹模拟等多个技术点。这篇文章我把旋转图片验证码的运行机制、识别思路和自动化适配过程整理出来,给正在搞自动化测试、爬虫调试或者纯粹对验证码技术感兴趣的朋友一份能直接上手的参考。
1. 旋转图片验证码的运行机制拆解
1.1 它一般出现在哪些场景
百度系的验证码不是固定一个地方出现,而是由风控系统动态下发。我实际遇到比较多的场景是这么几类:
- 未登录状态下批量访问搜索结果页,触发风控后弹出验证。
- 百度网盘在分享链接下载大文件时,需要先完成验证。
- 贴吧发帖、回帖频率过高,被临时要求验证。
- 新设备、新网络环境下登录账号,为了确认是真实用户在操作。
这个机制跟大多数验证码平台一样,本质是后端根据用户行为、设备指纹、IP可信度算出一个风险分值,达到阈值就弹验证码。旋转样式的验证码属于“高交互成本”那一类,比滑块要难一点,但比点选文字要简单一些。因为图片倾斜角度一旦超过45度,人的眼睛也需要反应一下才能判断哪边是上,自动化程序如果要精确识别,就需要真正理解图像内容,而不是靠简单的模板匹配就能过关。
1.2 从页面加载到校验通过的完整链路
我拆过百度的旋转验证码交互流程,大致是这么走的:
- 前端发起验证初始化请求,拿到一个验证码ID和图片数据。
- 图片数据通常以base64格式返回,前端把它渲染到一个圆形容器里。
- 用户按住图片下方的滑块按钮拖拽,图片跟随旋转。
- 松手后,前端把旋转的角度值连同用户操作轨迹提交给后端校验接口。
- 后端判断角度是否在误差允许范围内,同时校验轨迹特征。
- 校验通过返回token,前端拿着这个token继续刚才被拦截的业务请求。
这里面有两个关键参数:一个是用户最终旋转的角度误差,一个是操作轨迹。角度误差是明面上可以量化的,轨迹数据则是后端暗地里用来判断“这个操作是真人还是脚本”的依据。
我实测下来,百度这个旋转验证的角度容错范围大概是正负6到8度,不是特别严格。也就是说你转了个大概齐,只要不超过误差阈值,后端是认的。真正卡人的其实是轨迹特征和请求频率。
1.3 角度是怎么生成和校验的
服务端在下发验证码的时候,会先选定一张正常的图片,记录图片原本“水平”的状态,然后随机生成一个旋转角度,比如逆时针137度,接着把图片旋转后再发给前端。前端用户操作的过程,本质上就是把图片再转回去,转的角度接近-137度或者说是223度,图片就回到水平状态。
这里有一个很容易踩坑的认知误区:很多人以为后端校验的是最终图片是否“回正”,不是的。后端拿到的只是用户最终停下的角度数值,它把这个数值跟原始随机角度做差值运算,差值落在容错区间内就算通过。
所以在做自动化适配的时候,没必要真的去做图像比对确认最终画面是否水平,只需要预测出“当前图片需要转多少度才能回正”,然后把这个角度提交上去就行。
另外,不同版本的验证码返回参数名可能不一样,有时候是rawAngle,有时候是angle,还有时候会把初始角度通过一个JSON字段下发到前端。拿到初始角度之后,配合预测出的修正角度,就能算出最终提交值。这个细节等后面实操环节再展开。
2. 交互设计与图片结构里的门道
2.1 为什么用旋转而不是滑块或点选
从产品设计角度看,滑块验证码已经被破解得太成熟了,很多开源方案能直接识别缺口位置并模拟拖动。点选文字验证码容易受中文识别模型训练集影响,题库一旦更新,旧模型立刻失效。旋转验证码相对来说更依赖“对图像语义的理解能力”,这种能力在深度学习普及之前是比较难无差别实现的。
而且旋转交互的过程天然会产生一条连续轨迹,这条轨迹比滑块那种单方向拖动包含更多特征维度。人用手控制鼠标转一圈,速度是时快时慢的,中间有时候还会停顿回拉一下,这些特征到了后端都会被提取出来做行为建模。
对于普通用户来说,旋转验证甚至不需要动脑子,就是“看着歪了什么转正就行”,门槛很低。但对自动化程序来说,既要识别图像方向,又要模拟出人类轨迹,难度直接翻倍。这应该就是旋转验证码能长期存在的原因。
2.2 图片资源里的猫腻
我抓到过多次旋转验证码图片,总结下来有几个比较有意思的特点:
- 图片固定是正方形,边长通常在300到500像素之间,加载后会按CSS缩放到指定显示尺寸。
- 图片背景一般是不透明的,可能是纯色背景,也可能是有语义的人像、风景、商品图。
- 大部分情况下,图片里会有明显的地平线、建筑物边缘、人像五官或者文字方向,这些都是用来判断方向性的关键线索。
- 部分验证码会在图片四周加入类锯齿状的白边,或者在角落加上倾斜的噪点线条,这是专门用来干扰计算机视觉算法识别主轴的。
如果深入研究过各家验证码会发现,百度这套旋转验证码的图片素材其实很多来源于自家生态内的内容,比如地图街景里的建筑物、百科词条里的标志物、网盘里被用户上传的相册图片。不同来源对应的特征差异很大,比如地图街景经常有道路和天际线,人像图片主要靠五官方向和下颌线来判定。
这个特点决定了:针对一种类型图片训练的模型,换到另一类图片上效果可能会掉得厉害。所以做工程化方案的时候,需要收集尽量多样化的图片样本,或者干脆在推理时加一个图像分类的预处理,先判断图片类型再选择对应的方向估计策略。
2.3 轨迹采集与风控模型
前端会把用户从按下滑块到松开的完整动作采集下来,包括鼠标或触摸的坐标序列、时间戳、压力值(移动端),甚至可能加上设备陀螺仪数据。这些数据打包成一个加密字段,跟在角度值后面一起提交。
风控后端拿到轨迹后,会提取类似下面这些特征:
- 总耗时:人类完成一次旋转通常需要0.8秒到3秒,太快或者太慢都异常。
- 轨迹点数:人类操作一秒钟大概会有60到100个采样点,脚本如果只上传了端点坐标,一查一个准。
- 移动是否平滑:人类拖拽会有微小抖动,轨迹点不会在一条完美直线上。
- 是否有停顿和回退:人偶尔会拖过再拖回来微调,这个很关键,纯脚本轨迹通常是一条直线到头。
所以哪怕你的角度预测非常准,只要轨迹数据是直接拉一条直线生成的,照样过不了校验。这就是为什么很多搞自动化的人角度识别做得很好,最后还是卡在“最后一公里”。
3. 自动化识别的几种技术路线
3.1 传统视觉方案:边缘检测加主轴估计
最早做旋转验证码识别,没上深度学习之前,大家用的都是传统图像处理。核心思路其实就一句话:找到图片中最显著的方向性线索,然后算出它和水平方向的夹角。
操作大概是这样的:先把图片转成灰度图,用Canny算子提取边缘,然后对边缘像素做主成分分析或者霍夫直线检测,得到图像的主方向。如果图片里有明显的地平线、建筑物轮廓、梯形结构,主方向往往能对应到真实的重力方向,用这个跟水平方向做差就是需要旋转的角度。
我实际试过这个方案,在纯风景、有明显天空和地面分界的图片上,准确率能到80%以上。但一碰到特写人像、抽象图案、光线杂乱的图片就崩了。因为主成分分析本质上是统计所有边缘的整体方向分布,当画面里有大量任意方向的纹理时,主方向会被带偏。
另一个传统思路是用特征点匹配,预先准备一批“正立”的参考图库,把待旋转图片跟参考图库做ORB或者SIFT特征匹配,通过匹配点对求解单应性矩阵,从而估算出旋转角度。这方案需要维护一个较大的正立图库,而且对图片内容要求很高,如果待验证图片在参考图库里找不到相似样本,匹配就会失败。
总结下来,传统方案优点是不需要训练数据、纯OpenCV就能跑,缺点是泛化能力差,只能在比较理想的图片上工作。现在做工程化方案,基本上都转向深度学习方向估计了。
3.2 深度学习方案:让模型直接预测角度
深度学习做旋转验证码识别,主流做法很有意思,跟一般图像分类不一样,它不是预测这是什么物体,而是把整张图片作为输入,输出一个代表旋转角度的数值。换句话说,这是一个回归任务,目标变量是0到360度之间的角度。
模型通常这样搭:用ImageNet预训练的ResNet50作为特征提取骨干网络,去掉最后的全连接分类层,接一个输出维度为1的全连接层,输出值乘以360就得到预测角度。还有一些变体做法是输出两个值,一个表示正弦、一个表示余弦,通过atan2还原角度。前者简单直接,但存在角度接近0度时误差被放大的问题;后者做了周期连续性建模,实际训练中更容易收敛。
关键技巧在数据准备上。需要准备一批“正立”图片,然后随机旋转任意角度作为训练输入,旋转前的原始角度就作为标签。数据增强时要注意,除了常规的平移、缩放、颜色抖动,不要做随机旋转增强,否则会把标签弄乱。
我搭过一个这样的模型,用1000张正立图,每张随机旋转10次生成1万条训练样本,在ResNet50上微调了10个epoch,验证集上的平均绝对误差能控制在5度以内。这个精度已经足够通过百度的旋转验证了。
提升精度的小技巧是把角度回归改成“角度区间分类加细粒度回归”的分层方案。也就是先判断图片属于哪个粗区间,比如每45度一个桶,共8个桶,做分类;然后在桶内做回归,预测与桶中心角度的偏移量。这种方案比直接回归误差更小,而且训练时更稳定。
3.3 第三方打码平台作为兜底
自己训练模型需要标注数据,如果只是临时接一下验证码,建模成本可能高于收益。这种情况下可以考虑接入第三方打码平台。
打码平台的接入逻辑比较简单:把验证码图片上传到平台接口,平台上的真人标注员会帮你旋转并返回正确角度,然后你再把这个角度模拟提交。平台通常按次收费,单次价格很低。
但这个方案有几个风险要单独提一下:
- 延迟不稳定,高峰期可能等几秒才有结果。
- 部分平台会收集图片数据,存在泄露风险。
- 用打码平台相当于把自动化特征暴露给了第三方,如果第三方接口不稳定,整体流程可靠性会受影响。
我的建议是:打码平台适合“应急”和“低频跑通”场景,适合做开发阶段测试,长期稳定跑的话还是自己训练模型更可控。
3.4 模拟交互的自动化方案
角度预测只是解决了“该转多少度”的问题,剩下还有“怎么把转的操作执行出来”。这一步需要在浏览器自动化环境里完成。
目前主流方案是Playwright,它同时支持Chromium、Firefox、WebKit,API也比旧时代的Selenium简洁很多。流程分三步:
- 启动浏览器,访问目标页面,触发验证码弹出。
- 通过CSS选择器定位到旋转滑块和图片容器。
- 根据预测角度计算滑块拖拽的距离(角度转距离需要知道图片半径),然后执行拖动操作。
这里最核心的难点是角度到像素距离的换算。旋转验证码的图片是圆形布局,滑块按某一半径拖动时,角度和弧长满足这样一个关系:
弧长 = 角度(弧度) * 旋转半径
所以只需要知道图片的显示半径和中心点,就能算出滑块的移动距离。半径可以从元素的渲染尺寸拿,中心点可以用元素边界加上偏移量计算出来。
轨迹模拟是关键。直接一步把鼠标从起点拖到终点,轨迹时长只有几十毫秒,必然会被风控拦下来。正常人的操作是一次完整的、有加速度变化、有转折停顿的拖拽过程。所以工程上会用贝塞尔曲线生成一条带随机性的轨迹,并在中段加入若干停顿点,模拟出“先看一下、再拖一下、拖过头再回拉一点”的真人习惯。
4. 一次完整实操:识别角度并自动通过验证
4.1 环境准备
先列一下我这次实操用的环境:
- Python 3.10
- OpenCV 4.8
- PyTorch 2.0
- Playwright 1.40及以上版本
- ResNet50预训练权重
安装命令很简单,我直接放在这里:
pip install opencv-python torch torchvision playwright playwright install chromium4.2 抓取验证码图片并分析接口
先用Playwright的录制功能打开触发验证码的页面,手动完成一次验证,通过开发者工具里的网络面板就能看到验证码相关的接口。
我一般会关注这几个字段:
- 图片接口返回的base64字符串。
- 初始化响应里是否有原始旋转角度参数。
- 提交验证时角度字段的加密情况。
实际操作中,为了做训练集,需要批量抓取验证码图片。这里有一个取巧的办法:反复触发验证码接口,但不做任何操作直接关闭弹窗,每次弹窗都会下发一张新图片。把这些图片保存下来,再筛掉重复的,就能积累一批样本。
不过要注意频率控制,短时间内高频请求接口很容易触发账号风控。我当时的节奏是每个账号间隔10秒才拉一次,每次拉完等2分钟再拉下一轮,跑了两个小时没有任何异常。
抓下来的图片统一存成JPG格式,命名用时间戳加随机字符串,方便后续处理。
4.3 训练角度预测模型
训练数据我采用的思路是:先整理一批“正立”图片,然后程序对每张图片随机旋转一个角度,旋转后的图作为输入,旋转角度作为标签。这样生成的训练集全部是自动标注,不需要人工参与。
生成样本的核心代码大概长这样:
import cv2 import numpy as np def generate_sample(image, max_angle=360): # 随机生成旋转角度 angle = np.random.uniform(0, max_angle) h, w = image.shape[:2] center = (w // 2, h // 2) # 旋转,保持原图尺寸,超出部分用黑边填充 rot_mat = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(image, rot_mat, (w, h), borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0)) return rotated, angle模型方面,我用了ResNet50做特征提取,把最后一层替换成输出维度为2的全连接层,分别代表角度的正弦和余弦值。推理时用atan2还原出最终角度。
训练时的损失函数用的是角度差的L1损失,但在计算之前需要先做角度差标准化。这里有个经验值:把角度差映射到-180到180度区间再算损失,模型收敛速度会快很多。
核心训练逻辑大概是这样的:
import torch import torch.nn as nn from torchvision import models class AngleModel(nn.Module): def __init__(self): super().__init__() self.backbone = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) self.backbone.fc = nn.Sequential( nn.Linear(2048, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, 2) ) def forward(self, x): out = self.backbone(x) return out def angle_loss(pred, target_angle): # 将预测值转为yiwei角度 pred_angle = torch.atan2(pred[:, 0], pred[:, 1]) * 180 / torch.pi diff = (pred_angle - target_angle + 180) % 360 - 180 return torch.abs(diff).mean()训练结束后,把模型转成ONNX格式部署,单张图片推理耗时在毫秒级,完全够用。实际测试中,对地图街景类图片平均误差约3.5度,对人像类图片约5.2度,对抽象纹理类图片约7.1度。整体命中率(预测角度和真实角度差值在正负8度以内)约为93%。
4.4 端到端自动化验证
模型有了,接下来就是把它接进Playwright流程里。
完整的操作逻辑我从经验出发,大概分这几步:
- 打开页面,等待验证码弹出。
- 定位图片容器,截取验证码区域图像。
- 用训练好的模型预测需要旋转的角度。
- 根据图片容器半径,把角度换算成滑块需要拖动的像素距离。
- 用模拟轨迹把滑块拖到指定位置。
- 松手,等待校验结果。
- 如果失败,重试并调整轨迹参数。
角度到像素距离的换算,用前面提到的弧长公式。这里补充一个细节:图片容器在页面上经常不是原始尺寸,而是缩放了。所以计算半径的时候一定要用element.bounding_box()拿渲染尺寸,不要用图片原始像素尺寸。
下面是核心交互代码的简化版本:
import asyncio import math from playwright.async_api import async_playwright async def rotate_slider(page, target_angle, center_x, center_y, slider_handle): # 计算需要旋转的弧度 radians = math.radians(target_angle) radius = 100 # 根据元素bounding_box计算得到 distance = radians * radius box = await slider_handle.bounding_box() start_x = box["x"] + box["width"] / 2 start_y = box["y"] + box["height"] / 2 # 用贝塞尔轨迹执行拖动,省略中间轨迹生成逻辑 await page.mouse.move(start_x, start_y) await page.mouse.down() # 分多步移动,模拟人类拖动,每步有停顿 steps = 50 for i in range(1, steps + 1): end_x = start_x + distance * i / steps await page.mouse.move(end_x, start_y, steps=3) await asyncio.sleep(0.01) await page.mouse.up()这段代码只是骨架,实际跑的时候需要加上轨迹随机化、停顿逻辑、失败重试。我建议把重试次数控制在3次以内,连续失败就放弃,不要硬杠,不然容易被风控记住。
5. 踩坑记录与排查技巧
5.1 图片被压缩或加入干扰导致识别不准
有段时间我在测试环境跑得好好的模型,一到线上识别率掉了将近20个百分点。排查之后发现,线上验证码图片经过了更激进的JPEG压缩,画质下降明显,而且边缘被加了模糊处理的白色锯齿。
针对这个问题,我做了两步优化:训练阶段就把训练集图片做随机JPEG压缩和质量降低,模拟线上压缩环境;推理阶段先对图片做一次轻量的锐化预处理,突出边缘信息。优化之后,识别率基本恢复到训练时的水平。
5.2 角度偏移量为什么忽正忽负
不少人在提交角度时发现,模型预测的角度明明很准,但验证就是不过。后来定位到问题是“角度符号”没有对齐。同一个旋转操作,前端和后端记录的可能是顺时针或逆时针两个方向,如果符号搞反了,差的就不是几度而是接近180度。
排查方法很简单:手动在页面上转一次,同时抓包看提交的参数值和前端初始化时返回的原始角度,把它俩对比一下,把角度值调整成统一坐标系。我习惯约定为:顺时针为正,逆时针为负,然后在代码里统一处理。
5.3 轨迹过于顺滑被判成机器人
第一次跑通自动化时,角度识别精确到了1度以内,但成功率只有不到三成。后来抓了提交的轨迹参数开始分析数据,发现我生成的轨迹点太均匀了,每两个点之间的时间间隔几乎完全一致,这种“完美轨迹”反而异常。
后来我在轨迹里加入了几种人类操作的随机特征:开始时停留100到200毫秒再拖动、拖动中点缓慢加速减速、松手前有微小的回拉动作。成功率直接提升到85%以上。事实证明,在旋转验证这个场景里,更像人比更精确更重要。
5.4 高频访问触发更强验证
即使全部校验通过,短时间多次触发验证码,后端可能直接甩出更复杂的点选验证码甚至短信验证。这说明风控不只看单次验证是否通过,还会统计触发频率。
应对策略无非是降低并发、增加随机延迟、使用多个账号分摊压力。但这里要非常严肃地提醒一句:如果你是在做正经的自动化测试,请务必在测试环境或者有授权的环境里跑;如果是爬虫业务,也要尊重目标网站的条款,别给人家服务器造成压力,别做撞库、批量注册这类违法操作。验证码本质上保护的是正常用户的安全,这个前提要想清楚。
5.5 不同产品线验证码样式不同
百度贴吧、百度网盘、百度搜索触发出来的旋转验证码,虽然交互形式差不多,但图片尺寸、滑块位置、DOM结构、接口参数名都有差异。适配新场景时,最快的方式是先用Playwright打开页面手动过一遍,抓完整的接口日志,把差异点列出来,然后再决定是改配置还是改代码。
我见过有些团队把不同产品线的验证码适配做成了可配置的JSON模板,DOM选择器、接口参数名、容错角度这些东西都抽到配置里,上线新场景只需要补充配置就行,不用动代码。这个思路在维护多套验证适配时非常实用。
6. 一些关于工程化的经验
做旋转验证码自动化,最容易翻车的地方其实不是模型精度,而是“把一次性脚本当成长期方案来用”。验证码是动态对抗的产物,今天能过的方案,明天页面改个DOM结构可能就废了。所以工程化的时候一定要把代码按照“采集层、识别层、交互层、调度层”四个模块拆开:
- 采集层负责拿图片和初始化参数,变化时只需要改这里。
- 识别层只负责输入图片输出角度,跟页面完全解耦。
- 交互层负责执行拖动动作,主要依赖页面DOM结构。
- 调度层负责频率控制、失败重试、账号管理等。
每一层独立维护,哪一层被风控针对了,就单独替换那一层。我见过有人把四层代码全写在一个文件里,页面一改就整个推倒重来,那基本是每天都在救火的节奏。
还有,模型要定期重新训练。验证码平台会不断更新图片素材,三个月前的素材分布和现在可能完全不一样。我自己的习惯是每个月跑一次批量采集,把新图片加入训练集,重新微调模型,保持在最新素材上的识别率不下滑。
最后再分享一个小技巧:角度预测的置信度可以拿到调度层做策略决策。比如模型对某张图片输出角度很犹豫,连续两次预测结果差异超过15度,那就说明这张图可能是平台的“对抗样本”,别硬刚,直接标记失败跳过,等下一轮验证码重新弹出来就行。单次通过率不需要做到100%,整体流程跑通才是目标。