61

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

 5 years ago
source link: http://www.10tiao.com/html/263/201806/2652567904/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

(点击上方蓝字,快速关注我们)


作者:小苏打

https://vhyz.me/2018/06/12/用Python实现模拟登录正方教务系统抢课/


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

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

尝试登录

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

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

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

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

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

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

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

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

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

服务器会返回一个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.        return request

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

  1. your_ip/(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.        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发送的包即可

发现有学号与姓名与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',

  13.    }

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

  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.        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.        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.        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.        ifnot error_tag isNone:

  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 

看完本文有收获?请转发分享给更多人

关注「Python开发者」,提升Python技能


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK