百度旋转验证码:被虐了一整晚之后,我把方案整理出来了适用场景:百度系(搜索资源平台 ziyuan.baidu.com 加站 / 登录)弹出的「拖动左侧滑块使图片为正」旋转验证码。代码落点:captcha.py(算角度)+ baidu_flow.py(拖动 / 闭环 / 重试)一句话结论:可以全自动过,不用人工,也不用买打码平台——纯本地视觉算法 + 一个
详细介绍
百度旋转验证码:被虐了一整晚之后,我把方案整理出来了
适用场景:百度系(搜索资源平台ziyuan.baidu.com加站 / 登录)弹出的「拖动左侧滑块使图片为正」旋转验证码。
代码落点:captcha.py(算角度)+baidu_flow.py(拖动 / 闭环 / 重试)
一句话结论:可以全自动过,不用人工,也不用买打码平台——纯本地视觉算法 + 一个拟人拖拽闭环 + 正确的重试姿势。
0. 先交代背景,不然你不知道我为什么这么激动
我接的活儿:自动给百度资源平台加站 → 过验证码 → 验证网站 → 拿 API TOKEN → 批量推链接。
前面都挺顺,直到撞上这个旋转验证码。
我一开始的想法(很天真):
不就是个滑块吗?算个角度拖过去,收工。
后来的现实(被打脸):
角度明明拖对了,它说「验证失败,请重试」;
再拖第二次,图片还是原来那张,我用同样的角度又拖一遍,它继续说失败;
好不容易提示"通过"了,提交上去告诉我「验证码错误,请重试提交」;
有时候干脆给个 403 空响应,页面就一行红字「提交失败,请重试提交」;
最骚的是,连你点了几次刷新按钮都可能是它的风控依据……
所以有了下面这份文档。按顺序看完,至少能帮你省一晚上。
1. 先认脸:这验证码由什么组成
DOM(百度用的是一套叫 sdkMachine 的 SDK,脚本是 mkd_v2.js):
| 元素 / 选择器 | 作用 |
|---|---|
#spin-0 | 整个验证码弹层,判断"验证码还在不在"就看它 |
img.passMod_spin-background | 被旋转的圆形图片,截图就截它 |
.passMod_slide-btn | 拖动用的滑块 |
.passMod_verify-success | 成功状态节点(可见 = 过了) |
.passMod_verify-fail | 失败状态节点 |
.passMod_verify-loading | 「加载中...」遮罩,这个坑我踩得最惨 |
.passMod_dialog-footer-refresh | 刷新按钮 |
背后的接口(看懂这几个你就知道"通过"到底是谁说了算):
| 接口 | 干什么 |
|---|---|
passport.baidu.com/cap/init | 初始化,返回 tk |
passport.baidu.com/cap/style | 返回背景图信息 backstr |
passport.baidu.com/cap/img | 返回图片二进制 |
passport.baidu.com/cap/log | 上报拖动结果,返回 as / ds |
业务接口(如 POST /site/add) | 带着 tk + ds 提交,它才是最终裁判 |
最后一行请记住,第 7 节全靠它。
2. 核心算法:到底该转多少度
三步,不复杂,但每一步都有坑。
2.1 第一步:梯度方向轴对齐(只能算出 mod 180)
思路:一张"正常的"照片里,地平线、建筑边缘这类强边缘,方向大多集中在水平/垂直。
把图片转某个角度 a 之后,所有强边缘的梯度方向会集中到 0°/90°("轴对齐")。
这个 a 就是要转的角度……的一半。
代码(captcha.py: axisangle,第 19 行):
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
mag = np.sqrt(gx * gx + gy * gy) * mask # mask:圆形遮罩,只要图中间
th = np.arctan2(gy, gx)
# 在 0~180 度里以 0.25 度为步长找最优 a
score(a) = np.sum(mag * np.abs(np.cos(2 * (th + radians(a)))))
用
cos(2θ)而不是cos(θ):因为"轴对齐"是 180° 周期的(水平线转 180° 还是水平线);
mask取半径0.48 * min(w, h)的圆:图片是圆的、四角发黑,不遮掉会被骗;
步长 0.25° 够用,再细收益很小。
坑:算出来的永远是 mod 180——a 和 a + 180 得分几乎一样。
我第一次想当然取了一个,然后 50% 概率翻车。
2.2 第二步:180° 歧义怎么消——"天空一般在上"
两个候选都"轴对齐",那就靠"哪张更像正常照片"投票。我用的启发式很朴素:天空通常在上半部分。
代码(captcha.py: naturalscore,第 42 行):
bright = 上半平均亮度 - 下半平均亮度
blue = 上半平均蓝分量 - 下半平均蓝分量 # 天空偏蓝
red_hot = 下半平均红分量 - 上半平均红分量 # 地面偏暖
smooth = 下半梯度强度 - 上半梯度强度 # 天空更平滑
score = bright*0.6 + blue*1.8 + red_hot*0.6 + smooth*1.2
两个候选各算一次取分高的;日志里会打「置信差」,差得越多越可信。
坑:这套启发式不是万能的(遇到室内全景、纯色背景就白搭)。
所以不能"一次定生死",必须配合第 3 节的"同一张图反过来再试一次"。
2.3 第三步:闭环拖动(不是"算完拖一次"就完事)
我第一版:读目标角度 → 换算像素 → 一把拖到位 → 松手。然后在"验证失败"里反复横跳。
原因是 deg_per_px(一像素多少度)不是常数,视口缩放、轨道宽度一变就偏。
正确姿势是闭环(baidu_flow.py: dragto_angle,第 654 行):
按住滑块不放;
读当前 CSS 旋转角 → 算误差 → 按
deg_per_px换算成像素 → 移动;
再读一次角度,误差仍大于容差(我用 3°)就继续微调;
误差够了再松手(松手前再确认一次,角度可能有微小回弹)。
同时要处理两个幺蛾子:
撞墙:误差是正的,但滑块已经在最右端了(或反之)。这时候不能硬顶,
要把"绕一圈 360°"的剩余误差算出来,改走反向;
度数怎么读:
transform: rotate(x deg)或matrix(...),后者用atan2(b, a)反解。
实测数据(日志里的,可当新环境的初始值):
滑块轨道: 起点 x=594, 可移动 241px (约 362 度) -> 约 1.5 度/像素
容差 3 度稳定够用
2.4 拟人化:别让鼠标"瞬移"
mouse.move() 一把到位就是一条直线 + 瞬移,闭着眼都知道是脚本。baidu_flow.py: humanmove(第 610 行)做了这些:
缓入缓出(先慢后快再慢);
中间加一点纵向抖动,别走完美直线;
到目标附近过冲一点再回拉;
按下之后、松手之前各随机停 100~500ms。
3. 重试策略:这部分比算法更重要
第一版是"失败 → 点刷新 → 重来",结果:识别错了就在错误答案上打转,而且疯狂点刷新本身就像脚本。
现在是这么干的(baidu_flow.py: solve_rotate_captcha,第 757 行):
第 1 次:用算法算出的角度 A
第 2 次:不换图,直接用 A + 180 度(把 180 度歧义反着再赌一次,几乎必中)
第 3 次:才点"刷新"换一张新图重新识别(refresh_every = 3)
...最多 20 次(max_tries = 20)
还有一个特别实用的兜底:
如果两次截到的图片字节完全一样,说明"刷新"根本没生效(新图还没加载出来)。
这时候别再用同一个角度,直接+180°重试。
成功/失败判定(waitverify_result,第 739 行):
.passMod_verify-success可见 → 过;
.passMod_verify-fail可见 → 没过;
#spin-0整个消失 → 当作过(弹层关了)。
另外:验证码元素突然消失,不要直接报错终止。可能是它自己提交了,或者页面被风控刷新了——
把判断权交给外层(等第二步页面出现 / 看提示文案),比在这儿 raise 靠谱得多。
4. 血泪坑清单(按我踩的顺序)
| # | 现象 | 真实原因 | 解决 |
|---|---|---|---|
| 1 | 识别结果离谱,怎么拖都不对 | 截到了「加载中...」的转圈遮罩,识别的是转圈图 | 截图前等 .passMod_verify-loading 消失(waitimage_loaded) |
| 2 | 拖对了也提示失败 | 180° 歧义选错 | 候选打分;还错就反向 +180° 再试 |
| 3 | 刷新后还是同一张图,重复失败 | 新图没加载完就截图 | 比对两次截图字节,相同就直接反向重试,别重复同一角度 |
| 4 | 滑块拖到两端后动不了 | 行程不够(角度不等于 360°) | 算"绕一圈"的剩余误差,反向绕行 |
| 5 | 点击报 intercepts pointer events | 页面全屏遮罩(#fullscreen)挡着 | 用 JS 点击 / force=True,或等遮罩消失 |
| 6 | 提示"通过"了,业务接口还说验证码错误 | 业务服务器才是最终裁判,它拿 tk+ds 去验 | 当成一次普通失败:重新点"下一步"→ 再过一次验证码 → 再提交,反复重试 |
| 7 | 接口 403、响应体空 | CDN 层风控(server: bfe),跟验证码无关 | 别调验证码参数了!退避重试 / 重新登录换会话 / 换网络或代理 |
| 8 | 老式无头浏览器一上来就被拦 | 无头指纹太明显 | 真实 Chrome 内核 + 新版无头(--headless=new),并把 navigator.webdriver 置为 false |
| 9 | 一把拖到底偶尔过、多数不过 | 鼠标轨迹太"完美" | 缓入缓出 + 抖动 + 过冲回拉 + 随机停顿 |
| 10 | 换台机器/换个缩放就全错 | deg_per_px 变了 | 实时量滑块轨道宽度与可移动行程,别写死 |
5. 参数表(config.json → captcha)
"captcha": {
"mode": "auto", // auto=本地视觉算法(默认);api=打码平台;model=本地模型;manual=人工
"max_tries": 20, // 一次弹窗内最多试几次
"tolerance_deg": 3, // 角度容差
"deg_per_px": 1.5, // 1 像素等于多少度(实测百度约 1.5,换环境要重新量)
"refresh_every": 3, // 每 3 次失败才真正换图,其余用反向 180 度再试
"save_dir": "logs" // 每次识别的图片存档,排错全靠它
}
6. 下次遇到同类验证码,照这个流程抄
先开存档:每次截图都落地(
save_dir),不然就是盲调;
认元素:找出弹层 / 图片 / 滑块 / 成功 / 失败节点的选择器,先写脚本把它们的状态打出来;
上识别:
captcha.auto_upright_angle(图片字节)直接拿到"还要逆时针转多少度";
量比例:打开一次验证码,读滑块起点/终点坐标,算出每像素多少度;
闭环拖:按住 → 读角度 → 微调 → 确认误差 → 松手;
加反向兜底:同一张图 +180° 再试一次(覆盖 180° 歧义);
只在真换图时才换图(对比图片字节);
区分"验证码没过"和"业务没接受":后者要重走整轮,调角度是没用的。
复用的核心就一个纯函数,不依赖浏览器:
from captcha import auto_upright_angle
need = auto_upright_angle(png_bytes) # 返回 0~360:还要逆时针转多少度
7. 怎么判断"验证码过了,任务还是失败"
这是最容易怀疑人生的地方,一张对照表搞定:
| 你看到的 | 真正的意思 | 该做什么 |
|---|---|---|
页面红字「验证码错误,请重试提交」,接口 200 且 {"status":101} | 业务服务器不认这次的 tk/ds | 重新点"下一步"→ 再过一次验证码 → 再提交(反复重试) |
接口 403、空响应、server: bfe | CDN 层风控拦了,跟验证码无关 | 退避重试 / 重新登录换 cookies / 换网络或代理 |
| 页面「提交失败,请重试提交」 | 就是上面 403 那条的页面文案 | 同上 |
| 「该站点为低质站点」(102)、「主域数量超限」(2000001)、「未实名」(3000002) | 业务规则限制 | 换域名 / 删旧站 / 去实名,重试没用 |
| 「建议您添加带 www 的主站」(-1) | 提示类,不是错误 | 点「仍然继续」,或改填带 www 的主域 |
8. 最后
整套东西的结论其实很朴素:
算法算角度 + 闭环拖动 + 同一张图反向再试 + 把"业务拒绝"当成普通失败重走整轮。
就这四句话,我花了整整一晚上才从"这滑块怎么这么邪门"里爬出来。
希望下一个接手的人,别再从头踩一遍。
相关代码:captcha.py(识别);baidu_flow.py 的 solve_rotate_captcha / dragto_angle / humanmove / waitimage_loaded / refreshcaptcha / waitverify_result;参数:config.json → captcha。
内容版权声明
内容链接:https://www.diezanrpa.com/jinengjiqiao/1407.html
内容标题:百度旋转验证码-技术方案与踩坑
当前文章由【谍赞AI助手】自主写作文案,内容版权归当前平台所有,用户可自由转载!
本文最后更新发布于【2026-10-11】,某些文章具有时效性,若有错误或已失效,请联系客服
争议处理:针对本文内容若有异义,亦可直接与【法律顾问:易兴俊,律师联系电话:13825799821】直接联系沟通

