news 2026/9/29 3:52:38

接口自动化中图形验证码OCR识别与重试降级实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
接口自动化中图形验证码OCR识别与重试降级实践

做接口自动化做到第二年,基本都会撞上同一堵墙——登录接口前面杵着一张图形验证码。我最早那套跑得好好的 pytest 用例集,就是因为后端给登录加了个 4 位字符的图形验证码,一夜之间从"全绿"变成"全红",二十多个用例里十九个挂在初始化阶段,剩下的那个还是因为 skip 了。后来我把验证码处理这一块单独拆出来研究了一遍,从图像预处理到会话复用再到降级重试,算是摸出了一套能长期无人值守跑下去的方案。这篇就聊聊接口自动化里图形验证码到底该怎么处理,涵盖方案选型、图像预处理的参数调优、requests + pytest 的落地代码、以及我踩过的那些坑。适合已经能跑通基础接口自动化、正在被验证码卡住的同学,纯新手也能看懂,因为我会把每一步的意图讲清楚。

1. 图形验证码为什么会成为接口自动化的拦路虎

1.1 一次登录链路断掉之后的连锁反应

先还原一下问题现场。假设你有一套标准的分层用例结构,conftest.py里挂了一个 session 级别的登录 fixture,所有业务用例都依赖它拿到的 token。这套结构在验证码出现之前非常优雅,一次登录、全局复用,跑 200 个用例只需要 1 次 HTTP 登录请求。

后端加上图形验证码之后,链路变成这样:先调/captcha拿图片,再人工看图输入字符,最后带着字符提交登录。自动化脚本卡在第二步——它看不见图片。

这时候很多人的第一反应是"那我加一个 input 让人工输一下"。这个方案在本地调试还行,扔到 CI 上直接完蛋,因为流水线里没有人。于是你开始找绕过方案,然后发现绕不过去,因为验证码的校验逻辑就在登录接口里,属于服务端强制约束。

真正麻烦的不是登录这一个接口,而是它的连锁反应。token 拿不到,所有依赖登录态的用例全部报错;如果用 function 级别的 fixture,那每个用例都要重复一次验证码流程,200 个用例就是 200 次识别,识别率哪怕有 95%,也会有 10 个用例因为识别错误而失败。这类失败最恶心的地方在于它"时好时坏"——重跑一次可能就过了,于是团队开始习惯性重跑,慢慢就没人认真看失败了,整个自动化体系的可信度就此崩塌。所以验证码处理的核心指标不是"能不能识别",而是"稳定性够不够高、失败后能不能自愈"。

1.2 常见图形验证码的形态与技术特征

不同类型的图形验证码,处理难度差着数量级,先把它们分个类会省很多事。

验证码类型典型特征识别难度推荐方案
纯数字 4 位无干扰线,字号固定极低直接 OCR,准确率 98%+
数字字母混合有轻微噪点、颜色干扰低灰度 + 二值化 + OCR
扭曲/粘连字符字符重叠、旋转、波浪变形中图像预处理 + 深度学习模型
算术题"3+5=?",结果需计算中OCR 后正则解析再求值
汉字点选按提示点击图中文字高坐标定位模型,成本高
滑块/行为验证拖拽轨迹、加速度校验极高不建议硬刚,走测试环境策略

这里有个很重要的判断原则:验证码的强度是给攻击者设计的,不是给测试脚本设计的。前四类属于"防君子"级别,OCR 完全能搞定;后两类已经涉及行为分析和风控体系,硬碰硬投入产出比极低,正确做法是跟后端协商测试环境的豁免策略,而不是写一个几百行的轨迹模拟器。

1.3 处理边界的界定:哪些该绕、哪些必须识别

我给自己定过一条线:接口自动化的目标是验证业务逻辑,不是验证识别算法。这条线一旦模糊,你就会陷进无穷无尽的调参里。

