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

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

來自專欄 Python中文社區46 人贊了文章

本文首發於個人博客

用Python實現模擬登錄正方教務系統搶課 | vhyzs Blog?

vhyz.me圖標

6月23日更新:由於國內高校正方教務系統或多或少都會有所不同,所以細節地方還是需要自己修改的,我這個過程也只是一個案例,但這其中的本質上是不變的,即是抓包分析。

如果有什麼不懂的,可以在評論區評論,或者附上自己學校的教務網站系統,有空我會看一下的,也可以私信我。

-------------------------優雅的分割線----------------------

最近學校開始選課,但是如果選課時間與自己的事情衝突,這時候就可以使用Python腳本自助搶課,搶課的第一步即是模擬登錄,需要模擬登錄後保存登錄信息然後再進行操作。

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

嘗試登錄

首先我們打開學校的教務系統,隨便輸入,然後提交表單,打開Chrome的開發者工具中的Network準備抓包

把css 圖片之類的過濾掉,發現了default.aspx這個東西

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

我們可以發現,真實的請求地址為110.65.10.xxx/(bdq1aj45/default2.aspx

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

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

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

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

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

伺服器會返回一個Cookie值,然後在本地保存,這與下面的會不相同。

獲取會話信息(不使用Cookie)

這裡我們要使用requests庫,並且要偽造header的UA信息

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

class Spider: def __init__(self, url):pp self.__uid = self.__real_base_url = self.__base_url = url self.__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() def __set_real_url(self): request = self.session.get(self.__base_url, headers=self.__headers) real_url = request.url self.__real_base_url = real_url[:len(real_url) - len(default2.aspx)] return request

上面獲取的url即為帶有會話信息的網址,保存的url格式為

your_ip/(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,headers=headers)

更多的用法可以查詢文檔

驗證碼的處理

分析r返回的文本信息

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

def __get_code(self): request = self.session.get(self.__real_base_url + CheckCode.aspx, headers=self.__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 = self.session.post(self.__real_base_url + default2.aspx, headers=self.__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

獲取選課信息

接下來就是獲取選課信息了,這裡我們以校公選課為例子,點擊進去,進行抓包,headers沒有什麼好注意的,我們只用關注get發送的包即可

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

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

def __enter_lessons_first(self): data = { xh: self.__uid, xm: self.__name.encode(gb2312), gnmkdm: N121103, } self.__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, headers=self.__headers) self.__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 = self.session.post(self.__headers[Referer], data=self.__base_data, headers=self.__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 = self.session.post(self.__headers[Referer], data=data, headers=self.__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即可完成操作,因為我們只需要提前登錄然後記錄網址即可。

由於不同學校的正方教務系統有可能不同,所以上面很多細節都是需要修改的。

本文github代碼地址為,歡迎star。

vhyz/ZF_Spider?

github.com圖標

有問題歡迎在評論區提出

推薦閱讀:

python用轉義「和『的問題?
優雅地解決「2018刑偵推理題」
Python參數傳遞,既不是傳值也不是傳引用
Pandas入門基本知識
第一節課:安裝python

TAG:爬蟲計算機網路 | 計算機科學 | Python |