用Python实现模拟登录正方教务系统抢课

开发 后端
最近学校开始选课,但是如果选课时间与自己的事情冲突,这时候就可以使用Python脚本自助抢课,抢课的第一步即是模拟登录,需要模拟登录后保存登录信息然后再进行操作。

最近学校开始选课,但是如果选课时间与自己的事情冲突,这时候就可以使用Python脚本自助抢课,抢课的***步即是模拟登录,需要模拟登录后保存登录信息然后再进行操作。

而且整个流程是比较简单,这是因为正方教务系统是比较旧的,全文的IP地址部分遮挡,请换成你们学校的IP地址。

尝试登录

首先我们打开学校的教务系统,随便输入,然后提交表单,打开Chrome的开发者工具中的Network准备抓包

 


把css 图片之类的过滤掉,发现了default.aspx这个东西

如果你们学校教务系统不使用Cookie则会是这样

 

我们可以发现,真实的请求地址为http://110.65.10.xxx/(bdq1aj45lpd42o55vqpfgpie)/default2.aspx

随后我们发现这个网址括号围起来的一串信息有点诡异,而且每次进入的时候信息都不一样,经过资料查询,这是一种ASP.NET不使用Cookie会话管理的技术。

那这样就很好办了,我们只需要登录时记录下这个数据即可保持登录状态。

经过测试发现,我们可以随便伪造一个会话信息即可一直保持登录状态,但是为了体现模拟登录的科学性,我们需要先获取该会话信息。

如果你们学校教务系统使用Cookie则会是这样

 

服务器会返回一个Cookie值,然后在本地保存,这与下面的会不相同。

获取会话信息(不使用Cookie)

这里我们要使用requests库,并且要伪造header的UA信息

经过测试发现,我们只访问学校的IP地址,会自动重定向至有会话信息的网址,所以我们先访问一下IP地址。

  1. class Spider: 
  2.     def __init__(self, url): 
  3.         self.__uid = '' 
  4.         self.__real_base_url = '' 
  5.         self.__base_url = url 
  6.         self.__headers = { 
  7.             'User-Agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36'
  8.         } 
  9.     def __set_real_url(self): 
  10.         request = requests.get(self.__base_url, headers=self.__headers) 
  11.         real_url = request.url 
  12.         self.__real_base_url = real_url[:len(real_url) - len('default2.aspx')] 
  13.         return request 

上面获取的url即为带有会话信息的网址,保存的url格式为http://110.65.10.xxx/(bdq1aj45lpd42o55vqpfgpie)/

保存为这样的格式是因为我们要访问其他地址

获取会话信息(使用Cookie)

有些学校的教务系统是使用Cookie的,我们只需要***get请求时保存Cookie即可,然后此后一直使用该cookie

  1. def get_cookie(): 
  2.     request = requests.get('http://xxx.xxx.xxx.xxx') #以某教务系统为例子 
  3.     cookie = requets.cookie 
  4.     return cookie 

而requests中使用Cookie很简单

只需要这样

  1. def use_cookie(cookie): 
  2.     request = requests.get('http://xxx.xxx.xxx.xxx',cookie=cookie) 

由于我们学校采用的是无Cookie方案,所以下面的代码均没有发送Cookie,如果你的学校采用了Cookie,只需要像我上面这样发送Cookie就行了。

而如果你们学校使用Cookie,就不必获取带有会话信息的地址了,直接存储Cookie即可。

验证码的处理

分析r返回的文本信息

