自动化测试验证码处理策略:从OCR识别到接口层绕过
2026/9/9 23:02:37 网站建设 项目流程

1. 先说个真事:我被验证码卡了一整个下午

之前做一个电商系统的自动化测试项目,UI 自动化跑得顺顺当当的,登录脚本、加购脚本、下单脚本全部写好了,结果一上 CI 就挂。一看日志,全是同一个报错——登录页弹出了图形验证码。开发说这是安全组件统一加的,测试环境也没关。那个下午我就一直在跟这个四字符的验证码较劲,截图、识别、填值、重试,折腾到下班也没完全跑稳。

后来我把这块彻底想明白了:验证码在自动化测试里不是"能不能识别"的问题,而是"该不该识别、在哪个层面解决"的问题。想通了这一点,后面再做任何项目的自动化,遇到验证码我基本都能在半小时内给出稳定方案。

这篇文章就想把我这几年的实践经验整理出来,覆盖图形验证码、滑块验证码、短信验证码、点选验证码这些常见类型,以及从 UI 层到接口层的处理思路。不管你是刚接触自动化测试,还是已经在写框架但被验证码折磨过,应该都能从这里找到能直接用的方案。

验证码这个东西,本质上是一个"反自动化"设计。它的目的就是区分人和机器,而我们做自动化测试,恰恰是在用机器模拟人的操作。所以这个问题天然就带着矛盾。但矛盾不代表无解——关键在于你要清楚验证码在保护什么,以及你的测试在验证什么。多数情况下,二者并不是非此即彼的关系。

2. 先想清楚:自动化测试里验证码到底挡住的是什么

很多人遇到验证码第一反应就是"怎么识别它",但我建议你先停下来想想另外三个问题。

第一,验证码是挡谁的?验证码是挡外部攻击者的,不是挡你们自己测试人员的。如果你的自动化脚本需要频繁登录,说明验证码的保护对象(通常是登录接口)正在被你自己高频请求,这本身就是个信号——测试环境的验证码策略可能配错了,或者根本没有区分环境。

第二,你要测的是验证码功能本身吗?如果被测系统里有一个专门的验证码模块,而你的测试用例里面确实包含了"验证码正确时能登录、错误时提示错误"这类用例,那你就必须真正处理验证码。如果验证码只是登录流程里的一个过场环节,那你的目标就是绕开它,而不是较劲。

第三,你的自动化是跑在什么环境里?这个问题的答案直接决定方案选型。同一套代码,在本地开发环境、测试环境、预发布环境、生产环境,处理逻辑应该完全不一样。我最常看到的问题就是:脚本在本地能跑,因为本地验证码是万能码;一上 CI 就挂了,因为 CI 环境走的是真实验证码策略。

把这三个问题过一遍,你大概就知道自己属于哪种情况了:

  • 测试环境:通常可以要求开发关闭验证码,或者开放万能验证码,这是最省力的方案。
  • 预发布/生产环境:不能关,但你也不应该频繁去动它,建议用少量冒烟用例覆盖即可。
  • 验证码本身是核心功能:必须专门设计测试数据和识别方案,认真对待。

接下来我按验证码类型逐一拆解决方案。每一种我都尽量给出具体的代码、工具和踩坑点,你可以直接拿去用。

3. 图形验证码:识别不是终点,稳定才是

图形验证码是最常见的一类,通常是 4-6 位字符,可能有扭曲、干扰线、噪点。它的处理链路是:截图 → 图像预处理 → OCR 识别 → 填入 → 验证。

3.1 先做图像预处理再识别,不要直接扔给 OCR

直接用 OCR 识别原始截图,成功率通常惨不忍睹。正确做法是先做几道预处理步骤:灰度化、二值化、去噪点、分割字符。这就像让一个人戴着墨镜看远处的小字,先把墨镜摘了再去看,识别率完全不一样。

我常用的组合是 Python + OpenCV + Tesseract。OpenCV 负责处理图片,Tesseract 负责识别文字。下面这段代码是一个比较通用的图形验证码处理流程:

import cv2 import numpy as np import pytesseract from PIL import Image def preprocess_captcha(image_path): # 读取图片并转为灰度 img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # 二值化:把灰度图转成黑白图,阈值可以调 _, binary = cv2.threshold(img, 150, 255, cv2.THRESH_BINARY) # 去噪点:用中值滤波去掉孤立像素 denoised = cv2.medianBlur(binary, 3) # 膨胀/腐蚀,把断开的笔画连起来 kernel = np.ones((2, 2), np.uint8) processed = cv2.dilate(denoised, kernel, iterations=1) # 保存处理后的图片 cv2.imwrite('processed.png', processed) return 'processed.png' def recognize_captcha(image_path): processed_img = preprocess_captcha(image_path) # 只识别数字和字母,PSM 7 表示单行文本 text = pytesseract.image_to_string( Image.open(processed_img), config='--psm 7 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ) # 清理结果,只保留字母数字 return ''.join(filter(str.isalnum, text))

这里有几个参数很关键。二值化的阈值 150不是固定的,有些验证码背景和前景的灰度差很明显,有些则很接近,需要根据实际图片调整。白名单 whitelist决定了 OCR 的输出范围,如果验证码只包含数字,就只写数字,识别率会明显提升。PSM 7告诉 Tesseract 图片是单行文本,避免它用复杂的版面分析去打乱识别顺序。

提示:Tesseract 对"形近字"很容易认错,比如 0 和 O、1 和 l、Z 和 2。如果验证码同时包含数字和字母,建议在代码里做一次归一化,或者干脆在测试数据设计时避免使用这种验证码。

3.2 用 ddddocr,十几行代码搞定 90% 的常规图形验证码

如果你不想折腾 OpenCV 的参数,有个更省心的库叫ddddocr。它是个开源的通用验证码识别库,基于深度学习训练好的模型,对常见的扭曲、干扰、粘连图形验证码识别率很高。我实测下来,对于中等复杂度的图形验证码,单次识别成功率在 90% 以上。

import ddddocr ocr = ddddocr.DdddOcr(show_ad=False) def recognize(image_path): with open(image_path, 'rb') as f: image_bytes = f.read() result = ocr.classification(image_bytes) return result

就这么多。不需要预处理,不需要调参。但要注意,ddddocr 的模型对某些特定风格的验证码(尤其是字符极度扭曲、加了复杂背景纹理的)效果会下降。我的经验是,先拿 100 张真实验证码图片离线测试一下识别率,如果低于 80%,再考虑叠加 OpenCV 预处理,或者换其他方案。

3.3 加了"识别"还不够,关键是识别失败的重试机制

图形验证码最大的坑不是识别不出来,而是识别出来了但提交超时。很多系统的验证码有效期只有 60 秒,而自动化脚本从截图、识别、填值到点击登录,中间稍微卡一下就可能过期。

所以图形验证码的处理必须配一套重试机制。我的做法是:把"获取验证码 → 识别 → 提交"封装成一个完整的操作,失败就重新获取验证码再试,最多尝试 3-5 次。这样做还有一个额外的好处——如果系统对验证码错误次数有限制,重试机制可以在达到上限前成功,或者尽早暴露问题。

def login_with_captcha(driver, username, password, max_retries=5): for attempt in range(max_retries): # 重新加载登录页,获取新的验证码(防止上一次的已过期) driver.refresh() captcha_img = driver.find_element(By.ID, 'captcha_img') captcha_img.screenshot('captcha.png') code = recognize('captcha.png') if len(code) != 4: # 识别结果长度不对,大概率是识别错了 continue driver.find_element(By.ID, 'username').send_keys(username) driver.find_element(By.ID, 'password').send_keys(password) driver.find_element(By.ID, 'captcha').send_keys(code) driver.find_element(By.ID, 'login_btn').click() # 关键:检查是否登录成功,而不是固定等待 try: WebDriverWait(driver, 5).until( EC.presence_of_element_located((By.ID, 'user_center')) ) return True except: continue raise Exception(f'登录失败,验证码重试 {max_retries} 次仍未成功')

这段代码里有几个细节值得注意。每次重试都先 refresh,确保拿到的验证码图片是最新的,避免验证码过期导致永远失败。验证识别结果的长度,如果验证码固定 4 位,识别结果不是 4 位就直接放弃,省去一次无意义的提交。登录成功判定用显式等待,而不是 time.sleep(3),这样每轮重试的耗时是动态的,整体效率高不少。

