滑动验证码

千辛万苦找到了2个不同的滑动验证码的网站,一个是极验的,是以canvas标签放验证图片的,第二个是YY的,使用iframe标签做验证的,两者获取图片的方式会略有不同。该贴仅做技术积累。

canvas验证码识别

canvas的标签比较容易定位:
在这里插入图片描述
我们需要的就是这三个canvas,其中geetest_canvas_fullbg不需要,因为它并不展示display:none;,而且它是原图,我们要的是滑块图geetest_canvas_slice以及缺口图geetest_canvas_bg

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
import base64
import cv2
import numpy as np

def test():
    url = 'https://demos.geetest.com/slide-float.html'
    driver_path = r"E:\selenium\driver\chromedriver.exe"
    chrome_path = r'E:\selenium\chrome\Application\chrome.exe'
    profile_path = r"E:\selenium\data\profiles"
    cache_path = r"E:\selenium\data\caches"
    options = webdriver.ChromeOptions()
    options.binary_location = chrome_path
    options.add_argument('--no-sandbox')
    options.add_argument("--start-maximized")
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument(f'--user-data-dir={profile_path}')
    options.add_argument(f'--disk-cache-dir={cache_path}')
    options.add_argument("--disable-blink-features=AutomationControlled")  # 禁用自动化控制提示
    options.add_experimental_option("excludeSwitches", ["enable-automation"])  # 隐藏自动化标志
    user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    options.add_argument(f"user-agent={user_agent}")
    #options.add_argument("--window-size=1920,1080")
    # 启用 WebGL 和 GPU 加速(部分反爬检测 GPU 支持)
    options.add_argument("--use-gl=desktop")
    options.add_argument("--enable-webgl")
    options.set_capability(
        "goog:loggingPrefs", {"performance": "ALL"}

    )  # 开启日志
    service = Service(executable_path=driver_path)
    driver = webdriver.Chrome(service=service, options=options)
    driver.get(url)
    tcaptcha = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
    #print("元素位置:" + tcaptcha.location)
    # can only concatenate str (not "dict") to str
    tcaptcha.click()
    tcaptcha_slice = WebDriverWait(driver, 15).until(
            EC.visibility_of_element_located((By.CLASS_NAME, 'geetest_canvas_slice')))
    js_bg = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");' # 获取背景图片
    js_slice = 'return document.getElementsByClassName("geetest_canvas_slice")[0].toDataURL("image/png");' # 获取滑块图片
    bg_data = base64.b64decode(driver.execute_script(js_bg).split(',')[1]) # 解码背景图片
    slice_data = base64.b64decode(driver.execute_script(js_slice).split(',')[1]) # 解码滑块图片

    img_bg = cv2.imdecode(np.frombuffer(bg_data, dtype=np.uint8), cv2.IMREAD_COLOR) # 解码背景图片
    img_slice = cv2.imdecode(np.frombuffer(slice_data, dtype=np.uint8), cv2.IMREAD_COLOR) # 解码滑块图片
    # 保存图片
    cv2.imwrite(r'E:\Temp\pic\2\bg4.png', img_bg) # 保存背景图片
    cv2.imwrite(r'E:\Temp\pic\2\slice4.png', img_slice) # 保存滑块图片
	if driver:
        driver.quit()# 退出

这里解释一下为什么要保存到本地:因为cv2.matchTemplate在匹配上存在一些问题,所以将图片下载下来进行匹配并找规律。

js_bg = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");' # 获取背景图片
js_slice = 'return document.getElementsByClassName("geetest_canvas_slice")[0].toDataURL("image/png");' # 获取滑块图片
# 这两句话是获取canvas图片的关键,因为从上面的截图可以发现,无法正常获取内容,没有src!没有url!所以只能通过执行js的方式获取了。

路径分析

将图片文件使用opencv-python读取,然后匹配位置,得到坐标。具体的做法是:

  • 读取图片那文件。
  • 移除阴影部分,当然不移除也行,可能结果上有偏差吧。我自己测试的话,还不如不移除!!!
  • 裁剪图片。因为我发现,如果不裁剪图片,每次计算的结果,或者说是应该移动的变量什么的,每次都是0,进一步发现,是解码的图片除了滑块的位置的BGR数值都是[6,6,6],可能是这些数值影响了匹配结果,那不管了,直接对他们进行裁剪,裁剪到只剩下滑块的大小。
  • 经过变换裁剪之后的图片通道是BGRA4通道,而缺口图片是BGR3通道,所以要进行变换
  • 匹配数值,并验证