具体来说,如果你的测试重点是订单创建、库存扣减、权限校验这些业务规则,那验证码只是挡路石,用什么方式挪开都行,测试环境白名单是最优解。反过来,如果你的项目本身就是做验证码安全评估、或者要给识别服务做质量回归,那识别环节才是被测对象,这时候必须真实识别。

大部分业务团队属于前者。所以我的建议顺序是:先争取测试环境的豁免能力,拿不到再上 OCR,OCR 不稳再加图像预处理和重试,最后才考虑第三方服务或自训练模型。这个顺序是按成本从低到高排的,很多人一上来就去研究深度学习模型,结果花了两周发现前端同学改一行配置就能解决,纯属白费功夫。

注意:测试环境豁免必须通过配置开关控制,绝不能把万能验证码硬编码在代码里合进主干分支,否则一旦误发到线上就是安全事故。

2. 方案选型:四条技术路线怎么挑

2.1 测试环境白名单与万能验证码

这是成本最低、稳定性最高的方案,没有之一。实现方式通常是三种:后端在测试环境配置里加一个固定验证码(比如0000);或者约定一个特殊请求头(如X-Test-Bypass: true)跳过校验;再或者把测试机的出口 IP 加入白名单。

我一般会这样跟后端同学沟通:"自动化用例每天要跑几百次登录,验证码会让失败率变成随机数,能不能在测试环境加个开关?" 只要说清楚这对 CI 稳定性的影响,绝大多数团队都愿意配合,因为改动量真的很小——在校验逻辑最前面加个if (testMode && code.equals("0000")) return true;就完事了。

这个方案唯一的坑是环境隔离。我踩过一次,测试配置文件和生产配置文件混在同一个仓库里靠启动参数区分,某次部署脚本传错了参数,导致预发环境也开了豁免。后来我们改成用独立的配置中心 namespace,测试环境的开关只在那一个 namespace 里存在,物理上不可能被生产读到。这个教训是:任何绕过安全机制的东西,都必须做到物理隔离,而不是逻辑上的条件判断。

2.2 会话复用:一次人工登录、长期复用凭证

拿不到后端配合的时候,这是第二选择。思路很简单:手工登录一次,把浏览器里的 cookie 或者接口返回的 token 抓出来,存到本地文件或者环境变量,脚本启动时直接读取,跳过整个登录流程。

优点是不需要识别、不需要后端配合、几分钟就能搞定。缺点是凭证会过期,你必须定期手工续一次。token 的有效期决定这个方案能不能用——如果是 30 天有效期的长效 token,那基本可以接受,每周手动刷一次;如果只有 30 分钟,那这个方案直接作废,因为脚本跑到一半就失效了。

我在一个内部系统上用过这个方案,token 有效期 7 天,我写了个小脚本,每次运行前先检查本地 token 文件的修改时间,超过 5 天就在日志里打一条醒目提醒,同时给团队群发一条消息。这样既保证了自动化能跑,又不会因为过期而突然全红。这里的关键是把过期这个不可控因素变成可预期的提醒,而不是让脚本在某个凌晨突然失败然后没人知道。

2.3 OCR 识别:PIL 预处理加 ddddocr

这是真正的"自动化"方案,不需要人工介入,适合长期无人值守的场景。技术栈通常就是两块:图像预处理用 Pillow,识别引擎用 ddddocr 或者 Tesseract。

ddddocr 是这几年做接口自动化的人用得比较多的一个开源库,它的优势是针对验证码场景做过专门训练,对粘连字符、噪点、扭曲的鲁棒性比通用 OCR 强不少,装完即用,不需要额外配语言包。Tesseract 更通用,但对验证码这种小尺寸、强干扰的图,默认参数下识别率会低很多,需要自己调 PSM 模式、字符白名单等一堆参数。

实测下来,对于 4 位数字字母混合、带轻微干扰的验证码,ddddocr 不预处理直接上大概能到 85% 左右,加上灰度化和二值化能到 93% 以上,再配合失败重试三次,单次登录的整体成功率能到 99.9% 以上。这个数字已经足够支撑无人值守了。

