记一次简单计算验证码的识别过程

admin 2025年7月1日18:53:58评论4 views字数 8986阅读29分57秒阅读模式

某CMS的验证码是简单的计算验证码,都是一位数的加减乘除运算,之前尝试用分割的方法识别,但成功率较低。后来采用了pytorch训练后进行识别,可以达到98%以上的识别率,于是整理一下过程,水一篇文章。

0x01  验证码的获取

首先是验证码的获取,由于爬取验证码还要手工标注,比较麻烦,这里可以通过修改程序来批量生成标注好的样本。

主要代码如下:

import com.google.code.kaptcha.Producer;

public class GenCalculateCaptcha {
    public static void main(String[] args) {
        Producer kaptcha = new CaptchaConfig().getKaptchaBeanMath();
        for(int i=0;i<100;i++){
            String capText = kaptcha.createText();
            String capStr = capText.substring(0, capText.lastIndexOf("@"));
            System.out.println(capStr);
        }
    }
}

执行结果:

6-1=?
9+1=?
0+5=?
8*6=?
6/1=?
5+7=?
9+3=?
0/5=?
1*2=?
...

通过执行的结果可以发现,生成的验证码中会出现*/?等字符,这些字符在作为文件名时不合法,所以需要进行替换。

例如将除号/替换为÷,乘号*替换为×,问号?替换为

同时为了避免生成的验证码重复,生成的文件名拼接了时间戳的md5,并用_连接。

替换的代码如下:

String newCapStr = capStr.replace("/","÷").replace("*","×").replace("?","?") + "_" + DigestUtils.md5Hex(""+System.currentTimeMillis())+".jpg";

再次执行结果如下:

8×4=?_b69426246067db177639de42b77082ab.jpg
3÷1=?_d0ad2c5c50b4d8db2736093bdf2c08e8.jpg
6÷3=?_d0a992ce88acf8a918f12646bef3a60d.jpg
3×8=?_d0a992ce88acf8a918f12646bef3a60d.jpg
9-3=?_d0a992ce88acf8a918f12646bef3a60d.jpg
2-0=?_d0a992ce88acf8a918f12646bef3a60d.jpg
0+9=?_3479d65864cdbbed02d5e9acb8e5fa37.jpg
4-1=?_3479d65864cdbbed02d5e9acb8e5fa37.jpg
6+2=?_3479d65864cdbbed02d5e9acb8e5fa37.jpg
7×2=?_3479d65864cdbbed02d5e9acb8e5fa37.jpg
...

然后就是验证码图片的保存。

主要代码如下:

BufferedImage bi = DefaultKaptcha.createImage(capStr);
String fileName = dir.getPath()+File.separator+capStr.replace("/","÷").replace("*","×").replace("?","?") + "_" + DigestUtils.md5Hex(""+System.currentTimeMillis())+".jpg";
ImageIO.write(bi, "jpg"new File(fileName));

生成的验证码如下:

记一次简单计算验证码的识别过程

2/1=?

记一次简单计算验证码的识别过程

3*5=?

到此获取标注好的验证码已经完成了,下面就开始进行验证码的识别。

0x02  验证码的识别

1.分割识别

最早是根据文章《自动识别验证码破解上学吧题目答案》中的方法来进行验证码的识别,但是由于验证码不太规则,导致识别效果较差,后面就放弃了。这里列出简要过程。

验证码图片为 60×160 像素的,两个数字的范围都是 0 到 9。对图片转成灰度图后并进行分割。

image = Image.open(path).convert("L")
cropped_image1 = image.crop((25, 13, 50, 44))  # 第一个数字的切图
cropped_image2 = image.crop((65, 13, 90, 44))  # 第二个数字的切图

可以自己找比较合适的分割位置。

分割的效果:

记一次简单计算验证码的识别过程

分割

然后进行二值化,遍历灰度图的像素点,这里以阈值66为界限,使得图片的像素点要么为纯黑 0,要么为纯白 255,下图是二值化之后的图片:

记一次简单计算验证码的识别过程

二值化

接着对验证码样本进行批量切图、转灰度图、二值化:

批量对图片进行分割,然后保存格式为数字_md5(时间戳).jpg

