验证码识别:找回四六级准考证

摘要 一晃时间过的真快,距离上次更新博客已经将近10天了,这十天来也没闲着,回家终于把杀千刀的科目三过了,再也不用看到教练那张凶神恶煞的脸。前段时间四六级考试成绩公布了,小伙伴们是不是都第一时间忙着去查自己的成绩,相信有很多小伙伴跟我一样苦逼,幸幸苦苦复习了好长时间,查成绩的时候却忘了自己的准考证号(温馨提示:以后考试之前一定要记得把准考证拍一张存起来)。在网上试过无数种找回办法后,我彻底绝望了。既然别人不靠谱,咱就靠自己,经过两天的努力之后,终于成功的找回了准考证号。这篇博客主要来介绍解决这个问题的一些方法和思路。

avatar

文章概览

  对于查询四六级成绩来说,官方的查询入口有学信网中国教育考试网,查询成绩需要提交的数据包括准考证号、姓名和验证码。要想查询到成绩,最简单的办法就是手工枚举准考证号,一个一个的尝试。我们知道四六级准考证的组成如下所示(第10位表示类别,四级是1,六级是2):

avatar

也就是说对于在同在一个考点的人来说前十位都是一致的(四级和六级不同),后面五位分别表示考场号和座位号(座位号从01到30),在我们忘记了考场号和座位号的情况下,我们至少要手工枚举几千次才有可能查询到成绩,这个工作难度可想而知。那如果我们不采用手工的方式进行枚举,而采用程序自动进行枚举呢?通过程序枚举准考证号不是什么问题,但是查询参数中包含验证码,现在需要解决地就是如何识别验证码。对于验证码地识别问题,我们可以利用机器学习的相关算法,建立识别模型,再利用识别模型来进行识别验证码。对于学信网和中国教育考试网两个网站,它们采用的验证码不同,学信网的验证码比较复杂,包含汉字等特殊字符,识别难度大,而中国教育考试网的验证码相对来说比较常规,识别难度相对小一点,本文的查询操作都是基于后者而言的。
  那么我们解决问题地大致思路就是:首先我们要获取大量的验证码数据,然后选择算法训练识别验证码的模型,最后通过重复识别查询页面的验证码,提交查询数据,分析响应数据来获得最终的结果。

获取训练数据

  通过抓取请求相应过程中的数据包,我们可以得到获取验证码的地址。

avatar

其中ik表示准考证号,我们可以随便填一个,t表示时间戳(这个可以不用管),我们可以不断地向这个地址发送请求,服务器的响应结果即为验证码的地址,我们再向获取到的验证码的地址发送请求,就可以得到验证码。

avatar

具体代码如下所示(该项目的所有代码都可以在我的Github中找到):

# 获取验证码
def save_image_to_file():
    myid = "123456789110211"
    new_id = myid.format(id=myid)
    img_api_url = image_api.format(id=new_id)
    img_api_resp = requests.get(img_api_url, headers=img_api_headers,timeout=10)
    img_url, filename = get_image_url_and_filename(img_api_resp.text)
    r = requests.get(img_url)
    with open("images/raw_picture/" + filename, "wb+") as f:
        f.write(r.content)

处理数据

  获取到一定数量的验证码图片后(大概需要100多张,收集的图片越多越好,之后我们会讲到一种快速收集和标注验证码的方法),接下来我们需要对获取到的验证码进行相应的处理。因为对于验证码的识别,我们一般采取监督学习的算法训练模型,所以首先要对获取到的验证码进行标注,即将验证码图片的文件名改为验证码对应的数字和字母组合,这一步必须要人工进行操作。然后,为了提高验证码识别的准确率,训练更好的识别模型,我们需要对验证码图片进行相应的处理,如灰度处理、二值化、降噪。经过这些手段处理后的验证码更能体现出图片本身的特征,同时也减小了训练模型时的计算量,具体代码如下所示。

