一种可能可行的滑动认证码处理方案
errol发表于2024-09-02 02:57:15 | 分类为 编程 | 标签为爬虫认证码python滑动认证码

现如今,认证码(CAPTCHA)已经成为自动化网络爬虫无法绕过的坎,所谓认证码,就是一种用于区分用户是机器或人的全自动程序,是一种反爬的手段&措施,由此可见,“自动化”与“认证码”是互相对立的存在,爬虫想要达成自动化,就必须攻克验证码。

滑动认证码就是验证码家族中的一员。

虽说经过几代的发展后,使用率大不如前,但也还占有一定的比率,因此,学习如何处理该种验证码仍具有意义,同时,它也是为数不多的比较容易处理的验证码类型之一。(较好欺负)

主要分为两种思路:

1)模板匹配;使用滑块来匹配背景图片中的缺口,将滑块的像素与背景图片一一对照

2)查找轮廓;在背景图片中查找“缺口”形状,检查形状的宽、高、面积、周长等

其中还有一种通过机器学习训练模型的方式,但它不在本文的讨论范围内

话虽如此,但从网络上检索的方案来看,效果都不尽人意。有些识别率低;有些是“针对某个网站”的特定方案,不具备通用性;有些甚至无法运行... 总之就是不好使。

当然,有些案例还是很值得借鉴的,同时也是本文灵感的由来。

一、设计思路

与模板匹配类似,但本方案不是读取滑块的全部像素来与背景图比较,而只读取滑块的边沿像素,并尝试在背景图中找到与之匹配的点,即缺口的位置

二、方法设计

姑且称之为“边沿像素点匹配法”,主要分为两个步骤,其一为采集滑块边沿像素点,其二为根据采集的像素点匹配背景图片缺口边沿像素点

即是说,要使用该方法首先必须满足条件:

  • 能够识别滑块的形状

  • 能够识别背景图片中的缺口形状

此外,还有一个隐藏条件,滑块与缺口要处于水平对齐状态。

一般情况下,展示给用户的滑动认证码的滑块与缺口是水平对齐的,但并不意味着它的原图也处于相同的状态,如果遇到这种情况,就需要找到该验证码对齐的规律并应用。

以下的实现需要依赖Pillow、opencv-python。

1、滑块边沿像素坐标采集

先后对读取的图片进行了灰度、二值化处理,随后进行采集像素点。

采集的方式也比较简单,从上到下、从左到右依次遍历,将第一个不为黑,即色块值不为0的像素点坐标保存至列表中,并将其返回。

from PIL import Image

def collect(path, start_x=0, start_y=0, offset_x=0, offset_y=0):
    """
    采集滑块边沿像素点坐标,可根据需要传入遍历的起始位置及偏移量,以优化执行效果

    :param path 图片路径
    :param start_x x轴起始位置
    :param start_y y轴起始位置
    :param offset_x x轴偏移量
    :param offset_y y轴偏移量
    :returns list
    """
    gray = Image.open(path).convert("L")
    threshold = 127
    table = [0 if _ < threshold else 1 for _ in range(256)]
    image = gray.point(table, "1")
    _size = image.size
    pos_list = []
    for y in range(start_y, _size[1] + offset_y, 4):
        for x in range(start_x, _size[0] + offset_x):
            if image.getpixel((x, y)) != 0:
                _x = x
                while image.getpixel((_x + 1, y)) == 0:
                    _x += 1
                pos_list.append((_x, y))
                break
    return pos_list

其中,图片处理前后的比较如下:

image

图1 滑块

image

图2 经灰度&二值化处理后

在本案例中,滑块与背景图缺口原本就处于水平对齐状态,所以可以直接采集像素点,其执行结果类似于:

[(33, 136), (27, 140), (24, 144), (24, 148), (25, 152), (5, 156), (5, 160), (5, 164), (5, 168), (5, 172), (5, 176), (5, 180), (5, 184), (5, 188), (5, 192), (5, 196), (5, 200), (5, 204), (5, 208), (5, 212), (5, 216)]

将其在原图上绘制后,如下:

image

图3 滑块边沿像素点示例

2、缺口边沿像素点匹配方法

匹配之前,需对图片做相应的处理:

  • 调用cv2.GaussianBlur()对图片进行模糊处理,使得图像变得平滑、柔化

  • 调用cv2.Canny()对图片进行边缘检测

该方法从左到右扫描图片,按照一定的规律查找目标像素点:如果指定坐标为白色,即色块值为255,则认为当前像素点匹配命中;匹配未命中时,会在误差范围内的坐标继续查找,最后若仍然未查找到目标时,会认为当前像素点匹配未命中。

最后,以字典的形式返回匹配结果,根据匹配命中的数量,分为“完全匹配”和“不匹配”两种类型。

import cv2

