零、准备工作

1、python3+


2、工具:pycharm


3、模块:os、sklearn、urllib.request、numpy、pandas、PIL(建议下载anaconda)


4、一颗对输入验证码感到厌烦的心


一、批量得到验证码图片


import urllib.request
def download(n):
    for i in range(1, n+1):
        img_src = 'http:xxxxx'
        i = str(i)             
        urllib.request.urlretrieve(img_src, './jpg/' + i + '.jpg') # 图片可能不是jpg格式,请注意


 想要识别验证码,首先要有验证码:使用循环访问n次验证码网址,下载n张二维码到当前同级目录


下的jpg文件夹中。


 得到验证码的生成网址 要在浏览器中使用开发者工具,即按F12。在右边栏中可以找到服务器发给浏


览器的验证码,然后 copy一下验证码网址就行。




python 当输入的验证码错误时再次输入 python123验证码校验_验证码




下载500例,可以看到验证码是彩色的,而且有奇奇怪怪的线条,我们接下来就要搞定它们




python 当输入的验证码错误时再次输入 python123验证码校验_Image_02

python 当输入的验证码错误时再次输入 python123验证码校验_验证码_03

python 当输入的验证码错误时再次输入 python123验证码校验_Image_04




二、转成黑白图片--二值化


def getTable(threshold=140):
    table = []
    for i in range(256):
        table.append(0) if i < threshold else table.append(1)
    return table


from PIL import Image
image = Image.open("jpg/418.jpg")
imgry = image.convert('L')    # 转化为灰度图
table = get_bin_table()       # table是list[]
out = imgry.point(table, '1') # out是image类型
out.show()


python 当输入的验证码错误时再次输入 python123验证码校验_验证码_05


out的图片如上所示,可见其变成了黑白二色,这代表out是由0和1构成的,黑色表示0,白色表示1。


这样更容易统计得到图片的特征,比如数字3 由几个0,1构成,进而得到各个数字的规律。




三、去除线条--除噪点


python 当输入的验证码错误时再次输入 python123验证码校验_灰度图_06




 可以看到,验证码图片中通常会有设置的干扰物,这会影响SVM训练时的效果。但是可以发现的是,


这种验证码图片的 噪点 并不多,而且处于边缘位置。需要说明的是,SVM训练的是一小块字符截图,所以


截图位置外的噪点皆可漠视,因此我 决定暂时不去除噪点,先看看截图效果。




四、得到字符截图




python 当输入的验证码错误时再次输入 python123验证码校验_验证码_07




 在pycharm中放大图片,开启网格,可以看到一些细节:z离左边缘4个像素;3离顶部4个像素;字


符之间的距离是2或3个 像素;所有字符离底边缘6个像素。看起来不好办啊!字符之间的距离竟然是浮动


的。然而仔细观察后我发现,字符似乎最大长 度都 是高12px,最大宽度10px。也就是说12*10的区域可


以恰好包含z,而不包含3。 对于字符z,截下(4,4,14,16);对于字符3,截下(14,4,24,16);以此类推。




def get_crop_imgs(img):
        child_img_list = []
        for i in range(4):
            x = 4 + i * 10                   # 第1个字符的截图位置是(4,4,14,16)
            y = 4
            child_img = img.crop((x, y, x + 10, y + 12))
            child_img_list.append(child_img) # child_img是image类型,child_img_list是含image类型元素的列表
    return child_img_list


list=get_crop_imgs(img)
list[0].show()




python 当输入的验证码错误时再次输入 python123验证码校验_灰度图_08

python 当输入的验证码错误时再次输入 python123验证码校验_灰度图_09



这就是截下来的字符,看得出噪点依然存在,但是不用急,大部分的字符截图都没有噪点,



五、保存字符截图




 每一张验证码都会截下四张字符截图,n张验证码会产生4n张字符截图,需要注意的一点是,字符截图


数不可太多, 因为 这是监督学习,是需要人为标注的,也就是你要手动分类,数字1归一类,数字2归一类,


等等。分别用文件夹储存。 经过后来的尝试我发现每类20个就可以训练得不错,这侧面反映这确实是很简单


的验证码。


def saveSpiltPicture(self, n):
        for i in range(0, n):
            i = i + 1                   # 循环为0时,i=1;循环为1时,i=2;方便计算
            image = Image.open('./jpg/' + str(i) + '.jpg')
            imgry = image.convert('L')  # 转化为灰度图

            out = imgry.point(self.table, '1')
            img_list = get_crop_imgs(out)

            img_list[0].save('./bmp/' + str(4 * i + 1) + '.bmp')
            img_list[1].save('./bmp/' + str(4 * i + 2) + '.bmp')
            img_list[2].save('./bmp/' + str(4 * i + 3) + '.bmp')
            img_list[3].save('./bmp/' + str(4 * i + 4) + '.bmp')




六、手动分类字符截图


