通过卷积神经网络识别方正教务系统验证码实现自动登录

通过卷积神经网络识别正方教务系统验证码

前言:

我们学校的的教务系统每次在可以查成绩的时候崩溃,需要刷个几百遍才能登录。正好最近练习爬虫,借助网上的cnn模板来破解学校的验证码,以后就可以自动查询啦。当别人还在苦恼登陆失败的时候,我早已得知挂科的真相流下了痛苦的泪水…ORZ。

什么是卷积神经网络?

我不打算讲有关卷积神经网络的具体推算方法以及数学上面的东西,因为网络上面已经有足够好的资源,我会在下面把自己学习的链接贴一下。我想说的是作为一个初学者,如何将这个东西拿来用。“使用”与“精通”之间有很大区别。

如何理解神经网络

借用吴军老师的话:


神经网络与人脑没有半点关系,它的本质是有向图。
不过它与一般的有向图有点区别
1、在人工神经网络中,所有的节点都是分层的,每层节可以通过有向弧指向上一层节点,
但是同级之间没有弧连接,而且不能越过上层直接连接到上上层。
2、每一条弧有一个值(称为权重和权值),根据这些值,可以计算出他们的下一个层的值。
人工神经网络就是上述的数学模型,它擅长的是模型分类。
在人工神经网络中,需要设计的部分只有俩个,一个是它的结构,即网络分成几层、每层
几个节点、节点之间如何连接:第二就是他们的计算函数的设计。
那么如何得到好的权值呢,这就要涉及到对人工神经网络的训练了。
训练神经网络主要依靠数据来对参数用梯度下降的方法找最优解。


假设在一个二维空间中,我们需要区分a与e区域,它们的分布如上图所示。我们这个模型的任务就是要在空间里切一刀,将a和e分开。上图中的虚线便是分割线,左边是a,右边是e,如果新的元素进来了,落到左边我们就将它判断成a,反之则被认为是e。现在可以用一个人工神经网络来实现这个简单的分类器(虚线),该网络的结构如下:



这是一个再简单不过的人工神经网络了。在这个人工神经网络中,有两个输入节点:X₁和X₂,一个输出节点Y。在X₁到Y的弧上,我们赋予一个权重W₁ = 3,在从X₂到Y的弧上,我们赋予权重W₂ = -1,然后将Y这一点上的数值设定为两个输入节点数值X₁和X₂的一种线性组合,即y = 3X₁ - X₂。注意上面的函数是一个线性函数,他也可以被看成是输入向量(X₁,X₂)和(指向Y的)各条有向弧的权重向量(W₁,W₂)的内积(也叫做点积)。为了后面判断时方便起见、不妨在公式中再加一个常数项-2,即


y = 3X₁ - X₂ - 2


现在将平面上面的一些点(0.5,1)、(2,2)、(1,-1)和(-1,-1)的坐标输入到第一层的两个节点上,然后看看在输出节点得到了什么值。



(0.5,1) >> -1.5
(2,2) >> 2
(1,-1) >> 2
(-1,-1) >> -4

于是就可以说,如果在输出的节点Y得到的值大于零,那么这个点就属于e类,反之则属于a类。我们用神经网络定义了一个线性分类器,它可以做到简单的任务。



上述例子摘自吴军老师的《数学之美》

CNN

关于cnn,它属于神经网络的一种,所以拥有上述神经网络的特性,它是特殊的有向图,它是一个关于图像的分类器。关于cnn的工作原理我推荐一篇文章,其中很详细的阐述了cnn的原理。


透析卷积神经网络

如何实现

原理就说到这儿了,那么用什么工具实现呢?我使用的是谷歌的开源人工智能系
TensorFlow。前面说过了人工神经网络其实就是有向图,那么TensorFlow其实就是搭建有向图的工具,具体的使用方法请查看官方文档,英文学渣(就是本人)可以看TensorFlow中文文档

训练代码CNN部分

处理文件的方法用的是斗大的熊猫博客例子,CNN框架以及参数来自知乎一篇专栏。

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
def crack_captcha_cnn(w_alpha=0.01, b_alpha=0.1):
# 将占位符 转换为 按照图片给的新样式
x = tf.reshape(X, shape=[-1, IMAGE_HEIGHT, IMAGE_WIDTH, 1]) # [占位, H, W, 灰度为1]

# 3 conv layer
w_c1 = tf.Variable(w_alpha * tf.random_normal([3, 3, 1, 32])) # 从正太分布输出随机值 卷积窗口3*3
b_c1 = tf.Variable(b_alpha * tf.random_normal([32]))
conv1 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(x, w_c1, strides=[1, 1, 1, 1], padding='SAME'), b_c1)) # 卷积过程 H, W上滑动的步长 让卷积的输入和输入保持同样的尺寸
conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') # 因为希望整体上缩小图片尺寸,因此池化层的strides也设为横竖两个方向上以2为步长。
conv1 = tf.nn.dropout(conv1, keep_prob)