def pic_match():

    img_bg = cv2.imread(r'E:\Temp\pic\2\bg4.png')
    img_slice = cv2.imread(r'E:\Temp\pic\2\slice4.png')

裁剪

问的deepseek怎么写的…

def auto_crop_slice(slice_img):
    """
    自动裁剪滑块有效区域(基于亮度阈值)
    返回值:
        cropped: 裁剪后的有效区域图像
        y_start: 有效区域的起始Y坐标(用于对齐背景图)
    """
    # 转换为灰度图
    gray = cv2.cvtColor(slice_img, cv2.COLOR_BGR2GRAY)
    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
    enhanced = clahe.apply(gray)
    # 应用阈值分割
    # 自适应阈值处理
    thresh = cv2.adaptiveThreshold(enhanced, 255,
                                   cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY_INV, 15, 5)

    # 形态学操作(先开运算去噪,再闭运算填充)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    opened = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
    closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2)

    # 寻找轮廓并筛选
    contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not contours:
        return cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA), 0

    # 根据面积和宽高比筛选有效轮廓
    valid_contours = []
    for cnt in contours:
        area = cv2.contourArea(cnt)
        x, y, w, h = cv2.boundingRect(cnt)
        aspect_ratio = w / float(h)
        if area > 100 and 0.5 < aspect_ratio < 2.5:  # 根据实际情况调整
            valid_contours.append(cnt)

    if not valid_contours:
        return cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA), 0

    # 合并有效轮廓
    combined_cnt = np.vstack(valid_contours)
    x, y, w, h = cv2.boundingRect(combined_cnt)

    # 创建透明通道并裁剪
    bgra = cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA)
    mask = np.zeros_like(enhanced)
    cv2.drawContours(mask, [combined_cnt], -1, 255, -1)
    bgra[:, :, 3] = cv2.bitwise_and(bgra[:, :, 3], mask)

    # 二次精确裁剪(去除边缘黑边)
    roi = bgra[y:y + h, x:x + w]
    gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGRA2GRAY)
    _, th_roi = cv2.threshold(gray_roi, 1, 255, cv2.THRESH_BINARY)

    contours_roi, _ = cv2.findContours(th_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours_roi:
        x2, y2, w2, h2 = cv2.boundingRect(max(contours_roi, key=cv2.contourArea))
        final_roi = roi[y2:y2 + h2, x2:x2 + w2]
        return final_roi, y + y2
    return roi, y

这段代码中:

  • clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)),创建一个CLAHE (Contrast Limited Adaptive Histogram Equalization) 对比度限制自适应直方图均衡化对象,用于增强图像的局部对比度,特别适用于低对比度区域。
    • clipLimit=3.0: 对比度限制阈值,控制过度增强的程度。较低的值会限制过度对比度增强,避免噪声放大。默认值通常是40.0,较低的值可以更好地抑制噪声。
    • tileGridSize=(8,8): 图像分块大小,用于局部直方图均衡化。
  • enhanced = clahe.apply(gray): 对灰度图像应用CLAHE算法,增强局部对比度。
  • thresh = cv2.adaptiveThreshold(enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 5):对增强后的图像应用自适应阈值分割,将图像转化为二值图像。
    • cv2.ADAPTIVE_THRESH_GAUSSIAN_C: 自适应阈值算法类型。ADAPTIVE_THRESH_GAUSSIAN_C 表示使用高斯加权平均来计算局部阈值。这种方法能更好地适应图像的亮度变化。
    • cv2.THRESH_BINARY_INV: 阈值类型。THRESH_BINARY_INV 表示将阈值处理为反二值,即低于阈值的像素变为白色(255),高于阈值的像素变为黑色(0)。
    • 15: 邻域大小,用于计算每个像素的局部阈值(15x15像素的邻域)。
    • 5: 常数C,减去它之后得到的局部阈值,这有助于调节图像的二值化结果。
  • opened = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1):进行开运算,主要用于去噪。
    • iterations=1: 迭代次数,表示开运算执行的次数。通常,较高的迭代次数有助于更彻底地去除噪声。
  • closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2) :进行闭运算,主要用于填充小的空洞。
  • cv2.findContours :查找所有外部轮廓。
    • cv2.RETR_EXTERNAL:轮廓检索模式,仅检测最外层轮廓(忽略孔洞)。
    • cv2.CHAIN_APPROX_SIMPLE:轮廓压缩方法,仅保留关键点坐标(如矩形只保留4个角点)。
    • 返回contours:检测到的轮廓列表,每个轮廓由点集组成。
  • valid_contours为了进行轮廓筛选,将筛选出面积大于100且宽高比在0.5到2.5之间的轮廓。
  • np.vstack(valid_contours)cv2.boundingRect(combined_cnt)是合并多个有效轮廓,并计算其外接矩形,返回外接矩形的左上角坐标和宽高。
  • cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA):将BGR图像转换为BGRA格式(添加Alpha通道)。
  • cv2.drawContours:在掩模上绘制轮廓,填充为白色(255)。
  • cv2.bitwise_and:将Alpha通道与掩模结合,去除背景。
  • cv2.boundingRect:计算最大轮廓的外接矩形。