首先要统计字符频率:共11类:123abcmnxvz。创建文件夹,命名为char,里面再创建11个文件夹,


分别以类名命名。


手动将bmp文件夹下的截图分类, 每类二三十个就可以了。





python 当输入的验证码错误时再次输入 python123验证码校验_验证码_10


这里7.bmp对应2.jpg内的第三个字符。




七、获得图片特征


(1)文件操作


 为什么又命名jpg文件夹,又命名bmp文件,甚至还有char文件夹呢?


 因为我要使用文件操作,自动对char下的每个文件夹下的每个图片使用“获取特征函数”,省时省力。


import os
def sortTable():
    list = []
    files_list = os.listdir('char')     # files_list->['1','2',...'x','z']
    num = 0                             # 第num张截图
    for i in files_list:                # i是第几个文件夹
        files = os.listdir('char/' + i) # 返回的files->['7.bmp','xx.bmp',...]
        for j in range(0, len(files)):  # j是第几张图片
            image = Image.open('char/' + i + '/' + files[j]) # files[0]->'7.bmp'
            list.append(get_feature(image)) # 使用获取特征函数返回list类型加在list后,所以list是二维数组
            list[num].append(i)         # 向list中的某list的尾加上文件名 *1
            num += 1                    #最后大list中含n个小list,每个小list都含有截图特征信息
    return list


     *1:为什么要加上文件名?打比方说,获得‘1’文件夹下某个字符‘1’的特征后,得再加一个特征,说前22个特征指向的


是字符‘1’,不然谁知道22个特征指向的是谁,是x还是z?所以新加的特征就是文件夹名。


(2)获取特征函数


def get_feature(img):
    """
    获取指定图片的特征值,
    一张截图为高12,宽10。统计各行的黑点,得到12个信息,统计各列的黑点,得到10个信息,用列表返回22个信息
    """
    pixel_cnt_list = []
    height = 12
    width = 10
    for y in range(height):
        pix_cnt_x = 0
        for x in range(width):
            if img.getpixel((x, y)) == 0:  # 黑色点
                pix_cnt_x += 1
        pixel_cnt_list.append(pix_cnt_x)

    for x in range(width):
        pix_cnt_y = 0
        for y in range(height):
            if img.getpixel((x, y)) == 0:  # 黑色点
                pix_cnt_y += 1
        pixel_cnt_list.append(pix_cnt_y)

    return pixel_cnt_list
(3)保存数据集


import pandas as pd
def save():
 names = [x for x in range(1, 24)]
 list=sortTable()
 test = pd.DataFrame(list, columns=names) # test类型为Dataframe
 test.to_csv('data.txt')  # 保存数据集




python 当输入的验证码错误时再次输入 python123验证码校验_验证码_11



 储存的好处是直接保存计算结果,调试时可以节省重复计算的时间,上图为data.txt的部分内


有23列, 表示23个特征, 末位特征表示此行指向什么字符,每行首位是行号。看得出虽然是4个字


符‘1’, 但是某些特征还是不同的。




八、SVM训练


from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC
from sklearn.cross_validation import train_test_split
from sklearn.externals import joblib


def train():
    data = pd.read_csv("data.txt")
    clf = OneVsRestClassifier(SVC(kernel='linear'))

    X = data.ix[:, 1:23]                     # 选取所有行的1-22列
    y = np.array(data.ix[:, 23]).astype(str) # 选取所有行的第23列,类型转换

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) #数据集分四类
    clf.fit(X_train, y_train)                # 训练ing
    y_pred = clf.predict(X_test)             # 测试ing

    rf = pd.DataFrame(list(zip(y_pred, y_test)), columns=['predicted', 'actual']) # rf类型是dataframe
    rf['correct'] = rf.apply(lambda r: 1 if r['predicted'] == r['actual'] else 0, axis=1)
    print(len(rf[rf['correct'] == 1]) / len(rf)) 
    joblib.dump(clf, "train_model.m")        # 保存训练模型




python 当输入的验证码错误时再次输入 python123验证码校验_验证码_12



 test_size参数表示分组规模,0.3表示70%的data拿去训练,30%的data拿去测试。
 我的data大概有900多行的特征向量,test_size=0.99时,准确率不到60%,test_size=0.9时,准确率就接近100%了。


而且我的字符中m宽度有15个px,截图只截下半个多m,然而训练后可以准确识别出m,且不怎么受噪点影响,所以除噪


也没什么必要了。


 如果训练效果不好,可能是算法有问题,或是训练量不够。




九、使用训练模型识别验证码


def seePicture():
    clf = joblib.load("train_model.m")
    image = Image.open("jpg/340.jpg")
    imgry = image.convert('L')       # 转化为灰度图
    table = get_bin_table()
    out = imgry.point(table, '1')
    img_list = get_crop_imgs(out)
    for x in range(0, 4):
        list = get_feature(img_list[x])
        y_pred = clf.predict([list]) # predict接受二维数组参数
        print(len(y_pred))



