docs: 新增验证码图片获取经验文档(对接方视角:captcha/get 取图、触发法、踩坑清单)

This commit is contained in:
杨豪
2026-09-16 15:59:03 +08:00
parent d643a7e05b
commit 8186b24b70
4 changed files with 139 additions and 1 deletions
+1
View File
@@ -19,6 +19,7 @@
- [captcha-screenshot-capture.md](browser-automation/captcha-screenshot-capture.md) — wasm/GPU 渲染图片的 fromSurface=False 截图采集法(已被网络层法取代)
- [human-drag-path.md](browser-automation/human-drag-path.md) — 拟人拖动路径生成算法(非直线/非匀速/过冲瞄准三要素)
- **[captcha-auto-solve-pipeline.md](browser-automation/captcha-auto-solve-pipeline.md) — ★ 最新:密码登录触发法、网络层样本采集、tip_y 真值校准、自动闭环与 5014 行为风控现状**
- **[captcha-image-acquisition.md](browser-automation/captcha-image-acquisition.md) — 对接方视角:如何获取验证码大图与 mark 小图(/captcha/get 取 URL、DevTools 手动采集、触发与限流、踩坑清单)**
## 快速开始
@@ -0,0 +1,135 @@
# 经验:验证码图片的获取方法(大图 + mark 小图)
> 面向对接方:要把图片喂给 `captcha_solver`,第一步是怎么把两张图拿到手。
> 一句话结论:**不要从页面 DOM 拿图,从 `/captcha/get` 网络响应里取 URL 再下载**。
## 0. 背景:为什么不能从页面元素拿图
抖音系验证码(rmc-nocaptcha)的图片由 SDK 的 **wasm 解码后直接进 GPU 合成层**渲染:
| DOM 手段 | 结果 |
| --- | --- |
| `img.src` / `currentSrc` | 是 iframe 页面自身 URL(占位),fetch 回来是 HTML |
| `img.naturalWidth` | 0(图片从未进入 img 解码管线) |
| `canvas.toDataURL()` | 输出全黑透明(RGBA 全 0) |
| CSS background / shadow DOM | 全部 none |
但图片 URL 会**正常发 HTTP 请求**,响应 JSON 里就有完整的 CDN 直链 —— 这才是可靠入口。
## 1. 推荐方法:从 `/captcha/get` 响应取图(自动化/生产)
### 1.1 响应结构
POST `https://verify.zijieapi.com/captcha/get` 的响应 JSON
```json
{
"data": {
"question": {
"url1": "https://p9-catpcha.byteimg.com/...~tplv-188rlo5p4y-2.jpeg",
"url2": "https://p9-catpcha.byteimg.com/...~tplv-188rlo5p4y-1.png",
"tip_y": 58
}
}
}
```
- `url1`:背景大图,jpeg**552×344**
- `url2`mark 小图,png **110×110**,带透明通道(RGBA
- `tip_y`:缺口顶 y 提示(340 显示系),求解时**直接传原始值**,求解器内部已处理,别自己乘 2
### 1.2 下载与使用
图片托管在 `p3/p9-catpcha.byteimg.com`,**公开直链,无需登录态或签名**,GET 即得原始字节:
```python
import base64, time, requests
from captcha_solver import solve_slide
q = resp_json["data"]["question"]
big = requests.get(q["url1"], timeout=10).content # jpeg 原始字节
mark = requests.get(q["url2"], timeout=10).content # png 原始字节(保持 PNG!)
# 方式 A:直接喂求解器
r = solve_slide(base64.b64encode(big).decode(),
base64.b64encode(mark).decode(),
tip_y=q["tip_y"])
# 方式 B:落盘成对保存(前缀一致,方便核对)
prefix = f"captcha_{int(time.time())}"
open(prefix + ".jpeg", "wb").write(big)
open(prefix + "-mark.png", "wb").write(mark)
```
下载到的字节**天然就是 552×344 / 110×110 规格**,与离线样本完全一致,无需任何缩放/转码。
### 1.3 三种拿到该响应的姿势
| 姿势 | 做法 | 适用 |
| --- | --- | --- |
| **CDP 监听(现成工具)** | `Target.setAutoAttach` + `Network.enable` 常驻监听,响应到达后 `Network.getResponseBody`mime 为 image/* 或 URL 含 captcha/verify 的落盘 | 浏览器自动化。直接用 `try/captcha_net_capture.py`(输出 `try/captures/net_*_get.json` + 图片字节) |
| **中间代理** | mitmproxy / Charles / Fiddler 抓 `captcha/get` 导出 JSON | 有代理环境、不想写监听代码 |
| **自建协议请求** | 自己走完整 SDK 协议调 get 接口 | 仅适合已逆向 SDK 协议的团队;对接方不建议(需 captchaBody 加密轨迹) |
## 2. 无代码快速验证:DevTools 手动采集
1. Chrome 打开目标站 → F12 → **Network** → 过滤框输入 `captcha`
2. 触发验证码(见 §3)→ 列表出现 `get` 请求 → 点开看 **Response**
3. 复制 `question.url1` / `url2` → 新标签页打开 → 右键另存为(或把 URL 直接发给求解方)
4. 自检尺寸(见 §6
## 3. 如何触发验证码(采集的前置条件)
已验证最可靠的触发法 —— **密码登录法**
> 主页点「登录」→ 弹窗点「密码登录」→ 输手机号 + **随机错误密码**(如 `26969877Ab!`)→ 勾协议 → 点「登录」→ 必弹拖动重叠验证码。
密码错误提示是预期的,验证码照样触发。
工程要点:
- **弹不出题(触发限流)→ 清 cookie 立即解除**CDP `Network.clearBrowserCookies` 后用 `Page.navigate` 显式回站(清 cookie 后 session 失效,`reload` 会带到空页)。
- **判断验证码是否真的弹出**`iframe[src*="captcha"]``offsetParent !== null`(页面加载时就预加载了隐藏 iframe,`display:none` 不算触发)。
## 4. 兜底:抓不到网络时的截图法(不推荐,有损)
仅当完全拿不到网络响应时应急。CDP `Page.captureScreenshot` **必须 `fromSurface=False`**——wasm/GPU 层不走默认 surface 截图通道,默认参数截出来是空白。
已知代价:
- `clip` 参数被忽略,返回全屏需自行裁剪;
- mark 裁出来是**灰底、无 alpha 通道**,需按背景色反推 alpha 才能喂求解器;
- UI 显示是 340×212(缩放系数 ≈0.616),尺寸与求解器输入规格不符,属于有损数据。
细节见 [captcha-screenshot-capture.md](captcha-screenshot-capture.md)。
## 5. 高频踩坑清单(每条都实测踩过)
| # | 坑 | 正确做法 |
| --- | --- | --- |
| 1 | 从 DOM / img.src / canvas 提图 | wasm 渲染层拿不到,走网络响应 |
| 2 | mark 转 JPEG 或从截图裁出(丢 alpha) | 保持 PNG 原始字节,求解器要求 alpha 通道 |
| 3 | 用 UI 显示尺寸(340 宽)的图去求解 | 用 CDN 原始字节,天然 552×344 / 110×110,无需缩放 |
| 4 | 手动重放/拼改 `captcha/get` 请求参数 | 会 `501 参数错误`;取响应里的 URL 直接 GET 即可 |
| 5 | 拿到题后慢慢处理 | challenge 有效期约 **60~90s**,超时 `NotFoundChallengeId`;预热/浏览必须放在**触发之前** |
| 6 | `tip_y` 自己乘 2 | 求解器内部已处理,直接传服务端原始值 |
| 7 | 存图命名随意 | 成对同前缀:`xxx.jpeg` + `xxx-mark.png` |
| 8 | 失败后高频重试 | 位置错换题即可;连续失败触发行风控 5014 需分钟级冷却或换环境(清 cookie) |
## 6. 采集后 30 秒自检
```python
import cv2
big = cv2.imread("xxx.jpeg")
mark = cv2.imread("xxx-mark.png", cv2.IMREAD_UNCHANGED)
assert big.shape[:2] == (344, 552), f"大图尺寸异常 {big.shape}"
assert mark.shape[:2] == (110, 110) and mark.shape[2] == 4, f"mark 缺 alpha 通道 {mark.shape}"
print("样本规格 OK")
```
通过后即可联调:
```bash
python captcha_solver.py xxx.jpeg xxx-mark.png --tip-y <get响应里的tip_y>
```
+1 -1
View File
@@ -421,7 +421,7 @@ toast「验证成功,正在为你登录」。
完整特征诊断确认该题与 live_162840 同型,为当前特征体系的已知盲区:
| 证据 | 数值 | 含义 |
|---|---|---|
| --- | --- | --- |
| GT(175,~140) coarse 分(全尺度) | 6.82-7.73 平坦无谷 | mark 轮廓与低对比贴片不对齐,chamfer 无形状信号 |
| 纹理假峰 B(366,128) coarse | U 型谷底 5.77 @ s=0.925 | 雪坡纹理被误判为轮廓贴合,霸占 NMS 前排 |
| GT 进 top-24 候选池 | 否 | 候选池在粗扫阶段就漏掉真解 |
+2
View File
@@ -6,6 +6,8 @@
典型场景:抖音系「拖动滑块完成拼图」验证码(`/captcha/get` 返回 `question.url1`(大图 552×344)与 `question.url2`mark 110×110 PNG 含透明通道))。
> **图片如何获取?** 两张图不从页面 DOM 提取(wasm 渲染层不可见),而是从 `/captcha/get` 响应取 `url1`/`url2` 直链下载——公开直链、无需签名,字节天然就是 552×344 / 110×110 规格。详细方法(DevTools 手动采集、触发法、限流处理、踩坑清单、采集后自检)见 [docs/browser-automation/captcha-image-acquisition.md](../docs/browser-automation/captcha-image-acquisition.md)。
## 2. 环境要求
- Python ≥ 3.10