# 8.2-极验滑动验证码识别

上节我们了解了可以直接利用 tesserocr 来识别简单的图形验证码。近几年出现了一些新型验证码，其中比较有代表性的就是极验验证码，它需要拖动拼合滑块才可以完成验证，相对图形验证码来说识别难度上升了几个等级。本节将讲解极验验证码的识别过程。

## 1. 本节目标

我们的目标是用程序来识别并通过极验验证码的验证，包括分析识别思路、识别缺口位置、生成滑块拖动路径、模拟实现滑块拼合通过验证等步骤。

## 2. 准备工作

本次我们使用的 Python 库是 Selenium，浏览器为 Chrome。请确保已经正确安装 Selenium 库、Chrome 浏览器，并配置 ChromeDriver，相关流程可以参考第 1 章的说明。

## 3. 了解极验验证码

极验验证码官网为：[http://www.geetest.com/。它是一个专注于提供验证安全的系统，主要验证方式是拖动滑块拼合图像。若图像完全拼合，则验证成功，即表单成功提交，否则需要重新验证，如图](http://www.geetest.com/%E3%80%82%E5%AE%83%E6%98%AF%E4%B8%80%E4%B8%AA%E4%B8%93%E6%B3%A8%E4%BA%8E%E6%8F%90%E4%BE%9B%E9%AA%8C%E8%AF%81%E5%AE%89%E5%85%A8%E7%9A%84%E7%B3%BB%E7%BB%9F%EF%BC%8C%E4%B8%BB%E8%A6%81%E9%AA%8C%E8%AF%81%E6%96%B9%E5%BC%8F%E6%98%AF%E6%8B%96%E5%8A%A8%E6%BB%91%E5%9D%97%E6%8B%BC%E5%90%88%E5%9B%BE%E5%83%8F%E3%80%82%E8%8B%A5%E5%9B%BE%E5%83%8F%E5%AE%8C%E5%85%A8%E6%8B%BC%E5%90%88%EF%BC%8C%E5%88%99%E9%AA%8C%E8%AF%81%E6%88%90%E5%8A%9F%EF%BC%8C%E5%8D%B3%E8%A1%A8%E5%8D%95%E6%88%90%E5%8A%9F%E6%8F%90%E4%BA%A4%EF%BC%8C%E5%90%A6%E5%88%99%E9%9C%80%E8%A6%81%E9%87%8D%E6%96%B0%E9%AA%8C%E8%AF%81%EF%BC%8C%E5%A6%82%E5%9B%BE) 8-5 和图 8-6 所示。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eTTLv_-8BAOQpW%2F8-5.jpg?generation=1565068123318817\&alt=media)

图 8-5 验证码示例

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eVdyWUQXMvtXtb%2F8-6.jpg?generation=1565068114649933\&alt=media)

图 8-6 验证码示例

现在极验验证码已经更新到 3.0 版本。截至 2017 年 7 月，全球有 16 万家企业使用极验，每天服务响应超过 4 亿次。极验验证码广泛应用于直播视频、金融服务、电子商务、游戏娱乐、政府企业等各大类型网站。下面图中是斗鱼、魅族的登录页面，它们都对接了极验验证码，如图 8-7 和图 8-8 所示。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eXz_66hgLn4gNO%2F8-7.jpg?generation=1565068124095346\&alt=media)

图 8-7 斗鱼登录页面

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eZRn3ezh472R5P%2F8-8.jpg?generation=1565068114724005\&alt=media)

图 8-8 魅族登录页面

## 4. 极验验证码的特点

极验验证码相较于图形验证码来说识别难度更大。对于极验验证码 3.0 版本，我们首先点击按钮进行智能验证。如果验证不通过，则会弹出滑动验证的窗口，拖动滑块拼合图像进行验证。之后三个加密参数会生成，通过表单提交到后台，后台还会进行一次验证。

极验验证码还增加了机器学习的方法来识别拖动轨迹。官方网站的安全防护有如下几点说明。

* 三角防护之防模拟

恶意程序模仿人类行为轨迹对验证码进行识别。针对模拟，极验拥有超过 4000 万人机行为样本的海量数据。利用机器学习和神经网络构建线上线下的多重静态、动态防御模型。识别模拟轨迹，界定人机边界。

* 三角防护之防伪造

恶意程序通过伪造设备浏览器环境对验证码进行识别。针对伪造，极验利用设备基因技术。深度分析浏览器的实际性能来辨识伪造信息。同时根据伪造事件不断更新黑名单，大幅提高防伪造能力。

* 三角防护之防暴力

