如何修复 ERROR_CAPTCHA_UNSOLVABLE 并安全重试

简短回答: ERROR_CAPTCHA_UNSOLVABLE 表示识别工具接受了你的任务、尝试过了,但没能给出答案。你的密钥没问题,配置也没问题,只是这一次尝试失败了。文档给出的修法是提交一个 全新的 任务并重试,而不是继续轮询同一个验证码 ID。本文讲清楚为什么重复轮询永远没用、每种验证码类型真正的失败成因,以及一个不会无限空转的重试循环。
这个错误的含义,以及它从哪里来
和密钥或参数错误不同,它不会在提交时出现。你的 /in.php 调用是成功的,而且返回了一个 ID。失败是在之后你轮询 /res.php 获取结果时才浮现的。
# Submit: this part worked, you got an ID back curl "http://127.0.0.1:8080/in.php?key=YOUR_API_KEY&method=userrecaptcha&googlekey=YOUR_SITEKEY&pageurl=https://example.com/page-with-recaptcha" OK|2122988149 # Poll: the attempt finished, and it finished badly curl "http://127.0.0.1:8080/res.php?key=YOUR_API_KEY&action=get&id=2122988149" ERROR_CAPTCHA_UNSOLVABLE
所以这个先后顺序本身就说明了问题。提交以及之前的每一步都是对的。问题出在你提交的内容上,而不是提交的方式上。
为什么重试同一个 ID 永远没用
两个原因,而且都是结构性的。
第一,这次尝试已经结束了。那个 ID 的结果是最终的,再去轮询它也不会开启第二次尝试。第二,结果只能读取一次。一旦你从 /res.php上读走了结果,那个 ID 就再也给不了你任何东西。
如果你的错误处理在失败之后还在同一个 ID 上循环,它会一直循环到超时触发,然后报告一个超时,把你引去调试完全不相干的东西。重试意味着重新调用 /in.php 并拿到一个新的 ID。这和 CAPCHA_NOT_READY正好相反,那种情况下正确的做法是继续轮询你手上的 ID。
按验证码类型看真正的成因
图片验证码:问题通常出在输入上
图片识别是最容易出现真正无法识别的输入的类型,因为像素是你自己控制的。在怪识别工具之前,先检查文件。
- 尺寸限制。 超过 600 kB,或任一边超过 1000px,返回的是
ERROR_TOO_BIG_CAPTCHA_FILESIZE,而不是本文这个错误,但接近这些上限的图片往往是整页截图。 - 你裁错了区域。 裁剪时把标签、边框或半个相邻字段也框进去,会让这张图比单独的验证码难得多。
- 你保存的是占位图。 单独去请求图片 URL,常常会拿到一张全新的、不一样的验证码,或者一张已经过期的,甚至是 1×1 的像素。请抓取浏览器已经拿到的那份字节。
- 格式问题。 损坏或非预期的格式会返回
ERROR_INVALID_IMAGE,而格式错误的 data URI 会返回ERROR_INVALID_BASE64.
把你实际发送的字节原样存到磁盘上打开看看。一半的图片失败两秒钟就能看出来。 图片验证码识别工具 页面列出了可接受的输入形式:文件路径、远程 URL,或者一个 data:image/png;base64, URI。
reCAPTCHA:过期的 sitekey 或错误的页面 URL
从缓存的页面源码里读到的 sitekey,或者从测试环境复制来的 sitekey,提交时不会报错,之后才失败。同样的问题也出在 pageurl 上:它和小组件实际所在的页面对不上,包括漏掉协议头,或者你没有跟进的重定向。
Enterprise 是另一个常见原因。如果站点跑的是 reCAPTCHA Enterprise,而你提交时没有带上 enterprise 标志,任务就会按错误的配置去识别。v3 也一样:要传 version,并且传站点真实的 action 值,而不是默认的 verify.
CapSkip 里没有最低分数选项,所以如果你是从别的服务移植代码,请去掉任何 min_score 或 minScore 参数。它不是这个错误的成因,但也不会按你期望的方式工作。
Turnstile:挑战页需要的不只是 sitekey
小组件形式的 Turnstile 只要 sitekey 和 URL。完整的挑战页还需要 data (也就是 cData 值)和 pagedata (也就是 chlPageData),两者都要在当时从页面上读取。把挑战页当成小组件提交,得到的就是一个识别工具无法完成的任务。这两个值的有效期都很短,所以要在提交前的一刻才去抓。
极验:多半是挑战已经过期
加载的 gt 值对每个站点是固定的。而 challenge 值只能用一次,大约一分钟就过期。如果你先取到挑战,做了别的事再提交,它可能已经失效了。请在识别前的一刻才去取。详情见 GeeTest 识别工具 页面。
一个真正在重试的重试循环
重新提交、限制次数、每次之间退避。三次是个合理的上限:如果一个目标连续三次全新提交都失败,那就是输入有问题,第四次也修不好。
# pip install capskip
import time
from capskip import CapSkip, ApiException
solver = CapSkip(host="127.0.0.1", port=8080)
def solve_with_retry(sitekey, url, attempts=3):
for n in range(attempts):
try:
# A fresh call means a fresh task, which is the fix.
return solver.recaptcha(sitekey=sitekey, url=url)
except ApiException as e:
if "UNSOLVABLE" not in str(e) or n == attempts - 1:
raise
time.sleep(2 ** n) # 1s, then 2s
token = solve_with_retry("YOUR_SITEKEY", "https://example.com/page-with-recaptcha")
print(token["code"][:40])注意它不做什么:它不会重试 ValidationException,因为错误的参数每次都会以同样的方式失败;它也不会重试 NetworkException,因为那意味着应用不可达,其他所有目标马上也会失败。
Node.js 里的同一套结构:
// npm install capskip
const { CapSkip } = require('capskip');
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
async function solveWithRetry(sitekey, url, attempts = 3) {
for (let n = 0; n < attempts; n++) {
try {
return await solver.recaptcha(sitekey, url);
} catch (e) {
const last = n === attempts - 1;
if (last || !String(e).includes('UNSOLVABLE')) throw e;
await new Promise(r => setTimeout(r, 1000 * 2 ** n));
}
}
}如何让它更少发生
- 从实时 DOM 中读取 sitekey,而不是从代码里的常量。站点会轮换它们。
- 发送你实际所在的 URL,也就是重定向之后的那个,并且带上协议头。
- 短时效的值最后再取。 极验挑战和 Turnstile 的 cData 不到一分钟就会失效。
- 把图片裁得紧一些 ,并发送页面已经加载好的那份字节。
- 每次失败都记录验证码 ID。 这样规律才看得出来:是某个目标有问题,还是整体出了问题。
- 先拿一个已知正常的页面测试。 如果平时正常的页面也失败了,那问题在你的环境,而不是目标站点。
相邻的错误码
这些是最容易和「无法识别」混淆的错误码。它们含义不同,处理方式也不同。
| 代码 | 含义 | 该怎么做 |
|---|---|---|
CAPCHA_NOT_READY | 仍在处理中 | 继续轮询同一个 ID |
ERROR_INVALID_IMAGE | 格式不对,或数据已损坏 | 重新保存图片并检查字节 |
ERROR_TOO_BIG_CAPTCHA_FILESIZE | 超过 600 kB,或超过 1000px | 裁剪到验证码本身 |
ERROR_INVALID_BASE64 | base64 内容无法解码 | 去掉 data URI 前缀,或修正填充位 |
ERROR_UPLOAD | 没有收到图片数据 | 检查字段名和请求体 |
ERROR_GOOGLEKEY | sitekey 被直接拒绝 | 从实时页面重新读取 |
ERROR_PAGEURL | 页面 URL 缺失或格式不对 | 发送包含协议头的完整 URL |
API 可能返回的每一个错误码及其确切含义,都在 API 文档.
常见问题
识别失败会产生费用吗?
不会。没有按次收费,也没有余额,因为工作是在你自己的机器上完成的。重试只花你的时间和 CPU,别的什么都不花,这也是三次重试循环在这里合理的原因。
读取过一次之后还能再取一次结果吗?
不能。结果只能读取一次。收到 token 的那一刻就把它存下来,把对同一个 ID 的二次读取当成代码里的 bug,而不是一种补救手段。
代理能修复无法识别的图片验证码吗?
不能。代理只适用于 reCAPTCHA、Turnstile 和极验,不适用于图片识别。图片任务完全按你发送的像素来判断,所以要去修裁剪。
某个站点上的每个任务都失败,也是这个错误吗?
通常是一个错误的常量被用在了所有地方:过期的 sitekey、漏掉的 enterprise 标志,或者把挑战页当成小组件提交。把这个目标修对一次,整批就都通过了。
小结
把 error_captcha_unsolvable 当作对你所提交任务的判决。不要再轮询旧的 ID。提交一个新的,把重试次数限制在三次,然后把调试时间花在输入上:sitekey、页面 URL、裁剪,以及任何你取得太早的短时效值。
自己运行一个 本地验证码识别工具 的好处在于,这种失败很便宜。不烧额度,不消耗速率限制,你可以把同一个任务重复复现,直到弄清楚到底是哪个值错了。