# 灰度处理,二值化(降噪部分的代码去掉了,效果不是太理想)
def img_denoise(img, threshold):
    def init_table(threshold=threshold):
        table = []
        for i in range(256):
            if i < threshold:
                table.append(0)
            else:
                table.append(1)

        return table

    img = img.convert("L").point(init_table(), '1')
    return img

  下面我们要对验证码进行分割,因为在识别的时候,我们是识别单个的数字或字母,所以我们要将验证码进行切分,提取出每个字符对应的区域,切割后的每张图片大小一致。

# 图片分割,参数img_split_start指定起始位置,参数img_split_width指定切割图片宽度
def img_split(img,img_split_start,img_split_width):
    start = img_split_start
    width = img_split_width
    top = 0
    height = img.size[1]
    img_list = []
    for i in range(4):
        new_start = start + width * i
        box = (new_start, top, new_start + width, height)
        piece = img.crop(box)
        #piece.save("%s.jpg" % i)
        img_list.append(piece)
    return img_list

  图片切割完成后,数据处理的最后一步是将切割后的图片转化为numpy array的形式。

# 将Image对象转换为array_list
def img_list_to_array_list(img_list):
    array_list = []
    for img in img_list:
        array_list.append(array(img).flatten())
    return array_list

以上这些操作大家可以在我的GitHub的项目文件中通过preprocessing()、make_train_data()和img_to_array()三个函数实现。

生成模型

  生成模型主要用到的就是sklearn机器学习库中相关的算法,验证码识别属于分类任务,对于分类任务我们可以采用K近邻、支持向量机、决策树和神经网络等算法,这里我们采用的是支持向量机。

# 训练模型
def svm_model(x_data,y_data):
    SVM = svm.SVC()
    x_train,x_test,y_train,y_test = train_test_split(x_data,y_data,random_state=14)
    SVM.fit(x_train,y_train)
    y_predict = SVM.predict(x_test)
    average_accuracy = np.mean(y_test==y_predict)*100
    print("准确率为:{0:.1f}%".format(average_accuracy))
    pickle.dump(SVM, open("model.pkl", "wb+"))

模型训练好之后,将模型对象存储在model.pkl文件中,需要识别验证码时,只需要读取model.pkl文件即可获得识别模型,不需要再次训练。

查询操作

发送请求

  模型训练好之后,我们就可以进行查询操作了。这一阶段的大致思路是,先获取查询页面的验证码,通过识别模型进行识别,然后再向服务器提交请求参数,包括枚举的准考证号、姓名和验证码。如果服务器返回验证码错误,则重复以上操作。如果服务器返回查询结果为空则说明验证码正确,但是准考证号和姓名不一致,此时可以枚举下一个准考证号,重复操作一直到获得正确结果为止。

  由于一开始我们训练模型时使用的训练数据量很小,所以该识别模型识别的准确率比较低,那么如何提高模型识别的准确率呢。最好的办法就是增大训练数据的数量,训练新的模型。这里提供一个更快更方便获取训练数据的方法,在发送请求的代码中,我们加入两行代码(倒数第三行和倒数第二行),该代码的作用时将识别正确的验证码加入到训练数据的文件夹中,并且会自动进行标注,可以通过该方式一边查询,一边收集大量的训练数据。我的项目中,一开始手工标注的验证码有200张,训练模型后采用这种方式自动收集了1600多张验证码,然后利用所有的训练数据重新建立模型,识别的准确率提高了30%。(但是这样的做法存在一个过拟合的问腿,训练模型对于类似于一开始200张验证码的图片的识别准确率比较高,而对于其他类型的图片识别的准确率比较低。不过这个问题对于我们找回准考证号影响不大,提高准确率最好的就是一开始手工标注更多的验证码)