def corpImg(name):
    imgPath = "MathCodes/" + name
    fname = name[0:1]
    img = cv2.imread(imgPath, 0)  # 直接读为灰度图像
    img1 = img[13:4428:52#分割
    cv2.imwrite(fname+"_"+getMd5()+".jpg", img1)

def main():
    names = os.listdir("MathCodes")
    for name in names:
        corpImg(name)

记一次简单计算验证码的识别过程

切图

从中挑选出噪点去除效果最好图片的作为模板,0 到 9 这 10 个数字各一个。

记一次简单计算验证码的识别过程

模板

分别遍历这几个模板图片的像素点并存为 0-1 矩阵:首先创建一个 24列 31 行的二维数组(所有元素都为 0),遇到黑色像素点就将 0 变成 1,此处需要注意二维数组中坐标与像素点坐标是相反的。

num_info = [([0] * 24for i in range(31)]  # 创建一个宽度为24,高度为31的二维数组
pixdata = img.load()
for y in range(31):
    for x in range(24):
        if pixdata[x, y] == 0:
            # print(x, y)
            num_info[y][x] = 1  # 注意二维数组中坐标是相反的
num_info_list.append(num_info)

接下来就是识别了

num_info_list = []  # 这个数组用以存储全部数字的 0-1 矩阵

for i in range(10):
    filename = 'temp/'+str(i) + '.png'
    img = Image.open(filename)

    num_info = [([0] * 24for i in range(31)]  # 创建一个宽度为24,高度为31的二维数组
    pixdata = img.load()
    for y in range(31):
        for x in range(24):
            if pixdata[x, y] == 0:
                # print(x, y)
                num_info[y][x] = 1  # 注意二维数组中坐标是相反的
    num_info_list.append(num_info)
img = Image.open('temp/067_1.jpg')
img = binarizing(img,66)
img.save("temp/01111.png")
count_list = [] # 记录当前图片像素信息与每一个 0-1 序列的匹配程度

pixdata = img.load()
for i in range(10):
    count = 0
    for y in range(31):
        for x in range(24):
            if pixdata[x, y] == 0 and num_info_list[i][y][x] == 1# 图片中黑色像素点出现的位置对应的矩阵点也是 1
                count = count + 1
    count_list.append(count)

print(count_list)
print('当前图片的识别结果:' + str(count_list.index(max(count_list)))) # 找到匹配数最大的那个元素的序号,而序号和数字是相同的。

记一次简单计算验证码的识别过程

分割识别

从上面来看识别效果不是太好,所以后面就放弃了这种方法。

也可以先分割验证码,之后用ddddocr进行识别,中间的运算符可以采用上述的方法进行识别。这里就说一个思路,不具体实现了。一来比较麻烦,二来是运算符处理的效果也不会太好,但最终的结果会比直接分割识别这种方法好。

这是ddddocr识别效果,有一个没识别出来,不过准确率还挺高。

记一次简单计算验证码的识别过程

ddddocr识别

接下来就使用pytorch进行训练。

2.pytorch识别

pytorch训练验证码的过程都差不多,这里从网上找了一套修改了一下。

样本已经有了,首先对验证码进行分析。验证码字符一共有16种,分别为:

0123456789+-×÷=?

验证码长度为5

captcha_array = list("0123456789+-×÷=?")
captcha_size = 5

接下来就是Datasets数据加载。

pytorch有非常方便高效的数据加载模块DatasetDataLoader
Dataset是数据样本的封装,可以很方便的读取数据。

实现一个Dataset的子类,需要重写__len____getitem__方法,__len__需要返回整个数据集的大小,__getitem__提供一个整数索引参数,返回一个样本数据(一个图片张量和一个标签张量)。主要代码如下:

class MyDataset(Dataset):
    def __init__(self, root_dir):
        super(MyDataset, self).__init__()
        self.image_path = [os.path.join(root_dir, image_name) for image_name in os.listdir(root_dir)]
        self.transforms = transforms.Compose(
            [
                transforms.ToTensor(),
                transforms.Resize((60160)),
                transforms.Grayscale()  # 灰色
            ]
        )

    def __len__(self):
        return self.image_path.__len__()

    def __getitem__(self, index):
        image_path = self.image_path[index]
        # print(image_path)
        image = self.transforms(Image.open(image_path))
        ll = image_path.split("/")[-1]
        ll = ll.split("_")[0#验证码文本
        label_tensor = one_hot.text2Vec(ll)  # [5,16]
        label_tensor = label_tensor.view(1-1)[0]  # [5*16]
        # print(label)
        return image, label_tensor

其中text2Vec是将验证码进行onehot编码,这里是变成一个5*16的数组。

主要代码:

def text2Vec(text):
    vec = torch.zeros(common.captcha_size, len(common.captcha_array))
    for i in range(len(text)):
        vec[i, common.captcha_array.index(text[i])] = 1
    return vec

比如说0×4=?转换的结果就如下:

tensor([[1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.], # 0
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.], # ×
        [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], # 4
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], # =
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]) # ?

对应的还原方法:

def vec2Text(vec):
    vec = torch.argmax(vec, dim=1)  # 取最大值,不是0的取出来
    text = ''
    for i in vec:
        text += common.captcha_array[i]
    return text

DataLoader是Dataset的进一步封装,Dataset每次通过__getitem__方法取到的是一个样本,经过DataLoader封装为dataloader后,每次取的是一个batch大小的样本批次。

主要代码:

transform = transforms.Compose([transforms.ToTensor()])  # 不做数据增强和标准化了
train_dataset = CaptchaData('./datasets/train/', transform=transform)
train_data_loader = DataLoader(train_dataset, batch_size=32, num_workers=0, shuffle=True, drop_last=True)

test_data = CaptchaData('./datasets/test/', transform=transform)
test_data_loader = DataLoader(test_data, batch_size=128, num_workers=0, shuffle=True, drop_last=True)

transforms是数据预处理操作,一般数据增强就通过transform实现,可以随机亮度,随机翻转,随机缩放等等。此处只使用了ToTensor(),将PIL.Image对象转换成Tensor。

训练采用了CNN神经网络,CNN主要由卷积层,池化层,激活函数组成,再加上一个BatchNorm,BatchNorm叫做批规范化,可以加速模型的收敛速度。

模型的主要代码:

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 第一层神经网络
        # nn.Sequential: 将里面的模块依次加入到神经网络中
        self.layer1 = nn.Sequential(
            nn.Conv2d(316, kernel_size=3, padding=1),  # 3通道变成16通道,图片:60*160
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        # 第2层神经网络
        self.layer2 = nn.Sequential(
            nn.Conv2d(1664, kernel_size=3),  # 16通道变成64通道,图片:30*80
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        # 第3层神经网络
        self.layer3 = nn.Sequential(
            nn.Conv2d(64128, kernel_size=3),  # 64通道变成128通道,图片:14*39
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        # 第4层神经网络
        self.fc1 = nn.Sequential(
            nn.Linear(138241024),
            nn.Dropout(0.2),  # drop 20% of the neuron
            nn.ReLU()
        )
        # 第5层神经网络
        self.fc2 = nn.Linear(1024, common.captcha_size * common.captcha_array.__len__())  # 5:验证码的长度, 16: 字母列表的长度

    # 前向传播
    def forward(self, x):
        x = x.to(device)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = x.view(x.size(0), -1)
        x = self.fc1(x)
        x = self.fc2(x)
        return x

nn.Sequential()可以看作模块的有序容器,可以方便快捷的搭建神经网络。
网络的输入是一个shape为[batch, 3, 60, 180]的张量,batch代表的是一个批次图片数量,3代表输入的图片是3通道的,即RGB,180和60则分别代表图片的宽和高。

经过上结构的卷积后,得到一个shape为[batch, 128, 6, 18]的张量,x.view(x.size(0), -1)将改变张量的shape为[batch, 128*6*18],再用一个[1024, 16*5]的全连接层映射为一个[batch, 16*5]张量,这个就是模型的输出,其中16代表字符的种类数量,5代表一张验证码图片含有的字符数量。

接下来就是验证码的训练了:

记一次简单计算验证码的识别过程

训练

这里使用的是CPU进行训练的,训练样本生成了2000张,测试样本200张,刚开始训练准确率就可以到100%,而且速度不是太慢。

验证码生成的脚本:https://github.com/fupinglee/Calculate_Captcha

如果是在GPU下训练,在CPU下使用模型时,需要进行转换:

torch.load(model_path, map_location=torch.device('cpu'))

训练后测试的结果(200张测试准确率是100%,又另外生成了2000张验证码进行测试):

记一次简单计算验证码的识别过程

预测

经过测试,使用pytorch训练的准确率可以达到99%。

附上完整的代码:

https://github.com/fupinglee/CalculateCaptcha_Recognition

0x03  总结

本文通过2种方法来对计算验证码进行识别。第一种方法使用简单,但识别率较低,可以针对一些比较简单的验证码(比如验证码未进行扭曲、干扰等)。第二种方法使用简单,但识别率比较依赖样本的数量,前期验证码标注是一件麻烦事,但对于本文这种简单的验证码,少量的样本准确率也会很高。

0x04  参考

1.自动识别验证码破解上学吧题目答案

https://guanqr.com/tech/computer/shangxueba-crack/

2.pyTorch -- 图形验证码识别

https://zhuanlan.zhihu.com/p/215700831

3.验证码代码

https://github.com/fupinglee/Calculate_Captcha

4.pytorch识别验证码代码

https://github.com/fupinglee/CalculateCaptcha_Recognition

原文始发于微信公众号(猎户攻防实验室):记一次简单计算验证码的识别过程

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月1日18:53:58
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   记一次简单计算验证码的识别过程https://cn-sec.com/archives/806719.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息