前言

在自动化测试与网络爬虫开发中,滑块缺口验证码(如网易易盾、极验等)是最常见的反爬风控手段之一。这类验证码通常由两部分组成:一张带有缺口的背景大图,以及一张与之匹配的滑块拼图小图。用户需要将滑块拖动到背景图中的缺口位置才能通过人机校验。

本文将以某在线评测平台(PTA 登录页)的滑块验证码为例,详细讲解如何使用 Python + Selenium + OpenCV 实现滑块验证码的自动化识别与绕过。整个方案涵盖:浏览器反爬检测规避 ➔ 验证码图片提取 ➔ OpenCV 边缘检测与模板匹配测距 ➔ 网页缩放比例换算 ➔ 拟人化微抖动轨迹拖拽

典型缺口滑块验证码示例


一、技术实现原理与流程架构

破解滑块验证码的核心在于两大关键环节:“看准距离”(机器视觉识别缺口位置)与“滑得像人”(模拟真实用户的鼠标轨迹)。整体处理流程如下:

  1. Selenium 自动化触发:模拟输入账号密码,触发滑块验证码弹出;
  2. 图片无损抓取:定位背景图与滑块小图的 src 地址,通过 requests + PIL 下载保存至本地;
  3. OpenCV 图像处理与模板匹配
    • 通道增强:将 RGB 图片转换为带 Alpha 透明度通道的 4 通道图;
    • 高斯滤波与 Canny 边缘检测:过滤背景噪点,提取背景缺口与滑块的轮廓边缘;
    • 模板匹配(cv2.matchTemplate):使用 TM_CCOEFF_NORMED 算法匹配最佳重合区域,计算出水平移动距离;
  4. 分辨率缩放校准:根据图片物理分辨率与网页 CSS 显示分辨率的比例换算实际移动距离;
  5. ActionChains 拟人化滑动:加入 Y 轴微小扰动与分段停顿,绕过行为轨迹风控,完成校验。

二、分步核心实现与代码详解

第 1 步:Selenium 驱动初始化与反爬特征伪装

为了防止目标平台在加载时直接通过 navigator.webdriver 判定为自动化脚本,我们需要在 ChromeOptions 中进行严格的伪装配置:

def __init__(self):
    """初始化浏览器配置与等待机制"""
    super(CrackSlider, self).__init__()
    self.opts = webdriver.ChromeOptions()
    # 禁用日志与自动化扩展
    self.opts.add_experimental_option('excludeSwitches', ['enable-logging', 'enable-automation'])
    self.opts.add_experimental_option('useAutomationExtension', False)
    # 禁用由自动化控制的 Blink 特征
    self.opts.add_argument("--disable-blink-features=AutomationControlled")
    
    chrome_path = r"./chromedriver.exe"  # ChromeDriver 路径
    self.driver = webdriver.Chrome(chrome_path, options=self.opts)
    self.url = 'https://pintia.cn/auth/login'  # 目标登录地址
    self.wait = WebDriverWait(self.driver, 10)

第 2 步:触发验证码并下载背景图与滑块图

定位页面输入框填入测试账号,点击登录触发验证码弹窗后,通过类名获取背景大图和滑块拼图的图片链接并保存:

def get_pic(self):
    """访问目标页面,触发并保存滑块与背景两张图片"""
    self.driver.get(self.url)
    time.sleep(3)
    
    # 输入账号密码并点击登录触发验证码
    self.driver.find_element(by=By.XPATH, value='//*[@id="username"]').send_keys("test_user@example.com")
    self.driver.find_element(by=By.XPATH, value='//*[@id="password"]').send_keys("password123")
    self.driver.find_element(by=By.XPATH, value='//*[@id="sparkling-daydream"]/div[2]/div/div[2]/div[2]/button/div/div').click()
    
    time.sleep(3)  # 等待验证码图片充分渲染加载
    
    # 获取背景大图与滑块小图的 src 地址
    target_link = self.driver.find_element(By.CLASS_NAME, "yidun_bg-img").get_attribute('src')
    template_link = self.driver.find_element(By.CLASS_NAME, "yidun_jigsaw").get_attribute('src')
    
    # 内存流式读取并保存
    target_img = Image.open(BytesIO(requests.get(target_link).content))
    template_img = Image.open(BytesIO(requests.get(template_link).content))
    
    target_img.save('PTA_A.jpg')    # 背景大图(含缺口)
    template_img.save('PTA_B.png')  # 滑块拼图(待匹配滑块)

获取到的两张图片如下:

① 背景大图(含缺口):

含缺口的背景大图

② 滑块拼图:

待匹配的滑块小图


第 3 步:OpenCV 图像预处理与模板匹配(核心算法)

直接拿彩色原图进行匹配容易受到背景色彩渐变和光影干扰,因此我们先对图像进行通道转换、灰度化、高斯滤波去噪及 Canny 算子边缘提取,将匹配转化为“纯轮廓”的相似度比对,识别率可接近 100%:

def add_alpha_channel(img):
    """为三通道 JPG 图像增加 Alpha 透明通道,统一数据结构并增强抗噪性"""
    r_channel, g_channel, b_channel = cv2.split(img)
    alpha_channel = np.ones(b_channel.shape, dtype=b_channel.dtype) * 255
    img_new = cv2.merge((r_channel, g_channel, b_channel, alpha_channel))
    return img_new


def handel_img(img):
    """图像预处理:灰度化 + 高斯模糊滤波 + Canny边缘提取"""
    imgGray = cv2.cvtColor(img, cv2.COLOR_RGBA2GRAY)  # 转换为灰度图
    imgBlur = cv2.GaussianBlur(imgGray, (5, 5), 1)     # 5x5 高斯核去噪
    imgCanny = cv2.Canny(imgBlur, 60, 60)             # Canny 边缘检测算子
    return imgCanny


def match(img_jpg_path, img_png_path):
    """利用 matchTemplate 计算滑块在背景大图中的水平移动像素距离"""
    # 读取两张图片
    img_jpg = cv2.imread(img_jpg_path, cv2.IMREAD_UNCHANGED)
    img_png = cv2.imread(img_png_path, cv2.IMREAD_UNCHANGED)

    # 若为 3 通道则转为 4 通道
    if img_jpg.shape[2] == 3:
        img_jpg = add_alpha_channel(img_jpg)

    # 提取轮廓边缘
    img = handel_img(img_jpg)
    small_img = handel_img(img_png)

    # 模板匹配:计算归一化相关系数
    res = cv2.matchTemplate(img, small_img, cv2.TM_CCOEFF_NORMED)
    
    # 寻找匹配度最高的位置坐标
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
    
    # 返回最佳匹配点的水平 X 坐标
    distance = max_loc[0]
    return distance

第 4 步:物理像素与网页 CSS 显示比例换算

很多开发者在计算完缺口位置后直接传入 Selenium 却发现滑块滑动不到位,原因是:下载下来的原始图片分辨率 与 网页上 CSS 渲染的图片尺寸不一致!

例如:

  • 图片原始物理宽度为 300px(可通过系统文件属性查看):

图片物理分辨率查看

  • 网页前端 CSS 实际渲染的显示宽度为 320px(通过 F12 审查元素查看):

网页元素渲染尺寸查看

比例换算公式

# 实际滑动距离 = OpenCV识别出的像素距离 / 图片实际物理宽度 * 网页显示宽度
actual_distance = distance / original_width * display_width

第 5 步:ActionChains 模拟拟人化轨迹滑动

很多网上的教程会推荐套用匀加速、匀减速物理公式 s = vt + 0.5at²,但风控系统很容易识破这类单维度的规则轨迹。最简单且极高通过率的实战秘诀在于:加入小幅度的 Y 轴抖动与分段停顿微调

def crack_slider(self, distance):
    """模拟人工按压拖拽滑块,加入微小的 Y 轴抖动"""
    slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'yidun_slider__icon')))
    
    # 1. 鼠标点击并按住滑块
    ActionChains(self.driver).click_and_hold(slider).perform()
    
    # 2. 分段移动:快速前移大段,加入微小 Y 偏移(模拟手抖)
    ActionChains(self.driver).move_by_offset(xoffset=distance - 5, yoffset=0.81).perform()
    ActionChains(self.driver).move_by_offset(xoffset=3, yoffset=0.77).perform()
    ActionChains(self.driver).move_by_offset(xoffset=2, yoffset=-1.30).perform()
    
    # 3. 停顿 0.5~1 秒,模拟人类观察确认
    ActionChains(self.driver).pause(1).perform()
    
    # 4. 松开鼠标
    ActionChains(self.driver).release().perform()
    time.sleep(0.5)
    
    # 5. 校验是否成功,失败则自动触发重试机制
    try:
        wait = WebDriverWait(self.driver, 5)
        success_el = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="success_flag_element"]')))
        print("[✓] 滑块验证码校验成功!")
    except TimeoutException:
        print("[!] 校验超时或未通过,自动重新识别并滑动...")
        self.get_pic()
        new_dist = match('PTA_A.jpg', 'PTA_B.png') + 9
        self.crack_slider(new_dist)
    finally:
        self.driver.quit()

三、完整可运行源代码

以下为经过全面规范化整合与注释的完整代码:

# -*- coding: utf-8 -*-
"""
项目:基于 Selenium + OpenCV 的缺口滑块验证码全自动识别与绕过
环境要求:Python 3.7+, selenium, opencv-python, numpy, Pillow, requests
"""

import time
import requests
from io import BytesIO
import cv2
import numpy as np
from PIL import Image

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait


class CrackSlider:
    """滑块验证码自动化破解类"""

    def __init__(self):
        super(CrackSlider, self).__init__()
        self.opts = webdriver.ChromeOptions()
        self.opts.add_experimental_option('excludeSwitches', ['enable-logging', 'enable-automation'])
        self.opts.add_experimental_option('useAutomationExtension', False)
        self.opts.add_argument("--disable-blink-features=AutomationControlled")

        chrome_path = r"./chromedriver.exe"
        self.driver = webdriver.Chrome(executable_path=chrome_path, options=self.opts)
        self.url = 'https://pintia.cn/auth/login'
        self.wait = WebDriverWait(self.driver, 10)

    def get_pic(self):
        """触发并抓取验证码图片"""
        self.driver.get(self.url)
        time.sleep(3)

        # 填写登录表单
        self.driver.find_element(By.XPATH, '//*[@id="username"]').send_keys("2313859086@qq.com")
        self.driver.find_element(By.XPATH, '//*[@id="password"]').send_keys("bo18212410475")
        self.driver.find_element(By.XPATH, '//*[@id="sparkling-daydream"]/div[2]/div/div[2]/div[2]/button/div/div').click()
        time.sleep(3)

        # 抓取图片链接并下载
        target_link = self.driver.find_element(By.CLASS_NAME, "yidun_bg-img").get_attribute('src')
        template_link = self.driver.find_element(By.CLASS_NAME, "yidun_jigsaw").get_attribute('src')

        target_img = Image.open(BytesIO(requests.get(target_link).content))
        template_img = Image.open(BytesIO(requests.get(template_link).content))

        target_img.save('PTA_A.jpg')
        template_img.save('PTA_B.png')

    def crack_slider(self, distance):
        """模拟人手滑动滑块"""
        slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'yidun_slider__icon')))
        
        ActionChains(self.driver).click_and_hold(slider).perform()
        # 拟人化微抖动位移
        ActionChains(self.driver).move_by_offset(xoffset=distance - 5, yoffset=0.81).perform()
        ActionChains(self.driver).move_by_offset(xoffset=3, yoffset=0.77).perform()
        ActionChains(self.driver).move_by_offset(xoffset=2, yoffset=-1.30).perform()
        ActionChains(self.driver).pause(1).perform()
        ActionChains(self.driver).release().perform()
        time.sleep(0.5)

        try:
            wait = WebDriverWait(self.driver, 5)
            # 校验成功后跳转的特征元素
            success_elem = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="sparkling-daydream"]/div[1]/div/div[1]/a[2]')))
            print("[✓] 验证通过,登录成功!")
        except TimeoutException:
            print("[!] 滑动未通过,重新拉取并重试...")
            self.get_pic()
            dist = match('PTA_A.jpg', 'PTA_B.png') + 9
            self.crack_slider(dist)
        finally:
            self.driver.quit()


def add_alpha_channel(img):
    """为 JPG 图片补充 Alpha 透明度通道"""
    r, g, b = cv2.split(img)
    alpha = np.ones(b.shape, dtype=b.dtype) * 255
    return cv2.merge((r, g, b, alpha))


def handel_img(img):
    """图像预处理:灰度化 + 高斯滤波 + Canny边缘提取"""
    gray = cv2.cvtColor(img, cv2.COLOR_RGBA2GRAY)
    blur = cv2.GaussianBlur(gray, (5, 5), 1)
    canny = cv2.Canny(blur, 60, 60)
    return canny


def match(img_jpg_path, img_png_path):
    """利用模板匹配算法测算缺口距离"""
    img_jpg = cv2.imread(img_jpg_path, cv2.IMREAD_UNCHANGED)
    img_png = cv2.imread(img_png_path, cv2.IMREAD_UNCHANGED)

    if img_jpg.shape[2] == 3:
        img_jpg = add_alpha_channel(img_jpg)

    img = handel_img(img_jpg)
    small_img = handel_img(img_png)

    # 归一化相关系数匹配
    res = cv2.matchTemplate(img, small_img, cv2.TM_CCOEFF_NORMED)
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
    
    # 返回最佳匹配位置的 X 坐标
    return max_loc[0]


if __name__ == '__main__':
    cs = CrackSlider()
    cs.get_pic()
    
    # 计算缺口坐标
    distance = match('PTA_A.jpg', 'PTA_B.png')
    # 根据实际微调修正量(+9 像素)
    distance = distance + 9
    
    print(f"[*] 识别到滑块需要移动的距离为: {distance} 像素")
    cs.crack_slider(distance)

四、总结与实战经验

  1. 识别率的核心在边缘提取:不要直接对彩色原图做模板匹配,先进行 GaussianBlur + Canny 提取轮廓,能极大削弱背景复杂图案和水印的干扰。
  2. 必须考虑分辨率缩放:当发现识别位置很准但总是滑不到位时,优先检查图片的物理像素与 CSS 渲染尺寸比例
  3. 避免过于规律的直线轨迹:简单的匀速滑动或纯一维水平滑动极易被行为风控捕捉,在 ActionChains 中添加微小的随机 Y 轴偏移(如 0.8px-1.3px)即可轻松过审。

希望本篇实战教程对您有所帮助!如果有任何疑问或测试反馈,欢迎在评论区一起讨论交流。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