Post

geetest极验点选验证码位置识别

前言

重拾blog, 说实话还是会有很多不习惯, 写一篇blog成本其实还是挺大的, 特别是想要开源一些东西的时候, 要准备的东西太多了, 自己用怎么都行, 但是开源的话, 就像自己使用的时候, 总希望体验好一点, 也希望别人能快速验证是否满足自己的需求.

背景

之前通过 Puppeter 自动登录B站, 那会是极验的滑动验证码, 那个相对来说还简单一点, 毕竟核心是定位缺口, 定位到缺口, 后面就是正常的鼠标模拟滑动, 缺口定位的话方案其实还是挺多的, 然后走 Puppeter 模拟滑动即可, 之前搞定了, 然后最近发现自动登录失效了, 检查了一下之后发现滑动验证码更新了, 换成了点选验证码

_极验点选验证码_ 极验点选验证码

本着极速解决不造轮子的想法, 搜索了一把, 居然没有相关的成熟可靠解决方案, 想了一下, 事情还得解决, 那就自己实现一个吧.

思路其实还是有的, 之前其实就用过好几次 ddddocr 这个python库, 而且里面的demo就有这个, 所以准备整合一下.

_原始图片_ 原始图片 上面这个是原始图片, 首先当然是目标检测, 找到具体的文字区域, 一个是待点击的左下角区域的文本, 另外一个则是主体部分需要找到对应的文本,最后通过OCR匹配到具体坐标.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 这是目标检测
import ddddocr
import cv2

det = ddddocr.DdddOcr(det=True)

with open("test.jpg", 'rb') as f:
    image = f.read()

poses = det.detection(image)
print(poses)

im = cv2.imread("test.jpg")

for box in poses:
    x1, y1, x2, y2 = box
    im = cv2.rectangle(im, (x1, y1), (x2, y2), color=(0, 0, 255), thickness=2)

cv2.imwrite("result.jpg", im)

# 这是OCR
import ddddocr

ocr = ddddocr.DdddOcr(beta=True)

with open("test.jpg", 'rb') as f:
    image = f.read()

res = ocr.classification(image)
print(res)

但是呢, 实际测试下来, 目标检测其实还是很准的, 基本上能够定位到具体的为止, 但是呢OCR精准度还是有点欠缺的, 这个当然是可以训练的, 具体训练姿势可以参考ddddocr的文档, 之前用官方的训练过一个加减法的验证码, 效果还是很不错的, 因为默认的模型, 他-, +, x 和 ? 识别经常出错, 当然这个case其实也不需要训练, 因为都是一位数, 所以第2位 肯定是 + - x, 第4和5位肯定是 =? , 所以代码加一写兼容处理, 可以不训练直接处理. 加减法验证码 这里依旧还是不训练, 因为准备训练集还是挺累的, 2000的样本, 还需要标注好具体的字, 工作量还是很大的, 我当前的场景要求没这么高, 所以就不这么折腾了.


具体思路

极验点选我测试下来基本上都是2-4个字

  • 2个字, 这个其实比较简单, 2个字全匹配最简单, 1个字匹配, 另外一个默认匹配, 0个字匹配, 这个时候就默认顺序吧, 赌一个概率, 反正可以重试.
  • 3个字, 同理, 3个字和2个字匹配其实都是全匹配, 1个字或者0个字, 我现在默认就当匹配失败, 其实也可以随机匹配.
  • 4个字, 测试下来, 识别成功率有点低, 所以默认就当匹配失败.

所以最后代码就是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# -*- coding: utf-8 -*-
import urllib.request
import copy
import cv2
import ddddocr
import flask
import json
import numpy as np

server = flask.Flask(__name__)

det = ddddocr.DdddOcr(det=True, show_ad=False)
ocr = ddddocr.DdddOcr(beta=True, show_ad=False)