2.4 第三方打码服务与自训练模型

第三方打码服务分两类:机器打码和人工打码。机器打码便宜但准确率跟 OCR 差不多,人工打码准确率接近 100% 但每次调用都要钱,而且有延迟(通常 1 到 3 秒)。如果你的用例量是每天几千次登录,人工打码的成本会很快变得难以接受,我一般只在极少数场景下用它,比如关键版本的冒烟测试,需要绝对可靠的登录保证。

自训练模型是另一个极端。你需要先收集几百张验证码样本、人工标注、训练一个 CNN、调参、部署成服务。整个流程走下来大概一周,效果可能比 ddddocr 高几个百分点。除非你的验证码样式非常特殊(比如自定义字体加复杂背景),否则我不建议自己训,性价比太低。

选型速查表:

方案实现成本稳定性维护成本适用场景
测试环境豁免极低100%极低有后端配合,业务逻辑测试
会话复用低取决于有效期中拿不到配合,token 有效期长
OCR 识别中93%~99%中长期无人值守,CI 流水线
打码服务低99%+高(按次付费)低频关键场景
自训练模型高95%~99%高验证码样式特殊,有算法能力

3. 图像预处理:把识别率从 60% 拉到 90% 的关键

3.1 灰度化、二值化与阈值怎么定

识别率上不去,八成问题出在预处理,而不是识别引擎。原始验证码图片通常是 RGB 的,带彩色干扰线和渐变背景,直接丢给 OCR,引擎要先花力气判断"哪些像素是字",效果自然打折。

第一步是灰度化,把三通道压成单通道。这一步的本质是降维,去掉颜色信息,只保留亮度差异。Pillow 里一行代码:img.convert("L")。

第二步是二值化,把灰度图变成纯黑白。这里最关键的是阈值选择。固定阈值(比如 140)简单粗暴,对背景干净、对比度高的验证码效果不错,但遇到背景有渐变或者光照不均的图,就会出现一半全黑一半全白的惨状。更好的做法是用大津法(Otsu)自动计算阈值,它通过最大化前景和背景的类间方差来找分割点,原理不用深究,知道它"自适应"就够了。

from PIL import Image, ImageOps import io def to_binary(img_bytes, fixed_threshold=None): img = Image.open(io.BytesIO(img_bytes)).convert("L") # 自动对比度拉伸,让明暗差异更明显 img = ImageOps.autocontrast(img) if fixed_threshold is not None: img = img.point(lambda p: 255 if p > fixed_threshold else 0) else: # 用直方图近似 Otsu,Pillow 没有内置实现,这里用分位数近似 hist = img.histogram() total = sum(hist) acc = 0 for value, count in enumerate(hist): acc += count if acc >= total * 0.5: threshold = value break img = img.point(lambda p: 255 if p > threshold else 0) return img

那什么时候该用固定阈值?当你发现验证码的背景色基本恒定、光照一致的时候,固定阈值反而更稳,因为它不会因为某张图干扰线特别多而算偏。我的做法是先跑 50 张样本,对比自动阈值和几个固定阈值的识别率,哪个高用哪个,然后把这个值写进配置。这活儿不优雅,但有效。

3.2 降噪、去干扰线与字符切割

二值化之后,图上通常还留着一些孤立的噪点像素和细干扰线。降噪的常用手段是中值滤波,原理是把每个像素替换成周围邻域的中位数,孤立的黑点会被周围的白点"投票"冲掉,而字符的笔画因为成片存在,能保留下来。

from PIL import ImageFilter def denoise(img): # size=3 的中值滤波,对椒盐噪声效果好,对字符损伤小 return img.filter(ImageFilter.MedianFilter(size=3))

这里有个细节:滤波窗口不要开太大。我试过 size=5,噪点是没了,但很多验证码的字符笔画本身就细,会被直接抹掉,识别率反而下降。size=3 是验证码场景下的甜点值。