w_c2 = tf.Variable(w_alpha * tf.random_normal([3, 3, 32, 64]))
b_c2 = tf.Variable(b_alpha * tf.random_normal([64]))
conv2 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv1, w_c2, strides=[1, 1, 1, 1], padding='SAME'), b_c2))
conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
conv2 = tf.nn.dropout(conv2, keep_prob)

w_c3 = tf.Variable(w_alpha * tf.random_normal([3, 3, 64, 64]))
b_c3 = tf.Variable(b_alpha * tf.random_normal([64]))
conv3 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv2, w_c3, strides=[1, 1, 1, 1], padding='SAME'), b_c3))
conv3 = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
conv3 = tf.nn.dropout(conv3, keep_prob)

# Fully connected layer 全连接层
w_d = tf.Variable(w_alpha * tf.random_normal([9 * 4 * 64, 1024])) # 隐含节点
b_d = tf.Variable(b_alpha * tf.random_normal([1024]))
dense = tf.reshape(conv3, [-1, w_d.get_shape().as_list()[0]])
dense = tf.nn.relu(tf.add(tf.matmul(dense, w_d), b_d))
dense = tf.nn.dropout(dense, keep_prob)

w_out = tf.Variable(w_alpha * tf.random_normal([1024, MAX_CAPTCHA * CHAR_SET_LEN]))
b_out = tf.Variable(b_alpha * tf.random_normal([MAX_CAPTCHA * CHAR_SET_LEN]))
out = tf.add(tf.matmul(dense, w_out), b_out)
return out

关于训练量,总共从官网爬了1500张下来,人眼识别了一下午(吐血)。成功率大概50%,我在登陆程序上让它自己迭代,失败就继续尝试访问,问题不大。


下面贴一下神经网络的相关学习链接

莫烦的人工神经网络教程

Hellobi Live的人工神经网络的教程

colah’s blog

## 登陆教务系统

模型训练完了,下一步就是实现自动登陆了。


难点


1.方正教务系统在提交表单的时候除了必要的学号密码等等,还需要提交一个VIEWSTATE值。解决方法就是先get一次,获取到VIEWSTATE值,再post。


2.方正教务系统在早上的url中会带有一串乱码字母,每次get网站这串字母都会变。需要保持验证码的地址和get的地址都带有一样的乱码,这样验证码才能与地址吻合。


3.登录成功后将会对登录界面进行一次get,用来获取成绩界面的__VIEWSTATE值,这次get必须带有表头,表头中的Referer要带有学号。这很关键。

登录代码

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import os
import os.path
import bs4
import requests
from bs4 import BeautifulSoup
import sys
from clear_up import clear
from request_change import ne_code
import random
import re
import shutil
import tensorflow as tf
import numpy as np
import tim
from deal_image import im_deal
from train_tf import convert2gray
from train_tf import vec2text
from train_tf import MAX_CAPTCHA
from train_tf import CHAR_SET_LEN
from train_tf import X
from train_tf import keep_prob
from train_tf import crack_captcha_cnn
from PIL import Image
import importlib
importlib.reload(sys)

n_cod = ne_code()
studentnumber = "0"
password = "0"
def students_message():
studentnumber = str(input('请输入你的学号'))
password = str(input('请输入你的密码'))
return studentnumber,password
#定义new_code函数,实时反馈验证码
def new_code(r):
if re.search(r"\w{24}",r) == None:
print("no")
new_url = "/"
return new_url
new = re.search(r"\w{24}",r).group()
new_url = str("("+new+")")
return new_url

#定义get_headers函数,可以将复制下来的Headers分成字典
def getHeaders(raw_head):
headers={}
for raw in raw_head.split('\n'):
headerKey = raw.split(':',1)[0]
headerValue = raw.split(':',1)[1]
headers[headerKey]=headerValue
return headers

#登录时的Header
login_head = '''Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.9
Cache-Control:max-age=0
Content-Length:201
Content-Type:application/x-www-form-urlencoded
Host:122.225.19.20
Origin:http://122.225.19.20
Proxy-Connection:keep-alive
Referer:http://122.225.19.20/'''+n_cod+"/"+'''default2.aspx
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36'''

#构建get的表单,这里很关键
get_headers = '''Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.9
Host:122.225.19.20
Proxy-Connection:keep-alive
Referer:http://122.225.19.20/'''+n_cod+"/"+"xs_main.aspx?xh="+studentnumber+'''
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36'''

#查询页面头部
mark_head = '''Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.9
Cache-Control:max-age=0
Host:122.225.19.20
Proxy-Connection:keep-alive
Referer:http://122.225.19.20/'''+n_cod+"/"+"xs_main.aspx?xh="+studentnumber+'''
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36'''

#输入我查表单获得的参数以及不变的参数(学号,密码)
url = "http://122.225.19.20/"+n_cod #学校访问地址,表单中获得
imgurl = "http://122.225.19.20/"+n_cod+"/CheckCode.aspx" #验证码地址,checkcode中获得