4. 滑块验证码:关键不是移动距离,而是移动轨迹

滑块验证码这几年越来越普及,尤其是极验、腾讯防水墙那类的。它的设计思路从"识别文字"变成了"模仿人类行为"。滑块验证码的核心校验点不只是"滑到位置",还包括拖动轨迹、速度变化、按压时间、是否模拟鼠标事件等。

4.1 先找到滑块缺口的位置

滑块验证码的第一步是找缺口。通常页面会有一个背景图和一个带缺口的图,或者是同一张图上滑块在一个位置、缺口在另一个位置。Selenium 可以通过计算两张图片的像素差异来定位缺口坐标。

import cv2 import numpy as np def find_gap(bg_path, full_path): bg = cv2.imread(bg_path, 0) full = cv2.imread(full_path, 0) # 对图片进行边缘检测,突出缺口轮廓 bg_edge = cv2.Canny(bg, 100, 200) full_edge = cv2.Canny(full, 100, 200) # 计算两张边缘图的差值 diff = cv2.absdiff(bg_edge, full_edge) # 找到差值最大的列,即为缺口位置 height, width = diff.shape max_diff = 0 gap_x = 0 for x in range(width): col_diff = np.sum(diff[:, x]) if col_diff > max_diff: max_diff = col_diff gap_x = x return gap_x

这个方法的原理是:完整图上有缺口位置的像素和背景图差异最大,边缘检测后这个差异更明显。实际项目里你可能拿不到"完整图",那就退而求其次,在背景图里找颜色明显不同的那块区域。

4.2 拖动轨迹是滑块验证码的核心校验逻辑

找到缺口位置后,接下来就是"拖"。很多人用 Selenium 的ActionChains直接drag_and_by_offset,一步到位。这种直线匀速的拖动,在极验等平台上几乎必挂,因为真人拖动不可能是一条直线匀速。

真人的拖动轨迹大致是这样的:开始慢、中间快、接近目标时慢下来,甚至会有小幅回弹。所以你的模拟轨迹需要体现加速、减速、回弹这几个特征。

import random from selenium.webdriver.common.action_chains import ActionChains def human_like_track(distance): track = [] current = 0 mid = distance * 0.7 t = 0.2 # 每步间隔(秒) while current < distance: if current < mid: # 加速阶段 move = random.randint(3, 10) else: # 减速阶段 move = random.randint(1, 4) current += move track.append(move) # 最后加一点回弹 track.append(-random.randint(1, 3)) track.append(-random.randint(1, 2)) return track def slide_captcha(driver, slider_element, distance): track = human_like_track(distance) actions = ActionChains(driver) actions.click_and_hold(slider_element) for move in track: actions.move_by_offset(move, random.randint(-1, 1)) # y方向轻微抖动 actions.pause(random.uniform(0.01, 0.03)) actions.release() actions.perform()

这套轨迹模型的思路是:用分段随机步长模拟加速和减速,在 y 方向加一个随机抖动,每步之间 pause 很短的时间。这些细节单个来看不起眼,但组合起来就比较接近真人操作了。

注意:滑块验证码的判定逻辑通常在服务端,而且平台会持续升级。这套轨迹模型不保证 100% 通过,但在多数场景下能大幅提升成功率。如果你的项目被卡得很死,建议考虑后文提到的"环境隔离"方案,而不是无限优化轨迹。

5. 点选验证码和短信验证码:UI 自动化该放弃时就放弃

点选验证码(按顺序点击图中文字)和短信验证码(输入手机收到的数字)与前面几种不同。它们的核心不是"识别",而是"数据来源"问题。

5.1 点选验证码:识别成本高,收益低

点选验证码目前主流实现是腾讯的 tcCaptcha 和网易易盾那套,核心是让你按顺序点击图片中的特定文字或图案。它的难点是:先要 OCR 识别出要点的文字,再要在图片上定位这些文字的位置,最后还要按正确顺序点击。三者叠加,识别率非常不稳定。

