1. 从“看得见”到“点得准”:滑块验证码的自动化破局思路
做爬虫的朋友,对滑块验证码这个“老朋友”肯定又爱又恨。爱的是,它比复杂的点选、文字识别验证码看起来要简单直观得多;恨的是,当你想用代码模拟人类“一拖而过”的动作时,会发现处处是坑。坐标计算不准、轨迹模拟不像、被风控系统识别为机器操作……这些问题让不少自动化脚本折戟沉沙。
传统的破解思路,比如直接计算缺口位置像素差,在背景图经过高斯模糊、随机噪点干扰后,准确率会大幅下降。而一些基于深度学习的方案,虽然效果好,但模型训练、部署的成本对很多轻量级爬虫任务来说又显得过于沉重。有没有一种折中的、稳定且易于实现的方案?这就是我们今天要讨论的核心:结合 OpenCV 进行图像识别定位,再通过 Selenium 精细化模拟人工操作。
这套方案的价值在于,它不依赖于任何第三方收费API,完全基于开源技术栈,将图像处理和浏览器自动化这两个领域的工具结合起来,形成了一个闭环解决方案。你不仅能用它来应对滑块验证码,其图像识别和轨迹模拟的思路,也能迁移到其他类似的交互式验证场景中。接下来,我将拆解整个流程,从环境搭建、核心原理到每一步的代码实现与避坑指南,手把手带你构建一个健壮的滑块验证码自动化通过工具。
2. 战场准备:环境搭建与核心库的“正确”安装姿势
工欲善其事,必先利其器。我们的技术栈非常明确:Python 作为主语言,Selenium 用于驱动浏览器模拟操作,OpenCV 用于图像分析与缺口定位。听起来很简单,但安装环节就有几个关键点,直接关系到后续代码能否顺利运行。
2.1 Python 与 Selenium 环境配置
首先确保你有一个 Python 3.7 及以上版本的环境。我强烈建议使用虚拟环境(如venv或conda)来管理项目依赖,避免包冲突。
# 创建并激活虚拟环境(以 venv 为例) python -m venv slider_env source slider_env/bin/activate # Linux/Mac # 或 slider_env\Scripts\activate # Windows接下来安装 Selenium。直接用 pip 安装最新版即可:
pip install selenium但 Selenium 本身只是一个控制浏览器的“遥控器”,它需要一个“浏览器驱动”才能实际工作。这里以最常用的 Chrome 浏览器为例。
- 查看 Chrome 版本:打开 Chrome 浏览器,在地址栏输入
chrome://version/,查看第一行的“Google Chrome”版本号(例如 120.0.6099.110)。 - 下载对应驱动:访问 ChromeDriver 的官方下载站点或国内镜像站。关键点在于驱动版本必须与你的 Chrome 主版本号完全一致。比如 Chrome 是 120.x.x.x,那么你就必须下载版本号为 120.x.x.x 的 ChromeDriver。大版本不匹配一定会报错。
- 放置驱动:下载的
chromedriver(Windows 是chromedriver.exe)可执行文件,需要放在系统 PATH 环境变量包含的目录下(如/usr/local/bin或C:\Windows),或者更简单的做法是,在代码中指定它的绝对路径。
一个常见的坑是,浏览器会自动更新,但驱动忘记更新,导致某天脚本突然无法运行。一种稳健的做法是在代码开始时,通过webdriver的Service对象来指定驱动路径,便于管理。
2.2 OpenCV-Python 的安装与“隐形”依赖
安装 OpenCV 对于 Python 通常是指安装opencv-python这个包。然而,这里有一个新手极易踩中的大坑。
如果你直接pip install opencv-python,在编写代码并导入import cv2时,很可能会遇到一个令人困惑的错误:ImportError: libGL.so.1: cannot open shared object file: No such file or directory(Linux)或类似的动态链接库缺失问题。这是因为opencv-python是一个“头”包,它依赖于系统底层的一些图形库(如 GTK、Qt、libGL等),而这些库在纯净的 Python 环境或某些服务器系统(如 Docker 基础镜像)中并不预装。
解决方案是安装opencv-python-headless。这个版本移除了对 GUI 功能(如cv2.imshow())的依赖,只保留核心的图像处理算法,非常适合服务器或无头环境下的自动化任务,并且避免了上述依赖问题。
pip install opencv-python-headless如果你确实需要在本地开发时显示图片进行调试,可以同时安装标准版,但用于生产或服务器部署时,headless版本是更安全、更轻量的选择。我们接下来的识别算法完全基于headless版本的功能,无需担心功能缺失。
2.3 辅助工具库
我们可能还需要numpy(OpenCV 处理后的图像本质是 numpy 数组)和Pillow(PIL,用于可能的图像格式转换或预处理),它们通常会被opencv-python自动安装,但为了明确,也可以一并安装:
pip install numpy pillow至此,我们的核心战场已经准备就绪。接下来,进入最核心的环节:如何让计算机“看见”并“找到”那个滑块缺口。
3. 核心原理拆解:OpenCV 如何“看见”缺口
滑块验证码通常由两张图组成:一张是完整的、带有缺口阴影的背景图,另一张是那个需要拖动的、孤立的小滑块图。我们的目标是在背景图上找到与小滑块图最匹配的位置,也就是缺口的位置。这本质上是一个模板匹配问题。
3.1 模板匹配算法选择
OpenCV 提供了多种模板匹配方法,通过cv2.matchTemplate函数实现。它会在背景图(大图)上滑动滑块图(模板),计算每个位置的相似度,最终得到一个结果矩阵,其中数值最极值(最大或最小,取决于方法)的点,就是最佳匹配位置。
常用的方法有:
cv2.TM_CCOEFF_NORMED:计算归一化相关系数。值越接近1,匹配度越高。这是我们最常用的方法,它对图像的亮度变化有一定鲁棒性。cv2.TM_SQDIFF_NORMED:计算归一化平方差。值越接近0,匹配度越高。
为什么首选TM_CCOEFF_NORMED?因为滑块验证码的背景往往有渐变、阴影,缺口边缘的像素值变化不是简单的“有无”,而是存在一定的相关性。相关系数方法能更好地捕捉这种结构相似性,抗干扰能力比单纯的像素差方法更强。
3.2 图像预处理:提升匹配成功率的关键
直接拿原始图片进行模板匹配,成功率可能不高。因为验证码图片常常会加入干扰,比如:
- 高斯模糊:让边缘变得不清晰。
- 随机噪点:在图片上撒上彩色或黑白的像素点。
- 背景干扰线:一些曲线或直线。
为了提高匹配的鲁棒性,我们需要对图片进行预处理。核心思路是强化边缘,弱化颜色和纹理干扰。最有效的预处理步骤之一是边缘检测。
我们可以使用 Canny 边缘检测算法,或者更简单的,将图像转换为灰度图后,应用阈值处理或 Sobel 算子来提取边缘。
import cv2 import numpy as np def preprocess_image(image): """预处理图像,增强边缘特征""" # 1. 转换为灰度图,减少颜色维度干扰 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 2. 应用高斯模糊,平滑微小噪点(内核大小需根据图片调整,通常用3x3或5x5) blurred = cv2.GaussianBlur(gray, (3, 3), 0) # 3. 使用Canny边缘检测(或Sobel)提取边缘 # Canny阈值需要根据实际情况调整 edges = cv2.Canny(blurred, threshold1=50, threshold2=150) return edges经验之谈:预处理没有“银弹”。不同的验证码风格需要不同的预处理参数。例如,有些验证码的缺口边缘非常模糊,Canny 的阈值就要调低;有些背景干扰线很强,可能需要先进行形态学操作(如开运算)去除细线。在实际项目中,你需要截取一批验证码图片,手动调整预处理流程和参数,观察处理后的边缘图像,确保缺口形状能被较好地保留,而背景干扰被最大程度抑制。这是一个需要耐心调试的过程。
3.3 执行匹配与位置计算
预处理后,我们对背景大图和滑块小图都进行同样的处理,然后进行模板匹配。
def find_gap_position(bg_image_path, slider_image_path): """识别缺口位置""" # 读取图片 bg_img = cv2.imread(bg_image_path) # 背景大图 slider_img = cv2.imread(slider_image_path) # 滑块小图 # 预处理 bg_processed = preprocess_image(bg_img) slider_processed = preprocess_image(slider_img) # 执行模板匹配 # bg_processed 是“大图”, slider_processed 是“模板” result = cv2.matchTemplate(bg_processed, slider_processed, cv2.TM_CCOEFF_NORMED) # 找到最大值(最佳匹配)的位置 min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) top_left = max_loc # 因为用的是CCOEFF_NORMED,取最大值位置 # top_left 是匹配区域左上角的坐标 (x, y) # 缺口在背景图中的x坐标通常是这个top_left的x值。 # 但注意:有些验证码的滑块图是带有透明背景或多余空白边的,匹配到的位置可能需要偏移。 gap_x = top_left[0] # 一个重要的修正:匹配到的是滑块“形状”的位置,但我们需要的是滑块“开始拖动”的位置。 # 通常,滑块拼图初始位置在背景图最左侧,其中心点或左侧边缘与背景图左边缘对齐。 # 我们计算出的 gap_x 是缺口左上角的x坐标。而滑块的移动距离,应该是 gap_x 减去滑块初始位置的x坐标。 # 假设滑块初始位置在x=0处,那么移动距离就是 gap_x。 # 但更严谨的做法是:在匹配前,先获取滑块元素在网页上的位置,计算其初始x坐标。 move_distance = gap_x # 这里先简单认为初始x为0 return move_distance这里引出了一个关键细节:cv2.matchTemplate匹配得到的位置(x, y),是模板(滑块图)左上角在背景图中的坐标。然而,我们最终需要传递给 Selenium 的拖动距离,是滑块需要水平移动的像素值。这个值通常是gap_x减去滑块拼图初始位置在背景图上的x坐标。如果验证码的滑块初始是紧贴背景图左边缘的,那么这个初始坐标可能就是0。但有些验证码设计会让滑块初始位置有一个小偏移,这就需要我们通过截图或计算来校准。
如何获取这个初始偏移?一个实用的方法是:在浏览器中,先获取滑块拼图元素和背景图元素的位置和大小,通过 JavaScript 或 Selenium 的location、size属性,计算出滑块拼图相对于背景图容器的初始位置。这涉及到下一部分的浏览器自动化操作。
4. Selenium 自动化操作:模拟“人类”的拖动
识别出缺口位置只是成功了一半。如何让 Selenium 精准、平滑地将滑块拖到目标位置,并且不被反爬机制判定为机器操作,是另一半挑战。
4.1 定位元素与获取关键坐标
首先,我们需要用 Selenium 定位到几个关键元素:
- 背景图元素:用于截图,供 OpenCV 分析。
- 滑块拼图元素:就是我们要拖动的那个小块。
- 滑块轨道/容器元素:有时用于计算相对位置。
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains import time driver = webdriver.Chrome() # 或指定Service路径 driver.get("你的目标网址") # 假设通过检查元素,找到背景图和滑块的选择器 # 实际选择器需要根据目标网站调整,可能是id, class, xpath等 bg_element = driver.find_element(By.CLASS_NAME, 'geetest_canvas_bg') slider_element = driver.find_element(By.CLASS_NAME, 'geetest_slider_button') # 获取元素的位置和大小 bg_location = bg_element.location bg_size = bg_element.size slider_location = slider_element.location print(f"背景图位置: {bg_location}, 大小: {bg_size}") print(f"滑块位置: {slider_location}")这里有一个巨坑:element.location返回的是该元素相对于整个浏览器窗口左上角的坐标。而cv2.imread读取的截图,其坐标系原点在图片的左上角。如果我们直接截取整个网页或者背景图元素,那么从 OpenCV 计算出的gap_x(图片内x坐标)需要经过转换,才能对应到浏览器坐标系中的绝对x坐标。
更可靠的做法是:直接对背景图元素进行截图。这样,截图本身的像素坐标就与元素内部的坐标一一对应,无需复杂的坐标系转换。
from PIL import Image import io # 对背景图元素进行截图 bg_screenshot = bg_element.screenshot_as_png # 将二进制数据转换为PIL Image对象,方便后续处理或保存 bg_image = Image.open(io.BytesIO(bg_screenshot)) bg_image.save('background.png') # 同样地,对滑块拼图元素截图(如果需要) slider_screenshot = slider_element.screenshot_as_png slider_image = Image.open(io.BytesIO(slider_screenshot)) slider_image.save('slider.png')通过元素截图,我们确保了 OpenCV 处理的图片范围与网页上显示的区域完全一致,计算出的像素距离就是我们需要在该元素内部移动的距离。
4.2 计算精确的拖动距离
现在,我们结合 OpenCV 的识别结果和 Selenium 获取的元素信息,来计算精确的拖动距离。
假设通过上一节的find_gap_position函数,我们得到了缺口在背景图截图中的x坐标gap_x_pixel(单位:像素)。
那么,滑块需要移动的距离move_distance就是:move_distance = gap_x_pixel
为什么这么简单?因为我们对bg_element进行了局部截图,gap_x_pixel已经是相对于这个截图左上角(也就是bg_element左上角)的x坐标。而滑块拼图slider_element的初始位置,通常就在这个背景图容器的左侧边缘(x坐标可能为0或一个很小的固定值)。因此,gap_x_pixel几乎就是滑块需要水平移动的绝对像素距离。
但为了应对更复杂的情况(比如滑块初始位置有偏移),我们可以计算一个偏移量:slider_offset = slider_location['x'] - bg_location['x']。这表示滑块相对于背景图左上角的初始x偏移。 那么,最终移动距离为:move_distance = gap_x_pixel - slider_offset。
4.3 设计拟人化拖动轨迹
直接让 Selenium 将滑块瞬间移动到目标位置是百分百会被识别为机器的。人类拖动滑块有加速、减速、甚至轻微回弹的过程。我们需要用ActionChains来模拟这个轨迹。
核心思路是:将总移动距离分解成多段小移动,每段移动的时间和位移量符合物理运动规律(如先加速后减速)。
def get_track(distance): """根据总距离生成移动轨迹列表(模拟先加速后减速)""" track = [] current = 0 mid = distance * 4 / 5 # 前4/5段加速,后1/5段减速 t = 0.2 # 计算间隔 v = 0 while current < distance: if current < mid: a = 2 # 加速阶段的加速度 else: a = -3 # 减速阶段的加速度 v0 = v v = v0 + a * t move = v0 * t + 0.5 * a * t * t current += move track.append(round(move)) # 确保最终刚好到达目标点,处理小数误差 track.append(distance - sum(track)) # 过滤掉移动距离为0的步骤 track = [x for x in track if x != 0] return track # 生成轨迹 track_list = get_track(move_distance) print(f"移动轨迹: {track_list}")这个get_track函数模拟了一个简单的匀变速运动。在实际应用中,你可以让轨迹更复杂,比如加入随机的小抖动、短暂的停顿,使其更接近真人操作。轨迹的精细程度是绕过高级风控的关键。
4.4 执行拖动并处理结果
使用ActionChains执行拖动操作。注意,要先点击并按住滑块,然后按照轨迹一步步移动,最后释放。
# 初始化ActionChains actions = ActionChains(driver) # 点击并按住滑块元素 actions.click_and_hold(slider_element).perform() # 按照轨迹移动 for move in track_list: actions.move_by_offset(xoffset=move, yoffset=0).perform() # 每移动一小段,加入一个极短的随机延迟,更像人类 time.sleep(random.uniform(0.001, 0.005)) # 释放鼠标 actions.release().perform() time.sleep(1) # 等待验证结果执行完毕后,页面通常会验证滑块位置。我们需要检测是否成功。成功标志可能是某个元素消失、出现成功提示文字、或者页面发生跳转。你需要根据目标网站的具体情况来编写检测逻辑。
# 示例:检测成功提示元素是否出现 try: success_element = driver.find_element(By.XPATH, "//div[contains(text(), '验证成功')]") print("滑块验证通过!") except: print("滑块验证可能失败,尝试重新识别或记录日志。") # 可以在这里加入重试逻辑,比如重新截图、识别、拖动5. 实战中的“坑”与精细化调优策略
将上述步骤组合起来,一个基本的自动化流程就完成了。但在实际生产环境中,你会遇到各种各样的问题。下面分享几个我踩过的坑和对应的解决方案。
5.1 验证码图片的动态加载与获取
很多网站的验证码图片不是简单的<img>标签,其src可能是动态生成的 Base64 数据或 Blob 链接,甚至是通过 Canvas 绘制的。直接用element.get_attribute('src')可能拿不到图片地址。
解决方案:
- 对于 Canvas 绘制:如我们之前所做,使用
element.screenshot_as_png是最通用、最可靠的方法。它能直接获取元素渲染后的像素数据。 - 对于 Base64 图片:可以通过
get_attribute('src')获取到以data:image/png;base64,开头的字符串,然后解码为图片数据。import base64 src = bg_element.get_attribute('src') if src.startswith('data:image'): # 去掉头部信息 base64_data = src.split(',')[1] image_data = base64.b64decode(base64_data) with open('bg.png', 'wb') as f: f.write(image_data) - 对于 Blob 链接:稍微复杂,可能需要通过 JavaScript 在浏览器上下文中将 Blob 转换为 Data URL 再获取。
经验:优先尝试screenshot_as_png,它几乎适用于所有可见元素,省去了分析复杂前端代码的麻烦。
5.2 识别失败与重试机制
OpenCV 的模板匹配不是 100% 准确的。干扰过强、图片缩放、缺口边缘过于模糊都可能导致匹配位置错误。
应对策略:
- 置信度判断:
cv2.matchTemplate返回的max_val(最大匹配值)是一个重要的置信度指标。可以设置一个阈值(如 0.7),只有当max_val大于该阈值时,才认为识别结果可信。否则,触发重试(如刷新验证码)。min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) if max_val < 0.7: print(f"匹配置信度过低: {max_val}, 可能识别错误,尝试刷新验证码。") # 触发刷新验证码的逻辑 return None - 多尺度匹配:有些网站在不同设备或缩放比例下,验证码图片的实际显示尺寸可能微调。可以在匹配前,将模板图在小范围内进行比例缩放(如 0.95, 1.0, 1.05),进行多次匹配,取置信度最高的一次。
- 重试逻辑:将整个识别-拖动流程封装在一个函数中,并加入重试计数器。如果验证失败(通过检测成功元素判断),则重新执行流程,最多重试 N 次。
5.3 轨迹模拟的进阶对抗
简单的匀加速轨迹可能被一些风控系统破解。更高级的模拟需要考虑:
- 随机化:总移动时间、加速段和减速段的比例、每步之间的延迟都可以在一定范围内随机。
- 人类抖动:在移动轨迹中,偶尔加入微小的、随机的垂直方向(y轴)移动,模拟手抖。
- 非匀速停顿:在移动过程中,随机插入几次 50-150 毫秒的短暂停顿。
- 轨迹回放:录制真人拖动滑块的鼠标坐标序列,然后让 Selenium 复现这个序列。这是最拟真但实现成本也最高的方法。
def human_like_move(actions, element, track_x): """更拟人化的移动,包含随机停顿和微小抖动""" actions.click_and_hold(element).perform() time.sleep(random.uniform(0.1, 0.3)) # 按下后停顿一下 for i, move_x in enumerate(track_x): # 加入微小的垂直抖动(范围-2到2像素) move_y = random.randint(-2, 2) if random.random() > 0.7 else 0 actions.move_by_offset(xoffset=move_x, yoffset=move_y).perform() # 随机加入停顿 if random.random() > 0.9 and i < len(track_x) - 2: time.sleep(random.uniform(0.05, 0.15)) else: time.sleep(random.uniform(0.002, 0.008)) # 基础移动间隔 actions.release().perform()5.4 浏览器指纹与环境伪装
即使滑块拖得再像人,如果你的浏览器指纹很“干净”或者很“机器”,也可能在整体风控评分中暴露。Selenium 驱动的浏览器有一些特征可以被检测到,例如navigator.webdriver属性为true。
缓解措施:
- 使用 undetected-chromedriver:这是一个修改过的 Chromedriver,可以隐藏部分自动化特征。但它可能与 Selenium 新版本存在兼容性问题,需谨慎使用。
- 注入 JavaScript 修改属性:在页面加载前,通过
execute_cdp_cmd执行 Chrome DevTools Protocol 命令来覆盖某些属性。driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); ''' }) - 使用真实的用户代理(User-Agent)和合理的浏览器窗口尺寸。
- 启用浏览器正常功能:如允许通知、设置语言、时区等,让浏览器环境看起来更真实。
需要注意的是,与风控的对抗是持续升级的。没有一劳永逸的方案,上述方法能提高通过率,但无法保证 100%。对于核心业务,需要准备备选方案,如打码平台、人工处理队列等。
6. 完整代码示例与集成思路
将以上所有环节整合,形成一个完整的、带有基础错误处理和重试机制的示例脚本。
import cv2 import numpy as np from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time import random import io from PIL import Image class SliderCaptchaSolver: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def preprocess_image(self, image): """图像预处理:转灰度、模糊、边缘检测""" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (3, 3), 0) # 尝试调整Canny阈值以适应不同图片 edges = cv2.Canny(blurred, 50, 150) return edges def get_element_screenshot(self, element, save_path=None): """获取WebElement的截图,返回cv2图像格式""" png_data = element.screenshot_as_png pil_image = Image.open(io.BytesIO(png_data)) if save_path: pil_image.save(save_path) # 将PIL Image转换为OpenCV格式 (BGR) opencv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) return opencv_image def calculate_slide_distance(self, bg_image, slider_image): """计算滑块需要移动的距离""" # 预处理 bg_processed = self.preprocess_image(bg_image) slider_processed = self.preprocess_image(slider_image) # 模板匹配 result = cv2.matchTemplate(bg_processed, slider_processed, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) print(f"模板匹配置信度: {max_val}") if max_val < 0.6: # 置信度阈值 raise ValueError(f"匹配置信度过低({max_val}),无法准确定位缺口。") top_left = max_loc # 假设滑块初始位置在背景图最左端,移动距离就是缺口左上角x坐标 # 更复杂的场景需要根据滑块实际初始位置调整 slide_distance = top_left[0] # 可选:由于滑块图可能包含透明边,匹配位置可能需要微调 # 例如,如果滑块图宽度是w,缺口实际位置可能在 top_left[0] + w/2 附近 # 这里需要根据实际验证码样式调整 # slide_distance = top_left[0] + slider_image.shape[1] // 2 return slide_distance def generate_track(self, distance): """生成模拟人类拖动的轨迹""" track = [] current = 0 mid = distance * 0.8 t = 0.2 v = 0 while current < distance: if current < mid: a = 2 + random.random() * 1 # 随机加速度 else: a = -3 - random.random() * 1 # 随机减速度 v0 = v v = v0 + a * t move = v0 * t + 0.5 * a * t * t if current + move > distance: move = distance - current current += move track.append(round(move)) # 最后可能还差一点,补上 if sum(track) < distance: track.append(distance - sum(track)) # 加入随机的小停顿和抖动 final_track = [] for i, step in enumerate(track): final_track.append(step) if i < len(track) - 1 and random.random() > 0.8: # 随机插入一个0-2像素的垂直抖动 final_track.append((0, random.randint(-1, 1))) return track def drag_slider(self, slider_element, distance): """执行拖动操作""" track = self.generate_track(distance) actions = ActionChains(self.driver) actions.click_and_hold(slider_element).perform() time.sleep(random.uniform(0.1, 0.3)) for step in track: if isinstance(step, tuple): # 处理抖动步骤 actions.move_by_offset(xoffset=step[0], yoffset=step[1]).perform() else: actions.move_by_offset(xoffset=step, yoffset=0).perform() time.sleep(random.uniform(0.001, 0.01)) # 随机延迟 # 模拟释放前的轻微回弹或抖动 actions.move_by_offset(xoffset=random.randint(-2, 0), yoffset=random.randint(-1, 1)).perform() time.sleep(0.05) actions.release().perform() def solve(self, bg_selector, slider_selector, max_retries=3): """主解决函数""" for attempt in range(max_retries): try: print(f"尝试第 {attempt + 1} 次验证...") # 等待元素出现 bg_element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, bg_selector))) slider_element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, slider_selector))) time.sleep(0.5) # 等待图片加载稳定 # 截图 bg_image = self.get_element_screenshot(bg_element, f'bg_attempt_{attempt}.png') slider_image = self.get_element_screenshot(slider_element, f'slider_attempt_{attempt}.png') # 计算距离 distance = self.calculate_slide_distance(bg_image, slider_image) print(f"计算出的移动距离: {distance} 像素") # 拖动滑块 self.drag_slider(slider_element, distance) # 等待并检查结果,这里需要根据目标网站的成功标识来修改 time.sleep(2) # 示例:检查某个成功元素是否出现 # success_indicator = self.driver.find_elements(By.XPATH, "//div[contains(@class, 'success')]") # if success_indicator: # print("滑块验证成功!") # return True # 暂时假设成功,实际应添加验证逻辑 print("拖动操作完成,请根据页面变化判断是否成功。") return True except Exception as e: print(f"第 {attempt + 1} 次尝试失败: {e}") # 可以在这里触发刷新验证码按钮 # refresh_button = driver.find_element(By.CLASS_NAME, 'geetest_refresh') # refresh_button.click() time.sleep(1) if attempt == max_retries - 1: print("已达到最大重试次数,验证失败。") return False # 使用示例 if __name__ == '__main__': # 初始化浏览器 options = webdriver.ChromeOptions() # 可选:添加一些选项来隐藏自动化特征 # options.add_argument('--disable-blink-features=AutomationControlled') driver = webdriver.Chrome(options=options) # 访问目标页面 driver.get('https://你的目标登录页或验证码页') # 初始化破解器 solver = SliderCaptchaSolver(driver) # 尝试破解验证码,需要替换为实际页面的CSS选择器 # 例如,某网站的验证码背景图class可能是 '.geetest_canvas_bg',滑块是 '.geetest_slider_button' is_success = solver.solve('.geetest_canvas_bg', '.geetest_slider_button', max_retries=3) if is_success: # 验证通过,继续后续操作(如输入账号密码登录) pass else: # 验证失败,采取其他策略 pass # driver.quit()这个类提供了一个基础的框架。在实际项目中,你需要根据目标网站的具体 HTML 结构调整选择器,根据验证码的视觉样式调整图像预处理参数和轨迹生成算法,并根据成功/失败的表现来完善结果检测逻辑。
这套基于 OpenCV + Selenium 的方案,其优势在于可控性强、成本低,并且能让你深入理解滑块验证码对抗的本质。它可能无法应对所有极端情况,但对于大多数中等难度的滑块验证码,经过精心调优后,能达到很高的通过率,是爬虫工程师工具箱中一件非常实用的武器。