python 当输入的验证码错误时再次输入 python123验证码校验_灰度图_13

python 当输入的验证码错误时再次输入 python123验证码校验_Image_14




识别成功,任务完成。



十、小结


 这是我第一次写博客,如有错误或不详尽之处请指出。大概50%是在前人的基础上完成的,


在研究的同时走了许多弯路,所以就写下这篇尽量完整详尽的博客(所以才有那么多注释),希


望所有人无论是新手都能看的明白。至于文章中的一些格式错误,我没办法了,编辑时是粗体,


发表时变成斜体,晕。 以下是类代码


from PIL import Image
import pandas as pd
import numpy as np
import urllib.request
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC
from sklearn.cross_validation import train_test_split
from sklearn.externals import joblib
import os


class verification_code:
    data = []
    table = []
    threshold = 140

    def __init__(self):
        for i in range(256):
            self.table.append(0) if i < self.threshold else self.table.append(1)

    def download(self, n, URL):
        for i in range(1, n + 1):
            img_src = URL
            i = str(i)
            urllib.request.urlretrieve(img_src, './jpg/' + i + '.jpg')

    def get_crop_imgs(self, img):
        child_img_list = []
        for i in range(4):
            x = 4 + i * 10  # 见原理图
            y = 4
            child_img = img.crop((x, y, x + 10, y + 12))
            child_img_list.append(child_img)

        return child_img_list

    def get_feature(self, img):
        pixel_cnt_list = []
        height = 12
        width = 10
        for y in range(height):
            pix_cnt_x = 0
            for x in range(width):
                if img.getpixel((x, y)) == 0:  # 黑色点
                    pix_cnt_x += 1
            pixel_cnt_list.append(pix_cnt_x)

        for x in range(width):
            pix_cnt_y = 0
            for y in range(height):
                if img.getpixel((x, y)) == 0:  # 黑色点
                    pix_cnt_y += 1
            pixel_cnt_list.append(pix_cnt_y)

        return pixel_cnt_list

    def saveSpiltPicture(self, n):
        for i in range(0, n):
            i = i + 1
            image = Image.open('./jpg/' + str(i) + '.jpg')
            imgry = image.convert('L')  # 转化为灰度图

            out = imgry.point(self.table, '1')
            img_list = self.get_crop_imgs(out)

            img_list[0].save('./bmp/' + str(4 * i + 1) + '.bmp')
            img_list[1].save('./bmp/' + str(4 * i + 2) + '.bmp')
            img_list[2].save('./bmp/' + str(4 * i + 3) + '.bmp')
            img_list[3].save('./bmp/' + str(4 * i + 4) + '.bmp')

    def sorttable(self):
        list = []
        files_list = os.listdir('char')
        num = 0
        for i in files_list:  # i是第几个文件夹
            files = os.listdir('char/' + i)
            for j in range(0, len(files)):  # j是第几张图片
                image = Image.open('char/' + i + '/' + files[j])
                list.append(self.get_feature(image))
                list[num].append(i)
                num += 1
        names = [x for x in range(1, 24)]
        test = pd.DataFrame(list, columns=names)  # test类型为Dataframe
        test.to_csv('data.txt')  # 保存数据集

    def train(self):
        data = pd.read_csv("data.txt")
        clf = OneVsRestClassifier(SVC(kernel='linear'))

        X = data.ix[:, 1:23]
        y = np.array(data.ix[:, 23]).astype(str)

        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
        clf.fit(X_train, y_train)
        y_pred = clf.predict(X_test)

        rf = pd.DataFrame(list(zip(y_pred, y_test)), columns=['predicted', 'actual'])
        rf['correct'] = rf.apply(lambda r: 1 if r['predicted'] == r['actual'] else 0, axis=1)
        print(len(rf[rf['correct'] == 1]) / len(rf))
        joblib.dump(clf, "train_model.m")
        print(rf)

    def seePicture(self, img):
        clf = joblib.load("train_model.m")
        image = Image.open(img)
        image.show()
        imgry = image.convert('L')  # 转化为灰度图
        out = imgry.point(self.table, '1')
        img_list = self.get_crop_imgs(out)
        for x in range(0, 4):
            list = self.get_feature(img_list[x])
            y_pred = clf.predict([list])
            print(y_pred)

    def workFrist(self, n, URL):
        self.download(n, URL)
        self.saveSpiltPicture(n)
 # 手动分组
    def workSecond(self):
        self.sorttable()
        self.train()

    def workTest(self, img):
        self.seePicture(img)

code=verification_code()
code.workFrist(100, 'http://csujwc.its.csu.edu.cn/verifycode.servlet')
code.workSecond()
code.workTest('jpg/1.jpg')



python 当输入的验证码错误时再次输入 python123验证码校验_验证码_15

python 当输入的验证码错误时再次输入 python123验证码校验_验证码_16



十一、参考文献


1、字符型图片验证码识别完整过程及Python实现   点击打开链接