而且点选验证码的拖动轨迹和点击时序也会被记录分析——点击太快会被判定为机器,点击顺序不对直接失败,失败几次还会弹出更难的二次验证。我的结论是:除非你专门做的是验证码识别产品,否则不要耗在点选验证码上。它消耗的开发时间远超其测试价值。

5.2 短信验证码:读取和注入都要有技巧

短信验证码的处理思路和图形验证码完全不同。它不需要识别,而是需要从短信里拿到那个数字。但手机短信在自动化测试里是个很麻烦的环节——你需要在测试代码里接住短信,读取验证码,再填回页面。

我之前做过一个 App 自动化项目,是这样处理的:

  1. 申请了一个测试专用手机号,绑定到被测系统。
  2. 手机端安装了一个短信转发工具,把收到的短信实时转发到一个 HTTP 接口。
  3. 测试代码里请求这个接口,解析短信内容,提取验证码。
  4. 把验证码填入 App 的输入框,继续后续流程。

这个方案的好处是测试数据是真实的,能覆盖从"获取短信验证码"到"登录成功"的完整链路。缺点是依赖一个外部的短信转发工具,稳定性需要保障。

5.3 短信验证码平台上,如何优雅地实现自动化

如果你的测试量比较大,短信验证码平台的方案是独立的。无论用哪种,核心思路都是一样的:把"收短信"这件事从一个物理设备上解耦出来,变成测试代码可以直接调用的接口。这样你的自动化脚本就不需要关心短信是从哪儿来的,只需要等待接口返回验证码,然后填入即可。

6. 接口层的降维打击:UI 层面解决不了的,就在接口层解决

我最推荐的一个思路是:能不在 UI 层碰验证码,就绝不在 UI 层碰。因为 UI 层是真实用户操作的模拟,你很难绕过安全设计;但接口层的自动化是可以直接调用业务逻辑的,验证码往往只是一个参数,甚至可以直接传入固定值。

6.1 测试环境万能码与验证码开关

这个方案不需要多少技术含量,但需要你和开发、运维做一些沟通。很多系统在开发时会预留验证码的后门:测试环境可以配置关闭验证码,或者设置一个固定的万能验证码(比如8888),只在 dev/test 环境生效,生产环境强制走真实验证码。

我自己做项目时,会在测试环境配置里加一行开关:

# application-test.yml captcha: enabled: false # 测试环境关闭验证码 mock-code: "8888" # 或者开启万能码

然后把自动化测试的 base_url 指向测试环境,登录流程里就不再需要处理验证码。这个方案的优势是:快、稳定、零维护。缺点是不能覆盖"验证码正确/错误"这类功能用例。如果有这类用例,单独写成针对验证码模块的专项测试,用真实验证码来跑就行。

6.2 让"图形验证码识别"在公司框架里沉淀为通用工具

如果你所在的团队有多个项目都在做自动化测试,验证码问题值得沉淀成一个公共组件。这个组件可以包含:图形验证码识别(封装 ddddocr 或 Tesseract)、滑块验证码轨迹模拟、验证码重试机制,以及一个统一的上层方法——LoginHelper.login(driver, username, password)。这样每个项目的测试脚本只需要调用这个公共方法,不需要各自处理和验证码相关的逻辑。

我见过不少团队,每个项目组都自己写一套验证码处理逻辑,识别率参差不齐,代码风格也完全不同。沉淀公共组件之后,维护成本会大幅下降。验证码策略升级导致识别率下降时,只需要改一个地方,所有项目跟着受益。

7. 稳定性与并发:验证码还会引起的一连串连锁反应

验证码问题不止是"能不能识别"那么单纯。如果你用的是 UI 自动化,验证码还牵扯到稳定性和并发下的表现,这两个问题往往比"识别率"更让人头疼。

7.1 并发执行时不要共享同一个验证码

如果你的自动化测试用例支持并发执行,你要特别小心:登录操作是高频操作,但验证码是有状态的。同一个验证码只能使用一次,而且有时间限制。如果并发场景下多个用例共用同一个登录态或者同一个验证码,就会互相干扰。

我的建议是:并发执行时,每个用例拿独立的登录态,避免复用同一个验证码。如果登录态可以提前批量创建,那就提前创建好,登录接口直接调用,不走 UI,也不走验证码。这个方案在接口层自动化里非常实用。

