1. 项目概述:当自动化遇上验证码这道“门”
做Web自动化测试或者数据采集的朋友,对登录环节的验证码绝对是又爱又恨。爱的是,它确实能有效防止恶意登录和爬虫;恨的是,它成了自动化流程中一道难以逾越的“门”。尤其是图片验证码,它不像短信验证码那样可以调用接口获取,也不像简单的算术验证码那样容易解析。它考验的是程序“看”和“认”的能力。今天,我们就来深入聊聊,如何用Selenium这套强大的浏览器自动化工具,来“敲开”这道由图片验证码把守的门。
简单来说,这个项目的核心就是:在Selenium驱动的自动化流程中,当遇到需要输入图片验证码才能登录的网页时,我们如何让程序自动或半自动地识别并填写这个验证码,从而实现登录流程的完整自动化。这不仅仅是写几行代码点击和输入,它涉及到验证码的定位、截图、预处理、识别以及结果回填等一系列环节。无论你是做自动化测试,需要模拟真实用户登录来执行后续用例;还是做数据采集,需要登录后才能获取目标数据;亦或是开发RPA流程,自动化处理某些Web任务,掌握这套方法都能让你的自动化脚本“功力大增”,从只能处理简单表单,升级到能应对更真实、更复杂的Web环境。
2. 核心思路与方案选型:识别验证码的“道”与“术”
面对图片验证码,我们的自动化脚本需要模拟人类用户的“眼睛”和“大脑”。整个处理流程可以抽象为四个核心步骤:定位捕获 -> 图像处理 -> 识别解析 -> 结果回填。但在具体实现上,根据验证码的复杂度和项目要求,我们有几种不同的“打法”。
2.1 方案一:人工半自动介入——稳定可靠的“保底”方案
这是最直接、也最稳定的方法。当脚本运行到验证码出现时,我们让程序暂停,弹出验证码图片给人工查看,由人工输入识别结果后,脚本再继续执行。
为什么首选这个方案?因为对于绝大多数商业网站,尤其是那些专门为防止自动化而设计的复杂验证码(如扭曲字符、干扰线、背景噪点、点击图中特定物体等),目前完全自动化的识别方案准确率无法达到100%。在自动化测试场景下,一次识别失败就可能导致整个测试用例中断,得不偿失。人工介入虽然牺牲了“全自动”,但保证了流程的绝对可靠性和成功率。
技术实现要点:
- 截图与保存:使用Selenium定位到验证码图片元素,然后将其截图保存到本地。
- 交互暂停:脚本通过
input()函数或弹窗,暂停并提示用户。 - 人工输入:用户查看本地图片,在终端或弹窗中输入看到的验证码。
- 流程继续:脚本获取用户输入,填入网页输入框,并点击登录。
这个方案的核心优势在于其普适性和零成本。它不依赖于任何额外的识别服务或模型,对任何类型的图片验证码都有效。在项目初期,或者对稳定性要求极高的生产环境中,我强烈建议先采用此方案作为基础框架。
2.2 方案二:调用第三方OCR API——平衡效率与成本的“折中”方案
当需要处理一定批量,且验证码样式相对固定、复杂度中等时,调用成熟的第三方OCR(光学字符识别)或专门的验证码识别API是一个不错的选择。
常见服务商:
- 通用OCR服务:如百度OCR、腾讯云OCR、阿里云OCR等。它们提供了高精度的文字识别能力,对于清晰、规整的字符验证码效果很好。
- 专业打码平台:如超级鹰、图鉴等。这些平台背后有真人打码或专门训练的模型,对于复杂、扭曲的验证码有更高的破解率,通常按次计费。
集成流程:
- 获取图片:同方案一,使用Selenium截图。
- 图片预处理(可选但重要):对截图进行二值化、降噪、裁剪等处理,可以显著提升识别API的准确率。例如,使用Python的PIL或OpenCV库。
from PIL import Image import io # 假设 `element` 是验证码图片的WebElement png_data = element.screenshot_as_png image = Image.open(io.BytesIO(png_data)) # 转换为灰度图 image = image.convert(‘L’) # 二值化处理 threshold = 150 image = image.point(lambda p: p > threshold and 255) # 保存预处理后的图片 image.save(‘captcha_processed.png’) - 调用API:将处理后的图片(或直接上传字节流)发送给第三方API。
- 解析结果:获取API返回的识别文本。
选择考量:
- 成本:通用OCR可能有免费额度,专业打码平台需要付费。
- 速度:API调用存在网络延迟,对于要求毫秒级响应的场景需谨慎。
- 合规性:确保你的使用方式符合目标网站的服务条款和法律法规。
2.3 方案三:自建机器学习模型识别——高定制化的“终极”方案
如果目标网站的验证码是自家设计的、风格独特且长期稳定,并且有海量的验证码样本可供收集,那么训练一个专属的识别模型可能是最一劳永逸的方案。
技术栈通常涉及:
- 图像处理库:OpenCV,用于样本的预处理(去噪、分割字符等)。
- 机器学习框架:TensorFlow或PyTorch。
- 模型选择:对于字符验证码,CNN(卷积神经网络)是主流选择。更复杂的验证码(如点选、滑块)可能需要目标检测模型(如YOLO)或更复杂的网络结构。
实施步骤简述:
- 数据收集:这是最大的难点。需要通过脚本大量访问登录页面,保存验证码图片,并人工或半自动地为其打上标签。
- 数据预处理与增强:清洗数据,统一尺寸、格式,并通过旋转、缩放、添加噪声等方式进行数据增强,以提升模型的泛化能力。
- 模型训练:设计或选择网络结构,使用标注好的数据进行训练。
- 模型集成:将训练好的模型封装成服务或直接嵌入Python脚本,在Selenium流程中调用。
这个方案投入最大,周期最长,但一旦成功,识别速度最快(本地调用)、长期成本可能最低,并且完全自主可控。它更适合有长期稳定需求且技术资源充足的团队。
注意:在实际项目中,我通常会采用“方案一 + 方案二” 的混合策略。即默认尝试使用成本较低的OCR API进行自动识别,如果识别失败或置信度太低,则自动降级到方案一,触发人工干预。这样既兼顾了效率,又保证了流程的最终成功率。
3. 基于Selenium的完整实现流程拆解
无论选择上述哪种识别方案,其前端与Selenium交互的部分是共通的。下面,我们以最常见的“用户名/密码 + 图片验证码”登录场景为例,拆解一个稳健的实现流程。
3.1 环境准备与基础操作
首先,确保你的环境已经就绪。
# 基础依赖安装 # pip install selenium Pillow opencv-python 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, os启动浏览器并访问目标登录页。这里以Chrome为例,使用显式等待是良好实践,能有效避免因页面加载速度导致的元素定位失败。
driver = webdriver.Chrome() # 或使用 Firefox(), Edge()等 driver.get(‘https://example.com/login’) wait = WebDriverWait(driver, 10) # 设置最大等待时间10秒3.2 验证码元素的定位与捕获
这是最关键的一步。验证码图片的定位方式五花八门,需要仔细分析页面HTML结构。
常见定位方式及实战技巧:
- 通过img标签定位:最理想的情况。验证码通常是一个 `` 元素。
# 假设验证码图片有id captcha_element = wait.until(EC.presence_of_element_located((By.ID, “captchaImg”))) # 或者通过src属性包含特定关键字 # captcha_element = driver.find_element(By.XPATH, “//img[contains(@src, ‘captcha’)]”) - 作为背景图:有时验证码是某个div的CSS背景图(
background-image)。这时你需要定位这个容器div,然后获取其style属性或计算样式中的背景图URL,再通过requests库下载图片。这比截图更精确。div_element = driver.find_element(By.CLASS_NAME, “captcha-bg”) style = div_element.get_attribute(‘style’) # 从 style 字符串中解析出 url(‘…’) 部分 - 通过相邻元素定位:如果图片本身没有明显特征,可以尝试通过其旁边的文字(如“验证码:”)或输入框来间接定位。
# 先找到验证码输入框,再找它前面的img标签 input_box = driver.find_element(By.NAME, “captcha”) captcha_element = input_box.find_element(By.XPATH, “./preceding-sibling::img”)
获取图片数据:定位到元素后,获取其截图。
# 方法1:对元素截图(推荐,能精准截取该元素区域) captcha_png_data = captcha_element.screenshot_as_png # 返回二进制数据 with open(‘captcha.png’, ‘wb’) as f: f.write(captcha_png_data) # 方法2:如果元素截图失败,可以截全屏再裁剪(备用方案) driver.save_screenshot(‘full_page.png’) # 需要获取元素的位置和大小信息进行裁剪 location = captcha_element.location size = captcha_element.size # 使用PIL进行裁剪3.3 识别环节的集成
这里以“人工半自动”和“调用百度OCR API”为例,展示如何将识别环节嵌入流程。
方案一集成示例(人工):
def manual_captcha_solver(image_path): “”“人工识别验证码”“” # 打开图片文件(根据系统不同,可能需要用其他方式) os.system(f‘start {image_path}’) # Windows # os.system(f‘open {image_path}’) # Mac # 等待用户输入 user_input = input(“请输入图片 ‘{}’ 中的验证码:”.format(image_path)) return user_input.strip() # 在流程中调用 captcha_code = manual_captcha_solver(‘captcha.png’)方案二集成示例(百度OCR):你需要先去百度AI开放平台创建应用,获取API Key和Secret Key。
import requests, base64 def baidu_ocr_solver(image_path, api_key, secret_key): “”“使用百度通用OCR识别验证码”“” # 1. 获取Access Token auth_url = f“https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={api_key}&client_secret={secret_key}” auth_resp = requests.get(auth_url).json() access_token = auth_resp.get(‘access_token’) # 2. 读取并编码图片 with open(image_path, ‘rb’) as f: img_data = base64.b64encode(f.read()).decode(‘utf-8’) # 3. 调用OCR接口 ocr_url = f“https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic?access_token={access_token}” headers = {‘Content-Type’: ‘application/x-www-form-urlencoded’} data = {‘image’: img_data} ocr_resp = requests.post(ocr_url, headers=headers, data=data).json() # 4. 解析结果 words_result = ocr_resp.get(‘words_result’, []) if words_result: # 通常验证码是单行,取第一个结果 return words_result[0].get(‘words’, ‘’).replace(‘ ‘, ‘’) return “” # 在流程中调用 api_key = “your_api_key” secret_key = “your_secret_key” captcha_code = baidu_ocr_solver(‘captcha.png’, api_key, secret_key) if not captcha_code: print(“OCR识别失败,转为人工识别”) captcha_code = manual_captcha_solver(‘captcha.png’)3.4 结果回填与登录完成
获取到验证码字符串后,剩下的就是标准的Selenium操作了。
# 1. 定位验证码输入框并输入 captcha_input = wait.until(EC.element_to_be_clickable((By.ID, “captchaInput”))) captcha_input.clear() captcha_input.send_keys(captcha_code) # 2. 填写用户名和密码(如果之前没填) username_input = driver.find_element(By.ID, “username”) password_input = driver.find_element(By.ID, “password”) username_input.send_keys(“your_username”) password_input.send_keys(“your_password”) # 3. 点击登录按钮 login_button = driver.find_element(By.XPATH, “//button[@type=‘submit’]”) login_button.click() # 4. 等待登录成功后的页面跳转或元素出现,进行验证 try: wait.until(EC.url_contains(“dashboard”)) # 或等待某个登录后特有的元素 print(“登录成功!”) except TimeoutException: print(“登录可能失败,检查验证码或账号密码。”) # 这里可以加入重试逻辑,例如刷新验证码重试4. 进阶技巧与实战避坑指南
在实际项目中,你会遇到比基础教程复杂得多的情况。下面分享一些我踩过坑后总结的进阶技巧。
4.1 处理动态刷新与多次尝试
很多网站的验证码在输入错误后不会自动刷新,或者有“看不清,换一张”的链接。
应对策略:
- 封装刷新函数:定位到刷新按钮或链接,点击它,然后需要重新等待新图片加载完成并重新截图。
def refresh_captcha(driver): refresh_btn = driver.find_element(By.LINK_TEXT, “换一张”) refresh_btn.click() time.sleep(1) # 等待新图片加载,最好用显式等待检查图片src或元素属性变化 # 重新定位并截图 new_captcha_element = driver.find_element(By.ID, “captchaImg”) return new_captcha_element.screenshot_as_png - 实现重试机制:当登录失败(可能是验证码错误)时,捕获异常,刷新验证码,重新识别,然后重试登录。注意要设置最大重试次数,避免死循环。
max_retries = 3 for attempt in range(max_retries): # … (执行截图、识别、填写、登录的代码) … if “登录成功”的判断条件: break else: print(f“登录失败,第{attempt+1}次重试…”) refresh_captcha(driver) # 清除旧的验证码输入 captcha_input.clear()
4.2 应对Canvas渲染的验证码
现代前端越来越多地使用HTML5 Canvas来绘制验证码,这给截图带来了挑战。element.screenshot对Canvas元素可能无效或截取到空白。
解决方案:
- 执行JavaScript获取数据URL:通过Selenium执行JS,调用Canvas的
toDataURL()方法,将其转换为Base64格式的图片数据。canvas_element = driver.find_element(By.TAG_NAME, “canvas”) # 获取Canvas的base64图片数据 canvas_base64 = driver.execute_script(“”” var canvas = arguments[0]; return canvas.toDataURL(‘image/png’).substring(22); // 去掉 data:image/png;base64, 前缀 “””, canvas_element) # 解码并保存 import base64 png_data = base64.b64decode(canvas_base64) with open(‘canvas_captcha.png’, ‘wb’) as f: f.write(png_data) - 全屏截图后裁剪:作为备选方案,如果JS方案不奏效,可以截取整个浏览器窗口,然后根据Canvas元素的位置和大小进行裁剪。但这种方法容易受页面滚动和布局影响,精度较差。
4.3 验证码识别后的校验与容错
不要认为识别出来的字符串就一定是正确的。一个好的自动化脚本应该有校验机制。
- 长度校验:很多验证码是4-6位数字或字母。如果识别结果长度不符,可以直接判定为识别失败,触发重试或人工干预。
- 格式校验:有些验证码只包含数字,或者不区分大小写。可以对识别结果进行简单的正则匹配过滤。
- 置信度检查:如果你使用的是提供置信度分数的OCR API(如某些服务返回
probability字段),可以设定一个阈值(如0.8),低于阈值则视为识别质量不高,主动重试。
4.4 关于验证码破解的伦理与法律边界
这是一个必须严肃对待的问题。我们的讨论仅限于学习自动化技术和测试自己拥有或有权测试的系统。
- 切勿滥用:不要用这些技术去攻击、爬取或干扰他人的网站和服务,这很可能违反《计算机信息网络国际联网安全保护管理办法》等相关法律法规,并构成对网站服务条款的违反。
- 尊重
robots.txt:即使技术上可行,也应尊重网站设置的爬虫协议。 - 测试环境:所有技术实践最好在本地搭建的测试环境、或公司内部的测试系统上进行。
- 频率限制:即使是测试,也应避免高频请求,以免对服务器造成不必要的压力。
5. 架构设计与代码封装建议
为了让你的验证码处理逻辑更清晰、更易复用,我建议进行简单的分层封装。
一个可参考的类结构:
class CaptchaHandler: def __init__(self, driver, locator_strategy, locator_value): self.driver = driver self.locator = (locator_strategy, locator_value) # 如 (By.ID, “captchaImg”) self.wait = WebDriverWait(driver, 10) def capture(self): “”“定位并捕获验证码图片,返回图片二进制数据”“” element = self.wait.until(EC.presence_of_element_located(self.locator)) return element.screenshot_as_png def save_to_file(self, png_data, filepath): with open(filepath, ‘wb’) as f: f.write(png_data) return filepath def solve_with_ocr(self, image_path, ocr_config): “”“调用OCR服务识别”“” # … 集成OCR API逻辑 … pass def solve_manually(self, image_path): “”“人工识别”“” # … 弹出图片并等待输入的逻辑 … pass def solve(self, strategy=‘auto’, fallback=‘manual’, **kwargs): “”“主解决函数,可配置策略和降级方案”“” png_data = self.capture() temp_file = self.save_to_file(png_data, ‘temp_captcha.png’) if strategy == ‘ocr’: result = self.solve_with_ocr(temp_file, kwargs.get(‘ocr_config’)) if not result and fallback == ‘manual’: result = self.solve_manually(temp_file) else: # default manual result = self.solve_manually(temp_file) # 清理临时文件 os.remove(temp_file) return result # 使用示例 handler = CaptchaHandler(driver, By.ID, “captchaImg”) code = handler.solve(strategy=‘ocr’, ocr_config={‘api_key’: ‘xxx’}, fallback=‘manual’)这样的封装将验证码处理的所有细节(定位、截图、识别)隐藏在一个类中,主登录流程会变得非常简洁。你可以轻松地在不同的识别策略之间切换,也方便后续维护和扩展。
6. 不同场景下的策略选择与实战心得
最后,结合我多年的经验,给不同场景下的朋友一些直接的建议:
- 如果你是自动化测试工程师:稳定性压倒一切。优先采用“人工半自动”方案,或者“OCR API + 人工降级”方案。在测试脚本中,可以将验证码识别步骤单独模块化,并通过环境变量控制使用哪种模式。在CI/CD流水线中,可以配置一个“安全模式”,强制使用人工输入,避免因识别失败导致构建中断。
- 如果你是数据采集开发者:需要权衡成功率、成本与速度。对于数据量不大、频率不高的采集任务,人工半自动可能就够了。对于需要批量处理的任务,可以调研目标网站验证码的难度,选择性价比高的第三方打码平台。切记遵守法律法规和网站协议,并务必设置合理的请求间隔(如
time.sleep(random.uniform(2,5))),做到友好爬取。 - 如果你面对的是内部系统或老旧系统:这些系统的验证码往往比较简单(如纯数字、无干扰)。可以尝试使用开源的OCR库(如
pytesseract,即Tesseract的Python封装)进行离线识别,成本为零。但Tesseract对复杂验证码效果一般,需要配合精细的预处理(灰度化、二值化、去噪)。import pytesseract from PIL import Image # … 预处理图片 … text = pytesseract.image_to_string(processed_image, config=‘--psm 8 --oem 3 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ’) # `--psm 8` 表示将图片视为单个单词,`--oem 3` 使用默认OCR引擎,`-c tessedit_char_whitelist=…` 限制识别的字符集,能大幅提升准确率。
一个最重要的心得:没有银弹。图片验证码技术本身也在不断进化(滑动拼图、点选文字、空间推理等)。本文介绍的方法主要针对传统的字符图片验证码。当遇到更复杂的交互式验证码时,可能需要结合图像识别、轨迹模拟等多种技术,甚至需要更深入的分析。自动化与反自动化是一场持续的博弈,我们的工具箱也需要不断更新。