# 发送请求
def send_query_until_true(num):
    # 生成准考证号
    global proxy
    new_id = myid.format(id=num)
    # 获取验证码图片地址
    img_api_url = image_api.format(id=new_id)
    while True:
        try:
            img_api_resp = requests.get(img_api_url,        headers=img_api_headers,timeout=10,proxies=proxy)
            img_url, filename = get_image_url_and_filename(img_api_resp.text)
            # 获取验证码图片并猜测
            img_resp = requests.get(img_url, timeout=10, proxies=proxy)
            if img_resp.status_code == 200:
                images = Image.open(BytesIO(img_resp.content))
                code = img_verify_code(images)
            else:
                code = "xxxx"
        except Exception:
            print("重新获取代理")
            p = str(get_proxy())
            proxy = {'http': 'http://' + p, 'https': 'http://' + p}
        else:
            break
    # CET4成绩查询选项
    # data = {"data": "CET4_181_DANGCI,{id},{name}".format(id=new_id, name=name),"v": code}
    # CET6成绩查询选项
    data = {"data": "CET6_181_DANGCI,{id},{name}".format(id=new_id, name=name),"v": code}
    query_resp = requests.post(query_api, data=data, headers=query_api_headers)
    query_text = query_resp.text
    log_info(query_text.split("'")[3],new_id)
    if "验证码错误" in query_text:
        query_text = send_query_until_true(num)
    # elif "您查询的结果为空" in query_text:
    #     images.save("images/save_picture/" + code + ".png")
    return query_text

使用代理

  在上面那段代码中,我们在请求过程中使用了代理,是为了防止频繁请求导致ip被封,代理功能可以自动切换代理,保证程序的正常运行。在测试过程中我们发现,该网站不会对ip进行封锁,所以代理可有可无。这里大致说一下代理功能是如何实现的。

  代理功能使用的代理池是Github上的开源项目,它通过从代理平台抓取可用的代理ip存储到本地Redis中,需要使用代理时,即从本地Redis中取出。使用代理功能需要进行相应的配置。

安装并开启Redis服务器
安装依赖 pip3 install -r requirements.txt
开启代理服务 python run.py

  在上述代码中,我们使用了捕捉异常的语句,因为在使用代理的过程中我们发现代理ip可能存在网络不稳定,传输有延时等问题。总的来说,使用代理的查询速度很慢,不想使用代理的话直接将proxy配置成本地的ip和端口即可。

多线程

  在开发过程中,想过用多线程,但是效果不太理想(对并行编程不熟悉),后来想想对于查找准考证号这种问题可以根据实际情况灵活,可能有些人会大致记得自己的考场位于哪个区间之内,所以在项目中,提供了输入查询区间的接口。如果想提高查询速度,可以开启多个终端,每个终端输入不同的查询区间,这样就类似于开启了多进程(一般查询的时候开启10个终端,每个终端的考场区间为10,10分钟内可以查询到结果)。

使用教程

  简单介绍一下该项目的文件结构,如图所示。

avatar
  • images:主要用来存放验证码图片,images中包含多个目录,row_picture存放原始验证码,change_picture存 * 放灰度化、二值化处理后的验证码,train_data存放分割后的验证码
  • proxypool:实现代理功能的相关代码
  • acquire_picture.py:包含验证码获取、处理相关操作的代码
  • model.pkl:存放识别模型
  • recongnition_code.py:项目的执行入口,包含向服务器发送请求、代理等相关代码
  • setting.py:项目相关的配置文件
  • train_data_preprocessing.py:整合验证码获取和处理相关操作
  • train_model.py:训练模型

  该项目使用的大致流程如下(要求python版本不低于3.5,该项目在win10环境测试运行无误)。

安装相关依赖PIL、requests、numpyy、sklearn等
修改recongnition_code.py文件中的myid(准考证号前10位)、name(自己的名字)
修改recongnition_code.py文件中成绩查询选项
如果需要使用代理,需要配置代理相关环境
在项目文件夹中打开终端输入:python recongnition_code.py 开始区间 结束区间
可同时开启多个终端,每个设置不同的区间,加快查找速度
avatar
avatar
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容