尺寸放大是另一个提升明显的手段。很多验证码图片只有 100x40 甚至更小,字符高度不到 20 像素,OCR 引擎在这种尺寸下特征提取很吃力。用 Lanczos 插值放大 2 到 3 倍,识别率经常能涨 5 个百分点以上。注意要用高质量插值算法,不能用最近邻,否则放大的只是马赛克块。

字符切割是可选步骤,只对固定位置、固定间距的验证码有效。做法是把二值图按列求和,统计每列黑色像素的数量,笔画所在列的黑色像素多,字符之间的空隙列接近零,找到这些零区就能把图切成单字符。切开后逐个识别,准确率比整图识别高,但一旦字符粘连(哪怕只有一个像素相连),这个方法就失效了,而且切错位置会导致整张图全错。我的经验是:能整图识别就整图识别,切割只在识别率实在上不去的兜底场景里用。

3.3 参数调优的实操记录

我在一个 4 位数字+小写字母、带 2 到 3 条干扰线、背景浅灰渐变的验证码上做过一组对比测试,样本是 200 张实拍截图。结果如下:

处理组合识别正确数准确率备注
原图直接识别11859.0%干扰线被当成字符
仅灰度化14170.5%颜色干扰消除
灰度 + 固定阈值 14017688.0%干扰线部分残留
灰度 + Otsu 近似阈值16884.0%渐变背景导致阈值偏斜
灰度 + 阈值 140 + 中值滤波18391.5%噪点基本清除
灰度 + 阈值 140 + 中值滤波 + 3 倍放大19195.5%笔画细节恢复
上一步 + 识别失败重试 3 次19999.5%单次登录成功率

这张表的读法是:预处理每一步都有边际收益,但重试策略的收益最大。因为验证码识别是独立的随机事件,单次 95% 的成功率,连续三次至少成功一次的概率是 1 - 0.05³ = 99.9875%。所以与其死磕把单次识别率从 95% 提到 97%,不如直接加个三次重试,成本几乎为零。

注意:重试时一定要重新请求一张新验证码,不能拿同一张图反复识别。我见过有同学写了个循环把同一张图识别三次然后取众数,结果三次结果一样,毫无意义,纯粹浪费时间。

4. 落地实现:用 requests 加 pytest 打通登录链路

4.1 目录结构与 fixture 设计

先说说工程结构,这个直接影响后续维护。我的习惯是按职责分层,而不是按接口分层:

api_auto/ ├── conftest.py # 全局 fixture:session、ocr 引擎、登录态 ├── utils/ │ ├── captcha.py # 验证码下载 + 预处理 + 识别 │ ├── assert_helper.py # 断言封装 │ └── logger.py # 日志 ├── apis/ │ └── login_api.py # 登录相关接口封装 ├── testcases/ │ └── test_order.py └── pytest.ini

关键点是 OCR 引擎要在 session 级别初始化一次。ddddocr 加载模型需要几百毫秒,如果每个用例都新建一个实例,200 个用例光初始化就要一分钟,而且内存占用会飙升。用scope="session"的 fixture 保证全局只有一个实例。

# conftest.py 片段 import ddddocr import pytest import requests BASE_URL = "http://127.0.0.1:8080" @pytest.fixture(scope="session") def ocr_engine(): # show_ad=False 关闭库自带的启动提示 return ddddocr.DdddOcr(show_ad=False) @pytest.fixture(scope="session") def api_session(): s = requests.Session() s.headers.update({ "User-Agent": "AutoTest/1.0", "Content-Type": "application/json", }) yield s s.close()

这里为什么用requests.Session而不是直接requests.post?因为 Session 会自动维护 cookie jar,验证码接口和登录接口之间的 JSESSIONID 就是靠它传递的。如果你混用requests.get和requests.post,每次都是新连接、新 cookie,后端会发现"这个 session 没请求过验证码",直接返回验证码错误。这个坑我在第 5 章还会细说。