发现验证码的标签的资源地址为 src=”CheckCode.aspx” ,我们可以直接requests然后下载验证码图片,下载图片的一种优雅的方式如下

  1. def __get_code(self): 
  2.     request = requests.get(self.__real_base_url + 'CheckCode.aspx', headers=self.__headers) 
  3.     with open('code.jpg''wb')as f: 
  4.         f.write(request.content) 
  5.     im = Image.open('code.jpg'
  6.     im.show() 
  7.     print('Please input the code:'
  8.     code = input() 
  9.     return code 

上面的代码把图片保存为code.jpg,Python有一个Image模块,可以实现自动打开图片

这样验证码就展示出来了,我们人工输入或者转入打码平台皆可

登录数据的构造

这是上面抓的登录post的数据包,

 

发现有信息无法被解码,应该是gb2312编码,查看解码前的编码

 

然后将不能解码的代码复制能够解码的地方

发现%D1%A7%C9%FA编码解码后为学生

这也就对应了学生选项的登录

学号和密码和验证码能够显而易见地知道是哪些信息,但是我们发现有__VIEWSTATE这一项

查找一下,这是一个表单隐藏信息,我们可以用BeautifulSoup库解析可以得出该一项数据的值

 

这是完整的登录数据包,

  1. def __get_login_data(self, uid, password): 
  2.     self.__uid = uid 
  3.     request = self.__set_real_url() 
  4.     soup = BeautifulSoup(request.text, 'lxml'
  5.     form_tag = soup.find('input'
  6.     __VIEWSTATE = form_tag['value'
  7.     code = self.__get_code() 
  8.     data = { 
  9.         '__VIEWSTATE': __VIEWSTATE, 
  10.         'txtUserName': self.__uid, 
  11.         'TextBox2'password
  12.         'txtSecretCode': code, 
  13.         'RadioButtonList1''学生'.encode('gb2312'), 
  14.         'Button1'''
  15.         'lbLanguage'''
  16.         'hidPdrs'''
  17.         'hidsc'''
  18.     } 
  19.     return data 

登录

如果登录完成了,如何判断是否登录成功呢?我们从登录成功返回的界面发现有姓名这一标签,而我们等一下也是需要学生姓名,所以我们用这个根据来判断是否登录成功。

 

代码如下,进行了验证码用户名和密码的提示信息判别

  1. def login(self,uid,password): 
  2.     while True
  3.         data = self.__get_login_data(uid, password
  4.         request = requests.post(self.__real_base_url + 'default2.aspx', headers=self.__headers, data=data) 
  5.         soup = BeautifulSoup(request.text, 'lxml'
  6.         try: 
  7.             name_tag = soup.find(id='xhxm'
  8.             self.__name = name_tag.string[:len(name_tag.string) - 2] 
  9.             print('欢迎'+self.__name) 
  10.         except
  11.             print('Unknown Error,try to login again.'
  12.             time.sleep(0.5) 
  13.             continue 
  14.         finally: 
  15.             return True 

获取选课信息

接下来就是获取选课信息了,这里我们以校公选课为例子,点击进去,进行抓包,headers没有什么好注意的,我们只用关注get发送的包即可

 

 

发现有学号与姓名与gnmkdm这一项,姓名我们需要编码为gb2312的形式才能进行传送

这里我们注意headers需要新增Referer项也就是当前访问的网址,才能进行请求

  1. def __enter_lessons_first(self): 
  2.     data = { 
  3.         'xh': self.__uid, 
  4.         'xm': self.__name.encode('gb2312'), 
  5.         'gnmkdm''N121103'
  6.     } 
  7.     self.__headers['Referer'] = self.__real_base_url + 'xs_main.aspx?xh=' + self.__uid 
  8.     request = requests.get(self.__real_base_url + 'xf_xsqxxxk.aspx', params=data, headers=self.__headers) 
  9.     self.__headers['Referer'] = request.url 
  10.     soup = BeautifulSoup(request.text, 'lxml'
  11.     self.__set__VIEWSTATE(soup) 

注意到上面有一个设置VIEWSTATE值的函数,这里等下在选课构造数据包的时候会讲

模拟选课

随便选一门课,然后提交,抓包,看一下有什么数据发送

 

前三个值可以在原网页中input标签中找到,由于前两项为空,就不获取了,而第三项我们使用soup解析获取即可,由于这个操作是每请求一次就变化的,我们写成一个函数,每次请求完成就设置一次。

 


  1. def __set__VIEWSTATE(self, soup): 
  2.     __VIEWSTATE_tag = soup.find('input', attrs={'name''__VIEWSTATE'}) 
  3.     self.__base_data['__VIEWSTATE'] = __VIEWSTATE_tag['value'

而其他数据,我们通过搜索响应网页就可以知道他们是干什么用的,这里我只说明我们要用的数据。

TextBox1为搜索框数据,我们可以用这个来搜索课程,dpkcmcGrid:txtPageSize为一页显示多少数据,经过测试,服务器最多响应200条。

值得注意的是ddl_xqbs这个校区数据信息,我所在的校区的数字代号为2,也许不同学校设置有所不同,需要自己设置一下,也可以从网页中获取

下面是基础数据包,由于我们搜索课程与选择课程都要使用这个基础数据包,所以我们直接在init函数里面新增

  1. self.__base_data = { 
  2.     '__EVENTTARGET'''
  3.     '__EVENTARGUMENT'''
  4.     '__VIEWSTATE'''
  5.     'ddl_kcxz'''
  6.     'ddl_ywyl'''
  7.     'ddl_kcgs'''
  8.     'ddl_xqbs''2'
  9.     'ddl_sksj'''
  10.     'TextBox1'''
  11.     'dpkcmcGrid:txtChoosePage''1'
  12.     'dpkcmcGrid:txtPageSize''200'

然后我们关注一下这条数据,我们搜索一下,发现这是课程的提交选课的代码,所以我们也可以直接从网页中获取,而on表示选项被选上

 

  1. kcmcGrid:_ctl2:xk:'on' 

搜索课程

课程有很多信息,比如名字,上课时间,地点,这些东西确定好了才知道选的是哪门课,所以我们先新建一个类来存储信息

  1. class Lesson: 
  2.     def __init__(self, name, code, teacher_name, Time, number): 
  3.         self.name = name 
  4.         self.code = code 
  5.         self.teacher_name = teacher_name 
  6.         self.time = Time 
  7.         self.number = number 
  8.     def show(self): 
  9.         print('name:' + self.name + 'code:' + self.code + 'teacher_name:' + self.teacher_name + 'time:' + self.time

有了这个类,我们就可以进行搜索课程了,具体代码看下面代码,解析网页内容就不细讲了。

  1. def __search_lessons(self, lesson_name=''): 
  2.     self.__base_data['TextBox1'] = lesson_name.encode('gb2312'
  3.     request = requests.post(self.__headers['Referer'], data=self.__base_data, headers=self.__headers) 
  4.     soup = BeautifulSoup(request.text, 'lxml'
  5.     self.__set__VIEWSTATE(soup) 
  6.     return self.__get_lessons(soup) 
  7. def __get_lessons(self, soup): 
  8.     lesson_list = [] 
  9.     lessons_tag = soup.find('table', id='kcmcGrid'
  10.     lesson_tag_list = lessons_tag.find_all('tr')[1:] 
  11.     for lesson_tag in lesson_tag_list: 
  12.         td_list = lesson_tag.find_all('td'
  13.         code = td_list[0].input['name'
  14.         name = td_list[1].string 
  15.         teacher_name = td_list[3].string 
  16.         Time = td_list[4]['title'
  17.         number = td_list[10].string 
  18.         lesson = self.Lesson(name, code, teacher_name, Time, number) 
  19.         lesson_list.append(lesson) 
  20.     return lesson_list 

进行选课

选课我们只要将lesson_list传入即可,这就是我们之前创建的Lesson类的实例的列表,’Button’的内容为’ 提交 ‘,这两边各有一个空格,完事后我们可以进行发送请求进行选课。

这里我们用正则提取了错误信息,比如选课时间未到、上课时间冲突这些错误信息来提示用户,我们还解析了网页的已选课程,这里也不细讲了,都是基础的网页解析。

  1. def __select_lesson(self, lesson_list): 
  2.     data = copy.deepcopy(self.__base_data) 
  3.     data['Button1'] = '  提交  '.encode('gb2312'
  4.     for lesson in lesson_list: 
  5.         code = lesson.code 
  6.         data[code] = 'on' 
  7.     request = requests.post(self.__headers['Referer'], data=data, headers=self.__headers) 
  8.     soup = BeautifulSoup(request.text, 'lxml'
  9.     self.__set__VIEWSTATE(soup) 
  10.     error_tag = soup.html.head.script 
  11.     if not error_tag is None: 
  12.         error_tag_text = error_tag.string 
  13.         r = "alert\('(.+?)'\);" 
  14.         for s in re.findall(r, error_tag_text): 
  15.             print(s) 
  16.     print('已选课程:'
  17.     selected_lessons_pre_tag = soup.find('legend', text='已选课程'
  18.     selected_lessons_tag = selected_lessons_pre_tag.next_sibling 
  19.     tr_list = selected_lessons_tag.find_all('tr')[1:] 
  20.     for tr in tr_list: 
  21.         td = tr.find('td'
  22.         print(td.string) 

总结

这次我们完成了模拟正方教务系统选课的过程,由于这个教务系统技术比较陈旧,所以比较好弄,事实上抢课的时候用Fiddler即可完成操作,因为我们只需要提前登录然后记录网址即可。

本文代码Github地址:https://github.com/vhyz/ZF_Spider

责任编辑:未丽燕 来源: whyz's Blog
相关推荐

2024-01-12 18:26:44

2023-03-09 08:12:08

免登录实Python脚本

2021-08-02 12:29:15

Python爬虫网站

2020-08-19 17:14:26

Python数据函数

2018-01-02 09:56:04

Python12306火车票

2023-07-02 14:05:13

2021-01-06 10:09:05

Spring Boothttps sslhttps

2012-08-21 11:26:17

Winform

2020-02-11 16:10:44

Redis分布式锁Java

2021-05-25 10:05:39

Python模拟导弹代码

2016-09-12 14:05:27

PythonPython解释器Web

2020-02-24 10:29:24

数据库系统肺炎

2020-12-18 08:55:20

Python火车票代码

2021-07-29 13:06:29

Python机器学习编程语言

2019-01-24 09:00:00

PythonAutoML机器学习

2019-01-09 09:35:41

抢票Python软件

2022-02-25 14:04:56

TS前端代码

2012-06-01 09:44:26

Python

2014-12-22 11:07:18

12306

2015-06-30 15:14:54

点赞
收藏

51CTO技术栈公众号