恶意程序短时间内进行密集的攻击，对验证码进行暴力识别 针对暴力，极验拥有多种验证形态，每一种验证形态都有利用神经网络生成的海量图库储备，每一张图片都是独一无二的，且图库不断更新，极大程度提高了暴力识别的成本。

另外极验的验证相对于普通验证方式更加方便，体验更加友好，其官方网站说明如下：

* 点击一下，验证只需要 0.4 秒

极验始终专注于去验证化实践，让验证环节不再打断产品本身的交互流程，最终达到优化用户体验和提高用户转化率的效果。

* 全平台兼容，适用各种交互场景

极验兼容所有主流浏览器甚至古老的 IE6，也可以轻松应用在 iOS 和 Android 移动端平台，满足各种业务需求，保护网站资源不被滥用和盗取。

* 面向未来，懂科技，更懂人性

极验在保障安全同时不断致力于提升用户体验，精雕细琢的验证面板，流畅顺滑的验证动画效果，让验证过程不再枯燥乏味。

因此，相较于一般验证码，极验的验证安全性和易用性有了非常大的提高。

## 5. 识别思路

对于应用了极验验证码的网站，如果我们直接模拟表单提交，加密参数的构造是个问题，需要分析其加密和校验逻辑，相对烦琐。所以我们采用直接模拟浏览器动作的方式来完成验证。在 Python 中，我们可以使用 Selenium 来完全模拟人的行为的方式来完成验证，此验证成本相比直接去识别加密算法少很多。

首先我们找到一个带有极验验证的网站，最合适的当然为极验官方后台了，链接为：<https://account.geetest.com/login>，首先可以看到在登录按钮上方有一个极验验证按钮，如图 8-9 所示：

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eaZNNbawUFhqoh%2F8-9.jpg?generation=1565068124527670\&alt=media)

图 8-9 验证按钮

此按钮为智能验证按钮。一般来说，如果是同一个会话，一段时间内第二次点击会直接通过验证。如果智能识别不通过，则会弹出滑动验证窗口，我们要拖动滑块拼合图像完成二步验证，如图 8-10 所示。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-ecjhbFqwkCyNBs%2F8-10.jpg?generation=1565068124649085\&alt=media)

图 8-10 拖动示例

验证成功后，验证按钮变成如图 8-11 所示的状态。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eeTHLgSMYdoU_H%2F8-11.jpg?generation=1565068113830650\&alt=media)

图 8-11 验证成功结果

接下来，我们便可以提交表单了。

所以，识别验证需要完成如下三步。

* 模拟点击验证按钮
* 识别滑动缺口的位置
* 模拟拖动滑块

第一步操作是最简单的，我们可以直接用 Selenium 模拟点击按钮即可。

第二步操作识别缺口的位置比较关键，这里需要用到图像的相关处理方法。首先观察缺口的样子，如图 8-12 和图 8-13 所示。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eg7reZmchuVYTJ%2F8-12.jpg?generation=1565068114507560\&alt=media)

图 8-12 缺口示例

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eiz1wUtUfwu-Nu%2F8-13.jpg?generation=1565068122558733\&alt=media)

图 8-13 缺口示例

缺口的四周边缘有明显的断裂边缘，边缘和边缘周围有明显的区别。我们可以实现一个边缘检测算法来找出缺口的位置。对于极验验证码来说，我们可以利用和原图对比检测的方式来识别缺口的位置，因为在没有滑动滑块之前，缺口并没有呈现，如图 8-14 所示。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-ekXGvVCqcXCV_y%2F8-14.jpg?generation=1565068123948248\&alt=media)

图 8-14 初始状态

我们可以同时获取两张图片。设定一个对比阈值，然后遍历两张图片，找出相同位置像素 RGB 差距超过此阈值的像素点，那么此像素点的位置就是缺口的位置。

第 (3) 步操作看似简单，但其中的坑比较多。极验验证码增加了机器轨迹识别，匀速移动、随机速度移动等方法都不能通过验证，只有完全模拟人的移动轨迹才可以通过验证。人的移动轨迹一般是先加速后减速，我们需要模拟这个过程才能成功。

有了基本的思路之后，我们就用程序来实现极验验证码的识别过程吧。

## 6. 初始化