4.2 验证码获取、识别、提交的完整链路

把验证码处理和登录封装成一个函数,对外只暴露"给我一个可用 token"这个语义,这是接口封装的基本素养。

# utils/captcha.py import io from PIL import Image, ImageFilter, ImageOps def preprocess(img_bytes, threshold=140, scale=3): img = Image.open(io.BytesIO(img_bytes)).convert("L") img = ImageOps.autocontrast(img) if scale > 1: img = img.resize((img.width * scale, img.height * scale), Image.LANCZOS) img = img.filter(ImageFilter.MedianFilter(size=3)) img = img.point(lambda p: 255 if p > threshold else 0) buf = io.BytesIO() img.save(buf, format="PNG") return buf.getvalue()
# apis/login_api.py import logging logger = logging.getLogger(__name__) class LoginApi: def __init__(self, session, ocr): self.session = session self.ocr = ocr def fetch_captcha(self): resp = self.session.get(f"{BASE_URL}/api/captcha", timeout=5) resp.raise_for_status() return resp.content def recognize(self, img_bytes): # ddddocr 的 classification 同时接受 bytes 和 PIL.Image code = self.ocr.classification(img_bytes) logger.info("识别结果: %s", code) return code.strip().lower() def login(self, username, password, retry=3): last_err = None for i in range(retry): img = self.fetch_captcha() code = self.recognize(preprocess(img)) payload = {"username": username, "password": password, "captcha": code} resp = self.session.post(f"{BASE_URL}/api/login", json=payload, timeout=5) body = resp.json() if body.get("code") == 0: logger.info("第 %d 次登录成功", i + 1) return body["data"]["token"] last_err = body.get("msg") logger.warning("第 %d 次登录失败: %s, 验证码=%s", i + 1, last_err, code) raise AssertionError(f"登录失败,重试 {retry} 次后仍未通过: {last_err}")

这段代码里有三个设计决策值得展开说。

第一个是retry参数放在登录函数内部,而不是用装饰器。原因是重试必须包含"重新获取验证码"这一步,如果只用通用的 HTTP 重试装饰器包住整个函数,每次重试会重新走一遍完整流程,虽然也能work,但日志会乱,而且不好在重试之间做特殊处理(比如换一个阈值再识别一次)。

第二个是识别结果做了strip().lower()。ddddocr 偶尔会在结果里带空格或者大小写不一致,而验证码校验通常是严格的字符串比较,这一个小处理能省掉不少莫名的失败。

第三个是失败时的日志。日志里必须打出来"失败的验证码是什么",这样当失败率异常升高时,你可以直接把日志里的错误识别结果和实际图片对比,快速判断是预处理参数漂移了,还是验证码样式改版了。没有这条日志,排查基本靠猜。

4.3 会话保持与 token 注入

登录成功后,后续接口怎么带上身份?两种常见方式:cookie 和 header token。用 Session 的话,cookie 是自动带的,但 header 需要手动加。我通常在登录成功后统一往 session 的 headers 里塞一个 Authorization:

@pytest.fixture(scope="session") def auth_token(api_session, ocr_engine): api = LoginApi(api_session, ocr_engine) token = api.login("autotest", "Test@123456") api_session.headers["Authorization"] = f"Bearer {token}" return token

这里有个容易忽略的点:token 应该挂在 session 级别的 fixture 上,而不是全局模块变量。我曾经见过有人把 token 存在一个config.py的模块级变量里,多个测试进程并发跑的时候互相覆盖,A 进程登录的 token 被 B 进程覆盖掉,然后 A 的所有用例开始报 401,排查了半天。用 fixture 的好处是生命周期由 pytest 管理,scope设成 session,每个 worker 进程都有自己的 session,互不干扰。

如果你的 token 有效期很短(比如 30 分钟),跑大批量用例可能会中途过期。这时候可以在 Session 级别再加一层失效检测:自定义一个requests.Session的子类,重写request方法,当返回 401 时自动调一次重新登录,然后重放原请求。这个方案稍微复杂,但对付短效 token 是唯一优雅的解法。

