歡迎光臨
每天分享高質量文章

用Python實現模擬登入正方教務系統搶課

python6359
點選關註可彈出二維碼

最近學校開始選課,但是如果選課時間與自己的事情衝突,這時候就可以使用Python指令碼自助搶課,搶課的第一步即是模擬登入,需要模擬登入後儲存登入資訊然後再進行操作。

而且整個流程是比較簡單,這是因為正方教務系統是比較舊的,全文的IP地址部分遮擋,請換成你們學校的IP地址。

嘗試登入

首先我們開啟學校的教務系統,隨便輸入,然後提交表單,開啟Chrome的開發者工具中的Network準備抓包把css 圖片之類的過濾掉,發現了default.aspx這個東西

如果你們學校教務系統不使用Cookie則會是這樣

我們可以發現真實的地址是這樣的

http://110.65.10.xxx/(bdq1aj45lpd42o55vqpfgpie)/default2.aspx

隨後我們發現這個網址括號圍起來的一串資訊有點詭異,而且每次進入的時候資訊都不一樣,經過資料查詢,這是一種ASP.NET不使用Cookie會話管理的技術。

不使用 Cookie 的 ASP.NET 會話管理

那這樣就很好辦了,我們只需要登入時記錄下這個資料即可保持登入狀態。

經過測試發現,我們可以隨便偽造一個會話資訊即可一直保持登入狀態,但是為了體現模擬登入的科學性,我們需要先獲取該會話資訊。

如果你們學校教務系統使用Cookie則會是這樣伺服器會傳回一個Cookie值,然後在本地儲存,這與下麵的會不相同。

獲取會話資訊(不使用Cookie)

這裡我們要使用requests庫,並且要偽造essay-header的UA資訊

經過測試發現,我們只訪問學校的IP地址,會自動重定向至有會話資訊的網址,所以我們先訪問一下IP地址。

class Spider:
    def __init__(self, url):
        self.__uid = ''
        self.__real_base_url = ''
        self.__base_url = url
        self.__essay-headers = {
            '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',
        }
    def __set_real_url(self):
        request = requests.get(self.__base_url, essay-headers=self.__essay-headers)
        real_url = request.url
        self.__real_base_url = real_url[:len(real_url) - len('default2.aspx')]
        return request

上面獲取的url即為帶有會話資訊的網址,儲存的url格式為

http://110.65.10.xxx/(bdq1aj45lpd42o55vqpfgpie)/

儲存為這樣的格式是因為我們要訪問其他地址

獲取會話資訊(使用Cookie)

有些學校的教務系統是使用Cookie的,我們只需要首次get請求時儲存Cookie即可,然後此後一直使用該cookie

