开头
每次在 Web 自动化框架里跑到登录步骤就突然停住,十有八九是撞上了图形验证码。这东西对真人来说有时都得眯着眼睛辨认,对自动化脚本来说更是一道硬关卡。做测试的朋友应该都体会过:运维环境功能可以关,但预发布环境还关不掉;验证码本身看着不难,但脚本总是识别失败,一失败就要等刷新、等下一次重试,整个用例的执行时间直接翻倍。
这篇文章我想把图形验证码处理这条链路完整拆开,从验证码类型、方案选型、图像预处理、OCR 识别,到和 Selenium、Playwright 这类 Web 自动化框架真正集成,一次性讲透。适合正在维护自动化测试框架的测试开发、刚接触自动化的小白,以及被验证码卡到头疼还找不到头绪的朋友。内容以实际项目为主,所有步骤都能直接抄走用。
1. 图形验证码为什么是自动化测试的拦路虎
1.1 常见验证码类型与识别难度分级
先给常见图形验证码做个分级,我从实际项目里遇到的频率和识别难易程度出发,列了一张表:
| 验证码类型 | 典型样式 | 识别难度 | 自动化处理的常见问题 |
|---|---|---|---|
| 纯数字 / 字母 | 4位或6位字符 | 较低 | 字体扭曲、颜色干扰、字符粘连 |
| 数字+字母混合 | 大小写随机组合 | 中等 | 容易把0认成O,1认成l,大小写敏感 |
| 中文汉字 | 2到4个词组 | 较高 | 字库覆盖不足,生僻字识别率低 |
| 数学运算 | 8+3=? 之类 | 中等 | 先识别再计算,一步出错全错 |
| 滑块验证 | 拖动缺口到指定位置 | 较高 | 依赖轨迹模拟和缺口定位,稳定性差 |
| 点选文字 / 图像 | 按顺序点击图中文字或物品 | 很高 | 需要目标检测模型辅助,成本高 |
| 极验 / 腾讯系行为验证 | 混合图形+行为轨迹 | 很高 | 需要逆向或专门的模拟策略,维护成本高 |
这里我重点展开纯图形验证码,也就是“图片上画了几个字符,让你输入”的这类。它是自动化测试中最常见、也是最容易通过技术手段解决的一类。滑块和点选类后续有机会单独写,但它们背后的图像定位思路和图形验证码有互通之处。
1.2 验证码在测试链路中的位置
验证码几乎只出现在两类地方:登录前和操作敏感动作前。登录前不用多说,每次自动化用例要走完整登录流程就绕不开。敏感动作前的验证码更麻烦,比如修改密码、绑定手机号、提交订单、领取优惠券,触发时机不确定,可能在用例中段突然弹出来。
对自动化脚本来说,验证码带来的影响不仅是“识别失败”,而是整个用例的稳定性崩塌:
- 用例时间不可控。一次识别失败就要重新加载验证码、重新识别,几轮下来用例超时。
- 用例结果不可复现。同样的代码,有时候识别成功,有时候失败,测试报告里飘红一片。
- 大量用例被卡在登录层。尤其回归测试需要多账号登录,每个账号跑一次登录就卡一次。
所以,验证码处理不是“锦上添花”的细节,而是自动化框架能不能稳定跑下去的基础设施之一。
1.3 为什么不能一刀切“绕过去”
很多刚接触自动化的同学第一反应是:能不能把验证码跳掉?比如调用后门接口直接获取登录态。这在纯接口测试场景下完全可行,但 Web 自动化测试的核心目标是验证前端流程、交互逻辑和联调结果,如果你直接绕过登录,页面跳转、Cookie 传递、权限控制这些环节就全部失去了验证意义。
而且验证码出现的位置往往不只是登录,还有下单确认、修改资料等业务环节。这些环节没法简单通过“跳过”处理,必须让自动化具备“看到验证码并正确输入”的能力。这就是图形验证码识别处理存在的意义。
2. 图形验证码处理方案选型与取舍
2.1 四类主流方案对比
我在不同项目里试过四类方案,分别适用于不同阶段和不同环境,这里先说结论再展开:
| 方案 | 实现难度 | 维护成本 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 测试环境关闭验证码 | 极低 | 无 | 100% | 开发测试环境、持续集成流水线 |
| 万能验证码 / 固定验证码 | 极低 | 无 | 100% | 所有非生产环境 |
| 图像预处理 + OCR | 中等 | 中 | 70%~95% | 图形验证码干扰不严重的场景 |
| 第三方云端识别 | 低 | 高(按量付费) | 95%以上 | 复杂验证码、生产环境联调 |
先说前两个,这两个方案往往是自动化的“保底手段”,也是我接手任何一个新项目时最先确认的资源。
2.2 测试环境关闭验证码与万能验证码
关闭验证码的方式很多:后台配置开关、通过接口开关、测试数据初始化时设置。最理想的是框架层面做一个基础抽象,让代码自动判断当前环境,如果是测试环境,直接跳过验证码输入步骤;否则才走 OCR 或标准识别流程。
万能验证码也很常见。就是在验证码校验逻辑里预留一个超级账号或固定字符串,测试时输入它,无论后台生成什么验证码都能通过。这种方案的好处是代码根本不用改,自动化脚本正常走输入流程,区分度仅在于“输入的内容是万能值”。但要注意两点:一是万能验证码千万不能上线到生产环境,否则安全形同虚设;二是如果测试环境偶尔因为数据初始化问题没有开启万能验证码,脚本回像“谜语人”一样摸不着头脑,得提前在配置中心里固定好。
我不建议把这两类方案作为唯一方案。因为预发环境、灰度环境往往不会开放这类开关,而那些环境恰恰是最需要跑完整回归的。所以一个好框架,应当优先走“真实识别”,并保留环境开关做兜底。
2.3 图像预处理 + OCR 的技术原理和应用边界
OCR 识别图形验证码的原理并不神秘:图像输入后,先做预处理提升字符和背景的对比度,再用 OCR 引擎把图像特征映射为文字,最后配合后处理规则(字符白名单、长度限制)提升准确率。
主流的 OCR 引擎是 Tesseract,配合 Python 的 pytesseract 库,适合中等干扰程度的验证码。判断适不适合的黄金准则:验证码图片里有没有明显的干扰线、噪点或背景花纹。如果只是简单的扭曲字体,比如深色字浅色底,预处理之后识别率能到 90% 以上。如果是渐变背景加密集噪点,识别率会掉到 50% 以下,那就要考虑云端识别或另想办法。
2.4 第三方云端识别如何选
市面上现在有不少验证码识别服务,它们通常提供 HTTP 接口,把图片传上去,返回文本结果。优点是识别率高、省掉模型训练成本;缺点是按次计费、依赖外网,内网隔离的项目没法用。
实际项目中我通常这么衔接:给 OCR 识别层做一个统一接口,内部先跑 Tesseract,如果结果的置信度不够或者连续重试仍然失败,再调用云端接口。这样既控制了成本,也保证了复杂场景下的成功率。
2.5 底层框架与工具选型
图形验证码识别的核心依赖无外乎三个:截图工具、图像处理库、OCR 引擎。截图工具由自动化框架自带;图像处理库我和大部分团队一样用 OpenCV;OCR 引擎用 Tesseract,这里先给出基础环境:
# 安装 Python 依赖 pip install opencv-python pytesseract requests # Ubuntu/Debian 安装 Tesseract 引擎(并支持中文) apt-get install -y tesseract-ocr tesseract-ocr-chi-sim另外说一句,如果你想识别效果再好一点,可以下载训练好的专用数字验证码字库放到 Tesseract 的 tessdata 目录,识别率和原版相比有质的提升。这个点我在后面的章节详细讲。
3. 图形验证码识别核心链路:从截屏到输出文本
3.1 第一步:拿到干净的验证码图片
识别之前必须保证图片质量。很多自动化脚本识别率低,不是算法不行,而是截图环节就出了幺蛾子。我用 Selenium 和 Playwright 分别给出两个获取图片的方式。
Selenium 获取验证码元素的 base64 截图:
import base64 from selenium.webdriver.common.by import By # 先确保页面结构加载完整 captcha_element = driver.find_element(By.CSS_SELECTOR, "img.captcha-img") # 方式一:直接截图并保存为文件(推荐) captcha_element.screenshot("captcha_raw.png") # 方式二:截图后转成 base64 字符串,方便直接扔给处理函数 screenshot_b64 = captcha_element.screenshot_as_base64 raw_data = base64.b64decode(screenshot_b64) with open("captcha_raw_base64.png", "wb") as f: f.write(raw_data)Playwright 获取验证码元素截图:
# 使用 Playwright 的同步 API cap_locator = page.locator("img#captcha-img") cap_locator.screenshot(path="captcha_raw_playwright.png") # 也可以直接截全页面然后裁剪,但我更推荐直接对元素截图,省掉坐标计算这里有几个注意事项是截图阶段很关键的:
- 等待验证码图片加载完成再截图。如果脚本点击“刷新验证码”后立刻截图,很容易拍到旧图或半加载图,识别出来的是上一个验证码,后续输入必然失败。建议显式等待图片的 src 或 class 变化,或者干脆固定等待 0.5~1 秒。
- 用元素截图,不要用全屏截图再裁剪。全屏截图依赖元素坐标,如果页面有滚动或响应式布局变化,坐标很容易偏移,导致裁剪到旁边的广告位。
- 图片可能带有 data URI 前缀。如果验证码图片直接是 base64 内嵌的(src 以 data:image 开头),那根本不需要浏览器截图,直接把 base64 解码成文件即可,速度和清晰度都有保障。
3.2 第二步:图像预处理,这一步决定识别率上限
拿到原始图像后,不能直接扔给 OCR。原图往往有背景纹理、干扰线、字符颜色与背景接近等问题,直接识别可能连 20% 都不到。我常用的预处理流程依次是:灰度化、二值化、降噪、去边框或去干扰线。
下面是一段我用 OpenCV 实现的预处理函数,覆盖大部分图形验证码的清洗需求:
import cv2 import numpy as np def preprocess_captcha(image_path: str) -> np.ndarray: """图形验证码预处理:灰度 -> 二值化 -> 形态学降噪""" # 1. 保持原始图像和通道信息 img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") # 2. 灰度化:去掉颜色,保留亮度信息 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 3. 直方图均衡化:增强字符与背景的对比度 gray = cv2.equalizeHist(gray) # 4. 自适应二值化:代替固定阈值,适应不同亮度的验证码 binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, # 邻域大小,可根据图片尺寸微调 15 # 常数 C,越大越容易保留浅色字符 ) # 5. 形态学操作:先腐蚀去掉细小的噪点,再膨胀恢复字符粗细 kernel = np.ones((3, 3), np.uint8) cleaned = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) cleaned = cv2.medianBlur(cleaned, 3) return cleaned二值化是整条链路中最需要调参的一步。固定阈值 cv2.threshold 在统一背景下挺好用,但验证码图片的背景色经常变化,今天浅黄明天浅蓝,固定阈值会导致字符被吞。自适应二值化通过局部邻域计算阈值,对这种场景明显更稳。常见验证码适配的关键参数如下:
| 图像特点 | 邻域大小(blockSize) | 常数 C | 推荐操作 |
|---|---|---|---|
| 字符粗大、图片清晰 | 15 | 10 | 直接自适应二值化即可 |
| 背景干扰线密集 | 31 | 15 | 先高斯模糊再二值化 |
| 字符细长、容易断线 | 11 | 20 | 二值化后额外做一次膨胀 |
| 背景与字符颜色接近 | 51 | 10 | 先做直方图均衡化 |
如果你看到的验证码带明显的弧线干扰,预处理阶段还可以加一步:按连通域面积过滤,只保留面积超过某个阈值的连通区域。干扰线通常是一条细长的连通域,面积占比远小于字符区域,过滤掉之后效果立竿见影。这个思路适合固定样式、干扰线位置相对稳定的验证码,通用性稍差,但对特定项目很有用。
3.3 第三步:用 Tesseract 做 OCR 识别
预处理完的图片就可以喂给 Tesseract 了。这里说的“喂”不只是调一个函数,关键是 Tesseract 的模式和参数设置。看一段代码:
import pytesseract from PIL import Image def recognize_captcha(processed_img: np.ndarray) -> str: """将预处理后的图像识别为字符串""" # OpenCV 的 BGR 数组先转成 PIL 的 RGB 格式 rgb_img = cv2.cvtColor(processed_img, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(rgb_img) # 给 Tesseract 一个字符白名单:只允许大写字母和数字 config = ( "--psm 7 " "-c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz0123456789" ) text = pytesseract.image_to_string(pil_img, config=config) text = text.strip().replace(" ", "").replace("\n", "") return text.upper()这里有一个核心参数 psm(Page Segmentation Mode),它告诉 Tesseract 图像应该按什么布局来切分。对图形验证码,我强烈建议用--psm 7(单行文本模式),因为绝大多数验证码都是一行字符。如果同时出现多行词组的验证码,可以用--psm 6。很多识别失败案例都是因为默认模式把验证码当成整页文档,切分出来一堆乱七八糟的框。
tessedit_char_whitelist的作用是告诉 Tesseract 只需要在这些字符里猜。如果明明是纯数字验证码,你却允许字母参与猜测,那么数字 8 被认成字母 B 的概率就会增高。所以白名单要精确,纯数字验证码就只放0123456789。
3.4 专用训练字库与二次校验
对识别率迟迟上不去的场景,可以自己训练 Tesseract 专用字库。这一步在小范围固定字体的验证码上非常有效,能把识别率从 70% 拉到 95% 以上。训练流程大致是这样的:
- 收集 100~200 张同一款式的验证码图片,并记录每张图片对应的真实文本(就是每条验证码应该是什么)。
- 用收集的数据生成 Tesseract 的训练文件:对每张图片做 box 标注(可以用 JTessBoxEditor 工具可视化标框)。
- 执行
tesseract训练命令生成 tr 文件、字体特征文件和最终 traineddata 文件。 - 把训练好的 traineddata 放到 tessdata 目录,并在调用时通过
--traineddata指定。
这个过程需要花一些时间,但对固定业务系统来说性价比极高。我的建议是,先判断验证码是不是内网业务系统里固定样式、长期不换的。如果是,优先考虑训练专用字库而不是反复堆预处理参数。如果验证码样式经常换,训练字库容易白费。
另外,OCR 识别出的结果不要直接拿去用,加一层校验逻辑会更稳妥:
def validate_and_normalize(text: str, expected_length: int = 4) -> str: """校验识别结果,如长度不对则返回空字符串,让外层走重试""" if len(text) != expected_length: return "" # 常见误识别纠正:数字 0 和字母 O、字母 I 和数字 1 等 # (如果业务是纯数字,这里可以统一替换) text = text.replace("O", "0").replace("I", "1").replace("l", "1") return text这一步属于经验积累,不同项目的识别混淆点不一样。比如电商系统的纯数字验证码,把 OCR 输出的全部非数字字符替换成数字是很常见的操作。如果业务允许大小写不敏感,全部转大写也能救回一部分混淆结果。
4. 在 Web 自动化框架中落地验证码处理
4.1 统一封装一个验证码处理模块
框架层落地时,最忌讳把识别逻辑散落在各个用例里。我在项目中习惯把验证码处理抽象成一个独立模块,统一暴露给自动化脚本调用。模块内部划分三层:
- 图片获取层:负责从页面元素或接口响应中获取验证码图片;
- 识别层:负责预处理 + OCR + 云端识别,返回识别文本;
- 执行层:负责输入验证码、点击提交、判断是否成功、失败重试。
这样做的价值在于,底层怎么识别对上层透明。今天用 Tesseract,明天换成识别服务,用例代码一行都不用改。
4.2 在 Selenium 中实现完整登录流程
下面给出一个在 Selenium 中完整处理图形验证码登录的例子:
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time def solve_captcha_and_login(driver, username, password, is_test_env=False): # 输入用户名和密码 driver.find_element(By.ID, "username").send_keys(username) driver.find_element(By.ID, "password").send_keys(password) # 定位验证码图片 captcha_img = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, "img.captcha-img")) ) captcha_img.screenshot("captcha_cur.png") # 识别验证码文字 if is_test_env: captcha_code = "1234" # 测试环境的万能验证码 else: processed = preprocess_captcha("captcha_cur.png") captcha_code = recognize_captcha(processed) # 校验长度,如果异常则点击刷新并重新获取一次 if captcha_code is None or len(captcha_code) < 4: driver.find_element(By.CSS_SELECTOR, "a.refresh-captcha").click() time.sleep(0.5) driver.find_element(By.CSS_SELECTOR, "img.captcha-img").screenshot("captcha_cur.png") processed = preprocess_captcha("captcha_cur.png") captcha_code = recognize_captcha(processed) # 输入验证码并登录 driver.find_element(By.ID, "captcha").send_keys(captcha_code) driver.find_element(By.ID, "loginBtn").click() # 真正的核心:判断是否登录成功,而不是猜时间 try: WebDriverWait(driver, 8).until( EC.visibility_of_element_located((By.CSS_SELECTOR, "div.user-panel")) ) print("登录成功") return True except Exception: error = driver.find_element(By.CSS_SELECTOR, "span.login-error") print(f"登录失败,原因:{error.text}") return False这段代码里有几个容易被忽略的细节,值得单独拿出来讲:
- 获取验证码元素前要等待可见,不要刚开始抢。如果页面有异步接口在生成验证码,立刻截图会拍到旧图或者空白图,这是 Selenium 新手最容易踩的坑。
- 识别失败不是直接抛异常,而是先刷新验证码再识别一次。很多验证码点击刷新后会生成新图,再试一次往往就成功了。
- 登录后的判断必须用显式等待。不能用
time.sleep(5)然后看一眼页面就完事。页面加载快慢受网络影响,sleep 长了浪费时间,短了误报失败。用 WebDriverWait 等待登录成功后的标志元素,才是靠谱的方式。
4.3 在 Playwright 中实现自动识别
Playwright 的 API 相比 Selenium 更简洁一些,并且自动处理了等待逻辑,示例代码可以更干净:
from playwright.sync_api import sync_playwright import time def playwright_login(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com/login") page.fill("#username", "test_user") page.fill("#password", "test_password") # 等待验证码图片稳定可见 captcha_locator = page.locator("img.captcha-img") captcha_locator.wait_for(state="visible") captcha_locator.screenshot(path="captcha_pw.png") processed = preprocess_captcha("captcha_pw.png") captcha_code = recognize_captcha(processed) page.fill("#captcha", captcha_code) page.click("button:has-text('登录')") # 等待登录成功的标志元素出现,超时会自动抛出异常 page.wait_for_selector("div.user-panel", timeout=8000) print("登录成功") browser.close()Playwright 自带的wait_for_selector实际上兼了 Selenium 里 WebDriverWait 的作用,代码少了一截。如果你负责的项目已经切到 Playwright,建议优先用它的自动等待能力,不要再手写 sleep,这会让用例稳定很多。
4.4 重试策略与用例级自愈
不管识别算法调得多好,验证码识别都不可能做到 100%。所以自动化框架在处理验证码时必须内建重试机制。我的经验是做成三层重试:
- 识别前重试:预处理结果如果明显异常(比如 OCR 返回空、返回长度不等于验证码位数),直接刷新验证码重来,不进入填写流程。
- 登录后重试:填写验证码后点击登录,如果页面提示“验证码错误”,自动刷新验证码并重新执行一次填充。注意此时账号密码可以不用重新输入,但有些系统会在登录提交失败后清空密码框,所以重试流程里要考虑重新填一遍。
- 用例级重试:整个登录步骤连续失败超过 3 次后,终止用例并标记失败,同时截图存档。不要无限制重试下去,否则一个故障用例会白白消耗十几分钟。
我给团队常用的次数阈值是:单次验证码识别重试最多 3 次,超过即走用例级自愈或失败。这个阈值既给了验证码算法自我纠正的空间,也保证了整体用例的执行时间不会因为服务偶发抖动而失控。
4.5 多账号与并行执行时的特别注意事项
回归测试经常要跑几十个账号并行登录,验证码模块在并发行下会遇到两个额外问题。
第一个是浏览器上下文隔离。Selenium Grid 下每个节点是独立浏览器进程,验证码截图和登录态不会互相污染,这还好。但如果你在同一个浏览器进程里用多标签页并行登录,验证码的 Cookie 和页面状态可能串台,强烈建议每个并行任务使用独立浏览器上下文。Playwright 的browser.new_context()天然支持这个,隔离性极佳。
第二个是验证码刷新时机。并行跑同一个系统的用例时,如果多台机器同时刷新验证码,后端接口可能会有频率限制,导致一部分刷新请求失败返回旧图。这个问题的表象是识别结果总是对不上。排查思路是看后端验证码刷新接口的日限量告警,或者在框架里给验证码刷新动作加一个全局互斥锁 / 小延时。个人经验是小规模并行加 200~500 毫秒的随机延时就能缓解,真正的大并发还得从后端接口限流入手。
5. 常见问题排查与实战避坑
5.1 识别率低,问题到底出在哪一环
碰到“验证码识别率老上不去”的情况,我整理了问题定位速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| OCR 总是输出空字符串 | 预处理过度,字符被吞掉 | 二值化阈值过大,减少常数 C;检查形态学操作是否腐蚀过度 |
| OCR 输出乱码 | 干扰线太多 | 增加降噪步骤,先高斯模糊再接二值化;使用连通域过滤 |
| 长度对,字符错一半 | 字体不在 Tesseract 训练范围 | 训练专用字库;缩小白名单范围 |
| 识别正确却登录失败 | 验证码时效太短或请求带着旧 Cookie | 缩短截图到输入的间隔;登录前刷新 Cookie |
| 偶发失败,重试即成功 | 网络延迟导致图片半加载 | 在截图前等待图片加载完成的信号,别用固定 sleep |
| 验证码刷新后永远失败 | 后端缓存旧图 | 查看接口是否设置了 CDN 缓存头,测试环境可请求禁用 |
如果用这个表逐项排查,大部分识别率问题都能找到答案。这里顺便说一句,遇到问题时先记录验证码图片和识别结果,对比着看比凭空想象高效得多。我习惯在处理函数里默认保存最近 50 张验证码原图和预处理结果图,排查问题时有据可查,这个习惯帮过大忙。
5.2 识别正确但登录总失败的细节坑
这是最让人抓狂的情况:OCR 输出的文本肉眼看着完全正确,但系统就是提示验证码错误。
我遇到过一种典型的“隐性时效”场景。页面刚打开时加载了一个验证码,脚本把页面晾了很久才开始操作,等填完账号密码,验证码早已过期。表面上你看不到任何提示,但提交时后端发现验证码过期,就返回错误。解决办法是:在登录流程中,先填账号密码,再截图验证码,截完图立刻填,中间不要夹带其它耗时操作。
还有一种是验证码校验绑定的是登录请求的 Session Cookie。如果自动化脚本在“获取验证码”和“提交验证码”之间不小心发起了一个会刷新 Session 的请求(比如点了一下页面其他区域触发了异步刷新),后端认为会话变了,旧验证码就失效了。排查方式是在提交时对比验证码刷新前后请求的 Cookie 是否有变化,如果有,就该在代码里避免这类额外请求。
5.3 固定 sleep 的滥用怎么破
代码里写time.sleep(2)等待验证码加载,是所有自动化工程师都干过的事。sleep 确实是调试阶段最简单的办法,但它有两个先天缺陷:无谓等待和误判成功。网络好的时候 2 秒是多余的,网络差的时候 2 秒又不够。而且 sleep 不校验结果,只要过了时间就继续往下走,哪怕图片还是空白。
我更推荐的做法是围绕验证码自身的特点做轮询式等待。比如验证码图片的 src 里通常带时间戳或随机参数,可以用 Playwright 或 Selenium 的条件等待,轮询判断这个 src 是否从“旧值”变成了“新值”。如果验证码图片接口的响应包含Set-Cookie,还可以在代码层面显式等待验证码接口的响应完成,然后再截图。这样既快又稳。
5.4 并行跑用例时的验证码冲突
这里再补充一个实战场景。某次大规模回归,框架并行跑 20 个账号,结果有一小部分用例登录后进入了别人的会话,排查到最后,发现是登录用例里用了共享的一个静态变量在保存验证码 Cookie,并行执行时变量被覆盖了。这种低级错误在并发场景下特别容易暴露。
解决办法是:验证码相关的 Cookie、验证码文本、处理进度等状态,一律保存在“当前任务上下文”对象里,不要放模块级全局变量。框架如果用线程池跑用例,务必要让每个线程有独立的上下文实例。不要问我为什么强调这点,团队里已经不止一次在这上面栽跟头了。
5.5 识别服务不可用时的降级策略
如果你的框架接入了云端识别服务,还会遇到外网 API 超时、限流或故障的情况。测试总不能因为一个外部服务挂了就全线红。我给框架设计的降级顺序是:
- 本地 Tesseract 优先,识别失败才请求云端;
- 云端超时或返回异常,自动切换回本地 OCR 结果;
- 本地和云端都不理想,走万能验证码(仅限非生产环境);
- 所有手段都用完仍失败,标记为“可重试失败”,由用例级 Runner 整体重跑该用例。
这套降级策略我要求所有接入验证码模块的团队都落地。它的好处是把外部依赖的影响控制在最小范围,测试平台不会因为验证码识别服务间歇性故障而大面积报红。
写在最后
图形验证码识别这件事,说到底没有银弹。不同项目用的验证码风格千差万别,今天解决了的方案,明天系统升级验证码加了两条干扰线,识别率可能又掉一半。我个人的体会是,处理这类问题要抓住关键环节:先把图片拿干净,再把预处理参数调准,最后用重试和降级把成功率兜起来。这三点做好,大部分图形验证码场景都能稳定跑起来。
如果验证码升级到滑块验证、点选验证这类更复杂的形式,很多团队选择在测试环境关掉验证码,或者在框架里引入专门的验证码识别服务来应对,这些方案我在文中的选型部分都有提到。对大多数做 Web 自动化的人来说,先把手头图形验证码的处理链路打通,已经能解决绝大多数登录难、被卡住的问题了。希望这篇攻略能让你少走几步弯路。