def match(path, pos_list, start_x=0, offset_x=0):
    """
    匹配边沿像素点坐标

    :param path 图片路径
    :param pos_list 滑块边沿坐标
    :param start_x x轴起始位置
    :param offset_x y轴起始位置
    :returns dict
    """
    image = cv2.imread(path)
    blurred = cv2.GaussianBlur(image, (5, 5), 0)
    canny = cv2.Canny(blurred, 200, 300)
    _size = canny.shape
    _len = len(pos_list)
    record = None
    for x in range(start_x, _size[1] + offset_x, 3):
        pos_list2 = []
        matched = False
        count = 0
        for i in range(0, _len):
            x0, y0 = pos_list[i]
            _x = x0 + x
            pixel = canny[y0, _x]
            if pixel == 255:
                pos_list2.append((_x, y0, True))
            else:
                for deviation in range(1, 3):
                    pixel = canny[y0, _x + deviation]
                    if pixel == 255:
                        pos_list2.append((_x + deviation, y0, True))
                        break
                if pixel == 0:
                    pos_list2.append((_x, y0, False))
            if len(pos_list2) > _len / 2:
                _count = 0
                _count2 = 0
                for pos in pos_list2:
                    if pos[2]:
                        _count += 1
                    else:
                        _count2 += 1
                count = _count
                if _count >= int(_len * 0.8):
                    matched = True
                    break
                if _count2 >= int(_len * 0.5):
                    matched = False
                    break
        if matched:
            record = {'offset': x, 'pos': pos_list2, 'type': 'matched'}
            break
        elif record is None or record['count'] < count:
            record = {'offset': x, 'pos': pos_list2, 'type': 'not_matched', 'count': count}
    return record

图片处理前后的比较如下:

image

图4 背景图

image

图5 经模糊&边缘检测后

三、使用方法

1、定义程序入口
if __name__ == '__main__':
    bg_path = "images/0-1.jpg"
    slider_path = "images/0-2.png"
    pos_list = collect(slider_path)
    # 根据情况,传入start_x、offset_x,如此处认为缺口不可能在图片的开头或结尾
    r = match(bg_path, pos_list, start_x=90, offset_x=-90)
    print(r)
    draw(r.get("pos"), bg_path)

执行结果如下:

{'offset': 255, 'pos': [(288, 136, False), (283, 140, True), (280, 144, True), (279, 148, True), (280, 152, False), (260, 156, False), (260, 160, True), (260, 164, True), (261, 168, True), (261, 172, True), (261, 176, True), (261, 180, True), (260, 184, True), (260, 188, True), (260, 192, True), (260, 196, True), (260, 200, True), (260, 204, True), (260, 208, True)], 'type': 'matched'}
2、根据返回的坐标在原图上绘制

image

图6 匹配结果示例

数据表明,本次成功匹配到了缺口的位置,其偏移量为255(像素)。

需要说明的是,该方案并非与案例“过度拟合”,它是一种“通用”的方案,以下列出了其他一些网站的匹配数据。

3、其他的匹配结果

image

图7 网易云音乐

image

图8 bilibili (但现在已改为点选)

image

图8 极验

image

图9 豆瓣

4、不满足使用条件的情况说明

当然,也并不总是能匹配成功,比较典型的就是刷到不满足使用条件的验证码,即是说,在以上匹配成功的网站中,也有可能刷出会匹配失败的码,需要看运气,遇到这种情况时,一般需要刷新重新获取。

1)无法正确识别缺口形状

image

图10 无法识别缺口示例图(原图)

image

图11 无法识别缺口示例图(处理后)

2)无法正确读取滑块边沿像素点

有些网站中,滑块图并不单独存在,此时需要找到网站认证码的排布规律,采集滑块像素点并使之与缺口水平对齐。

image

图12 无法正确读取滑块边沿像素点示例图


本文的源代码已经上传至github,可根据需要自行获取。

该方案取巧的地方在于,它假设背景图片中缺口所在的水平线上,有且仅有一个“缺口”的形状,假设满足条件,一般都能够匹配成功,这好比在只有一个字符“a”的数组里查找“a”,只要从头遍历到尾,是必定可以查找到的;相反,如果水平线上存在多个“缺口”形状,那将会导致失败。

总的来说,如文章开头所言,滑动认证码是一种较容易处理的验证码类型,使用通常的点子与思路足以应付,但也需要一些库的支持,如Pillow、opencv-python,同时也不能保证百分百识别成功。

不过,随着越来越多的网站使用的是与用户交互更加密切相关的认证码,如点选(文字&图像&方向等)、图像识别等,滑动认证码的生存空间正在进一步受到“排挤”,以后会被完全取代也说不定。

而这些新型的验证码,一般来说,人类很容易辨别,但机器却很难,甚至无从下手,此时,就必须要接入专门的机器学习算法了,或者也可以选择接入打码平台。

以上。

评论区已关闭
返回