Selenium实战:破解动态算术验证码的自动化方案

📅 发布时间:2026/7/7 9:13:23 👁️ 浏览次数:
Selenium实战:破解动态算术验证码的自动化方案
1. 为什么动态算术验证码让自动化测试头疼大家好我是老张一个在自动化测试和爬虫领域摸爬滚打了十来年的老码农。今天咱们来聊聊一个让很多刚接触Selenium自动化的朋友感到棘手的问题——动态算术图片验证码。你肯定遇到过在模拟登录或者注册的时候页面上突然弹出一个图片上面写着“3加5等于”或者“12.5 ÷ 2.5 ?”要求你手动输入答案。这种验证码学名叫“动态算术验证码”它的核心目的就是区分你是真人还是机器。对于纯手工操作来说这很简单心算一下或者用计算器按两下就完事了。但一旦你想用Selenium写个脚本来自动完成登录流程这玩意儿就成了拦路虎。它不像固定的文本验证码每次刷新页面数字和运算符都会变你没法写死一个答案。更“狡猾”的是有些验证码还会用中文的“加、减、乘、除”来代替“、-、*、/”甚至故意把图片弄得有点扭曲增加机器识别的难度。我刚开始做自动化的时候也在这上面栽过跟头。当时项目急着要上线一个自动巡检功能需要定时登录后台系统。系统登录页就用了这种算术验证码。我的第一个想法是“绕过去”比如找开发同学要个万能验证码或者直接关闭验证码功能。但在生产环境或者测试第三方网站时这根本行不通。于是硬着头皮也得解决它。经过多次实践和优化我总结出了一套比较稳定、通用的自动化方案核心思路就是用Selenium抓图用OCR光学字符识别读图再用代码解析计算最后自动回填。听起来是不是有点像“眼睛看、脑子算、手输入”的自动化版本下面我就把这套方案的里里外外、坑坑洼洼都给你讲明白。2. 搭建你的自动化作战环境工欲善其事必先利其器。在开始写代码破解验证码之前我们得先把“战场”布置好。这里不需要什么高深莫测的工具都是Python生态里非常成熟、易用的库。2.1 核心武器库安装首先确保你的电脑上已经安装了Python建议3.7及以上版本。然后打开你的命令行终端CMD或者Terminal我们依次安装几个核心的包。第一个是Selenium这是我们自动化操作浏览器的主力。安装命令很简单pip install selenium光有Selenium还不够它需要对应的浏览器驱动才能控制浏览器。最常用的是Chrome浏览器和它的驱动ChromeDriver。你需要做两件事1. 确保电脑上安装了Chrome浏览器2. 下载与你的Chrome浏览器版本匹配的ChromeDriver并将其所在目录添加到系统的环境变量PATH中或者直接放在Python的脚本目录下。这一步稍微有点繁琐但网上教程很多搜一下“Selenium ChromeDriver配置”就能搞定。第二个核心武器是OCR识别库。我们不需要自己从零开始训练图像识别模型直接用现成的轮子。这里我强烈推荐ddddocr带带弟弟OCR这个库在中文社区非常火对于常见的验证码识别尤其是数字和简单字符准确率相当高而且使用极其简单。安装命令如下pip install ddddocr这个库是离线的不需要联网调用API速度很快也没有调用次数的限制对于自动化脚本来说非常友好。最后我们还需要Pillow库来处理图片。虽然ddddocr可以直接接受字节流但有时候我们可能需要对截取的验证码图片进行一些预处理比如灰度化、二值化让图片只有黑白两色来提升识别率。安装命令pip install Pillow环境搭好了我们就可以开始构思整个战斗流程了。简单来说脚本需要像一个人一样完成以下步骤打开登录页面 - 找到验证码图片元素 - 把它截图保存下来 - 调用OCR识别图片中的文字 - 从文字里提取出算术表达式 - 计算出结果 - 找到输入框并填入结果 - 点击登录按钮。2.2 编写你的第一个Selenium脚本骨架在深入每个环节之前我们先搭一个最基础的Selenium脚本框架把浏览器打开访问目标页面。这里我假设你有一个本地的测试页面test.html上面有一个算术验证码图片和一个输入框。#!/usr/bin/env python # encoding: utf-8 from selenium import webdriver from selenium.webdriver.common.by import By import time # 1. 配置浏览器选项可选常用于设置无头模式、禁用GPU等 options webdriver.ChromeOptions() # options.add_argument(--headless) # 无头模式不显示浏览器窗口 # options.add_argument(--disable-gpu) # 2. 启动浏览器驱动 driver webdriver.Chrome(optionsoptions) # 确保chromedriver在PATH中 driver.implicitly_wait(10) # 设置隐式等待10秒让元素加载 # 3. 打开目标页面 driver.get(http://127.0.0.1/test.html) # 替换成你的测试页面地址 time.sleep(2) # 强制等待2秒确保页面完全加载尤其是验证码图片 # 4. 后续步骤定位验证码图片、识别、计算、填写... # ... (我们会在下面填充) # 5. 脚本最后留点时间看看效果然后关闭浏览器 time.sleep(5) driver.quit()运行这段代码你应该能看到Chrome浏览器自动打开并访问了指定页面。这就是我们自动化的起点。接下来我们要解决最核心的问题怎么把图片里的算式“读”出来。3. 从图片到文字OCR识别实战验证码图片本质上是一张包含文字的图片。我们要做的就是让计算机“看懂”这张图片上的文字。这就是OCR技术干的事情。前面我们安装的ddddocr库就是一个封装好的OCR引擎特别擅长处理这类验证码图片。3.1 捕获验证码图片首先我们需要用Selenium定位到页面上的验证码图片元素。通常这个图片会有特定的id、class或者xpath。你需要用浏览器的开发者工具F12查看一下它的属性。假设它的id是captcha-img。Selenium提供了非常方便的方法来直接获取这个元素的截图不需要全屏截图再裁剪。代码如下# 定位验证码图片元素 captcha_element driver.find_element(By.ID, captcha-img) # 根据实际情况修改定位方式 # 将元素截图保存为PNG格式的字节数据 captcha_png_bytes captcha_element.screenshot_as_pngscreenshot_as_png属性直接返回图片的二进制数据省去了我们保存到文件再读取的步骤效率更高。如果你需要对图片进行复杂的预处理也可以将这个字节数据用Pillow库打开成一个Image对象from PIL import Image import io image Image.open(io.BytesIO(captcha_png_bytes)) # 接下来可以对image进行灰度化、二值化等操作 # processed_image image.convert(L) # 转为灰度图3.2 调用ddddocr进行识别拿到图片数据后调用ddddocr就非常简单了。初始化一个识别器然后把图片数据喂给它就行。import ddddocr # 初始化OCR引擎show_adFalse是关闭一些无关输出 ocr ddddocr.DdddOcr(show_adFalse) # 进行识别captcha_png_bytes就是上一步获取的图片字节数据 ocr_result_text ocr.classification(captcha_png_bytes) print(fOCR识别出的原始文本是: {ocr_result_text})实测下来ddddocr对于清晰的数字和运算符识别率非常高。但验证码为了增加难度可能会加入一些干扰线、背景噪点或者字体有点扭曲。这时识别结果就可能出现一些“杂质”。比如一个理想的算式“8*5”OCR可能识别成“8 * 5”中间多了空格甚至识别成“8乘5”把星号认成了中文“乘”字或者更糟识别成“8x5”把星号认成了字母x。所以我们打印出原始文本后很可能会看到五花八门的结果比如“请计算3加2”、“5.2 - 1.1 ?”、“12乘3等于”。我们的下一个任务就是从这段可能包含多余文字的字符串中精准地提取出纯粹的数字和运算符。4. 核心魔法解析与计算算术表达式OCR给了我们一串文本现在需要我们的脚本像人一样从这串文本里“理解”出算术题是什么并算出答案。这一步是整个方案的大脑也是最体现编程技巧的地方。核心工具是字符串处理和正则表达式。4.1 清洗与归一化文本首先我们需要对OCR识别出的文本进行清洗。常见的干扰词有“请计算”、“等于”、“”、“?”、“:”等。我们可以用字符串的replace()方法或者正则表达式直接去掉它们。更重要的是要把中文运算符统一替换成Python能理解的英文运算符。import re def clean_and_normalize(text): # 去除常见的干扰词和符号 text text.replace(请计算, ).replace(等于, ).replace(?, ).replace(, ).replace(, ).replace( , ) # 统一运算符将中文和常见变体替换成英文 text text.replace(加, ).replace(减, -).replace(乘, *).replace(除, /) text text.replace(x, *).replace(X, *).replace(÷, /) return text # 测试一下 test_text 请计算9.1加3.2等于 cleaned_text clean_and_normalize(test_text) print(cleaned_text) # 输出: 9.13.2经过这一步文本干净多了基本变成了“数字运算符数字”的格式。4.2 使用正则表达式精准提取接下来我们需要从清洗后的文本中把第一个数、运算符和第二个数分别提取出来。这里就是正则表达式大显身手的时候了。我们需要写一个能匹配“整数或小数”“运算符”“整数或小数”的模式。def parse_math_expression(text): # 先进行清洗和归一化 text clean_and_normalize(text) # 核心正则表达式匹配 数字可能含小数点 运算符 数字可能含小数点 # (\d(?:\.\d)?) 解释 # \d 匹配一个或多个数字整数部分 # (?:\.\d)? 是一个非捕获组匹配一个小数点加数字?表示整个小数部分是可选的 # \s* 匹配零个或多个空白字符空格、制表符等 # ([\-*/]) 匹配一个运算符 - * /注意-在括号内要转义 pattern r(\d(?:\.\d)?)\s*([\-*/])\s*(\d(?:\.\d)?) match re.search(pattern, text) if not match: print(f正则表达式未能匹配文本: {text}) return None # 使用 .groups() 方法提取三个捕获组 num1_str, operator, num2_str match.groups() print(f提取成功: 数字1{num1_str}, 运算符{operator}, 数字2{num2_str}) try: # 将字符串转为浮点数进行计算 num1 float(num1_str) num2 float(num2_str) if operator : result num1 num2 elif operator -: result num1 - num2 elif operator *: result num1 * num2 elif operator /: if num2 0: print(错误除数不能为0) return None result num1 / num2 else: return None # 格式化结果如果是整数就返回整数否则保留两位小数 # 注意round函数在处理某些小数时会有“四舍六入五成双”的问题对于验证码通常够用 if result.is_integer(): return int(result) else: return round(result, 2) except ValueError as e: print(f转换数字时出错: {e}) return None except Exception as e: print(f计算过程中发生未知错误: {e}) return None # 测试各种情况 print(parse_math_expression(9.13.2)) # 12.3 print(parse_math_expression(10-4)) # 6 print(parse_math_expression(6*7)) # 42 print(parse_math_expression(15/3)) # 5 print(parse_math_expression(10除以0)) # 错误除数不能为0 - None这个parse_math_expression函数就是我们破解验证码的“计算大脑”。它非常健壮能处理小数、能处理常见的运算符变体还考虑了除零错误。在实际项目中你可能遇到更复杂的情况比如表达式是“352”多个运算符或者数字之间有多个空格。这时就需要根据实际情况调整正则表达式。但就我经验来看绝大多数网站的动态算术验证码都是简单的二元一次运算上面的正则表达式已经能覆盖90%以上的场景。5. 组装完整流程并处理边界情况现在我们已经有了所有“零件”Selenium操控浏览器、OCR识别图片、正则表达式解析计算。是时候把它们组装成一个完整的、健壮的自动化脚本了。同时我们也要聊聊在实际运行中可能会遇到的“坑”以及怎么填平它们。5.1 端到端的自动化脚本让我们把前面的代码片段整合起来形成一个完整的登录流程自动化脚本。假设我们的测试页面有一个ID为captchaImage的验证码图片一个ID为answer的输入框和一个ID为loginBtn的登录按钮。#!/usr/bin/env python # encoding: utf-8 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 import ddddocr import re # ---------- 1. 表达式解析函数 (复用上面的代码) ---------- def clean_and_normalize(text): # ... (省略见上一节) pass def parse_math_expression(text): # ... (省略见上一节) pass # ---------- 2. 主流程 ---------- def auto_solve_captcha_and_login(url, username, password): driver None try: # 启动浏览器 options webdriver.ChromeOptions() # options.add_argument(--headless) # 生产环境可启用无头模式 driver webdriver.Chrome(optionsoptions) driver.implicitly_wait(10) wait WebDriverWait(driver, 10) print(f正在访问页面: {url}) driver.get(url) time.sleep(3) # 等待页面和验证码初始化 # 步骤1: 输入用户名和密码 (假设有这些字段) # wait.until(EC.presence_of_element_located((By.ID, username))).send_keys(username) # driver.find_element(By.ID, password).send_keys(password) # 步骤2: 定位并截图验证码 print(正在定位验证码图片...) captcha_element wait.until(EC.presence_of_element_located((By.ID, captchaImage))) captcha_png captcha_element.screenshot_as_png # 步骤3: OCR识别 print(正在调用OCR识别...) ocr ddddocr.DdddOcr(show_adFalse) ocr_text ocr.classification(captcha_png) print(fOCR原始识别结果: {ocr_text}) # 步骤4: 解析并计算 print(正在解析计算表达式...) answer parse_math_expression(ocr_text) if answer is None: print(【失败】无法从识别结果中解析出有效算式尝试刷新验证码...) # 可以在这里添加点击“刷新验证码”按钮的逻辑 # driver.find_element(By.ID, refreshCaptcha).click() # time.sleep(1) # 然后可以选择重试识别或者直接退出 return False print(f计算得到答案: {answer}) # 步骤5: 填写答案并提交 answer_input driver.find_element(By.ID, answer) answer_input.clear() answer_input.send_keys(str(answer)) login_button driver.find_element(By.ID, loginBtn) login_button.click() print(已提交登录表单) # 步骤6: 验证登录是否成功根据实际页面判断 time.sleep(2) # 例如检查是否跳转到新页面或者页面上出现了“登录成功”的提示 # if dashboard in driver.current_url: # print(登录成功) # else: # print(登录可能失败请检查。) return True except Exception as e: print(f自动化流程出现异常: {e}) # 可以在这里截图保存方便排查问题 # driver.save_screenshot(error_screenshot.png) return False finally: if driver: # 稍作停留后关闭浏览器 time.sleep(3) driver.quit() # ---------- 3. 执行 ---------- if __name__ __main__: TEST_URL http://127.0.0.1:8080/login.html # 你的测试地址 USERNAME test_user PASSWORD test_pass auto_solve_captcha_and_login(TEST_URL, USERNAME, PASSWORD)这个脚本已经具备了完整的自动化能力。它使用了WebDriverWait进行显式等待比单纯的time.sleep更智能只在元素真正出现时才进行操作提高了脚本的稳定性和运行速度。5.2 你可能遇到的坑与优化策略在实际运行中脚本不可能一帆风顺。下面是我踩过的一些坑和对应的解决方案坑1OCR识别率不稳定。有时候图片背景噪点太多或者字体特别扭曲ddddocr也可能识别错误比如把“8”识别成“B”把“”识别成“t”。对策引入图片预处理。在将图片字节传给OCR之前先用Pillow库处理一下。from PIL import Image, ImageFilter, ImageEnhance import io def preprocess_image(image_bytes): 对验证码图片进行预处理提高OCR识别率 image Image.open(io.BytesIO(image_bytes)) # 1. 转为灰度图 image image.convert(L) # 2. 增强对比度 enhancer ImageEnhance.Contrast(image) image enhancer.enhance(2.0) # 增强2倍 # 3. 二值化阈值可根据实际情况调整 # threshold 150 # image image.point(lambda p: p threshold and 255) # 4. 降噪轻度模糊 # image image.filter(ImageFilter.MedianFilter(size3)) # 将处理后的图片转回字节 byte_arr io.BytesIO() image.save(byte_arr, formatPNG) return byte_arr.getvalue() # 使用预处理后的图片进行识别 processed_bytes preprocess_image(captcha_png) ocr_text ocr.classification(processed_bytes)坑2验证码刷新机制。很多网站在你识别错误一次后会要求刷新验证码才能再次尝试。或者验证码本身有过期时间。对策在脚本中增加重试逻辑。如果解析失败或者登录后提示验证码错误就主动点击页面的“换一张”按钮重新执行截图、识别、计算的流程。这需要你定位到刷新按钮的元素。坑3网络延迟与元素加载时机。即使使用了WebDriverWait在网速慢或页面复杂的场景下验证码图片可能还没加载完就被截图了导致截到一张空白或破损的图。对策可以尝试在截图前等待图片元素的某个属性如naturalWidth变为有效值或者简单粗暴地多等一会儿。也可以捕获截图或识别失败异常然后触发重试。坑4更复杂的表达式。虽然少见但有些网站可能会出“35-2”或者“12*34”这类多运算符或带括号的题目。对策对于这种情况简单的正则提取就不够用了。一个更强大的方法是使用eval()函数但必须极度谨慎因为它会执行任何字符串形式的Python代码如果OCR识别结果被恶意篡改会有安全风险。只能在100%信任OCR结果和验证码来源的情况下使用。更安全的做法是使用ast.literal_eval()或编写更复杂的解析器。# 警告仅在绝对可信的环境下使用eval def safe_calculate(expression_str): 在清洗和严格检查后使用eval计算慎用 # 清洗后表达式应只包含数字、小数点、空格和-*/ cleaned re.sub(r[^\d\.\\-\*/\(\)\s], , expression_str) # 进一步检查格式确保没有连续运算符等非法形式 if re.search(r[\\-\*/]{2,}, cleaned): # 检查连续运算符 return None try: # 使用eval计算 result eval(cleaned) return round(result, 2) if not isinstance(result, int) else result except: return None把这些优化点融入到你的脚本中它的鲁棒性会大大增强。自动化脚本的价值就在于7x24小时稳定运行处理好边界情况是关键。6. 进阶思考与最佳实践做到上面那一步你已经可以解决大部分动态算术验证码了。但如果你想把这个方案用到生产环境或者应对更严苛的场景还有一些进阶的思考和最佳实践值得分享。6.1 如何应对识别失败引入重试与降级策略没有任何OCR是100%准确的。我们需要为失败做好准备。一个健壮的系统应该有重试机制和降级策略。重试机制当OCR识别出的文本无法被解析或者提交后服务器返回验证码错误时不要直接让脚本崩溃退出。可以设计一个循环比如最多重试3次。每次失败后先尝试点击页面上的“刷新验证码”按钮等待新验证码加载然后重新执行识别和计算流程。降级策略当重试多次仍然失败后怎么办一种策略是“降级”到人工干预。比如脚本可以捕获当前验证码图片并保存到本地同时通过日志、邮件或即时通讯工具如企业微信、钉钉机器人发出告警附上图片提示人工计算并输入答案。另一种策略是如果网站允许可以尝试切换到其他类型的验证码如滑动拼图、点选文字或者直接暂停任务等待一段时间后再试。6.2 提升效率多线程与异步处理如果你需要批量处理大量带有验证码的页面串行操作会非常慢。因为每个步骤打开页面、等加载、截图、OCR识别、计算、提交都有等待时间。这时可以考虑使用多线程或异步IO来提升效率。例如你可以使用concurrent.futures库的ThreadPoolExecutor来并发处理多个页面。但要注意Selenium的WebDriver对象不是线程安全的每个线程应该拥有自己独立的driver实例。同时也要注意目标网站的并发请求限制避免被封IP。from concurrent.futures import ThreadPoolExecutor, as_completed def process_single_task(task_url): 处理单个URL的任务函数内部创建独立的driver # 这里封装前面写的 auto_solve_captcha_and_login 逻辑 # 但每个任务都创建和退出自己的driver pass url_list [url1, url2, url3] # 你的URL列表 max_workers 3 # 并发线程数不宜过大 with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_url {executor.submit(process_single_task, url): url for url in url_list} for future in as_completed(future_to_url): url future_to_url[future] try: success future.result() print(f任务 {url} 处理结果: {success}) except Exception as exc: print(f任务 {url} 产生异常: {exc})6.3 关于验证码破解的伦理与法律边界最后也是最重要的一点我们必须谈谈伦理和法律。我分享这套技术方案初衷是用于自动化测试、企业内部系统巡检、个人学习研究等合法合规的场景。绝对不能用于恶意爬取他人网站数据、刷票、撞库攻击等侵犯他人权益和违反法律法规的行为。很多网站的验证码是保护其数据和用户安全的重要防线。滥用自动化技术绕过这些防护不仅可能违反网站的《服务条款》导致你的IP被封禁在更严重的情况下还可能触及法律红线。作为技术人员我们应该用技术去创造价值、提升效率而不是去破坏规则。在实施任何自动化操作之前请务必确认你的行为获得了相关方的授权或者是在法律允许的范围内进行。技术本身是中立的但使用技术的人需要有自己的准则。希望你在使用这套方案时能把它用在正道上解决那些重复、枯燥但又不得不做的工作解放你的双手去进行更有创造性的思考。