Python模拟登陆正方教务系统并抓取成绩单
学校的教务系统是正方的,在大学期间无论是选课、报名还是查成绩,几乎都要和它打交道,上学期在积累了一定的爬虫和web知识后,我就想着用Python模拟登陆教务系统,实现在命令行里方便地进行成绩或课表的查询。
首先先来看看登陆的过程。
首先是向default.aspx POST过去了一些字段,其中txtUserName是我的学号,TextBox2是密码,RadioButtonList1是学生选项,还有个txtSecretCode是验证码。
此外,还出现了__EVENTVALIDATION和__VIEWSTATE这两个相对陌生的东西,百度了一下:
__EVENTVALIDATION:
__EVENTVALIDATION只是用来验证事件是否从合法的页面发送,只是一个数字签名,所以一般很短。
“id”属性为“__EVENTVALIDATION”的隐藏字段是ASP.NET 2.0的新增的安全措施。该功能可以阻止由潜在的恶意用户从浏览器端发送的未经授权的请求.
为了确保每个回发和回调事件来自于所期望的用户界面元素,ASP.NET运行库将在事件中添加额外的验证层。服务器端通过检验表单提交请求的内容,将其与“id”属性为“__EVENTVALIDATION”隐藏字段中的信息进行匹配。根据匹配结果来验证未在浏览器端添加额外的输入字段(有可能为用户在浏览器端恶意添加的字段),并且该值是在服务器已知的列表中选择的。ASP.NET运行库将在生成期间创建事件验证字段,而这是最不可能获取该信息的时刻。像视图状态一样,事件验证字段包含散列值以防止发生浏览器端篡改。
说明:“id”属性为“__EVENTVALIDATION”隐藏字段一般在表单的最下方,如果表单在浏览器端尚未解析完毕时,用户提交数据有可能导致验证失败。
__VIEWSTATE
ViewState是ASP.NET中用来保存WEB控件回传时状态值一种机制。在WEB窗体(FORM)的设置为runat="server",这个窗体(FORM)会被附加一个隐藏的属性_VIEWSTATE。_VIEWSTATE中存放了所有控件在ViewState中的状态值。
ViewState是类Control中的一个域,其他所有控件通过继承Control来获得了ViewState功能。它的类型是system.Web.UI.StateBag,一个名称/值的对象集合。
当请求某个页面时,ASP.NET把所有控件的状态序列化成一个字符串,然后做为窗体的隐藏属性送到客户端。当客户端把页面回传时,ASP.NET分析回传的窗体属性,并赋给控件对应的值
了解了这么多后,我们知道这两个字段肯定是不可缺少的,那么它们可以从哪获取到呢?
我们右键查看网页的源代码,在源代码中发现了两个type为hidden的输入框,它们的值正是我们所需要的。
在此,我们可以先构造一个payload字段:
payload={'__VIEWSTATE':'',
'txtUserName':'',
'TextBox2':'',
'txtSecretCode':'',
'RadioButtonList1':'',
'Button1':'',
'lbLanguage':'',
'hidPdrs':'',
'hidsc':'',
'__EVENTVALIDATION':'',
}
对于__EVENTVALIDATION和__VIEWSTATE值的获取我用了BeautifulSoup这个库。
soup=BeautifulSoup(index.content,'lxml')
value1=soup.find('input',id='__VIEWSTATE')['value']
value2=soup.find('input',id='__EVENTVALIDATION')['value']
登录时还要输入图形验证码,这种验证码大部分都倾斜和粘连,用简单的OCR估计处理不了,所以我就把验证码下载到了本地然后显示出来手动输入,此处用到了PIL库。
我使用requests库处理get和post请求,第一行使用requests.Session()会话对象让你能够跨请求保持某些参数。它也会在同一个 Session 实例发出的所有请求之间保持 cookie。这样我们就不用额外操心cookie的变更与存储了。
s=requests.Session()
index=s.get(url,headers=headers)
img=s.get(checkcode,stream=True,headers=headers)
with open('checkcode.gif','wb') as f:
f.write(img.content)
image=Image.open('checkcode.gif')
image.show()
最后把各字段该输入的输入,该赋值的赋值,整合到payload字典里就行啦
payload['txtUserName']=raw_input("UserName:")
payload['TextBox2']=raw_input("Password:")
payload['txtSecretCode']=raw_input("checkcode:")
#下面的value1和value2都不需要转码,直接post过去即可。在此浪费了好长时间
payload['__VIEWSTATE']=value1
payload['__EVENTVALIDATION']=value2
payload['RadioButtonList1']= '%D1%A7%C9%FA'
post1=s.post(url,data=payload,headers=headers)
POST过去后我们发现http状态码返回的是302,302代表的意思是 Move temporarily
我们直接找到成绩查询的url:http://xk2.edu.cn/xscjcx.aspx?xh=xxxxxx&xm=xxx&gnmkdm=N121605
此URL中xh是学号,xm是经过URL编码的学生姓名。
后面我们要做的和前面所讲的差不多,首先要通过GET方法获取页面源代码,从中取得__EVENTVALIDATION和__VIEWSTATE的值,然后再次POST过去。
data={
'btn_zcj':'%C0%FA%C4%EA%B3%C9%BC%A8',#学年成绩:btn_xn 历年成绩:btn_zcj
'ddlXN':'',
'ddlXQ':'',
'__EVENTVALIDATION': '',
'__EVENTTARGET':'',
'__EVENTARGUMENT' :'',
'__VIEWSTATE':'',
'hidLanguage':'',
'ddl_kcxz':'',
}
#注意!先获取框架源代码,提取__EVENTARGUMENT和__VIEWSTATE值后作为post内容进行下一步
get_source=s.get('http://xk2.edu.cn/xscjcx.aspx?xh=xxxxxx&xm=xxx&gnmkdm=N121605',headers=headers).content
soup=BeautifulSoup(get_source,'lxml')
value3=soup.find('input',id='__VIEWSTATE')['value']
value4=soup.find('input',id='__EVENTVALIDATION')['value']
data['__VIEWSTATE']=value3
data['__EVENTVALIDATION']=value4
get_score=s.post('http://xk2.edu.cn/xscjcx.aspx?xh=xxxxxx&xm=xxx&gnmkdm=N121605',data=data,headers=headers)
print (get_score.content)
最后通过解析get_score.content中的源代码就可以获得历年成绩数据了。