至此,图形裁剪告一段落了 ~ ~ ~

匹配

我们先接收一下裁剪之后的滑块图片,

def pic_match():
	#...
	cropped_slice, y_start = auto_crop_slice(img_slice)
    bg_roi = img_bg[y_start:y_start + cropped_slice.shape[0], :]
    # 根据原理,滑块的y轴位置,必定与缺口位置一致,不一致就无法正常匹配。当然,也有恶心的会将滑块位置与缺口位置的y轴拆开,那就有点反人类了。
    # 仅考虑滑块与缺口处在同一水平上,然后裁剪bg图片。得到bg_roi

从上文中我们得知,cropped_slice是4通道的,因为有执行cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA)这句话,而bg_roi则是3通道的,所以我们需要对他进行通道转换。将cropped_slice转换为3通道的,这样才能进行匹配,否则会报错。

def pic_match():
	#...
    bg_roi = img_bg[y_start:y_start + cropped_slice.shape[0], :]
	# 检查通道数
    #print("bg_roi通道数:", bg_roi.shape[2] if len(bg_roi.shape) == 3 else 1)
    #print("cropped_slice通道数:", cropped_slice.shape[2] if len(cropped_slice.shape) == 3 else 1)
	# 如果cropped_slice是4通道(BGRA),转换为3通道(BGR)
    if cropped_slice.shape[2] == 4:
        cropped_slice = cv2.cvtColor(cropped_slice, cv2.COLOR_BGRA2BGR)

    # 如果bg_roi是3通道,确保滑块也是3通道
    if bg_roi.shape[2] == 3 and cropped_slice.shape[2] != 3:
        cropped_slice = cv2.cvtColor(cropped_slice, cv2.COLOR_GRAY2BGR)

使用匹配模板,cv2的匹配模板有两种cv2.TM_SQDIFF_NORMEDcv2.TM_CCOEFF_NORMED,两种里面,匹配的结果略有不同,我更倾向于使用,cv2.TM_CCOEFF_NORMED,从我的匹配经验来看,cv2.TM_CCOEFF_NORMED的结果更接近一点,deepseek还跟我犟,说第四个参数是移动距离,人家就是给的这么用的!=。=可能最后是他对吧,但是我觉得从这个验证码来看,我觉得还是绝的第三个参数是移动距离。

def pic_match():
	#...
	if bg_roi.shape[2] == 3 and cropped_slice.shape[2] != 3:
        cropped_slice = cv2.cvtColor(cropped_slice, cv2.COLOR_GRAY2BGR)
	result = cv2.matchTemplate(bg_roi, cropped_slice, cv2.TM_CCOEFF_NORMED)
	_, _, max_loc , min_loc = cv2.minMaxLoc(result)
	print(max_loc)
    print(min_loc)

打印结果可以得到

(69, 0)
(28, 0)

使用电脑自带的画图工具可以查看像素位置:
在这里插入图片描述
也就是说,max_loc和真实的像素位置进差6个像素点,所以他更加接近。

我们通过将max_loc提取出x轴,然后通过再通过计算浏览器渲染与实际情况的比较,计算真实应该移动的距离。

