记一次web自动化测试中遇到的滑块验证码的解决方案实践

问题描述:

在我要进行web自动化测试中,第一步就遇到了一个拦路虎,滑块验证码验证,单纯使用selenium去滑动滑块无法确定要移动的距离,每次滑块的图案都有所不同。

解决思路:

  1. 使用 Selenium 获取滑块验证码的背景图和滑块图,并保存到本地。

  2. 使用 OpenCV 的模板匹配算法找到滑块图在背景图中的位置,计算出滑块距离背景图左侧边缘的距离。

  3. 根据滑块距离的值,生成一条滑动轨迹,包括加速和减速两个阶段。加速阶段随机产生一个加速度,减速阶段随机产生一个减速加速度。每个阶段的时间为0.3秒。

  4. 使用 Selenium 模拟人的操作,按住滑块并滑动,最后释放滑块。在滑动过程中,添加随机的偏移量,以模拟人的操作。滑动结束后,将滑块移回起始位置,并重置操作。

代码实践;

一:使用 Selenium 获取滑块验证码的背景图和滑块图,并保存到本地(注意这里一些方法我使用了自己二次封装的selenium方法)。

import base64
import random
import time
import cv2
from selenium.webdriver.common.by import By

from common.basepage import BasePage


class SliderCaptcha(BasePage):

    def slider_verify(self):
        # 获取背景图并保存
        js = """
                      return document.getElementsByTagName('canvas')[0].toDataURL()
                  """
        base64str = self.script(js)
        resultstr = base64str.strip("data:image/png;base64")
        resultstr = resultstr[1:]
        imagedata = base64.b64decode(resultstr)
        file = open('./background.png', "wb")
        file.write(imagedata)
        file.close()

        # 获取滑块图并保存
        js = """
                   return document.getElementsByClassName('slide-verify-block')[0].toDataURL()
               """
        base64str = self.script(js)
        resultstr = base64str.strip("data:image/png;base64")
        resultstr = resultstr[1:]
        imagedata = base64.b64decode(resultstr)
        file = open('./slider.png', "wb")
        file.write(imagedata)
        file.close()

分析网页源码中背景图,背景图与滑块均是一个<canvas> 元素,无法直接下载到本地,需要处理:这里使用了 toDataURL 方法将 <canvas> 元素转换为 base64 编码的数据URL,并通过 Python 的 base64 模块将其解码并保存为图像文件。

定位背景图定位滑块

二:使用 OpenCV 的模板匹配算法找到滑块图在背景图中的位置,计算出滑块距离背景图左侧边缘的距离。


    def findfic(self, target='background.png', template='slider.png'):
        """

        :param target: 滑块背景图
        :param template: 滑块图片路径
        :return: 模板匹配距离
        """
        target_rgb = cv2.imread(target)
        target_gray = cv2.cvtColor(target_rgb, cv2.COLOR_BGR2GRAY)
        template_rgb = cv2.imread(template, 0)
        # 使用相关性系数匹配, 结果越接近1 表示越匹配
        # https://www.cnblogs.com/ssyfj/p/9271883.html
        res = cv2.matchTemplate(target_gray, template_rgb, cv2.TM_CCOEFF_NORMED)
        # opencv 的函数 minMaxLoc:在给定的矩阵中寻找最大和最小值,并给出它们的位置
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
        # 因为滑块只需要 x 坐标的距离,放回坐标元组的 [0] 即可
        if abs(1 - min_val) <= abs(1 - max_val):
            distance = min_loc[0]
        else:
            distance = max_loc[0]
        return distance
  1. 使用 OpenCV 的 cv2.imread 函数读取滑块背景图和滑块图片,并将背景图转换为灰度图像。

  2. 使用 cv2.imread 函数读取滑块图片,并将其转换为灰度图像。

  3. 使用 cv2.matchTemplate 函数进行模板匹配,将滑块图片与背景图进行匹配。这里使用了相关性系数匹配方法,即 cv2.TM_CCOEFF_NORMED。相关性系数越接近1,表示两个图像匹配度越高。

  4. 使用 cv2.minMaxLoc 函数在匹配结果中寻找最大值和最小值,并返回它们的位置。根据最小值和最大值的相关性系数大小来判断选择哪个位置作为滑块验证码需要滑动的距离。具体来说,如果相差不大,则选择最小值对应的位置 min_loc[0];否则,选择最大值对应的位置 max_loc[0]

