发送图片验证码时如何修复 ERROR_UPLOAD

error_upload - How to Fix ERROR_UPLOAD When Sending an Image CAPTCHA

ERROR_UPLOAD 表示没有任何可用的图片数据到达识别工具。请求本身没问题,API 密钥也没问题,但本该携带图片的那部分内容却是空的,或者根本没有到达。它是覆盖上传环节所有故障的四个错误码之一,而且很容易混淆,因为从你这一侧看,四者看起来完全一样。本文会把它们区分开,给出真实的限制,并展示一段不会触发其中任何一个错误的上传代码。

你需要什么

  • CapSkip 正在运行,无论是以本地模式跑在回环地址上,还是以服务器模式跑在你的代码可以访问的机器上,两种方式都记录在 连接设置中,选择哪一种都不会改变下面这些错误。
  • Python 3.10 或更新版本,并安装好 SDK;如果你更想直接调用接口,也可以用 curl。
  • 一张保存在磁盘上的真实验证码图片。整页截图是错误的测试用例,会让你去追查大小限制,而不是真正的问题。

每个图片错误码分别是什么意思

以下就是 API 针对图片提交返回的错误码,直接来自 CapSkip API 文档。请先看含义这一列,因为区分它们的是故障发生的位置,而不是问题的严重程度。

代码含义问题出在哪里
ERROR_UPLOAD没有提供图片数据,或者上传失败请求体。没有任何可用内容到达
ERROR_INVALID_IMAGE图片格式无效,或者图片数据已损坏字节数据到达了,但无法解码为图片
ERROR_INVALID_BASE64base64 编码无效该字符串从未成功还原为字节
ERROR_TOO_BIG_CAPTCHA_FILESIZE超过 600 kB,或者某个维度超过 1000px图片是真实且有效的,只是太大了
ERROR_BAD_PARAMETERS缺少必需参数,或参数无效图片周围的表单字段

还有第五种故障,它根本到不了 API 这一层。给 SDK 一个不存在的路径,它会抛出一个指明文件名的 ValidationException,而不会发出任何请求。这个区别值得尽早记在心里:Python 异常意味着是你自己的机器拦下了它,而以 ERROR 开头的错误码则意味着是识别工具拦下了它。

第一步:以 API 接受的形式发送图片

SDK 接受三种输入形式,并把它们都转换成同样的提交内容:本地路径、远程 URL,或者内嵌图片的 data URI。

# pip install capskip
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# All three of these are accepted by the same method.
result = solver.normal("captcha.png")
result = solver.normal("https://example.com/captcha.jpg")
result = solver.normal("data:image/png;base64,iVBORw0KGgo...")

print(result["code"])   # the recognized text

请使用绝对路径,而不是相对路径。相对路径是相对于进程的工作目录来解析的,而这个目录很少是你写代码时所在的目录,这也是脚本在编辑器里能跑、放到调度器或服务里却失败的最常见原因。

在原始 HTTP API 上,携带图片有两种方式,选错字段正是产生 ERROR_UPLOAD 最常见的原因。multipart 方式发送文件本身,base64 方式则把它作为字符串发送。

# no install needed, curl only
# method=post carries the file as multipart/form-data
curl -X POST "http://127.0.0.1:8080/in.php" \
  -F "key=capskip" \
  -F "method=post" \
  -F "[email protected]"

# method=base64 carries the same image as a string
curl -X POST "http://127.0.0.1:8080/in.php" \
  -d "key=capskip" \
  -d "method=base64" \
  --data-urlencode "body=BASE64_STRING_HERE"

请留意字段名。用 multipart 方式时,图片放在名为 file 的字段中;用 base64 方式时,图片放在名为 body 的字段中。把文件放进 body 字段,或者漏掉那个告诉 curl 去读取文件内容、而不是把文件名当作字面文本发送的 @ 符号,都会导致上传的内容里没有图片。

第二步:修复 ERROR_INVALID_BASE64

base64 失败几乎总是因为换行。GNU coreutils 默认会在 76 个字符处对输出换行,而这些换行符会一路带进请求里,所以在命令行里编码时要把换行宽度设为零。各个编码器在这件事上并不一致,有的根本不换行,这也正是应该在代码里编码而不是在命令行里编码的理由。

还有一个区别,会让在 SDK 和原始 API 之间切换的人栽跟头。SDK 接受完整的 data URI,前缀也包含在内;而原始接口上的 body 参数只要 base64 载荷本身,前面不能带任何前缀。

# pip install capskip
import base64

with open("captcha.png", "rb") as fh:
    raw = base64.b64encode(fh.read()).decode()

# One continuous string. No newlines, no spaces, no truncation.
assert "\n" not in raw and len(raw) > 50

# The SDK wants the prefix, the raw body parameter does not.
result = solver.normal("data:image/png;base64," + raw)

这个长度检查比看起来更重要。短于大约五十个字符的 base64 字符串会被当作文件路径来读取,而不是图片数据,所以被截断的载荷根本不会以错误 base64 的形式失败,而是以文件缺失的形式失败,这会让你去错误的地方排查。

第三步:修复 ERROR_TOO_BIG_CAPTCHA_FILESIZE

这一条有明确公布的数字,而不是模糊的指导原则。API 会拒绝超过 600 kB 的图片,也会拒绝任意维度超过 1000px 的图片。对于真正的验证码而言,这两个限制都相当宽松,所以一旦触及,通常意味着发出去的是整页截图,而不是验证码小组件本身。

把图片裁剪到验证码本身是更好的修复方式,因为它同时也能提高准确率。当你无法可靠地裁剪时,缩小尺寸则是备用方案。

# pip install capskip pillow
from PIL import Image

img = Image.open("captcha.png")