7.2 验证码过期时间对超时机制的考验

验证码过期时间短的时候,你的脚本如果等待时间过长,即使识别成功,提交时也会失败。这时候把超时时间缩短,或者把"重新获取验证码"的时间点提前,比无限重试更有效。最好的做法是把验证码识别的超时和登录请求的超时分开设置,识别 3 秒内完成,登录请求 5 秒内完成,整体控制在 10 秒以内。这样即使一次验证失败,重试时的开销也很低。

经验之谈:验证码识别本身花不了太多时间(ddddocr 单次识别在几百毫秒以内),真正的耗时往往在截图、加载页面、网络请求上。优化识别速度的意义不大,优化流程才是关键。

7.3 跑批式回归测试时的失败率控制

UI 自动化在跑大批量任务时,如果登录环节验证码识别失败,整个任务的失败率会被拉高。你可以考虑把登录态复用机制做进框架里:登录成功后拿到 token 或 cookie,存在本地或内存里,后续用例直接复用,而不是每次都能登录走一遍。这样验证码只在"初始登录"环节出现一次,后面全部跳过,失败率自然就下来了。

本质上这个思路就是把验证码从高频操作变成低频操作,问题难度骤降。

8. AI 时代的验证码处理:做识别,还是做格局?

现在 AI 技术发展很快,验证码识别也涌现了一批新方案。ddddocr 本身就是深度学习模型的产物,后面还有更强劲的模型不断出现。但我认为,AI 时代的验证码处理,大家反而要把眼光放得更远一点。

8.1 不成熟的识别模型容易让你陷入"打地鼠"

验证码服务商也没有闲着,他们不断在升级干扰策略——更扭曲的字体、更复杂的背景、更隐蔽的缺口位置、更严格的轨迹校验。你今天用某个模型识别成功率 95%,明天服务商一更新,成功率可能掉到 40%。如果你把大量精力花在持续优化识别模型上,就会陷入无穷尽的"打地鼠"循环。

这也是为什么我前面反复强调"能绕就绕"——识别只是最后的兜底手段,不是首选方案

8.2 更好的选择:从测试策略层面规避验证码

从测试策略的角度看,验证码问题的本质是:你的自动化流程依赖了一个不应该被自动化的环节。聪明的做法是从测试设计阶段就规避它:

  • 测试数据构造时,通过接口或数据库直接造数据,绕过登录。
  • 登录态提前准备好,测试用例直接从已登录状态开始。
  • 测试环境关闭验证码,或者配置万能码。
  • 单独把验证码相关功能作为专项测试,不进回归主链路。

这几条做到了,你会发现大多数项目的验证码问题在需求分析阶段就解决了,根本不需要在代码层面跟验证码死磕。

9. 我的个人总结:遇到验证码,先想这四步

如果说要我用几句话总结处理验证码问题的方法论,那就是下面这四步:

  1. 先问能不能关:测试环境能不能关掉验证码?能不能配万能码?这个优先级最高。
  2. 再问能不能绕:能不能直接走接口登录、复用登录态,不经过验证码环节?
  3. 然后问能不能降低频率:能不能让验证码只在登录环节出现一次,后续都用已登录状态?
  4. 最后才考虑识别:如果必须走 UI 且必须输验证码,才考虑 OCR 识别、滑块轨迹模拟等方案。

我见过太多同行一上来就研究怎么识别验证码,结果项目快结束了才发现,测试环境的验证码本来就能关。先把最简单的路走通,再考虑复杂方案,这是我在这个领域最想分享的体会。

另外还有个小技巧:不管用哪种方案,记得给验证码处理留充足的日志。每次识别结果是什么、识别耗时多少、重试了几次,都记录下来。这些数据能帮助你判断是识别率的问题、超时的问题,还是验证码策略变了的问题。没有日志,排查问题就像闭着眼睛找东西,效率极低。

验证码问题是自动化测试里最典型的"看似是技术问题,实则是策略问题"的案例。希望这篇分享能让你少走一些弯路。如果你在实际项目里有更好的处理思路,也欢迎交流——毕竟这个领域的攻防战,短时间内还看不到终点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询