用Python模拟登录学校教务系统抢课

640?wx_fmt=jpeg

--  Illustrations by Vladislav Solovjov --

作者:小苏打

博客地址:https://vhyz.me

GitHub地址:github.com/vhyz/ZF_Spider


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

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

尝试登录

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

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

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

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

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

不使用 Cookie 的 ASP.NET 会话管理

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

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

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

640?wx_fmt=jpeg

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

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

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

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

  1. classSpider:

  2.    def__init__(self,url):pp

  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.        returnrequest

上面获取的url即为带有会话信息的网址,保存的url格式为

  1. your_ip/(bdq1aj45lpd42o55vqpfgpie)/

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

获取会话信息(使用Cookie)

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

  1. defget_cookie():

  2.    request=requests.get('http://xxx.xxx.xxx.xxx')#以某教务系统为例子

  3.    cookie=requets.cookie

  4.    returncookie

而requests中使用Cookie很简单

只需要这样

  1. defuse_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.        withopen('code.jpg','wb')asf:

  4.            f.write(request.content)

  5.        im=Image.open('code.jpg')

  6.        im.show()

  7.        print('Please input the code:')

  8.        code=input()

  9.        returncode

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

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

登录数据的构造

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

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

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

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

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

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

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

640?wx_fmt=jpeg

这是完整的登录数据包,

  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.        returndata

登录

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

640?wx_fmt=jpeg

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

  1.    deflogin(self,uid,password):

  2.        whileTrue:

  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.                returnTrue

获取选课信息

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

发现有学号与姓名与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值的函数,这里等下在选课构造数据包的时候会讲

模拟选课

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

640?wx_fmt=jpeg

640?wx_fmt=jpeg

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

640?wx_fmt=jpeg

  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',

  13.    }

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

640?wx_fmt=jpeg

  1. kcmcGrid:_ctl2:xk:'on'

搜索课程

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

  1.    classLesson:

  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.        defshow(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.        returnself.__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.        forlesson_taginlesson_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.        returnlesson_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.        forlessoninlesson_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.        ifnoterror_tagisNone:

  12.            error_tag_text=error_tag.string

  13.            r="alert\('(.+?)'\);"

  14.            forsinre.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.        fortrintr_list:

  21.            td=tr.find('td')

  22.            print(td.string)

总结

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

由于不同学校的正方教务系统有可能不同,所以上面很多细节都是需要修改的。


640?wx_fmt=jpegPython中文社区 全球Python中文开发者的 精神部落640?wx_fmt=jpeg


640?wx_fmt=gif

Python中文社区作为一个去中心化的全球技术社区,以成为全球20万Python中文开发者的精神部落为愿景,目前覆盖各大主流媒体和协作平台,与阿里、腾讯、百度、微软、亚马逊、开源中国、CSDN等业界知名公司和技术社区建立了广泛的联系,拥有来自十多个国家和地区数万名登记会员,会员来自以公安部、工信部、清华大学、北京大学、北京邮电大学、中国人民银行、中科院、中金、华为、BAT、谷歌、微软等为代表的政府机关、科研单位、金融机构以及海内外知名公司,全平台近20万开发者关注。

640?wx_fmt=jpeg

▼ 点击下方阅读原文免费成为社区会员