这次我们选定的链接为 [https://account.geetest.com/login，也就是极验的管理后台登录页面。在这里我们首先初始化一些配置，如](https://account.geetest.com/login%EF%BC%8C%E4%B9%9F%E5%B0%B1%E6%98%AF%E6%9E%81%E9%AA%8C%E7%9A%84%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0%E7%99%BB%E5%BD%95%E9%A1%B5%E9%9D%A2%E3%80%82%E5%9C%A8%E8%BF%99%E9%87%8C%E6%88%91%E4%BB%AC%E9%A6%96%E5%85%88%E5%88%9D%E5%A7%8B%E5%8C%96%E4%B8%80%E4%BA%9B%E9%85%8D%E7%BD%AE%EF%BC%8C%E5%A6%82) Selenium 对象的初始化及一些参数的配置，如下所示：

```python
EMAIL = 'test@test.com'
PASSWORD = '123456'

class CrackGeetest():
    def __init__(self):
        self.url = 'https://account.geetest.com/login'
        self.browser = webdriver.Chrome()
        self.wait = WebDriverWait(self.browser, 20)
        self.email = EMAIL
        self.password = PASSWORD
```

其中 EMAIL 和 PASSWORD 就是登录极验需要的用户名和密码，如果没有的话可以先注册一下。

## 7. 模拟点击

实现第一步的操作，也就是模拟点击初始的验证按钮。我们定义一个方法来获取这个按钮，利用显式等待的方法来实现，如下所示：

```python
def get_geetest_button(self):
    """
    获取初始验证按钮
    :return: 按钮对象
    """
    button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
    return button
```

获取一个 WebElement 对象，调用它的 click() 方法即可模拟点击，代码如下所示：

```python
# 点击验证按钮
button = self.get_geetest_button()
button.click()
```

第一步的工作就完成了。

## 8. 识别缺口

接下来识别缺口的位置。首先获取前后两张比对图片，二者不一致的地方即为缺口。获取不带缺口的图片，利用 Selenium 选取图片元素，得到其所在位置和宽高，然后获取整个网页的截图，图片裁切出来即可，代码实现如下：

```python
def get_position(self):
    """
    获取验证码位置
    :return: 验证码位置元组
    """
    img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))
    time.sleep(2)
    location = img.location
    size = img.size
    top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
    return (top, bottom, left, right)

def get_geetest_image(self, name='captcha.png'):
    """
    获取验证码图片
    :return: 图片对象
    """
    top, bottom, left, right = self.get_position()
    print(' 验证码位置 ', top, bottom, left, right)
    screenshot = self.get_screenshot()
    captcha = screenshot.crop((left, top, right, bottom))
    return captcha
```

这里 get\_position() 函数首先获取图片对象，获取它的位置和宽高，随后返回其左上角和右下角的坐标。get\_geetest\_image() 方法获取网页截图，调用了 crop() 方法将图片裁切出来，返回的是 Image 对象。

接下来我们需要获取第二张图片，也就是带缺口的图片。要使得图片出现缺口，只需要点击下方的滑块即可。这个动作触发之后，图片中的缺口就会显现，如下所示：

```python
def get_slider(self):
    """
    获取滑块
    :return: 滑块对象
    """
    slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
    return slider
```

这里利用 get\_slider() 方法获取滑块对象，调用 click() 方法即可触发点击，缺口图片即可呈现，如下所示：

```python
# 点按呼出缺口
slider = self.get_slider()
slider.click()
```

调用 get\_geetest\_image() 方法将第二张图片获取下来即可。

现在我们已经得到两张图片对象，分别赋值给变量 image1 和 image2。接下来对比图片获取缺口。我们在这里遍历图片的每个坐标点，获取两张图片对应像素点的 RGB 数据。如果二者的 RGB 数据差距在一定范围内，那就代表两个像素相同，继续比对下一个像素点。如果差距超过一定范围，则代表像素点不同，当前位置即为缺口位置，代码实现如下：

```python
def is_pixel_equal(self, image1, image2, x, y):
    """
    判断两个像素是否相同
    :param image1: 图片 1
    :param image2: 图片 2
    :param x: 位置 x
    :param y: 位置 y
    :return: 像素是否相同
    """
    # 取两个图片的像素点
    pixel1 = image1.load()[x, y]
    pixel2 = image2.load()[x, y]
    threshold = 60
    if abs(pixel1[0] - pixel2[0]) <threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(pixel1[2] - pixel2[2]) < threshold:
        return True
    else:
        return False

def get_gap(self, image1, image2):
    """
    获取缺口偏移量
    :param image1: 不带缺口图片
    :param image2: 带缺口图片
    :return:
    """
    left = 60
    for i in range(left, image1.size[0]):
        for j in range(image1.size[1]):
            if not self.is_pixel_equal(image1, image2, i, j):
                left = i
                return left
    return left
```

get\_gap() 方法即获取缺口位置的方法。此方法的参数是两张图片，一张为带缺口图片，另一张为不带缺口图片。这里遍历两张图片的每个像素，利用 is\_pixel\_equal() 方法判断两张图片同一位置的像素是否相同。比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold。如果绝对值均在阈值之内，则代表像素点相同，继续遍历。否则代表不相同的像素点，即缺口的位置。

两张对比图片如图 8-15 和图 8-16 所示。

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-emNbPN1O9gaeue%2F8-15.png?generation=1565068123931828\&alt=media)