4.4 断言规范:接口自动化断言怎么写才不脆弱

验证码这一环打通之后,用例就该关注业务断言了。但断言写不好,用例的稳定性照样堪忧。我把断言分成四层,从外到内依次校验:

层级校验内容示例失败含义
协议层HTTP 状态码assert resp.status_code == 200服务或网关异常
结构层JSON Schema字段存在、类型正确接口契约变更
业务层业务返回码assert body["code"] == 0业务逻辑失败
数据层关键字段值订单号非空、金额匹配数据计算错误

结构层推荐用jsonschema库做校验,比一堆assert "xx" in body可维护得多:

from jsonschema import validate LOGIN_SCHEMA = { "type": "object", "required": ["code", "msg", "data"], "properties": { "code": {"type": "integer"}, "msg": {"type": "string"}, "data": { "type": "object", "required": ["token", "userId"], "properties": { "token": {"type": "string", "minLength": 10}, "userId": {"type": "integer"}, }, }, }, } def assert_login_schema(body): validate(instance=body, schema=LOGIN_SCHEMA)

数据层断言有个大坑:不要断言易变字段。比如创建时间的精确值、自增 ID 的具体数字、列表的排序顺序(如果接口没承诺排序)。我踩过的典型例子是断言订单号等于某个固定值,结果数据库迁移后起始 ID 变了,用例全红,但业务其实完全正常。正确做法是断言"订单号符合 20 位数字的格式"而不是"等于 12345678901234567890"。断言要描述不变量,而不是快照。

5. 常见问题与排查技巧实录

5.1 识别率忽高忽低的排查路径

识别率不稳定的原因很多,按排查顺序我一般这么走。

先看是不是验证码本身变了。后端改版换了字体、加了新的干扰元素、调整了字符集,都会导致原有参数失效。判断方法是把最近失败用例对应的图片保存下来,人工看一眼,如果明显能看出样式变化,那就去更新预处理参数。

再看是不是图片没刷新。有些实现里,/captcha接口返回的是缓存的图片,同一个 session 短时间内请求两次拿到的是同一张图,而你第一次已经把这张图对应的答案提交过了,服务端那边已经作废,第二次必然失败。解决方式是每次登录前先请求一次/captcha/refresh或者带一个随机时间戳参数打散缓存。

还有一种情况是并发导致的。如果你用 pytest-xdist 并行跑用例,多个 worker 共享了同一个 Session 对象(这种情况通常出现在你把 session 定义在模块级或者用全局单例的时候),就会出现 A 拿的验证码被 B 提交了的情况。解决方式是确保每个 worker 有独立的 session,或者干脆用--dist loadscope让同一个模块的用例分到同一个 worker。

5.2 验证码与 session 不匹配的几个坑

这个问题的表现是"识别结果明明是对的,但服务端就是说过期或错误"。原因基本都是会话没对上。

最常见的错误是混用requests.get和requests.Session。比如你用requests.get(url)拿验证码,然后用session.post提交登录,这两次请求的 cookie 是独立的,服务端存的验证码和你提交的 session 对不上号。必须全程用同一个 Session 对象。

第二个坑是手动设置了Cookieheader。有些人为了图省事,直接在 headers 里硬写一个 Cookie 字符串,这时候 requests 库会忽略 cookie jar,Session 自动维护的 cookie 就失效了。正确的做法是让 Session 自己管 cookie,不要手动碰这个 header。

第三个坑是重定向。有些系统登录接口会返回 302 跳转,requests 默认会跟随重定向,重定向过程中可能触发一次额外的请求,导致 session 状态被改变。排查方法是在请求里加allow_redirects=False观察一下真实响应,如果确实是重定向问题,就改成手动处理跳转逻辑。

5.3 高频问题速查表