def get_cookie():
    request = requests.get('http://xxx.xxx.xxx.xxx'#以某教務系統為例子
    cookie = requets.cookie
    return cookie

而requests中使用Cookie很簡單

只需要這樣

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

由於我們學校採用的是無Cookie方案,所以下麵的程式碼均沒有傳送Cookie,如果你的學校採用了Cookie,只需要像我上面這樣傳送Cookie就行了。

而如果你們學校使用Cookie,就不必獲取帶有會話資訊的地址了,直接儲存Cookie即可。

或者也可以使用requests的Session自動管理會話資訊,這樣文章下麵的程式碼的請求全部改成Session的請求即可,但是首先需要在類的初始化方法中初始化。

def __init__(self):
    self.session = requests.Session()

然後我們首先訪問一次網站即可獲取Cookie並且儲存

def get(self):
    r = self.session.get(url,essay-headers=essay-headers)

更多的用法可以查詢檔案

驗證碼的處理

分析r傳回的文字資訊

發現驗證碼的標簽的資源地址為 src=”CheckCode.aspx” ,我們可以直接requests然後下載驗證碼圖片,下載圖片的一種優雅的方式如下

    def __get_code(self):
        request = requests.get(self.__real_base_url + 'CheckCode.aspx', essay-headers=self.__essay-headers)
        with open('code.jpg''wb')as f:
            f.write(request.content)
        im = Image.open('code.jpg')
        im.show()
        print('Please input the code:')
        code = input()
        return code

上面的程式碼把圖片儲存為code.jpg,Python有一個Image模組,可以實現自動開啟圖片

這樣驗證碼就展示出來了,我們人工輸入或者轉入打碼平臺皆可

登入資料的構造

這是上面抓的登入post的資料包,

發現有資訊無法被解碼,應該是gb2312編碼,檢視解碼前的編碼

然後將不能解碼的程式碼複製能夠解碼的地方

發現%D1%A7%C9%FA編碼解碼後為學生

這也就對應了學生選項的登入

學號和密碼和驗證碼能夠顯而易見地知道是哪些資訊,但是我們發現有__VIEWSTATE這一項

查詢一下,這是一個表單隱藏資訊,我們可以用BeautifulSoup庫解析可以得出該一項資料的值

這是完整的登入資料包,

    def __get_login_data(self, uid, password):
        self.__uid = uid
        request = self.__set_real_url()
        soup = BeautifulSoup(request.text, 'lxml')
        form_tag = soup.find('input')
        __VIEWSTATE = form_tag['value']
        code = self.__get_code()
        data = {
            '__VIEWSTATE': __VIEWSTATE,
            'txtUserName'self.__uid,
            'TextBox2': password,
            'txtSecretCode': code,
            'RadioButtonList1''學生'.encode('gb2312'),
            'Button1''',
            'lbLanguage''',
            'hidPdrs''',
            'hidsc''',
        }
        return data

登入

如果登入完成了,如何判斷是否登入成功呢?我們從登入成功傳回的介面發現有姓名這一標簽,而我們等一下也是需要學生姓名,所以我們用這個根據來判斷是否登入成功。

程式碼如下,進行了驗證碼使用者名稱和密碼的提示資訊判別

    def login(self,uid,password):
        while True:
            data = self.__get_login_data(uid, password)
            request = requests.post(self.__real_base_url + 'default2.aspx', essay-headers=self.__essay-headers, data=data)
            soup = BeautifulSoup(request.text, 'lxml')
            try:
                name_tag = soup.find(id='xhxm')
                self.__name = name_tag.string[:len(name_tag.string) - 2]
                print('歡迎'+self.__name)
            except:
                print('Unknown Error,try to login again.')
                time.sleep(0.5)
                continue
            finally:
                return True

獲取選課資訊

接下來就是獲取選課資訊了,這裡我們以校公選課為例子,點選進去,進行抓包,essay-headers沒有什麼好註意的,我們只用關註get傳送的包即可

發現有學號與姓名與gnmkdm這一項,姓名我們需要編碼為gb2312的形式才能進行傳送

這裡我們註意essay-headers需要新增Referer項也就是當前訪問的網址,才能進行請求

    def __enter_lessons_first(self):
        data = {
            'xh'self.__uid,
            'xm'self.__name.encode('gb2312'),
            'gnmkdm''N121103',
        }
        self.__essay-headers['Referer'] = self.__real_base_url + 'xs_main.aspx?xh=' + self.__uid
        request = requests.get(self.__real_base_url + 'xf_xsqxxxk.aspx', params=data, essay-headers=self.__essay-headers)
        self.__essay-headers['Referer'] = request.url
        soup = BeautifulSoup(request.text, 'lxml')
        self.__set__VIEWSTATE(soup)

註意到上面有一個設定VIEWSTATE值的函式,這裡等下在選課構造資料包的時候會講

模擬選課

隨便選一門課,然後提交,抓包,看一下有什麼資料傳送

前三個值可以在原網頁中input標簽中找到,由於前兩項為空,就不獲取了,而第三項我們使用soup解析獲取即可,由於這個操作是每請求一次就變化的,我們寫成一個函式,每次請求完成就設定一次。

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

而其他資料,我們透過搜尋響應網頁就可以知道他們是乾什麼用的,這裡我只說明我們要用的資料。

TextBox1為搜尋框資料,我們可以用這個來搜尋課程,dpkcmcGrid:txtPageSize為一頁顯示多少資料,經過測試,伺服器最多響應200條。

值得註意的是ddl_xqbs這個校區資料資訊,我所在的校區的數字代號為2,也許不同學校設定有所不同,需要自己設定一下,也可以從網頁中獲取

下麵是基礎資料包,由於我們搜尋課程與選擇課程都要使用這個基礎資料包,所以我們直接在init函式裡面新增

        self.__base_data = {
            '__EVENTTARGET''',
            '__EVENTARGUMENT''',
            '__VIEWSTATE''',
            'ddl_kcxz''',
            'ddl_ywyl''',
            'ddl_kcgs''',
            'ddl_xqbs''2',
            'ddl_sksj''',
            'TextBox1''',
            'dpkcmcGrid:txtChoosePage''1',
            'dpkcmcGrid:txtPageSize''200',
        }

然後我們關註一下這條資料,我們搜尋一下,發現這是課程的提交選課的程式碼,所以我們也可以直接從網頁中獲取,而on表示選項被選上

kcmcGrid:_ctl2:xk:'on'

搜尋課程

課程有很多資訊,比如名字,上課時間,地點,這些東西確定好了才知道選的是哪門課,所以我們先新建一個類來儲存資訊

    class Lesson:
        def __init__(self, name, code, teacher_name, Time, number):
            self.name = name
            self.code = code
            self.teacher_name = teacher_name
            self.time = Time
            self.number = number
        def show(self):
            print('name:' + self.name + 'code:' + self.code + 'teacher_name:' + self.teacher_name + 'time:' + self.time)

有了這個類,我們就可以進行搜尋課程了,具體程式碼看下麵程式碼,解析網頁內容就不細講了。

    def __search_lessons(self, lesson_name=''):
        self.__base_data['TextBox1'] = lesson_name.encode('gb2312')
        request = requests.post(self.__essay-headers['Referer'], data=self.__base_data, essay-headers=self.__essay-headers)
        soup = BeautifulSoup(request.text, 'lxml')
        self.__set__VIEWSTATE(soup)
        return self.__get_lessons(soup)
    def __get_lessons(self, soup):
        lesson_list = []
        lessons_tag = soup.find('table', id='kcmcGrid')
        lesson_tag_list = lessons_tag.find_all('tr')[1:]
        for lesson_tag in lesson_tag_list:
            td_list = lesson_tag.find_all('td')
            code = td_list[0].input['name']
            name = td_list[1].string
            teacher_name = td_list[3].string
            Time = td_list[4]['title']
            number = td_list[10].string
            lesson = self.Lesson(name, code, teacher_name, Time, number)
            lesson_list.append(lesson)
        return lesson_list

進行選課

選課我們只要將lesson_list傳入即可,這就是我們之前建立的Lesson類的實體的串列,’Button’的內容為’ 提交 ‘,這兩邊各有一個空格,完事後我們可以進行傳送請求進行選課。

這裡我們用正則提取了錯誤資訊,比如選課時間未到、上課時間衝突這些錯誤資訊來提示使用者,我們還解析了網頁的已選課程,這裡也不細講了,都是基礎的網頁解析。

    def __select_lesson(self, lesson_list):
        data = copy.deepcopy(self.__base_data)
        data['Button1'] = '  提交  '.encode('gb2312')
        for lesson in lesson_list:
            code = lesson.code
            data[code] = 'on'
        request = requests.post(self.__essay-headers['Referer'], data=data, essay-headers=self.__essay-headers)
        soup = BeautifulSoup(request.text, 'lxml')
        self.__set__VIEWSTATE(soup)
        error_tag = soup.html.head.script
        if not error_tag is None:
            error_tag_text = error_tag.string
            r = "alert\('(.+?)'\);"
            for s in re.findall(r, error_tag_text):
                print(s)
        print('已選課程:')
        selected_lessons_pre_tag = soup.find('legend', text='已選課程')
        selected_lessons_tag = selected_lessons_pre_tag.next_sibling
        tr_list = selected_lessons_tag.find_all('tr')[1:]
        for tr in tr_list:
            td = tr.find('td')
            print(td.string)

總結

這次我們完成了模擬正方教務系統選課的過程,由於這個教務系統技術比較陳舊,所以比較好弄,事實上搶課的時候用Fiddler即可完成操作,因為我們只需要提前登入然後記錄網址即可。

完整程式碼

import requests
from PIL import Image
from bs4 import BeautifulSoup
import copy
import time
import re
import os


class Spider:
    class Lesson:

        def __init__(self, name, code, teacher_name, Time, number):
            self.name = name
            self.code = code
            self.teacher_name = teacher_name
            self.time = Time
            self.number = number

        def show(self):
            print('  name:' + self.name + '  code:' + self.code + '  teacher_name:' + self.teacher_name + '  time:' + self.time)

    def __init__(self, url):
        self.__uid = ''
        self.__real_base_url = ''
        self.__base_url = url
        self.__name = ''
        self.__base_data = {
            '__EVENTTARGET''',
            '__EVENTARGUMENT''',
            '__VIEWSTATE''',
            'ddl_kcxz''',
            'ddl_ywyl''',
            'ddl_kcgs''',
            'ddl_xqbs''',
            'ddl_sksj''',
            'TextBox1''',
            'dpkcmcGrid:txtChoosePage''1',
            'dpkcmcGrid:txtPageSize''200',
        }
        self.__essay-headers = {
            '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',
        }
        self.session = requests.Session()
        self.__now_lessons_number = 0


    def __set_real_url(self):
        request = self.session.get(self.__base_url, essay-headers=self.__essay-headers)
        real_url = request.url
        if real_url != 'http://218.75.197.123:83/' and real_url != 'http://218.75.197.123:83/index.apsx':   # 湖南工業大學
            self.__real_base_url = real_url[:len(real_url) - len('default2.aspx')]
        else:
            if real_url.find('index') > 0:
                self.__real_base_url = real_url[:len(real_url) - len('index.aspx')]
            else:
                self.__real_base_url = real_url
        return request

    def __get_code(self):
        if self.__real_base_url != 'http://218.75.197.123:83/':
            request = self.session.get(self.__real_base_url + 'CheckCode.aspx', essay-headers=self.__essay-headers)
        else:
            request = self.session.get(self.__real_base_url + 'CheckCode.aspx?', essay-headers=self.__essay-headers)
        with open('code.jpg''wb')as f:
            f.write(request.content)
        im = Image.open('code.jpg')
        im.show()
        print('Please input the code:')
        code = input()
        return code

    def __get_login_data(self, uid, password):
        self.__uid = uid
        request = self.__set_real_url()
        soup = BeautifulSoup(request.text, 'lxml')
        form_tag = soup.find('input')
        __VIEWSTATE = form_tag['value']
        code = self.__get_code()
        data = {
            '__VIEWSTATE': __VIEWSTATE,
            'txtUserName'self.__uid,
            'TextBox2': password,
            'txtSecretCode': code,
            'RadioButtonList1''學生'.encode('gb2312'),
            'Button1''',
            'lbLanguage''',
            'hidPdrs''',
            'hidsc''',
        }
        return data

    def login(self, uid, password):
        while True:
            data = self.__get_login_data(uid, password)
            if self.__real_base_url != 'http://218.75.197.123:83/':
                request = self.session.post(self.__real_base_url + 'default2.aspx', essay-headers=self.__essay-headers, data=data)
            else:
                request = self.session.post(self.__real_base_url + 'index.aspx', essay-headers=self.__essay-headers, data=data)
            soup = BeautifulSoup(request.text, 'lxml')
            if request.status_code != requests.codes.ok:
                print('4XX or 5XX Error,try to login again')
                time.sleep(0.5)
                continue
            if request.text.find('驗證碼不正確') > -1:
                print('Code error,please input again')
                continue
            if request.text.find('密碼錯誤') > -1:
                print('Password may be error')
                return False
            if request.text.find('使用者名稱不存在') > -1:
                print('Uid may be error')
                return False
            try:
                name_tag = soup.find(id='xhxm')
                self.__name = name_tag.string[:len(name_tag.string) - 2]
                print('歡迎' + self.__name)
                self.__enter_lessons_first()
                return True
            except:
                print('Unknown Error,try to login again.')
                time.sleep(0.5)
                continue

    def __enter_lessons_first(self):
        data = {
            'xh'self.__uid,
            'xm'self.__name.encode('gb2312'),
            'gnmkdm''N121103',
        }
        self.__essay-headers['Referer'] = self.__real_base_url + 'xs_main.aspx?xh=' + self.__uid
        request = self.session.get(self.__real_base_url + 'xf_xsqxxxk.aspx', params=data, essay-headers=self.__essay-headers)
        self.__essay-headers['Referer'] = request.url
        soup = BeautifulSoup(request.text, 'lxml')
        self.__set__VIEWSTATE(soup)
        selected_lessons_pre_tag = soup.find('legend', text='已選課程')
        selected_lessons_tag = selected_lessons_pre_tag.next_sibling
        tr_list = selected_lessons_tag.find_all('tr')[1:]
        self.__now_lessons_number = len(tr_list)
        try:
            xq_tag = soup.find('select', id='ddl_xqbs')
            self.__base_data['ddl_xqbs'] = xq_tag.find('option')['value']
        except:
            pass

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

    def __get_lessons(self, soup):
        lesson_list = []
        lessons_tag = soup.find('table', id='kcmcGrid')
        lesson_tag_list = lessons_tag.find_all('tr')[1:]
        for lesson_tag in lesson_tag_list:
            td_list = lesson_tag.find_all('td')
            code = td_list[0].input['name']
            name = td_list[1].string
            teacher_name = td_list[3].string
            Time = td_list[4]['title']
            number = td_list[10].string
            lesson = self.Lesson(name, code, teacher_name, Time, number)
            lesson_list.append(lesson)
        return lesson_list

    def __search_lessons(self, lesson_name=''):
        self.__base_data['TextBox1'] = lesson_name.encode('gb2312')
        request = self.session.post(self.__essay-headers['Referer'], data=self.__base_data, essay-headers=self.__essay-headers)
        soup = BeautifulSoup(request.text, 'lxml')
        self.__set__VIEWSTATE(soup)
        return self.__get_lessons(soup)

    def __select_lesson(self, lesson_list):
        data = copy.deepcopy(self.__base_data)
        data['Button1'] = '  提交  '.encode('gb2312')
        for lesson in lesson_list:
            code = lesson.code
            data[code] = 'on'
        request = self.session.post(self.__essay-headers['Referer'], data=data, essay-headers=self.__essay-headers)
        soup = BeautifulSoup(request.text, 'lxml')
        self.__set__VIEWSTATE(soup)
        error_tag = soup.html.head.script
        if not error_tag is None:
            error_tag_text = error_tag.string
            r = "alert\('(.+?)'\);"
            for s in re.findall(r, error_tag_text):
                print(s)
        print('已選課程:')
        selected_lessons_pre_tag = soup.find('legend', text='已選課程')
        selected_lessons_tag = selected_lessons_pre_tag.next_sibling
        tr_list = selected_lessons_tag.find_all('tr')[1:]
        self.__now_lessons_number = len(tr_list)
        for tr in tr_list:
            td = tr.find('td')
            print(td.string)

    def run(self):
        print('請輸入搜尋課程名字')
        lesson_name = input()
        lesson_list = self.__search_lessons(lesson_name)
        print('請輸入想選的課的id,id為每門課程開頭的數字,如果沒有課程顯示,代表公選課暫無')
        for i in range(len(lesson_list)):
            print(i, end='')
            lesson_list[i].show()
        select_id = int(input())
        lesson_list = lesson_list[select_id:select_id + 1]
        while True:
            try:
                number = self.__now_lessons_number
                self.__select_lesson(lesson_list)
                if self.__now_lessons_number > number:
                    break
            except:
                print("搶課失敗,休息0.5秒後繼續")
                time.sleep(0.5)


if __name__ == '__main__':
    print('請輸入你們學校教務系統的地址,不用加上前面的http://')
    url = input()
    url = 'http://' + url
    spider = Spider(url)
    print('請輸入學號')
    uid = input()  #學號
    print('請輸入密碼')
    password = input() #密碼
    if (spider.login(uid, password)):
        spider.run()
    os.system("pause")

贊(0)

分享創造快樂