三:根据滑块距离的值,生成一条滑动轨迹,包括加速和减速两个阶段

    def get_tracks(self, distance):
        """

        :param distance: 缺口距离
        :return: 轨迹
        """
        # 分割加减速路径的阀值
        value = round(random.uniform(0.55, 0.75), 2)
        # 划过缺口 20 px
        distance += 20
        # 初始速度,初始计算周期, 累计滑动总距
        v, t, sum = 0, 0.3, 0
        # 轨迹记录
        plus = []
        # 将滑动记录分段,一段加速度,一段减速度
        mid = distance * value
        while sum < distance:
            if sum < mid:
                # 指定范围随机产生一个加速度
                a = round(random.uniform(2.5, 3.5), 1)
            else:
                # 指定范围随机产生一个减速的加速度
                a = -round(random.uniform(2.0, 3.0), 1)
            s = v * t + 0.5 * a * (t ** 2)
            v = v + a * t
            sum += s
            plus.append(round(s))

        # end_s = sum - distance
        # plus.append(round(-end_s))

        # 手动制造回滑的轨迹累积20px
        # reduce = [-3, -3, -2, -2, -2, -2, -2, -1, -1, -1]
        reduce = [-6, -4, -6, -4]
        return {'plus': plus, 'reduce': reduce}

函数名为 get_tracks,接受一个参数 distance,表示缺口距离。函数返回一个字典,包含两个键值对,分别是 plus 和 reduce,分别表示滑动过程和回滑过程的轨迹。

函数实现过程如下:

  1. 首先生成一个随机数 value,用于分割加减速路径的阀值,取值范围为 0.55 到 0.75。

  2. 将缺口距离加上 20 像素,用于划过缺口。

  3. 初始化初始速度 v、初始计算周期 t、累计滑动总距离 sum 和轨迹记录 plus。

  4. 使用 while 循环,不断计算加速度 a 和滑动距离 s,并更新速度 v 和累计滑动总距离 sum,将每次计算得到的滑动距离 s 添加到轨迹记录 plus 中。

  5. 在滑动距离达到一定阈值 mid 后,开始减速,加速度变为负数,且取值范围为 -3.0 到 -2.0。

  6. 生成完滑动轨迹后,手动制造回滑的轨迹,将 reduce 初始化为 [-6, -4, -6, -4]。

  7. 返回轨迹字典。

v = v0 + at # 速度公式 ;

s = v0t + 1/2at^2 # 位移公式

四:使用 Selenium 模拟人的操作,按住滑块并滑动,最后释放滑块。

    def slider_verify(self):
        # 获取背景图并保存
        js = """
                      return document.getElementsByTagName('canvas')[0].toDataURL()
                  """
        base64str = self.script(js)
        resultstr = base64str.strip("data:image/png;base64")
        resultstr = resultstr[1:]
        imagedata = base64.b64decode(resultstr)
        file = open('./background.png', "wb")
        file.write(imagedata)
        file.close()

        # 获取滑块图并保存
        js = """
                   return document.getElementsByClassName('slide-verify-block')[0].toDataURL()
               """
        base64str = self.script(js)
        resultstr = base64str.strip("data:image/png;base64")
        resultstr = resultstr[1:]
        imagedata = base64.b64decode(resultstr)
        file = open('./slider.png', "wb")
        file.write(imagedata)
        file.close()

        distance = self.findfic(target='background.png', template='slider.png')
        print(distance)
        # 初始滑块距离边缘 4 px
        trajectory = self.get_tracks(distance + 4)
        print(trajectory)
        # 添加行动链
        self.click_and_hold((By.XPATH, "//i[@class='el-icon-arrow-right']"))
        for track in trajectory['plus']:
            self.move_by_offset(track, round(random.uniform(1.0, 3.0), 1))
        time.sleep(0.5)

        for back_tracks in trajectory['reduce']:
            self.move_by_offset(back_tracks, round(random.uniform(1.0, 3.0), 1))

        for i in [-4, 4]:
            self.move_by_offset(i, 0)

        time.sleep(0.1)
        self.release()
        # self.reset_actions()
  1. 首先,通过 self.findfic 函数找到缺口距离 distance。然后将 distance 加上 4 像素作为初始滑块距离边缘,通过调用 self.get_tracks 函数得到加减速轨迹 trajectory。接着,通过 self.click_and_hold 函数模拟鼠标左键按下事件,并使用 for 循环和 self.move_by_offset 函数按照轨迹移动滑块,其中随机生成偏移量的值,以模拟人类的操作。

  2. 移动完加速路径后,再使用 for 循环和 self.move_by_offset 函数按照回滑路径移动滑块,同样随机生成偏移量的值。移动完回滑路径后,再使用 for 循环和 self.move_by_offset 函数使滑块左右摆动,最后等待 0.1 秒后,通过 self.release 函数模拟鼠标左键松开事件。

  3. 最后,该段代码注释掉了代码 self.reset_actions(),该函数是 Selenium 中的一个重置行动链方法,作用是清空之前积累的行动链,避免影响后续的操作。