自动化测试做到登录这一环,十有八九都会被验证码卡一下。我刚入行那年第一次写UI自动化脚本,信心满满地跑起来,结果一晚上没睡都在盯着那个登录页面的验证码图片刷新,整个人心态直接崩掉。后来见得多了,踩过的坑也多了,才慢慢摸清楚验证码处理的完整套路。这篇东西不写教科书式的理论,纯粹是把我这些年实际用过的验证码处理方案、OCR实操细节、工程化接入方式,以及那些坑到没朋友的排查经历,一次性梳理清楚。适合正在做Web自动化、接口自动化的测试开发同学,也适合刚准备动手写自动化脚本却不知道怎么过验证码这一关的新手。
1. 验证码处理策略:先想清楚再动手
1.1 为什么验证码是自动化测试的"拦路虎"
验证码这个东西,设计初衷就是要在互联网上区分"真人"和"机器"。它故意把文字扭曲、加点干扰线、混上一堆噪点,就是要让人眼看起来费力、让机器读起来更费力。可自动化测试偏偏就是要用机器去模拟人的操作,这跟验证码的设计目标天然就是对着干的。
在实际项目里,验证码给自动化测试带来的麻烦远不止"识别不出来"这一点。我总结下来主要有四个层面:
一是识别难度。简单验证码识别率能到九成以上,但稍微加点扭曲、干扰线、背景纹理,OCR的正确率可能直接腰斩。二是易变化性。验证码的样式会随系统的验证码组件升级而变,今天能识别的图片,明天换了一套字体或者多了一层干扰,识别逻辑就得重新调。三是时效性问题。验证码一般有有效期,有的60秒,有的120秒。自动化脚本里网络一慢、页面加载卡一下,等OCR跑完再填入,验证码可能已经过期了。四是环境差异。测试环境、预发布环境、生产环境的验证码策略经常不一样,测试环境可能关闭了,预发布开了简单的,生产开了复杂的,一套脚本在不同环境跑起来行为完全不统一。
这几点叠加在一起,才是验证码真正让人头疼的地方。所以我一贯的主张是:处理验证码的第一步,不是急着写识别脚本,而是先想清楚在你的项目里,验证码到底能不能绕过去。
1.2 四种主流处理策略的取舍
我见过太多人一上来就死磕OCR,最后被识别率折磨得欲仙欲死。实际上,验证码处理有四种主流的策略,各有各的适用场景。
第一种是测试环境关闭验证码,或者加入白名单机制。这是最省事也最推荐的做法。跟开发协商,在测试环境把验证码功能关掉,或者做成配置项,测试环境默认关闭,只在生产环境开启。有些系统会在验证码后端加一个允许指定测试账号绕过的开关,比如特定的内部账号不触发验证码逻辑。这样做的好处是自动化脚本稳定到飞起,完全不用考虑识别率的问题。
第二种是万能验证码,也叫测试后门。开发在代码里预留一个固定的验证码值,比如"0000"或者"pass1234",只要输入的验证码匹配这个值,就直接校验通过。这种做法比关闭验证码更接近真实逻辑,至少验证码的整个流程是能自测到的,只是跳过了图片识别这一步。缺点是生产环境如果不小心把后门留下来,那就是重大安全隐患了,所以一定要做好环境隔离和配置项的管控。
第三种是自动识别,包括OCR识别、模板匹配、深度学习等。这是最通用的方案,不管什么环境都能用,但稳定性最差,需要持续维护。适合必须要在开启验证码的环境里做自动化,又没办法跟开发协调改配置的场景。
第四种是人工介入,半自动化处理。脚本跑到验证码环节停下来,弹窗让测试人员手动输入验证码,然后再继续执行。这种方案成本最低,但只能用在少量用例的场景,跑批量回归完全不现实。
我建议的选型原则很简单,六个字:能关就关,能绕就绕,实在不行再识别。很多测试工程师觉得自动识别验证码很酷,但作为过来人我要说一句,在企业项目里稳定性和效率永远优先于所谓的技术含量,你写了个完美的识别算法,结果识别率98%,可那2%的失败率依然要你每天处理告警,一点都不酷。
2. 验证码类型全景:识别难度一次讲清楚
2.1 字符型验证码与算术题
字符型验证码是目前最常见的类型,就是那类让你输入图片里几个字母数字的。复杂一点的会加扭曲、旋转、干扰线、噪点、背景干扰色等。这类验证码的机器识别流程基本是固定的:拿到图片,预处理,分割字符,逐个识别。识别率取决于验证码的复杂度,也取决于图片的质量,模糊、拉伸、高对比度等都会直接影响最终效果。
算术型验证码本质上是字符型的变体,只不过把单个字符换成了"3+5=?"这类的算式。处理思路和字符型验证码没什么区别,只要先把整张图识别出来,再提取数字和运算符计算一下就行。这种验证码对机器来说甚至比纯字符型还简单,因为数字和加号减号的形态相对固定。
老实说,字符型验证码只要预处理做得到位,识别率做到90%以上并不难。难的是那些故意增加难度的版本:字符重叠、背景和文字颜色接近、字符间距不均匀、加入大量随机的干扰线。遇到这种验证码,我一般会评估一下投入产出比,如果只是为了自动化测试,那实在不行就回到策略层面,想办法跟开发沟通。
2.2 滑块、点选与行为验证码
滑块验证码这两年越来越多,就是那种拖到最右边拼图的。它表面上是让你"滑",实际上后端在收集你的鼠标轨迹、滑动的加速度、停顿位置等一系列行为数据。机器模拟滑块的主要难点不在图像识别,而在轨迹模拟。如果轨迹太完美,比如一条直线匀速滑动,反而会被风控判定为机器。所以做滑块自动化,除了要识别缺口位置,还得模拟人类不规律的滑动行为,这个需要单独调参。
点选型验证码难度更高一点,它让你按顺序点击图片里的某些文字或者图形。这需要OCR识别加上语义理解,比如识别出"请点击图中的红色圆圈",你不仅要把全图里的圆圈找出来,还得判断颜色,再按顺序点击。这类验证码对机器来说工作量非常大,自动化脚本处理这类验证码的成本很高,通常需要上深度学习的目标检测模型。
还有一类行为验证码,用户什么都不用做,页面上只有一个打勾的按钮,点击一下甚至不用点击就通过了。它主要是通过浏览器指纹、鼠标移动轨迹、页面停留时间等综合判断你是不是真人。这类验证码在自动化测试里有时候反而最轻松,因为某些情况下直接点击就能过,但有时候又很玄学,同样的脚本这次能过明天就过不了,非常薛定谔。
对于滑块、点选和行为验证码,我个人的建议是:别指望单靠一套OCR方案通吃,要么用专门的第三方服务,要么想办法通过测试环境配置绕过。硬啃这类验证码的投入回报比很低。
3. 自动识别字符型验证码的完整实操
3.1 环境准备与工具选型
如果你的项目确实没办法绕过验证码,那自动识别这条路就得认真研究。先说一下我常用的工具链:Python语言,OpenCV做图像处理,识别引擎选用Tesseract OCR或PaddleOCR。
Tesseract是Google开源的OCR引擎,老牌、轻量、离线可用,识别英文和数字的基础版本效果还凑合,识别中文就需要额外的语言包。PaddleOCR是百度的开源OCR工具,对中文支持很好,识别精度比Tesseract高不少,但模型相对重一些,首次运行需要加载模型文件。对于验证码这种短文本识别场景,如果验证码是纯英文数字,Tesseract够用;如果涉及中文或者识别率要求高,选PaddleOCR更稳。
环境安装我直接给命令,基于当前比较主流的版本:
pip install opencv-python pytesseract paddleocr另外需要注意,Tesseract本身是一个独立软件,pip装的只是它的Python封装,还需要单独安装Tesseract程序本体。Windows用户直接去官方GitHub下载安装包,Linux用户用包管理器装就行:
sudo apt install tesseract-ocr安装完之后可以用命令行验证一下:
tesseract --version如果报错找不到tesseract,大概率是环境变量没配好,Windows上要把Tesseract的安装目录加到PATH里。
3.2 图像预处理:识别率提升的关键
很多新手上来就把验证码图片直接丢给OCR引擎,识别率惨不忍睹,然后开始怀疑是识别引擎不够强。其实绝大部分问题出在预处理环节。预处理做得有多到位,识别率就有多高,这才是最关键的部分。OCR引擎擅长识别干净的黑底白字,对花里胡哨的验证码图,直接喂进去效果当然差。预处理的目标就是把这些花里胡哨的东西去掉,把文字区域凸显出来。
我常用的预处理流程是这样几步:灰度化,二值化,去噪,形态学处理。
灰度化就是把三通道的彩色图转成单通道灰度图。二值化是把灰度图转成黑白两色,让文字和背景彻底分离。去噪是干掉图片里的随机噪点和干扰线,最常用的是中值滤波或者高斯模糊。形态学处理包括膨胀和腐蚀,用来填补文字上的断裂或者去掉细小的噪点区域。
下面是具体的代码示意:
import cv2 import numpy as np def preprocess(image_path): # 1. 读图并灰度化 img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 二值化,反色处理让文字变白,背景变黑 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 3. 中值滤波去噪 denoised = cv2.medianBlur(binary, 3) # 4. 形态学操作:先膨胀后腐蚀,填充字符断裂 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) morph = cv2.dilate(denoised, kernel, iterations=1) morph = cv2.erode(morph, kernel, iterations=1) return morph注意一下二值化的阈值参数。大多数验证码图片对比度都可以,直接用Otsu自适应阈值就能拿到不错的结果。如果遇到背景和文字颜色相近的图,Otsu效果不好,就得手动调阈值,这个必须具体情况具体调试。我在实际项目中,通常会把预处理的中间结果保存成图片看一眼,确认二值化之后文字是完整的、背景是干净的,才进行下一步。别嫌这一步麻烦,图片处理链路里每一环节都靠肉眼确认效果,比闷头调参高效得多。
3.3 两种识别路线的代码实现
预处理完成之后,就可以交给识别引擎了。先说Tesseract路线,代码特别简单:
import pytesseract import re def ocr_tesseract(image_path): processed = preprocess(image_path) # 指定psm 7表示按一行文本识别,对验证码友好 # whitelist限定只识别字母和数字,能显著提升准确率 config = '--psm 7 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' text = pytesseract.image_to_string(processed, config=config) # 去掉非字母数字的字符 text = re.sub(r'[^a-zA-Z0-9]', '', text) return text这里有一个非常关键的参数:--psm 7,意思是把图片当作单行文本识别。默认的psm 3是全自动布局分析,对验证码这种图片反而容易搞出多余的空格和换行。白名单参数tessedit_char_whitelist也很实用,如果确认验证码里只有字母和数字,就限定识别范围,识别率能提升很大一截。
再说PaddleOCR路线:
from paddleocr import PaddleOCR import re ocr = PaddleOCR(use_angle_cls=False, lang='en', show_log=False) def ocr_paddle(image_path): processed = preprocess(image_path) # 临时保存预处理后的图片,因为PaddleOCR接收的是文件路径或图像数组 tmp_path = 'temp_captcha.png' cv2.imwrite(tmp_path, processed) result = ocr.ocr(tmp_path, cls=False) text = '' if result and result[0]: for line in result[0]: text += line[1][0] text = re.sub(r'[^a-zA-Z0-9]', '', text) return text注意PaddleOCR的模型是通用的,对反色的图片依然有效。但PaddleOCR模型体积大,首次调用会加载模型,耗时会比较久。在自动化测试里,如果每次都重新加载,速度会很感人,所以务必在脚本初始化时只创建一次OCR实例,后续循环复用。
实测下来,Tesseract的优势是轻量、速度快,但对字符粘连和复杂背景的容错率低;PaddleOCR精度更高,尤其是处理有轻微倾斜或者带小图标的验证码。性能方面,Tesseract一次识别大约200到400毫秒,PaddleOCR首次加载模型需要几秒,之后单次识别大概500到800毫秒。这两个数字对单次登录操作来说都不是瓶颈,但如果一晚上要跑几千条用例,那就得算算总量了。
3.4 字符分割与模板匹配的思路
OCR识别是通用方案,但在某些场景下不好使。比如验证码图片字符被压得很扁,或者被极度扭曲,OCR整个识别出来都是乱的。这时候可以试试"分割加模板匹配"的思路。
核心逻辑是:先对预处理过的图片做字符分割,把每个字符剪出来,然后跟预先准备好的字符模板比对,按相似度判断是哪个字符。
字符分割最常用的方法是投影法,统计图片在水平或者垂直方向上的黑色像素分布,有像素的区间就是字符区域。代码实现大概这样:
def split_characters(binary_img): # 垂直投影:统计每列有多少黑色像素 h, w = binary_img.shape col_sum = np.sum(binary_img == 255, axis=0) # 根据投影值切分字符区间 in_char = False start = 0 chars = [] for x in range(w): if col_sum[x] > 0 and not in_char: in_char = True start = x elif col_sum[x] == 0 and in_char: in_char = False chars.append((start, x)) # 按每个区间裁剪出字符图 char_imgs = [binary_img[:, start:end] for start, end in chars] return char_imgs分割完成之后,把每个字符图片归一化到统一的尺寸,比如20x30像素,然后跟模板库里每个模板做像素对比,算相似度,取最接近的那个。模板匹配的做法在验证码样式固定、字符库有限的时候非常实用,识别率甚至可以接近百分之百,而且速度快。缺点是一旦换了字体或者加了新的字符集,模板库就得重新采集制作。这个办法更适合企业内部系统长期固定一种验证码样式的场景。
4. 验证码处理功能的工程化落地
4.1 封装统一的验证码处理模块
识别脚本写得再好,如果不能方便地接入自动化框架,那也只是个玩具。工程化落地第一步,是把验证码处理能力封装成独立模块,提供统一接口,上层用例不用关心底下是OCR还是模板匹配还是调用第三方服务。
我通常定义一个这样的接口,代码结构做个参考:
class CaptchaSolver: def __init__(self, strategy='ocr'): self.strategy = strategy if strategy == 'ocr': # 全局只初始化一次识别引擎,复用实例 self.ocr = self._init_ocr() elif strategy == 'template': self.template_lib = self._load_templates() def solve(self, image_path): """ 统一的入口:输入验证码图片路径或图片数组,返回识别出的验证码字符串 """ if self.strategy == 'ocr': return self._solve_by_ocr(image_path) elif self.strategy == 'template': return self._solve_by_template(image_path) elif self.strategy == 'manual': return self._solve_by_manual(image_path) else: raise ValueError('unknown strategy')这个模块独立出来之后,最大的好处是方便切换策略。比如测试环境关了验证码,那CaptchaSolver根本不会被调用;预发布环境开了验证码,策略配成ocr就行;万一识别率不够挂了,还能临时把策略切到manual人工顶上,不影响整体框架架构。
4.2 在 UI 自动化流程中接入
UI自动化里接入验证码处理的流程说起来很简单:定位验证码图片元素,拿到图片,调用识别模块,填入识别结果。但真正操作起来,有几个细节容易被忽略。
第一个细节是图片获取方式。有些页面因为安全策略,验证码图片是动态刷新的,每次访问URL都不一样,或者带了一段时间有效的token。用Selenium直接去访问那个图片的src地址重新下载图片,拿到的可能已经不是页面上展示的那张了。正确的做法是用Selenium自带的截图功能把验证码元素截下来:
from selenium import webdriver from io import BytesIO from PIL import Image element = driver.find_element(By.ID, 'captcha_img') # 先截取整个页面或元素区域 element.screenshot('captcha.png')Selenium的element.screenshot()方法会精确截取该元素的图,这样拿到的图片和页面上显示的完全一致,不会出现URL失效或图片不匹配的问题。
第二个细节是识别结果的填充时机。验证码图片加载完成后,先不要立刻去识别,稍微等一下,确保图片完全渲染出来,避免截到半张图。识别过程是同步阻塞的,如果识别引擎响应慢,会拖垮整个测试用例的执行时间。建议给识别设置超时,比如5秒,超时就按失败处理,走重试逻辑。
第三个细节是验证码输入后必须做二次校验。最常见的场景是识别出的验证码不对,提交后页面提示验证码错误。脚本不能只填一次就断言登录成功,一定要等登录结果出来之后再判断。如果提示验证码错误,就刷新验证码重新来一轮。我把这个过程封装成了一个函数:
def login_with_captcha(driver, username, password, max_retries=3): for attempt in range(max_retries): driver.get(login_url) driver.find_element(By.ID, 'username').send_keys(username) driver.find_element(By.ID, 'password').send_keys(password) captcha_img = driver.find_element(By.ID, 'captcha_img') captcha_img.screenshot('captcha.png') code = solver.solve('captcha.png') driver.find_element(By.ID, 'captcha_code').send_keys(code) driver.find_element(By.ID, 'login_btn').click() # 通过URL变化或页面元素判断是否登录成功 if is_login_success(driver): return True # 识别失败或验证码错误,重试 time.sleep(1) return False这种封装方式让登录态获取变得很稳定,即使识别率只有百分之八十,多试两次也能糊弄过去。实际跑下来,三次重试内成功的概率能接近99%。
4.3 在接口自动化流程中接入
接口自动化里处理验证码,跟UI自动化不太一样。UI那一套关心的是怎么拿到图片怎么填,接口层更关心的是如何保持会话状态,以及验证码前后端的联动逻辑。
接口自动化的典型流程是先调获取验证码的接口拿到图片,再调登录接口提交账号密码和验证码。这里有个很重要的坑:验证码校验通常依赖于Session或者某个加密串,获取验证码和提交登录必须在同一个会话里。用requests的话,必须用同一个requests.Session对象来维持Cookie。
代码示意如下:
import requests from io import BytesIO from PIL import Image session = requests.Session() # 1. 获取验证码接口,验证码内容一般以图片流返回 captcha_resp = session.get('https://example.com/api/captcha') img = Image.open(BytesIO(captcha_resp.content)) img.save('captcha.png') # 2. 识别 code = solver.solve('captcha.png') # 3. 用同一个session提交登录 login_resp = session.post('https://example.com/api/login', json={ 'username': 'test', 'password': '123456', 'captcha': code })这里要注意,有些系统的验证码接口会返回一个captchaId之类的参数,登录时需要把这个参数一起提交。这个值跟获取到的验证码图片是有对应关系的,必须在同一流程里取出来并传给登录接口。丢了它,哪怕验证码识别得再准也是白搭。
4.4 失败重试与降级策略
不管用多好的识别方案,验证码识别都不可能做到百分百成功。工程化实践里必须接受这个现实,然后通过重试和降级机制来兜底。
重试机制最简单,识别失败或者提交后提示验证码错误就重来。但要注意控制重试次数,设置上限,避免死循环。我一般设3次到5次,超过阈值就算用例失败,并自动把当次验证码图片和识别结果存到测试报告中,方便后续排查。
降级策略就更多样了。识别连续失败多次,可以自动切换到备用方案,比如切换到手动输入模式,由人工操作一次,让用例继续跑下去。在CI环境里跑定时任务的时候,我还会在验证码处理模块里做一个缓存机制,识别成功的验证码图片、预处理参数、识别结果都记录下来,后期可以分析哪些图片容易失败,进而针对性地优化预处理链路。
日志记录这个事很多人不重视,但我要特别强调,它能在你排查验证码问题时节省大量时间。每次调用验证码模块,至少要记录:原始图片路径、预处理参数、识别引擎类型、识别原文、最终结果、耗时。有了这些日志,你才能在验证码识别率突降的时候快速定位到问题出在哪一环——是验证码样式变了,还是预处理参数失效了,还是识别引擎抽风了。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
我在实际项目中收集了不少高频问题,按场景整理成一个速查表,大家在踩坑的时候可以先对照着查。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Tesseract识别结果为空 | 预处理后文字颜色反了,或者图片太模糊 | 检查二值化后文字是否为白色背景是否为黑色,必要时手动调阈值 |
| 识别结果多出奇怪的字符 | 没有设置白名单,或psm模式不对 | 使用--psm 7并限定tessedit_char_whitelist |
| PaddleOCR首次调用特别慢 | 模型文件首次加载 | 全局初始化OCR实例,不要每次调用都创建 |
| 验证码图片截取不完整 | 页面图片未加载完成就截图 | 在截图前加显式等待,确保元素渲染完毕 |
| 滑块验证码拖动后提示失败 | 轨迹太像机器,缺人机行为特征 | 加入随机的加速度变化和微小回退动作 |
| 同一个验证码反复刷新导致对不上 | 每次访问URL都返回新的验证码 | 改用元素截图方式获取当前显示的验证码图片 |
| 生产环境验证码样式与测试环境不一致 | 环境配置不同或验证码服务版本不同 | 优先在测试环境关闭验证码,环境差异别硬扛 |
| 识别正确但登录一直失败 | 提交时缺少captchaId等关联参数 | 检查接口文档,确认登录接口需要哪些与验证码关联的字段 |
5.2 一次识别失败的真实排查过程
说一个我自己经历过的案例。某次跑一个老系统的自动化用例,Tesseract识别验证码一直很稳定,识别率在85%左右。结果某天开始,连续两天登录用例全部失败,日志里看识别结果,出来的字符串不是乱码就是长度不对。
我第一反应是验证码样式变了。于是把失败的验证码图片从日志里捞出来,预处理后存下来,自己肉眼对比了一下。果然,图片里字符多了一层非常淡的背景纹理,肉眼看着不明显,但二值化之后,背景纹理变成了一片灰色的噪点,跟字符粘连在一起,字符区域被噪点彻底污染了,OCR自然识别失败。
解决方案是调整预处理流程,在二值化之前先对灰度图做一轮降噪,使用cv2.GaussianBlur把细碎的背景纹理模糊掉,再做阈值分割,识别率恢复了。整个过程下来,问题本身不算复杂,但如果没有日志里存的原始图片和预处理图片,排查起来就得靠猜,效率低很多。
这件事给我的教训有三个:第一,验证码识别不是一劳永逸的,线上线下的验证码样式随时可能变,持续观察日志里的识别率很关键;第二,预处理参数不能固化,要留出调试空间;第三,任何一次验证码处理的中间产物都值得留底。
5.3 关于验证码处理的几条经验总结
最后聊几条我个人的体会。
第一条,验证码处理在自动化测试里的定位,永远是"兜底"而不是"主力"。如果项目能通过测试环境配置绕过验证码,绝对优先选择绕过。这不是偷懒,是把精力花在更有价值的地方。自动化测试要的是稳定可重复的执行结果,不是炫技。
第二条,识别方案一定要设计降级路线。不管你的OCR模型多准,都要预设它失败的情况,安排好重试、人工介入或者跳过,保证整个自动化体系不会因为一个验证码图片就全面瘫痪。
第三条,多做图片样本收集。经常有验证码样式更新导致识别率下降的情况,建议在平时跑自动化的时候,把遇到的所有验证码图片都存下来,打上标注。积累上千张之后,你就能在样式变更时快速评估影响,甚至用来微调模型。
第四条,也是我个人做得最多的一次反思:越到后面我越发现,验证码处理真正的难点不是写识别脚本,而是定义清楚"什么情况下值得识别,什么情况下应该绕过去"。这个问题想透了,再去看OCR、模型、模板匹配,都是工具层面的选择而已。
做自动化测试这些年,我踩过的验证码坑比写过的代码还多,从最初的死磕OCR到后来跟开发约定测试环境后门,再到搭建完整的识别加降级体系,整个过程最深刻的体会就是:别把验证码当敌人,它只是系统安全设计的一部分,想办法跟它合理共存,才是自动化测试真正成熟的标志。