# Bring the longest side under the documented 1000px ceiling.
if max(img.size) > 1000:
    img.thumbnail((1000, 1000))

# optimize=True usually clears the 600 kB limit on its own.
img.save("captcha-ready.png", optimize=True)

第四步:修复 ERROR_INVALID_IMAGE

这个错误码表示字节数据完整到达,但并不是图片。实际上它通常只有一个主要原因:你用 HTTP 客户端下载了验证码,服务器返回的是一个错误页面或重定向,而你的代码把响应体原样保存成了一个以 .png 结尾的文件,从未检查过里面的内容。这个文件是真实存在的,大小也说得过去,但开头四个字节其实是 HTML。

# no install needed, standard library only
SIGNATURES = (b"\x89PNG", b"\xff\xd8\xff", b"GIF8", b"RIFF")

with open("captcha.png", "rb") as fh:
    head = fh.read(4)

# An error page saved as .png starts with <!DO or <htm instead.
print(any(head.startswith(sig) for sig in SIGNATURES))

请在下载时就做这个检查,而不是等到识别时才做。读取四个字节几乎不花成本,却能把一个令人困惑的识别错误,在网络错误真正发生的那一刻,转变成一个一目了然的网络错误。

完整可运行示例

把这四处修复合在一起,就能得到一个先验证、再发送的函数,并把你自己机器抛出的错误和识别工具返回的错误清楚地区分开。

# pip install capskip pillow
from PIL import Image
from capskip import CapSkip, ApiException, ValidationException

solver = CapSkip(host="127.0.0.1", port=8080, defaultTimeout=120)

def solve_image(path):
    """Return the CAPTCHA text, or None with a reason printed."""
    img = Image.open(path)          # raises if it is not an image
    if max(img.size) > 1000:
        img.thumbnail((1000, 1000))
        img.save(path, optimize=True)

    try:
        return solver.normal(path)["code"]
    except ValidationException as exc:
        print("never left this machine:", exc)
    except ApiException as exc:
        print("the solver rejected it:", exc)
    return None

先用 Pillow 打开图片,能把四个错误码中的两个合并成一个本地异常,因为在这里都打不开的文件,到了那边同样也不会被解码成功。之后到达 API 的,就是一张已知有效、大小也已知合适的图片。

有一样东西不要加进去:代理。reCAPTCHA、Turnstile、GeeTest 和 CaptchaFox 支持代理,图片验证码不支持。传入代理会产生参数错误,而不是被静默忽略,所以调用会直接失败。

改成把求解器跑在服务器上

图片上传是唯一一种请求会携带真实载荷的验证码类型,所以搞清楚这份载荷去了哪里是值得的。CapSkip 提供两种部署方式:

模式监听地址适用场景
本地127.0.0.1,仅限该设备你的脚本和识别工具跑在同一台机器上
服务器你的内网地址或公网 IPWorker、VPS 或托管平台通过 API 调用

在服务器模式下,你只需把 SDK 的 host 指向识别工具所在的机器,代码里的其他部分都不用改,这样一组工作进程就能共用同一个实例。如果调用方位于你自己网络之外,建议使用静态公网 IP。详情记录在 连接设置中,服务器模式依然是你自己的硬件,依然不计量计费:它改变的只是识别工具运行在哪里,而不是它归谁所有,或者怎么收费。

常见问题

CapSkip 接受的图片最大是多大?

600 kB,以及任意维度 1000px。只要超过其中一个,得到的就是 ERROR_TOO_BIG_CAPTCHA_FILESIZE,而不是部分识别结果。这两个数字都公布在 API 参考页面上,也都远远高于任何真实验证码图片的大小,所以一旦触及,就该把它当作一个信号:你发送出去的内容,比你原本想发送的页面范围更大。

为什么同一张图片在本地能用,放到服务器上却失败?

通常问题出在路径,而不是图片本身。相对路径是相对于进程启动时的工作目录来解析的,而服务或定时任务启动的位置,很少是你以为的那个位置。请使用绝对路径,并检查该文件对进程运行所用的账户是否可读。如果识别工具本身在另一台机器上,请确认 host 和 port 指向的是那台机器,而不是回环地址。

识别图片验证码时可以使用代理吗?

不可以。代理支持覆盖的是 reCAPTCHA、Turnstile、GeeTest 和 CaptchaFox,这些类型都是由识别工具自己去加载页面。而图片验证码是你手上已经有的字节数据,代理在这里无事可做,所以这个参数会被拒绝,而不是被忽略。

识别一个图片验证码应该花多长时间?

在正常情况下远低于一秒。图片任务的轮询超时默认是 120 秒,这是一张安全网,而不是一个预期值;SDK 会以四分之一秒的间隔开始轮询,然后逐步放慢。如果你等待的是以秒计而不是以毫秒计的时间,值得追问的问题是识别工具在做什么,而不是你的超时设置够不够长。

最短的版本

在做任何改动之前,先弄清楚是哪一侧出了问题。Python 异常意味着图片从未离开过你的机器,而以 ERROR 开头的错误码意味着它已经到达,只是被拒绝了。然后再对上具体的错误码:什么都没到达是 ERROR_UPLOAD,到达了但无法解码是 ERROR_INVALID_IMAGE,字符串无法解码是 ERROR_INVALID_BASE64,太大则意味着超过 600 kB 或超过 1000px。在下载时就验证字节数据,这些问题大多就不会再发生。上传干净却识别失败,是另一个问题,参见 ERROR_CAPTCHA_UNSOLVABLE 指南。至于参数本身, 图片验证码识别页面 是参考资料,Python 相关的细节则在 Python 验证码识别页面上。这里的经济账同样有用: 无限量验证码识别工具 的容量运行在你自己拥有的硬件上,意味着一次上传出错之后的重试,除了重试本身不会产生任何其他成本。