def get_match_result(target, source):
    output = [None] * 2
    keys = list(source.keys())

    if len(target) == 2:
        match_size = 0

        for i in range(len(keys)):
            if target[i] in source and source[target[i]]:
                match_size += 1
                output[i] = source[target[i]]

        if match_size == 0:
            output[0] = source[keys[0]]
            output[1] = source[keys[1]]

        if match_size == 1:
            remaining_key = keys[0] if len(keys) > 0 else None
            if not output[0]:
                output[0] = source[remaining_key]
            if not output[1]:
                output[1] = source[remaining_key]

    if len(target) == 3:
        output = [None] * 3
        match_size = 0

        for i in range(len(keys)):
            if target[i] in source and source[target[i]]:
                match_size += 1
                output[i] = source[target[i]]

        if match_size == 1:
            output = [None] * 3

        if match_size == 2:
            remaining_key = keys[0] if len(keys) > 0 else None
            if not output[0]:
                output[0] = source[remaining_key]
            if not output[1]:
                output[1] = source[remaining_key]
            if not output[2]:
                output[2] = source[remaining_key]

    coords = [None] * len(output)

    for i, v in enumerate(output):
        if v:
            coords[i] = [(v[0] + v[2]) / 2, (v[1] + v[3]) / 2]

    return coords


def process_image(image_data):
    np_array = np.frombuffer(image_data, np.uint8)
    image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)

    order = image[344:384, 0:150]
    _, order = cv2.imencode('.jpg', order)
    target = ocr.classification(order.tobytes())

    new = copy.copy(image)
    new[340:390, 0:400] = (0, 0, 0)
    new = cv2.imencode('.jpg', new)[1]
    poses = det.detection(new.tobytes())

    source = {}
    for box in poses:
        x1, y1, x2, y2 = box
        part = image[y1:y2, x1:x2]
        img = cv2.imencode('.jpg', part)[1]
        result = ocr.classification(img.tobytes())
        if len(result) > 1:
            result = result[0]
        source[result] = [x1, y1, x2, y2]

    return target, source


@server.route('/geetest_click', methods=['GET'])
def geetest_click():
    image_url = flask.request.args.get('image_url')

    try:
        with urllib.request.urlopen(image_url) as response:
            image_data = response.read()
        target, source = process_image(image_data)

        return {
            'target': target,
            'source': source,
            'coords': get_match_result(target, source)
        }
    except Exception as e:
        return {
            'error': str(e)
        }, 500


@server.errorhandler(500)
def handle_error(e):
    res = {"error": "服务器内部错误"}
    return json.dumps(res), 500


if __name__ == "__main__":
    server.run(port=9991, host='0.0.0.0', debug=False)

大概逻辑就是目标检测文字区域, 然后分别OCR识别, 然后匹配待点击区域和识别区域的文本, 最后给出点击坐标.具体源码在 geetest, 不会写python, 所以代码比较丑, 但是能用, python在某些领域是真的强, 强到离谱.

同时我也搭建了一个demo, 方便测试, 你要找到geetest的验证码图片

1
2
# image_url geetest的验证码图片 地址
curl http://38.147.170.248:9990/geetest_click?image_url=https://static.geetest.com/captcha_v3/batch/v3/46335/2023-09-07T15/word/270a3da4a8eb4e66a014de56300073dd.jpg

返回的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "coords": [
        [49, 114.5],
        [148.5, 170],
        [196, 79.5]
    ],
    "source": {
        "e": [164, 48, 228, 111],
        "发": [115, 137, 182, 203],
        "德": [14, 80, 84, 149]
    },
    "target": "德发长"
}

一共是3个数据

  • target: 识别出来待点击区域的文本.
  • source: 识别区域的文本以及坐标
  • coords: 最终实际文本顺序的中心相对坐标.

有这份数据之后, 配合Puppeter那么就毫无难度了!

使用方法

建议自己单独部署, 虽然这个没有隐私数据, 可以直接使用我上面部署的demo, 但是别人的东西毕竟不稳定, 所以还是建议自己个人部署.

1
docker run -d --name geetest -p 9990:9991 zscorpio/geetest:v0.0.1

记得放开防火墙端口

This post is licensed under CC BY 4.0 by the author.

Using the Chirpy theme for Jekyll.

© 2025 Scorpio. Some rights reserved.