寫在最前面:本篇文章僅供學習Mathematica編程及WebTools的使用,並無鼓勵大家使用刷課機的意思,作者並未使用過該程序刷課。為了防止大面積的刷課機使用導致其它同學選不上課,並不會直接提供全部信息,WebTools包需要自己下載安裝哦~(溫馨提示:WebTools的開發者很粗心的,新的包裡面有個小地方更新的時候沒改過來#微笑#自己找找咯~)

代碼鏈接:

https://pan.baidu.com/s/1fo0u2GZVP-5PBxru3-PCmA?

pan.baidu.com

wjxway/PKUShuaKeJi-Mathematica?

github.com
圖標

背景

在北大選課大致分為兩輪:第一輪預選,大家通過投「意願點」表明意願,而後加權隨機選出選上課的人。第二輪補選,也就是「搶課」,一旦有空位,先到先得。這也就是為何會出現「刷課機」,即一直刷新界面,空位出現時就可以立刻選上。本文介紹的就是如何使用Mathematica及WebTools包製造一個好用的刷課機。

WebTools包由Wolfram公司員工Arnoud Buzing開發,GitHub鏈接如下:

arnoudbuzing/webtools?

github.com
圖標

這裡使用的版本是WebTools-0.1.1.它可以讓Mathematica控制一個瀏覽器窗口並進行各種操作,如點擊、輸入等。這些即我們製作刷課機所需要的功能。

在下一個版本(11.3)中其改版WebUnit會被內置到Mathematica中。

目標

製造一個選課機,從最基礎到最高級功能支持:

  1. 選課&退課
  2. 自動輸入驗證碼(選課系統的驗證碼十分規則,很方便識別)
  3. 選課退課邏輯
  4. 簡單的GUI

測試視頻:

https://www.zhihu.com/video/954353420933619712

可以看出來相比手動選課還是快很多,而且自動填入驗證碼,假如需要刷課(就是反覆檢查是否有人退課,放出名額)刷新速度也還可以接受。

實現過程

首先分析選課的過程:

  1. 到各個頁面上去看需要的課是否有空位(在各個頁面內切換、獲取頁面信息查看是否有空位)
  2. 若有空位則進行選課操作:
  3. (可能)退掉一些衝突的課(找到要退的課,點擊按鍵)
  4. 輸入驗證碼(驗證碼獲取、識別、輸入)
  5. 點擊選課按鍵(找到要選的課,點擊按鍵)
  6. 檢查是否選上了課(檢查課是否在「已選」中)

然後一個個實現:

登錄選課系統準備選課

默認已經用PacletInstall裝上WebTools包。

首先載入包並開啟一個瀏覽器(這裡我用的是Chrome)

<<WebTools`;wtInstallWebTools[];wtStartWebSession[];

這時候應該裝傻(劃掉)看到一個瀏覽器界面彈出來了,下一步就是通過Mathematica控制這個瀏覽器像人一樣執行操作。這樣的話速度顯然會比直接發Request要慢一些,但是好處在於很難被抓到……

首先登錄選課網(就是我的封面圖):

wtOpenWebPage["http://elective.pku.edu.cn/"]

然後要在指定位置裡面輸入用戶名、密碼和點擊登錄窗口並確認是否登錄成功:

WebTools包輸入內容需要知道輸入區域的Id和輸入的內容,在瀏覽器裡面F12查看網頁HTML,然後用右側上方箭頭樣子的這個工具來探查對應位置的HTML在哪裡

找到對應位置之後查看Id,發現用戶名、密碼的框Id分別為user_name和password

於是就可以寫:

wtTypeElement[wtId["user_name"], un]wtTypeElement[wtId["password"], pw];

即輸入用戶名、密碼。後面這樣先在瀏覽器上查詢HTML信息,分析後再寫代碼完成輸入、點擊、獲取等功能的操作還很多。例如如何獲取代碼塊的XPath、Selector等等,後面遇到這些就不一一贅述了。

下一步即點擊登錄按鈕並確認是否登錄成功,這次用XPath來給出點擊的位置:

wtClickElement[wtXPath["//*[@id="logon_button"]"]]

再通過判斷網頁URL有沒有變來檢查是否成功(失敗了就會留在登錄驗證的頁面麼~)

wtJavascriptExecute["return window.location.href;"]=="https://iaaa.pku.edu.cn/iaaa/oauth.jsp?appID=syllabus&appName=%E5%AD%A6%E7%94%9F%E9%80%89%E8%AF%BE%E7%B3%BB%E7%BB%9F&redirectUrl=http://elective.pku.edu.cn:80/elective2008/agent4Iaaa.jsp/../ssoLogin.do"

稍微封裝一下,作為登錄的部分,這樣我們就可以在後面直接調用了。

(*Verify Whether current URL is url*)SuccQ[url_String] := (wtJavascriptExecute["return window.location.href;"] === url)(*Login and check if successful*)login[un_String,pw_String,delay_:1.5]:=((*Login*)wtTypeElement[wtId["user_name"],un];wtTypeElement[wtId["password"],pw];wtClickElement[wtXPath["//*[@id="logon_button"]"]];(*check if successful*)Pause@delay;!SuccQ["https://iaaa.pku.edu.cn/iaaa/oauth.jsp?appID=syllabus&appName=%E5%AD%A6%E7%94%9F%E9%80%89%E8%AF%BE%E7%B3%BB%E7%BB%9F&redirectUrl=http://elective.pku.edu.cn:80/elective2008/agent4Iaaa.jsp/../ssoLogin.do"])

再加個弱弱的GUI,再在進入的頁面上點入補選那欄,就成了這樣子,就可以直接作為主程序開始的第一部分了:

loginDialog[text_:"Please Input ID and Password",textf_:"UN/PW incorrect, PLZ re-enter!"]:=Block[{display=text,un,pw},DialogInput[{ ExpressionCell[Dynamic[display]], TextCell["ID: "], InputField[Dynamic[un],String], TextCell["Password: "], InputField[Dynamic[pw],String,FieldMasked->True], Button["Login",display="Logging in..."; If[login[un,pw],DialogReturn[],display=textf]]},WindowTitle->"Log in"];{un,pw}](*再次封裝*)EnterElective[] := ( wtStartWebSession[]; wtOpenWebPage["http://elective.pku.edu.cn/"]; $unpw = loginDialog[]; (*點擊一個按鈕進入最終的補選窗口*) wtClickElement[wtXPath["/html/body/table[4]/tbody/tr[2]/td[2]/table/tbody/tr[7]/td/div[2]/a"]]; Pause@2; SuccQ["http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/SupplyCancel.do"])EnterElective[{un_, pw_}] := ( wtOpenWebPage["http://elective.pku.edu.cn/"]; If[login[un, pw], wtClickElement[tXPath["/html/body/table[4]/tbody/tr[2]/td[2]/table/tbody/tr[7]/td/div[2]/a"]]; Pause@2; SuccQ["http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/SupplyCancel.do"], False])

很簡單~

其實後面和瀏覽器打交道時也大同小異,只不過可能需要分析一下這個XPath和我要選的課在什麼位置有啥關係啊之類的。

獲取課程信息和相關網頁操作

進入界面後就是正正經經的識別課程信息和選課了。識別課程信息是通過獲取HTML文檔後進行分析得到的,這部分需要通過Javascript獲取部分的HTML(Javascipt其實我也不會,但是就這點的話,從網上搜一下就行……)。

稍微要注意的一點是,前面視頻中出現過的綠色提示信息會對各種東西的絕對路徑產生影響,我們用一個函數CheckInfo把這個體現出來:

CheckInfo[] := Boole[Head@wtJavascriptExecute["return document.getElementById(msgTips).innerHTML;"] === String]GetPageText[n_Integer]:=wtJavascriptExecute["return document.querySelector(body > table:nth-child(3) > tbody > tr:nth-child("<>ToString[7+CheckInfo[]]<>") > td > table > tbody > tr:nth-child("<>ToString[1+n]<>")).outerHTML;"]

獲取文本消息之後可以投機取巧用Mathematica自帶的XML的解釋器進行解釋:

Quiet@ImportString[StringDelete[raw, "<br>"], "XML"]

得到的一條信息大概是這樣:

XMLElement["tr", {"class" -> "datagrid-even"}, {XMLElement[ "td", {"class" -> "datagrid"}, {XMLElement[ "a", {"href" -> "/elective2008/edu/pku/stu/elective/controller/supplement/goNested.do?course_seq_no=BZ1718200432198_13533", "target" -> "_blank", "style" -> "width: 80"}, {XMLElement[ "span", {}, {"理論力學(A)"}]}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", {"style" -> "width: 60"}, {"任選"}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", {"style" -> "width: 30"}, {"4.0"}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", {"style" -> "width: 45"}, {"4.0"}]}], XMLElement[ "td", {"class" -> "datagrid"}, {XMLElement[ "span", {"style" -> "width: 40%"}, {"陳曉林(教授)"}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", {"style" -> "width: 30"}, {"1"}]}], XMLElement[ "td", {"class" -> "datagrid"}, {XMLElement[ "span", {"style" -> "width: 85"}, {"物理學院"}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", {"style" -> "width: 30"}, {"16"}]}], XMLElement[ "td", {"class" -> "datagrid"}, {XMLElement[ "span", {"style" -> "width: 60%"}, {"1~16周 每週週一1~2節 理教1021~16周 每週週四1~2節 理教102考試時間:20180628上午;"}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", {"id" -> "electedNum44", "style" -> "width: 60"}, {"160 / 95"}]}], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "a", {"href" -> "a link here", "style" -> "width: 30", "onclick" -> "return confirmSelect(MYNAME,理論力學(A),1,false,4,bkkc201600295663,false,160);"}, {XMLElement[ "span", {}, {"補選"}]}]}]}]

我們只簡單的提取課程名稱,課程班級號,選課人數,別的嫌麻煩就算了吧23333,用模式匹配可以很容易的解決這個問題,簡單來說就是模仿麼……:

Cases[**XML parse result**, XMLElement[ "tr", {"class" -> ("datagrid-even" | "datagrid-odd" | "datagrid-all")}, {XMLElement[ "td", {"class" -> "datagrid"}, {XMLElement[ "a", ___, {XMLElement["span", ___, {name_}]}]}], Repeated[XMLElement["td", {"class" -> "datagrid", ___}, ___], 4], XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement[ "span", ___, {classnum_}]}], ___, XMLElement[ "td", {"class" -> "datagrid", "align" -> "center"}, {XMLElement["span", ___, {number_}]}], Shortest[___]}] :> {name, ToExpression@classnum, ToExpression@StringSplit[number, "/"]}, Infinity]

封裝起來即可以得到獲取課程信息的幾個函數,我們叫它們GetPageText[]GetPageContent[_String]吧。

類似的我們還可以從這個GetPageText獲得的信息中獲得現在是多少頁,總共有多少頁等等的信息,這對翻頁是很有幫助的。翻頁的代碼由於比較重複又煩,我這裡就不展示了,若有興趣可以直接看代碼。大概結構是先寫出了四個函數用來完成 First/Previous/Next/Last四種操作,然後再用一個函數ToPage[i_Integer,raw_String]來綜合這四種操作。

前面已經學會瞭如何查「一頁」上的所有課和如何翻頁,但是假如我待選的課有好幾頁怎麼辦呢?原來的刷課機就矇蔽了,但是這個刷課機不會:我就一頁一頁掃一遍,把每頁的課程信息都搞下來就可以了啊!代碼如下:

GetPages[] := With[{raw = GetPageText[]}, Block[{pages = GetPageCount[raw][[2]], textl = {}}, FrontPage[raw]; Do[AppendTo[textl, GetPageText[]]; If[i != pages, NextPage[textl[[-1]]]], {i, pages}]; GetPageContent /@ textl ] ]

(按理來說這種時候應該使用Reap,Sow解決,Reap+Sow相比於AppendTo在添加次數多時可以快很多很多。而且這個也可以用Table解決。但是我就用AppendTo,哼!大家不要學我……)

到此為止,我們就能成功的提取所有的待選的課程信息了!

同理可以獲得所有已選課程的信息,結構完全一樣,只不過是要獲取另一塊HTML而已。

長度也許夠了?要麼如何識別驗證碼下篇一起講吧,內容比較多,驗證碼搞定就可以真·選上課了。再來終篇講講GUI的實現和邏輯


推薦閱讀:
相关文章