图 8-15 初始状态

![](https://2845657633-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LlHCNUN3da3TDR8UxA0%2F-Ll_Jt7xfnZXDjP39Jgc%2F-Ll_K-eo6S1JKCUUJrzU%2F8-16.png?generation=1565068123205178\&alt=media)

图 8-16 后续状态

两张图片有两处明显不同的地方：一个就是待拼合的滑块，一个就是缺口。滑块的位置会出现在左边位置，缺口会出现在与滑块同一水平线的位置，所以缺口一般会在滑块的右侧。如果要寻找缺口，直接从滑块右侧寻找即可。我们直接设置遍历的起始横坐标为 60，也就是从滑块的右侧开始识别，这样识别出的结果就是缺口的位置。

现在，我们获取了缺口的位置。完成验证还剩下最后一步 —— 模拟拖动。

## 9. 模拟拖动

模拟拖动过程不复杂，但其中的坑比较多。现在我们只需要调用拖动的相关函数将滑块拖动到对应位置，是吗？如果是匀速拖动，极验必然会识别出它是程序的操作，因为人无法做到完全匀速拖动。极验验证码利用机器学习模型，筛选此类数据为机器操作，验证码识别失败。

我们尝试分段模拟，将拖动过程划分几段，每段设置一个平均速度，速度围绕该平均速度小幅度随机抖动，这样也无法完成验证。

最后，完全模拟加速减速的过程通过了验证。前段滑块做匀加速运动，后段滑块做匀减速运动，利用物理学的加速度公式即可完成验证。

滑块滑动的加速度用 a 来表示，当前速度用 v 表示，初速度用 v0 表示，位移用 x 表示，所需时间用 t 表示，它们之间满足如下关系：

```
x = v0 * t + 0.5 * a * t * t 
v = v0 + a * t
```

利用这两个公式可以构造轨迹移动算法，计算出先加速后减速的运动轨迹，代码实现如下所示：

```python
def get_track(self, distance):
    """
    根据偏移量获取移动轨迹
    :param distance: 偏移量
    :return: 移动轨迹
    """
    # 移动轨迹
    track = []
    # 当前位移
    current = 0
    # 减速阈值
    mid = distance * 4 / 5
    # 计算间隔
    t = 0.2
    # 初速度
    v = 0

    while current < distance:
        if current < mid:
            # 加速度为正 2
            a = 2
        else:
            # 加速度为负 3
            a = -3
        # 初速度 v0
        v0 = v
        # 当前速度 v = v0 + at
        v = v0 + a * t
        # 移动距离 x = v0t + 1/2 * a * t^2
        move = v0 * t + 1 / 2 * a * t * t
        # 当前位移
        current += move
        # 加入轨迹
        track.append(round(move))
    return track
```

这里定义了 get\_track() 方法，传入的参数为移动的总距离，返回的是运动轨迹。运动轨迹用 track 表示，它是一个列表，列表的每个元素代表每次移动多少距离。

首先定义变量 mid，即减速的阈值，也就是加速到什么位置开始减速。在这里 mid 值为 4/5，即模拟前 4/5 路程是加速过程，后 1/5 路程是减速过程。

接着定义当前位移的距离变量 current，初始为 0，然后进入 while 循环，循环的条件是当前位移小于总距离。在循环里我们分段定义了加速度，其中加速过程的加速度定义为 2，减速过程的加速度定义为−3。之后套用位移公式计算出某个时间段内的位移，将当前位移更新并记录到轨迹里即可。

直到运动轨迹达到总距离时，循环终止。最后得到的 track 记录了每个时间间隔移动了多少位移，这样滑块的运动轨迹就得到了。

最后按照该运动轨迹拖动滑块即可，方法实现如下所示：

```python
def move_to_gap(self, slider, tracks):
    """
    拖动滑块到缺口处
    :param slider: 滑块
    :param tracks: 轨迹
    :return:
    """
    ActionChains(self.browser).click_and_hold(slider).perform()
    for x in tracks:
        ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
    time.sleep(0.5)
    ActionChains(self.browser).release().perform()
```

这里传入的参数为滑块对象和运动轨迹。首先调用 ActionChains 的 click\_and\_hold() 方法按住拖动底部滑块，遍历运动轨迹获取每小段位移距离，调用 move\_by\_offset() 方法移动此位移，最后调用 release() 方法松开鼠标即可。

经过测试，验证通过，识别完成，效果如图 8-17 所示。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://python3webspider.cuiqingcai.com/8.2-ji-yan-hua-dong-yan-zheng-ma-shi-bie.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