现象最可能原因快速验证方式解决方向
识别结果空字符串图片未下载完整打印 img_bytes 长度检查响应编码、加超时
识别结果总是错预处理把字符抹掉了保存处理后的图片人工看降低阈值、关掉滤波
提示验证码过期会话不一致打印两次请求的 cookie统一用同一个 Session
登录偶发失败率高单次识别率不足统计 100 次识别正确数加三次重试
token 中途失效有效期短记录 token 下发时间401 自动重登
并行跑必失败session 被共享打印 Session 对象 id每 worker 独立 session
图片是同一张服务端缓存连续请求两次比对 md5加随机参数打散缓存

注意:排查时一定要把处理后的中间图片保存到本地,文件名带上时间戳和识别结果。这个习惯帮我省了大量时间,因为问题往往是"某几张特定样式的图识别不对",光看日志根本看不出规律,看图一眼就明白了。

6. 稳定性与工程化的一点个人经验

6.1 失败重试与降级策略

重试不是简单地包个 for 循环,需要有梯度。我用的策略是三级降级:第一级用最优参数识别一次;失败的话,换一组更保守的预处理参数(比如降低阈值、去掉滤波)再识别一次,因为不同样式的验证码对参数的敏感度不同,换参数往往能救回来;第二次还失败的话,就休眠 200 毫秒重新请求一张全新的验证码,避免陷入同一张图的死循环。

三级都失败的概率大约是 0.05³,两千次里出现一次,这个频率下即使偶发失败也不会影响整体判断。如果真的遇到连续失败,我建议直接在代码里抛异常并触发告警,而不是无限重试,因为连续失败通常意味着系统性故障(验证码服务挂了、样式改版了),无限重试只会掩盖问题。

降级策略还有一个方向是"缓存可用的登录态"。如果你的测试环境允许多个 token 并存,可以在每次成功登录后把 token 写到一个本地文件里,下次脚本启动时先尝试用缓存的 token 调一个轻量接口(比如获取用户信息)验证是否有效,有效就直接用,无效再走完整的登录流程。这样即使在验证码服务临时故障的情况下,已经跑过的用例也不会因为登录环节而全部失败。这个方案的关键是缓存的 token 要有明确的过期标记,不能无限信任。

6.2 把验证码处理封装成独立模块

最后说说封装。验证码处理这块逻辑,跟业务测试完全无关,应该彻底隔离成一个可以单独测试的模块。我会给它设计这样几个接口:fetch()负责下载、preprocess()负责图像处理、recognize()负责识别、solve()是对外的组合方法。这样分层的好处是,当识别率下降时,你可以写一个测试脚本直接喂 100 张历史图片给recognize(),快速定位问题出在下载、预处理还是识别环节,而不需要跑整个用例集。

另外,建议把历史图片和识别结果存一份到本地(或者对象存储),形成一个小型的回归集。每次调整预处理参数后,用这个回归集跑一遍,看看准确率是升了还是降了,比凭感觉改参数靠谱得多。我这个回归集从最初的 50 张积累到了现在的 400 多张,覆盖了各个时期的不同样式,每次改参数跑一次只要十几秒,已经成为我改这块代码的固定动作。

提示:回归集里的图片记得脱敏,验证码本身虽然价值不高,但如果图片里混进了其他敏感信息(比如截图带上了页面其他内容),存档到仓库里就不合适了。我一般只裁剪验证码区域再保存。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 3:52:23

多项式黑盒的两次测试:差分法看穿未知系统

很多人第一次听说“多项式黑盒”这个词,第一反应是:这不就是个输入输出规则未知的盒子吗?没错,它确实是个盒子,但真正让它有意思的地方在于——你不知道里面装的是什么数学函数,却可以通过少量输入输出反推…

作者头像 李华
网站建设 2026/9/29 3:52:14

GLM-5.1 与 GLM-5.2 关键区别:TaoToken 统一 Key 接入配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 3:51:09

Ubuntu 24.04 NVIDIA驱动安装与排错:DKMS内核模块指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华