def pic_match():
	# ...
	_, _, max_loc , min_loc = cv2.minMaxLoc(result)
	gap_center_x = max_loc[0] - random.randint(3,5) # 其实有个范围上的误差,只要再这个误差里面,都判定是人的操作,这里就随机搞一下,不搞也没事。
	# 因为y轴必须再同一水平面上,所以y轴的差值为0,就不考虑y轴了。
	# 获取Canvas元素在页面中的实际尺寸
	#canvas = driver.find_element(By.CLASS_NAME, "geetest_canvas_bg")
	#canvas_width = canvas.size['width']

	# 计算图像到实际页面的缩放比例
	#scale_ratio = canvas_width / bg_roi.shape[1]
	# 假设比例为1,真实的情况以上面注释的内容为主
	scale_ratio = 1
	adjusted_distance = gap_center_x * scale_ratio

随机轨迹

来分析一下人的正常的操作:
1.按住然后加速拖动前20%,
2.匀速拖动中间的60%,
3.后面20%减速。
我觉的很合理,时间上在2.2s2.8s这个区间很合理。

def generate_optimized_track(distance):
    """ 优化版轨迹生成函数 (时间控制: 2.2-2.8秒) """
    track = []
    current = 0

    # 阶段划分参数
    accelerate_end = distance * 0.2  # 前20%加速
    decelerate_start = distance * 0.8  # 后20%减速

    while current < distance:
        # 动态调整步长范围
        if current < accelerate_end:
            step = random.randint(4, 8)  # 加速段大步长
        elif current < decelerate_start:
            step = random.randint(3, 5)  # 中段稳定速度
        else:
            step = random.randint(1, 3)  # 减速段小步长

        # 添加随机扰动 (更符合人手特征)
        step += random.choice([-1, 0, 0, 1])  # 偏向正向扰动
		 # 防止超出目标距离
        if current + step > distance:
            step = distance - current

        track.append(step)
        current += step

步长OK了,但是时间还需要重新计算,就比如要讲时间控制在2.2s2.8s区间里面。所以我们要计算一下时间。

def generate_optimized_track(distance, min_time=2.2, max_time=2.8):
    """ 优化版轨迹生成函数 (时间控制: 2.2-2.8秒) """
    track = []
    current = 0

    # 阶段划分参数
    accelerate_end = distance * 0.5  # 前50%加速
    decelerate_start = distance * 0.8  # 后20%减速

    while current < distance:
        if current < accelerate_end:
            step = random.randint(int(distance * 0.08), int(distance * 0.15))  # 大跨度步长
        elif current < decelerate_start:
            step = random.randint(int(distance * 0.04), int(distance * 0.08))
        else:
            step = random.randint(1, max(2, int(distance * 0.02)))

        # 智能防溢出
        step = min(step, distance - current)
        track.append(step)
        current += step

    # 计算目标时间参数
    target_time = random.uniform(min_time, max_time)
    time_per_step = target_time / len(track)

    # 生成紧凑时间序列
    time_sequence = []
    for i in range(len(track)):
        if i < len(track) // 3:
            # 前段快速移动 (60%时间分配)
            t = random.uniform(time_per_step * 0.4, time_per_step * 0.8)
        elif i < len(track) * 2 // 3:
            # 中段正常速度 (30%时间)
            t = random.uniform(time_per_step * 0.8, time_per_step * 1.2)
        else:
            # 末段减速 (10%时间)
            t = random.uniform(time_per_step * 1.5, time_per_step * 2.0)
        time_sequence.append(round(t, 3))

    # 强制时间总量校准
    total_time = sum(time_sequence)
    scale_factor = target_time / total_time
    time_sequence = [t * scale_factor for t in time_sequence]
    return list(zip(track, time_sequence))

移动

将使用selenium自带的移动模块,因为滑动移动肯定要滑动的!

from selenium.webdriver import ActionChains
def human_drag(driver, element, distance):
    optimized_track = generate_optimized_track(distance)
    actions = ActionChains(driver, duration=0)
    actions.click_and_hold(element)

    start = time.time()
    # 执行滑动
    actions.click_and_hold(element)
    for step, t in optimized_track:
        y = random.choice([0, 0, 0, 1, -1])
        actions.move_by_offset(step, y)
        if t > 0.01:  # 仅对大于10ms的暂停生效
            actions.pause(t)
    actions.release().perform()
    # 计算实际耗时
    actual_total = time.time() - start
    print(actual_total)

def test():
	# ...
	adjusted_distance = gap_x * scale_ratio

    print(f"需滑动像素距离: {adjusted_distance:.1f}")
    slider = driver.find_element(By.CLASS_NAME, "geetest_slider_button")
    human_drag(driver, slider, adjusted_distance)