output = crack_captcha_cnn()
saver = tf.train.Saver()
sess = tf.Session()
saver.restore(sess, ".model/crack_capcha.model-6900")

#验证码识别
def crack_captcha(image,output,sess):

image = convert2gray(image).flatten() / 255 # 一维化
predict = tf.argmax(tf.reshape(output, [-1, MAX_CAPTCHA, CHAR_SET_LEN]), 2)

text_list = sess.run(predict, feed_dict={X: [image], keep_prob: 1})

text = text_list[0].tolist()
vector = np.zeros(MAX_CAPTCHA * CHAR_SET_LEN)
i = 0
for n in text:
vector[i * CHAR_SET_LEN + n] = 1
i += 1

predict_text = vec2text(vector)
return predict_text

def login_in():

#访问教务系统,获取__VIEWSTATE值、验证码
s = requests.Session()
response = s.get(url+"/default2.aspx")
#用正则表达式匹配__VIEWSTAT值,或者直接用beautifulsoup找到valu的值,这里用了后者。
soup = BeautifulSoup(response.text,"lxml")
__VIEWSTATE = soup.find("input",attrs={'name':'__VIEWSTATE'}).get("value")
#获取验证码
imgresponse = s.get(imgurl,stream=True)
image = imgresponse.content
local = os.getcwd() #获取本文件的路径
filelocal = os.path.join(local,"code.jpg") #验证码名为code
with open(filelocal,"wb") as jpg:
jpg.write(image) #将文件保存到本地

ps_image = im_deal(filelocal) #处理验证码
fileps = os.path.join(local,"code.bmp")
ps_image.save(fileps)
one_ps_image = np.array(Image.open(fileps))

code_text = crack_captcha(one_ps_image,output,sess)

#获得cnn识别后的验证码
code = code_text
print(code)
#数据都拿到后,构建post

data = {
"RadioButtonList1":'%D1%A7%C9%FA',
"__VIEWSTATE":__VIEWSTATE,
"txtUserName":studentnumber,
"Textbox1":"",
"Textbox2":password,
"txtSecretCode":code,
"Button1":"",
"lbLanguage":"",
"hidPdrs":"",
"hidsc":""
}
#获取登录界面
headers = getHeaders(login_head)
#登录教务系统
res = s.post(url+'/default2.aspx',data=data,headers=headers)

#登录成功
#获取验证码

#主页面
#获取成绩查询页面的url
headers = getHeaders(get_headers)
page = s.get(url+"/xs_main.aspx?xh="+studentnumber,headers=headers)
print(url+"/xs_main.aspx?xh="+studentnumber)
soup2 = BeautifulSoup(page.text,'lxml')
markurl = soup2.find("a",attrs={'onclick':"GetMc('成绩查询');"}).get("href")
print(markurl)


#获取头部
headers = getHeaders(mark_head)
#表单
mark_data = {
"__EVENTTARGET":"",
"__EVENTARGUMENT":"",
"__VIEWSTATE":"",
"hidLanguage":"",
"ddlXN":"",
"ddlXQ":"",
"ddl_kcxz":"",
"btn_zcj":u"历年成绩".encode('gb2312','replace')
}
#获取__VIEWSTATE
markpage = s.get(url+"/"+markurl,headers=headers)
soup3 = BeautifulSoup(markpage.text,"lxml")
__VIEWSTATE = soup3.find("input",attrs={'name':'__VIEWSTATE'}).get("value")
mark_data['__VIEWSTATE']=__VIEWSTATE

#提交表单,获取成绩界面
markpage = s.post(url+"/"+markurl,mark_data,headers=headers)
soup4 = BeautifulSoup(markpage.text,'lxml')

marktable = str(soup4.find_all(id="Datagrid1")[0])
marklist = clear(marktable)
return marklist

def deal_marklist(marklist):
for line in marklist:
for column in [0,1,3,4,6,7,8]:
print('%-20s' % line[column],end = '')
print("")

def error():
global boom
try:
boom = login_in()
except AttributeError as e:
print("验证失败")
error()

if __name__ == '__main__':
studentnumber,password = students_message()
boom = []
error()
deal_marklist(boom)

关于验证码的处理模块,我自己用PIL对验证码进行了一个预处理,将图像二值化并且去掉了噪点,我会在最下面将自己的代码分享


贴一下成功后的截图~~



可以看到失败一次后继续访问,然后就登录成功了

最后

在对神经网络的学习过程中,感到数学的力量真的是强大的。数学家们用非常简洁的数学模型确能形成如此复杂的系统,从而揭示规律。然而数学也是艰涩的,我在查看文档资料的时候充分认识到自己数学的薄弱。但是如果一头扎进数学的海洋,我可能就不能在短时间内解决教务系统自动登陆的问题了。在今后的实践中肯定也有这种问题出现,知识具有层级结构,在深入多少层后停止,将剩余看做黑箱是需要思考的。目前我想遵循的原则是AK47原则,简单、杀伤大:也就是说用最简单的方法达到目的。

这是这个项目的仓库,可以在这里找到所有代码