
1. 项目概述当自动化测试遇上“加减乘除”验证码在自动化测试和爬虫开发的日常工作中验证码就像一道绕不开的“门神”。特别是那种要求你计算“35”或者“8÷2”的简单算术图片验证码看似简单却常常让我们的Selenium脚本“卡壳”。这类验证码的设计初衷就是为了区分人类和机器它们通常包含扭曲的数字和运算符背景带有干扰线或噪点对OCR光学字符识别技术构成了第一道挑战。我遇到过不少项目在登录、注册或关键操作步骤前系统会随机弹出这样的算术题。手动处理那自动化就失去了意义。直接硬闯成功率低得可怜还容易触发IP封禁。所以“优雅地绕过”成为了一个必须解决的工程问题。这里的“优雅”指的是高成功率、低资源消耗、易于集成并且行为足够“像人”以避免触发更高级别的反爬机制。本文将基于我多年的实战经验拆解一套完整的解决方案。我们不会依赖任何付费的第三方打码平台虽然它们有时很有效而是聚焦于如何利用开源工具和自定义策略构建一个健壮、可维护的本地化验证码处理模块。无论你是进行UI自动化测试还是需要处理类似场景的数据采集这套思路都能提供直接的参考。2. 核心思路与方案选型面对“加减乘除”图片验证码我们不能蛮干。一个完整的绕过方案需要像外科手术一样精准包含从图像获取、预处理、识别到模拟输入的完整链条。同时必须考虑整个过程的稳定性和反检测能力。2.1 技术路线图从图像到答案整个流程可以分解为以下几个核心环节我将其称为“验证码处理流水线”定位与捕获在网页上精准找到验证码图片元素并将其下载或截图保存为程序可处理的图像数据。图像预处理这是提升识别率的关键。原始验证码图片往往包含噪声、扭曲、颜色干扰预处理的目的就是“净化”图像让字符尽可能清晰可辨。字符识别将处理后的图像中的数学表达式如“3”、“”、“5”转换为文本字符串。表达式计算解析识别出的文本字符串计算出算术题的正确答案。模拟输入与提交将计算结果填入网页对应的输入框并触发提交动作。2.2 核心工具选型与考量为什么选择以下工具链这是经过多次踩坑后总结出的平衡点。Selenium WebDriver这是我们的自动化基础。它提供了对浏览器的完全控制能够执行点击、输入、获取元素等所有用户交互。选择它而不是纯HTTP请求库如requests的原因是很多网站的验证码图片的src属性可能是动态生成的Blob URL或带有一次性Token直接下载困难。Selenium可以轻松处理这些动态内容。Pillow (PIL)Python下最强大的图像处理库之一。我们将用它来完成图像的裁剪、灰度化、二值化、降噪等预处理操作。它比OpenCV更轻量对于此类简单的图像处理任务绰绰有余。Tesseract-OCR开源的OCR引擎之王。虽然对于复杂验证码效果一般但经过精心预处理后的、相对规整的“加减乘除”数字和符号Tesseract的识别准确率可以做到很高。关键是它是离线的免费且可定制。Python标准库用于计算表达式eval需谨慎使用或解析字符串以及流程控制。为什么不直接用深度学习模型对于固定的、样式单一的验证码训练一个CNN模型确实能达到近乎100%的准确率。但这需要收集大量样本、标注、训练和部署成本较高。对于样式可能变化或项目初期采用“预处理Tesseract”的方案更快速、更灵活。本文会以这种经典方法为主并在最后探讨深度学习的升级方案。3. 实战拆解构建验证码处理流水线理论说再多不如一行代码。让我们一步步搭建这个处理模块。我将以一个假设的登录页面为例该页面有一个ID为captcha_image的图片标签和一个ID为captcha_input的输入框。3.1 环境准备与依赖安装首先确保你的Python环境已经就绪。我们将使用pip安装必要的库。# 安装Selenium和浏览器驱动管理工具 pip install selenium webdriver-manager # 安装图像处理库 pip install pillow # 安装Tesseract-OCR的Python封装及语言包 pip install pytesseract特别注意pytesseract只是一个Python调用接口你还需要在系统上安装Tesseract-OCR引擎本体。Windows从 GitHub - tesseract-ocr/tesseract 下载安装程序安装时记得勾选中文语言包虽然我们主要用英文数字。安装后需要将Tesseract的安装路径如C:\Program Files\Tesseract-OCR添加到系统环境变量PATH中或者后续在代码中指定路径。macOS使用Homebrew安装brew install tesseract。Linux(Ubuntu/Debian)sudo apt-get install tesseract-ocr。验证安装在命令行输入tesseract --version能显示版本信息即成功。3.2 步骤一定位并捕获验证码图像使用Selenium获取验证码图片元素并将其保存到本地或内存中。这里有两种常见情况情况A图片是普通的URL链接src为http://...或/captcha.jpgfrom selenium import webdriver from selenium.webdriver.common.by import By import requests from io import BytesIO from PIL import Image driver webdriver.Chrome() # 或使用 webdriver-manager 自动管理驱动 driver.get(你的目标登录页面URL) # 1. 定位验证码图片元素 captcha_element driver.find_element(By.ID, captcha_image) # 根据实际情况调整定位方式 # 2. 获取图片URL img_src captcha_element.get_attribute(src) # 3. 下载图片 response requests.get(img_src) captcha_image Image.open(BytesIO(response.content))情况B图片是Base64编码或动态Canvas更常见于现代网站这种情况下直接下载URL行不通。我们需要对元素进行截图。from selenium import webdriver from selenium.webdriver.common.by import By import base64 from io import BytesIO from PIL import Image driver webdriver.Chrome() driver.get(你的目标登录页面URL) captcha_element driver.find_element(By.ID, captcha_image) # 方法1如果src是base64编码以‘data:image’开头 img_src captcha_element.get_attribute(src) if img_src and img_src.startswith(data:image): # 格式通常为data:image/png;base64,iVBORw0KGgoAAA... base64_data img_src.split(,)[1] image_data base64.b64decode(base64_data) captcha_image Image.open(BytesIO(image_data)) else: # 方法2对元素进行截图万能方法但可能截到多余部分 location captcha_element.location size captcha_element.size # 先保存整个页面的截图 driver.save_screenshot(full_page.png) full_page_image Image.open(full_page.png) # 计算裁剪区域 left location[x] top location[y] right location[x] size[width] bottom location[y] size[height] captcha_image full_page_image.crop((left, top, right, bottom)) # 注意这种方法需要确保页面已完全稳定没有滚动。对于有固定定位的验证码弹窗可能不准。实操心得优先尝试获取src属性并判断是否为Base64。截图法是备选因为其坐标计算受页面布局、缩放比例影响容易出错。在实际操作中可以先用print(img_src)查看一下属性值再决定用哪种方法。3.3 步骤二图像预处理——提升OCR识别率的魔法原始的验证码图片可能背景杂乱、字符扭曲、有干扰线。预处理的目的就是让Tesseract“看”得更清楚。这是一个典型的预处理流水线from PIL import Image, ImageFilter, ImageOps import pytesseract import re def preprocess_captcha(image): 对验证码图像进行预处理。 :param image: PIL.Image对象 :return: 处理后的PIL.Image对象二值化 # 1. 转换为灰度图减少颜色维度简化处理 gray_image image.convert(L) # 2. 提高对比度使用PIL的ImageOps.autocontrast contrasted_image ImageOps.autocontrast(gray_image) # 3. 二值化阈值处理将图像转为纯黑白彻底消除颜色和灰度干扰 # 阈值可以根据实际情况调整130是一个常用起始值。 threshold 130 binary_image contrasted_image.point(lambda p: 255 if p threshold else 0) # 4. 降噪去除孤立的黑白点 # 使用中值滤波器或最小值滤波器对椒盐噪声效果好 denoised_image binary_image.filter(ImageFilter.MinFilter(3)) # 3x3窗口的最小值滤波 # 5. (可选) 形态学操作如果字符有断裂可以用膨胀连接如果字符粘连可以用腐蚀分离。 # 这里以闭合操作先膨胀后腐蚀为例连接细小断裂。 # from PIL import ImageMorph # 需要更复杂的库如OpenCVPillow的形态学操作有限。对于简单验证码前几步通常足够。 # 6. 缩放有时Tesseract对小分辨率图片识别不好可以适当放大。 # width, height denoised_image.size # scaled_image denoised_image.resize((width*2, height*2), Image.Resampling.LANCZOS) return denoised_image # 使用函数 processed_image preprocess_captcha(captcha_image) # 可以保存查看预处理效果 processed_image.save(processed_captcha.png)预处理效果可视化对比 你可以通过保存中间每一步的图像直观感受预处理的作用。通常经过二值化和降噪后图像会变得干净很多。3.4 步骤三调用Tesseract进行OCR识别现在我们将处理干净的图像喂给Tesseract。def ocr_captcha(image): 使用Tesseract识别图像中的文本。 :param image: 预处理后的PIL.Image对象 :return: 识别出的字符串 # 配置Tesseract参数这对识别率至关重要 custom_config r--oem 3 --psm 7 -c tessedit_char_whitelist0123456789-x÷ # 参数解释 # --oem 3: 使用默认的基于LSTM的OCR引擎模式效果最好 # --psm 7: 将图像视为单行文本。对于单行验证码这个模式很有效。 # -c tessedit_char_whitelist0123456789-x÷ : 白名单。只识别数字、加减乘除符号和等号。 # 注意乘号可能是*或x除号可能是/或÷需要根据实际验证码样式调整。 # 如果验证码只有数字和加减号可以简化为‘0123456789-’ text pytesseract.image_to_string(image, configcustom_config) # 清理识别结果去除空格、换行等无关字符 cleaned_text re.sub(r\s, , text) return cleaned_text captcha_text ocr_captcha(processed_image) print(f识别出的原始文本: {captcha_text})注意事项tessedit_char_whitelist字符白名单是大幅提升准确率的秘诀。它限制了Tesseract只寻找你指定的字符避免了将干扰线误认为字母。务必根据实际验证码包含的字符集进行设置。3.5 步骤四解析表达式并计算结果识别出的文本可能是“35”、“8/2”或“6x7”。我们需要解析这个字符串并计算出答案。import re import operator def calculate_expression(expression_str): 解析并计算简单的算术表达式。 :param expression_str: 识别出的字符串如‘35’‘8/2’‘6x7’ :return: 计算结果整数或浮点数若解析失败返回None # 1. 清理字符串去除等号、空格等无关字符 expr expression_str.strip().replace(, ).replace( , ) # 2. 统一运算符表示将‘x’或‘X’替换为‘*’将‘÷’替换为‘/’ expr expr.replace(x, *).replace(X, *).replace(÷, /) # 3. 使用正则表达式匹配出数字和运算符 # 这个正则匹配一个数字、一个运算符、再一个数字的简单格式 pattern r^(\d)([\\-\*/])(\d)$ match re.match(pattern, expr) if not match: print(f无法解析的表达式格式: {expr}) return None num1, op, num2 match.groups() num1, num2 int(num1), int(num2) # 4. 执行计算 ops { : operator.add, -: operator.sub, *: operator.mul, /: operator.truediv, # 返回浮点数 } try: result ops[op](num1, num2) # 如果是除法且期望结果是整数可以四舍五入或取整 # 例如 7/2 在验证码中可能期望答案是3或4需要观察实际验证码逻辑。 # 通常验证码是整除。这里我们先按浮点数处理最后返回时处理。 return result except KeyError: print(f不支持的运算符: {op}) return None except ZeroDivisionError: print(除零错误) return None # 示例 answer calculate_expression(captcha_text) if answer is not None: # 很多验证码要求整数答案特别是除法。这里我们四舍五入到整数。 final_answer str(int(round(answer, 0))) print(f识别出的表达式: {captcha_text}, 计算结果: {answer}, 提交答案: {final_answer}) else: print(表达式计算失败可能需要重新获取验证码。)关于除法结果的坑这是最容易出错的地方。如果验证码是“7÷2”网站期望的答案可能是“3”、“4”或“3.5”。必须人工观察确认。大多数简单算术验证码会设计成整除如6÷23或结果明确为整数。如果遇到非整除可能需要向上/下取整。处理策略应在分析真实验证码后确定。3.6 步骤五模拟输入与提交拿到答案后最后一步就是让Selenium将其填入输入框并提交。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def submit_captcha_answer(driver, answer): 将答案填入输入框并提交。 :param driver: Selenium WebDriver 实例 :param answer: 计算出的答案字符串 try: # 1. 定位答案输入框 answer_input WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, captcha_input)) # 根据实际页面调整 ) # 2. 清空输入框如果有旧内容 answer_input.clear() # 3. 模拟人类输入稍微延迟不要一次性输入太快 import time for char in answer: answer_input.send_keys(char) time.sleep(0.1) # 每个字符输入间隔0.1秒 print(f答案 {answer} 已填入。) # 4. 定位并点击提交按钮例如登录按钮 submit_button driver.find_element(By.ID, login_button) # 根据实际页面调整 submit_button.click() # 5. 等待页面跳转或进行下一步操作 time.sleep(2) # 简单等待生产环境建议使用EC条件等待 # WebDriverWait(driver, 10).until(EC.url_changes(driver.current_url)) except Exception as e: print(f提交验证码答案时出错: {e}) # 这里可以添加重试逻辑或截图保存现场 # 整合调用 if final_answer: submit_captcha_answer(driver, final_answer)4. 集成与优化打造健壮的自动化模块将上述步骤封装成一个类并加入错误处理、重试和日志功能才能用于生产环境。4.1 完整类封装示例import logging import time from io import BytesIO from PIL import Image, ImageOps, ImageFilter import pytesseract import re import requests from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class ArithmeticCaptchaSolver: def __init__(self, driver, char_whitelist0123456789-*/x÷): 初始化算术验证码解决器。 :param driver: Selenium WebDriver 实例 :param char_whitelist: Tesseract字符白名单 self.driver driver self.char_whitelist char_whitelist self.logger logging.getLogger(__name__) def get_captcha_image_element(self, locator(By.ID, captcha_image)): 定位验证码图片元素 try: element WebDriverWait(self.driver, 10).until( EC.presence_of_element_located(locator) ) return element except Exception as e: self.logger.error(f定位验证码图片元素失败: {e}) return None def capture_image(self, element): 从元素捕获图像优先尝试Base64其次截图 img_src element.get_attribute(src) try: if img_src and img_src.startswith(data:image): # Base64 解码 import base64 base64_data img_src.split(,)[1] image_data base64.b64decode(base64_data) return Image.open(BytesIO(image_data)) else: # 备用方案截图可能不精确 location element.location size element.size self.driver.save_screenshot(temp_screenshot.png) full_img Image.open(temp_screenshot.png) left location[x] top location[y] right left size[width] bottom top size[height] return full_img.crop((left, top, right, bottom)) except Exception as e: self.logger.error(f捕获验证码图像失败: {e}) return None def preprocess_image(self, image): 图像预处理流水线 # 转换为灰度 gray image.convert(L) # 增强对比度 contrasted ImageOps.autocontrast(gray) # 二值化 threshold 130 binary contrasted.point(lambda p: 255 if p threshold else 0) # 简单降噪 denoised binary.filter(ImageFilter.MinFilter(3)) return denoised def ocr_image(self, image): 调用Tesseract进行OCR识别 custom_config f--oem 3 --psm 7 -c tessedit_char_whitelist{self.char_whitelist} try: text pytesseract.image_to_string(image, configcustom_config) cleaned_text re.sub(r\s, , text) self.logger.info(fOCR识别结果: {cleaned_text}) return cleaned_text except Exception as e: self.logger.error(fOCR识别过程出错: {e}) return def calculate_answer(self, expression): 计算表达式答案 expr expression.strip().replace(, ).replace( , ) expr expr.replace(x, *).replace(X, *).replace(÷, /) # 更健壮的正则支持可能的多余字符 match re.match(r(\d)([\\-\*/])(\d), expr) if not match: self.logger.warning(f无法解析表达式: {expr}) return None num1, op, num2 match.groups() try: num1, num2 int(num1), int(num2) if op : result num1 num2 elif op -: result num1 - num2 elif op *: result num1 * num2 elif op /: # 关键决策点验证码的除法如何处理 # 方案1整除 result num1 // num2 # 方案2四舍五入 # result int(round(num1 / num2, 0)) # 方案3保留浮点数不常见 # result num1 / num2 else: return None self.logger.info(f计算表达式: {expr} {result}) return str(result) except Exception as e: self.logger.error(f计算表达式时出错: {e}, 表达式: {expr}) return None def solve_and_input(self, image_locator, input_locator, submit_locatorNone, max_retries3): 主流程解决验证码并输入。 :param image_locator: 验证码图片定位器 :param input_locator: 答案输入框定位器 :param submit_locator: 提交按钮定位器可选可在外部处理 :param max_retries: 最大重试次数 :return: 成功返回True失败返回False for attempt in range(max_retries): self.logger.info(f尝试第 {attempt 1} 次解决验证码...) try: # 1. 获取图像元素 img_element self.get_captcha_image_element(image_locator) if not img_element: continue # 2. 捕获图像 raw_image self.capture_image(img_element) if not raw_image: continue # 3. 预处理 processed_image self.preprocess_image(raw_image) # 4. OCR识别 expression self.ocr_image(processed_image) if not expression: self.logger.warning(OCR未能识别出文本准备重试...) self._reload_captcha() # 假设有一个刷新验证码的方法 time.sleep(1) continue # 5. 计算答案 answer self.calculate_answer(expression) if not answer: continue # 6. 输入答案 input_box WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable(input_locator) ) input_box.clear() for char in answer: input_box.send_keys(char) time.sleep(0.05) # 模拟输入 self.logger.info(f验证码答案 {answer} 已输入。) return True except Exception as e: self.logger.error(f第 {attempt 1} 次尝试失败: {e}) time.sleep(2) # 失败后等待一段时间再重试 self.logger.error(f经过 {max_retries} 次尝试仍未能解决验证码。) return False def _reload_captcha(self): 模拟点击验证码图片以刷新如果支持 try: # 很多网站的验证码图片本身就是一个刷新按钮 captcha_img self.driver.find_element(By.ID, captcha_image) captcha_img.click() time.sleep(1) # 等待新验证码加载 except: pass # 如果找不到或不可点击则忽略4.2 高级策略当Tesseract力不从心时如果预处理后Tesseract的识别率仍然很低低于70%可以考虑以下升级方案1. 模板匹配法适用于固定字体、固定位置的验证码如果验证码的数字和符号样式完全固定可以事先制作0-9和-*/的模板图片然后使用OpenCV的模板匹配功能来识别。import cv2 import numpy as np def match_with_templates(processed_image, templates_dict): 使用模板匹配识别字符。 :param processed_image: 预处理后的二值化图像OpenCV格式numpy数组 :param templates_dict: 字典键为字符值为对应的模板图像数组 :return: 识别出的字符串 result_text # 假设字符是水平排列的可以先进行轮廓检测分割出单个字符 # 这里简化处理假设知道每个字符的大致宽度 char_width 20 # 示例值需要根据实际情况调整 height, width processed_image.shape for i in range(0, width, char_width): char_roi processed_image[0:height, i:ichar_width] best_match_char None best_match_val -1 for char, template in templates_dict.items(): # 调整模板大小与ROI匹配如果需要 resized_template cv2.resize(template, (char_roi.shape[1], char_roi.shape[0])) # 进行模板匹配 result cv2.matchTemplate(char_roi, resized_template, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(result) if max_val best_match_val and max_val 0.8: # 设置置信度阈值 best_match_val max_val best_match_char char if best_match_char: result_text best_match_char return result_text2. 深度学习模型终极方案使用CNN卷积神经网络训练一个分类器。这需要收集数百张该网站的验证码图片并进行标注。虽然前期工作量大但一旦模型训练好识别率可达99%以上。可以使用TensorFlow或PyTorch框架。# 伪代码示例 # 1. 数据收集手动或半自动标注一批验证码图片建立数据集。 # 2. 模型训练构建一个简单的CNN模型如LeNet-5进行多字符分类或端到端识别。 # 3. 集成使用将训练好的模型集成到上述CaptchaSolver类的ocr_image方法中。5. 常见问题排查与实战心得在实际使用中你肯定会遇到各种各样的问题。下面是我踩过的一些坑和解决方案。5.1 识别率低怎么办检查预处理效果把预处理后的图片(processed_captcha.png)保存下来用肉眼看看字符是否清晰、背景是否干净。如果效果不好调整二值化的阈值(threshold)、尝试不同的滤波器或增加形态学操作。调整Tesseract参数--psm页面分割模式参数非常关键。对于单行文本--psm 7或--psm 8单词可能更合适。多试试--psm 6统一区块、--psm 13原始行。使用命令tesseract --help-psm查看所有选项。确认字符白名单仔细检查验证码到底用了哪些字符。是只有数字和-*/还是包含了括号确保白名单完全覆盖。图像尺寸问题Tesseract对太小的图片识别不好。尝试将图片放大2倍再识别。语言包问题确保安装了正确的语言数据。对于纯数字和符号eng英语数据包通常足够。可以通过tesseract --list-langs查看已安装的语言。5.2 如何应对动态变化的验证码验证码刷新有些网站每次获取的验证码图片URL不变但内容会变。解决方案是在识别前不要提前将图片URL存入变量而应该在需要识别的那一刻重新获取元素的src属性或重新截图。Token绑定验证码与一次会话或表单提交绑定。必须在同一个会话中完成识别和提交且不能间隔太久。确保Selenium的driver实例在获取图片和提交答案期间没有重启或跳转到无关页面。5.3 行为伪装与反检测即使验证码识别对了过于机械的操作也可能被网站的风控系统识别。随机化等待时间在关键操作如输入答案、点击按钮前后加入随机延迟。import random time.sleep(random.uniform(0.5, 2.0))模拟人类输入不要用send_keys一次性输入整个字符串而是拆分成单个字符输入并加入随机间隔。处理鼠标轨迹对于有点击需求的验证码如点选式使用ActionChains来模拟带有一点随机偏移的鼠标移动和点击。管理浏览器指纹长期运行脚本时考虑使用undetected-chromedriver或手动注入JS来隐藏WebDriver特征但需注意合规性。5.4 网络与稳定性问题设置超时与重试对网络请求如下载图片和元素查找操作设置合理的超时时间并实现重试机制如上文类中的max_retries。验证码服务降级如果自建识别模块连续失败多次可以设计一个“降级策略”例如触发人工干预通知或者在合规前提下切换到可靠的第三方打码API作为备份。日志记录详细的日志包括成功、失败、识别出的中间文本、计算过程对于后期排查问题至关重要。5.5 法律与道德边界这是最重要的部分。自动化工具是一把双刃剑。遵守Robots协议检查目标网站的robots.txt文件尊重其禁止爬取的目录。确认合规性仅将技术用于授权的自动化测试、学习研究或个人合法数据备份。切勿用于恶意刷票、撞库攻击、爬取受版权保护或明确禁止的数据。控制访问频率给目标网站留出喘息之机避免高频请求导致对方服务器压力过大。这既是道德要求也能降低你被封锁的风险。用户协议仔细阅读目标网站的用户协议明确其是否禁止自动化操作。绕过“加减乘除”验证码本质上是一个结合了图像处理、模式识别和自动化控制的综合工程问题。从简单的预处理OCR到复杂的深度学习模型解决方案的复杂度取决于验证码本身的防御强度。对于大多数中等强度的算术验证码本文提供的经典流水线已经足够有效。关键在于细致的调试——预处理参数、OCR配置、表达式解析规则都需要根据目标验证码的具体样式进行微调。记住没有一劳永逸的银弹耐心分析和持续优化才是成功的关键。在实际项目中建议先将这个模块独立测试达到稳定的识别率后再集成到主自动化流程中。