请添加图片描述

canvas就酱~

iframe验证

iframe的验证与canvas类似,只不过要切换到iframe里面。

在这里插入图片描述
可以看到,有两个iframe,所以要切换两次iframe,切换一次是不够的。

def iframe_verify(driver, url):
    try:
        driver.get(url)
        #parent_html
        WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.CLASS_NAME, 'mod-forgotPrd')))

        iframe_father_xpath = "//iframe[contains(@src,'https://zc.yy.com/reg/udb/reg4udb.do?mode=udb&type=&appid=1&action=3&busiurl=https%3A%2F%2Faq.yy.com&fromadv=udbclsd_r&reqDomainList=')]"
        iframe_child_xpath = "//iframe[contains(@src,'https://zc.yy.com/reg/ca/complexVerify.do?isNeedVcode=11&mode=aquname&appid=1&anchorId=interActiveWrap')]"
        WebDriverWait(driver, 10).until(
            EC.frame_to_be_available_and_switch_to_it((By.XPATH, iframe_father_xpath)))
        WebDriverWait(driver, 10).until(
            EC.frame_to_be_available_and_switch_to_it((By.XPATH, iframe_child_xpath)))
    except Exception as e:
        print("iframe验证失败:", e)
    finally:
        if driver:
            driver.quit()

获取背景图片和滑块图片。
在这里插入图片描述
可以看到,一开始并没有给缺口的图片,也没有给滑块的图片,需要我们点击之后才会出现。
请添加图片描述
那么我们就点击之后,再搞图片咯。

在这里插入图片描述
可以看到,第一个是背景(包含缺口的),第二个是滑块,然后又发现,诶嘿!有路径,直接搞!

def iframe_verify(driver, url):
		# ...
		WebDriverWait(driver, 10).until(
            EC.frame_to_be_available_and_switch_to_it((By.XPATH, iframe_child_xpath)))
        slice_button = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.XPATH, '//span[@node-name="dragPanel"]')))
        actions = ActionChains(driver)
        actions.move_to_element(slice_button).pause(0.5).click().perform()
        img_bg_src = WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.XPATH, "//img[@node-name='bgImageSrc']"))
        )
        img_bg_display_width = int(img_bg_src.get_attribute("width")) # 到时候计算放大或者缩小的比例的。

        img_bg_response = requests.get(img_bg_src.get_attribute("src"))
        img_bg = cv2.imdecode(np.asarray(bytearray(img_bg_response.content), dtype=np.uint8), cv2.IMREAD_COLOR)
        img_slice_src = WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.XPATH, "//img[@node-name='dragImageSrc']"))
        )
        img_slice_response = requests.get(img_slice_src.get_attribute("src"))
        img_slice = cv2.imdecode(np.asarray(bytearray(img_slice_response.content), dtype=np.uint8), cv2.IMREAD_COLOR)
        

在这里插入图片描述
在这里插入图片描述
可以使用canvas里面的裁剪的方式吗,可以试试,只不过答案是不可以的,因为两者的画布大小并不一致!所以直接匹配即可。

def iframe_verify(driver, url):
	# ...
		img_slice = cv2.imdecode(np.asarray(bytearray(img_slice_response.content), dtype=np.uint8), cv2.IMREAD_COLOR) # 接上面的内容
        result = cv2.matchTemplate(img_bg, img_slice, cv2.TM_CCOEFF_NORMED)
        _, _, max_loc, min_loc = cv2.minMaxLoc(result)
        gap_x = max_loc[0] - random.randint(15, 20) # 差距好像挺大的,所以这里就随机减一段像素

        scale_ratio = img_bg_display_width / img_bg.shape[1]
        adjusted_distance = gap_x * scale_ratio

        print(f"需滑动像素距离: {adjusted_distance:.1f}")
		

闪动一段时间之后,不能够进行移动,所以我们就点击之后,沉默2秒以上。

        print(f"需滑动像素距离: {adjusted_distance:.1f}")
        slider = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.XPATH, '//span[@node-name="dragPanel"]')))
        time.sleep(5)
        human_drag(driver, slider, adjusted_distance)
    except Exception as e:
        print("iframe验证失败:", e)
    finally:
        if driver:
            driver.quit()

请添加图片描述

简单版本就酱,iframe还需要进一步训练,存在以下问题:

  • 当存在两个缺口时,可能会识别到第一个缺口
  • iframe允许的像素误差太小了!
Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