├── .gitignore ├── README.md ├── model.h5 └── yzuCourseBot.py /.gitignore: -------------------------------------------------------------------------------- 1 | # img 2 | *.png 3 | 4 | # jupyter 5 | debug.ipynb 6 | .ipynb_checkpoints 7 | 8 | # VSCode 9 | .vscode 10 | 11 | # private data 12 | accounts.ini 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yzuCourseBot 元智選課機器人 2 | 3 | ## Update log 4 | 5 | - 2021/01/09 6 | - [修改] 修改模擬點擊選課後 alertMsg 的取得方式 (`.text` -> `.string`),似乎是選課系統有修改,原本的方式 (`.text`) 無法獲取訊息 7 | - [新增] 判斷帳號密碼是否輸入錯誤 (僅比較帳號密碼錯誤及登入成功的訊息,不確定是否還有其他種情況也符合這個條件) 8 | - [新增] 判斷使用者是否不在選課時程內 9 | - 2020/06/25 10 | - [更新] 把22號的更動改回來,我不知道暑修的網址跟一般修課的網址是否有差,還是資服處嗯真的一直改檔名。 11 | - 2020/06/22 12 | - [更新] 因資服處更改檔名 (`SelCurr.aspx` -> `SelSc.aspx`),所以更新登入檢查的String 13 | - [說明] 因為這個Bot其實沒什麼用(有更好的方式可以搶課),加上我要畢業了,所以不打算再做太大的更動,歡迎學弟妹們發PR來維護,有任何想討論的請來信 aa0917954358@gmail.com 謝謝 14 | - 2020/02/26 15 | - [新增] 異常登入判斷,會休息10分鐘後再繼續 16 | - [新增] 系所編號判斷,填錯會報錯誤訊息 17 | - [新增] 新增delay功能,預設為1秒 (太快容易被server判定為異常request,請自行斟酌) 18 | - 2019/12/11 19 | - [修改] 優化判斷是否登入成功的方式 20 | - [修改] 優化部分hard code的地方 21 | - [新增] 判斷使用者的課程ID是否存在 22 | 23 | ## Introduction 24 | 最近在整理code丟上github,況且上學期還訓練了一個高辨識率的model(請參考 [CNN-model-for-YZU-cpatcha-OCR](https://github.com/Doem/CNN-model-for-YZU-cpatcha-OCR)),所以就順手將之前寫的選課機器人從pytesseract替換成自己訓練的CNN model,並移除selenium的依賴改純用requests的方式去模擬選課動作。由於改寫的時間不是很多並沒有寫得很好,歡迎各位提交Bug上來我會再找時間修正的。 25 | 26 | ## Dependencies 27 | |Name| 28 | |----| 29 | |lxml| 30 | |keras| 31 | |numpy| 32 | |opencv| 33 | |requests| 34 | |configparser| 35 | |BeautifulSoup| 36 | 37 | ### For Ubuntu 38 | ``` 39 | apt-get install -y libsm6 libxext6 libxrender-dev 40 | pip3 install opencv-python beautifulsoup4 keras tensorflow lxml 41 | ``` 42 | 43 | ## Remind 44 | 請斟酌使用本機器人程式,並自行負責使用後所造成的損失! 45 | 46 | ## Usage 47 | 48 | ### 1. 新增 `accounts.ini` 存放Portal帳密的檔案,格式如下: 49 | ``` 50 | [Default] 51 | Account= your account 52 | Password= your password 53 | ``` 54 | 55 | ### 2. 修改 `yzuCourseBot.py` 中的`coursesList`變數新增想選的課程清單,格式如下: 56 | ``` 57 | coursesList = [ 58 | '304,CS352A', 59 | '901,LS239A', 60 | '304,CS354A' 61 | ] 62 | ``` 63 | 64 | **304**: 為系所編號 65 | 66 | **CS352A**: 為課程編號加上班級編號,CS352 + A 67 | 68 | 以上資訊都能在課程查詢網站或是選課系統中得知的訊息 69 | 70 | ### 3. 執行 `yzuCourseBot.py` 71 | ``` 72 | $ python yzuCourseBot.py 73 | ``` 74 | 75 | **請用Python3以上的版本執行** 76 | 77 | 78 | ## Bug Report 79 | 請來信 aa0917954358@gmail.com 或是開issue,謝謝! 80 | -------------------------------------------------------------------------------- /model.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Doem/yzuCourseBot/b54cc7ddbf7262fccf01517f87f53476ef2687ce/model.h5 -------------------------------------------------------------------------------- /yzuCourseBot.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Date : 2019/09 3 | Author: Doem 4 | E-mail: aa0917954358@gmail.com 5 | ''' 6 | 7 | import os 8 | import cv2 9 | import time 10 | import requests 11 | import numpy as np 12 | import configparser 13 | from bs4 import BeautifulSoup 14 | from keras.models import load_model 15 | 16 | class CourseBot: 17 | def __init__(self, account, password): 18 | self.account = account 19 | self.password = password 20 | self.coursesDB = {} 21 | 22 | # for keras 23 | self.model = load_model('model.h5') 24 | self.n_classes = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 25 | 26 | # for requests 27 | self.session = requests.Session() 28 | self.session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36' 29 | 30 | self.loginUrl = 'https://isdna1.yzu.edu.tw/CnStdSel/Index.aspx' 31 | self.captchaUrl = 'https://isdna1.yzu.edu.tw/CnStdSel/SelRandomImage.aspx' 32 | self.courseListUrl = 'https://isdna1.yzu.edu.tw/CnStdSel/SelCurr/CosList.aspx' 33 | self.courseSelectUrl = 'https://isdna1.yzu.edu.tw/CnStdSel/SelCurr/CurrMainTrans.aspx?mSelType=SelCos&mUrl=' 34 | 35 | self.loginPayLoad = { 36 | '__VIEWSTATE': '', 37 | '__VIEWSTATEGENERATOR': '', 38 | '__EVENTVALIDATION': '', 39 | 'DPL_SelCosType': '', 40 | 'Txt_User': self.account, 41 | 'Txt_Password': self.password, 42 | 'Txt_CheckCode': '', 43 | 'btnOK': '確定' 44 | } 45 | 46 | self.selectPayLoad = {} 47 | 48 | def predict(self, img): 49 | prediction = self.model.predict(np.array([img])) 50 | 51 | predicStr = "" 52 | for pred in prediction: 53 | predicStr += self.n_classes[np.argmax(pred[0])] 54 | return predicStr 55 | 56 | def captchaOCR(self): 57 | captchaImg = cv2.imread('captcha.png') / 255.0 58 | return self.predict(captchaImg) 59 | 60 | # login into system and get session 61 | def login(self): 62 | 63 | while True: 64 | # clear Session object 65 | self.session.cookies.clear() 66 | 67 | # download and recognize captch 68 | with self.session.get(self.captchaUrl, stream= True) as captchaHtml: 69 | with open('captcha.png', 'wb') as img: 70 | img.write(captchaHtml.content) 71 | captcha = self.captchaOCR() 72 | 73 | # get login data 74 | loginHtml = self.session.get(self.loginUrl) 75 | 76 | # check if system is open 77 | if '選課系統尚未開放!' in loginHtml.text: 78 | self.log('選課系統尚未開放!') 79 | continue 80 | 81 | # use BeautifulSoup to parse html 82 | parser = BeautifulSoup(loginHtml.text, 'lxml') 83 | 84 | # update login payload 85 | self.loginPayLoad['__VIEWSTATE'] = parser.select("#__VIEWSTATE")[0]['value'] 86 | self.loginPayLoad['__VIEWSTATEGENERATOR'] = parser.select("#__VIEWSTATEGENERATOR")[0]['value'] 87 | self.loginPayLoad['__EVENTVALIDATION'] = parser.select("#__EVENTVALIDATION")[0]['value'] 88 | self.loginPayLoad['DPL_SelCosType'] = parser.select("#DPL_SelCosType option")[1]['value'] 89 | self.loginPayLoad['Txt_CheckCode'] = captcha 90 | 91 | result = self.session.post(self.loginUrl, data= self.loginPayLoad) 92 | if ("parent.location ='SelCurr.aspx?Culture=zh-tw'" in result.text): #成功登入訊息可能一直改,挑個不太能改的 93 | self.log('Login Successful! {}'.format(captcha)) 94 | break 95 | elif ("資料庫發生異常" in result.text): # 僅比較成功登入及帳號密碼錯誤的訊息,不確定是否還有其他種情況也符合這個條件 96 | self.log('帳號或密碼錯誤,請重新確認。') 97 | elif ("您未在此階段選課時程之內!請於時程內選課!!" in result.text): 98 | self.log('您未在此階段選課時程之內!請於時程內選課!!') 99 | else: 100 | self.log("Login Failed, Re-try!") 101 | continue 102 | exit(0) 103 | 104 | def getCourseDB(self, depts): 105 | 106 | for dept in depts: 107 | # use BeautifulSoup to parse html 108 | html = self.session.get(self.courseListUrl) 109 | if "異常登入" in html.text: 110 | self.log("異常登入,休息10分鐘!") 111 | time.sleep(600) # sleep 10 min 112 | continue 113 | parser = BeautifulSoup(html.text, 'lxml') 114 | 115 | self.selectPayLoad[dept] = { 116 | '__EVENTTARGET': 'DPL_Degree', 117 | '__EVENTARGUMENT': '', 118 | '__LASTFOCUS': '', 119 | '__VIEWSTATE': parser.select("#__VIEWSTATE")[0]['value'], 120 | '__VIEWSTATEGENERATOR': parser.select("#__VIEWSTATEGENERATOR")[0]['value'], 121 | '__VIEWSTATEENCRYPTED': '', 122 | '__EVENTVALIDATION': parser.select("#__EVENTVALIDATION")[0]['value'], 123 | 'Hidden1': '', 124 | 'Hid_SchTime': '', 125 | 'DPL_DeptName': dept, 126 | 'DPL_Degree': '6', 127 | } 128 | 129 | # use BeautifulSoup to parse html 130 | html = self.session.post(self.courseListUrl, data= self.selectPayLoad[dept]) 131 | if "Error" in html.text: 132 | self.log('Wrong coursesList, please check it again!') 133 | exit(0) 134 | parser = BeautifulSoup(html.text, 'lxml') 135 | 136 | # parse and save courses information 137 | courseList = parser.select("#CosListTable input") 138 | for courseInfo in courseList: 139 | tokens = courseInfo.attrs['name'].split(',') # SelCos,CS354,A,1,F,3,Y,Chinese,CS354,A,3 電腦與網路安全概論 140 | 141 | key = tokens[1] + tokens[2] 142 | courseName = '{} {}'.format(key, tokens[-1].split(' ')[1]) 143 | 144 | self.coursesDB[key] = { 145 | 'name': courseName, 146 | 'mUrl': courseInfo.attrs['name'] 147 | } 148 | # self.log(self.coursesDB[key]) 149 | 150 | self.log('Get {} Data Completed!'.format(dept)) 151 | 152 | 153 | 154 | def selectCourses(self, coursesList, delay = 0): 155 | while len(coursesList) > 0: 156 | for course in coursesList.copy(): 157 | tokens = course.split(',') 158 | dept = tokens[0] 159 | key = tokens[1] 160 | 161 | # check if the classID is legal 162 | if key not in self.coursesDB: 163 | self.log('{} is not a legal classID'.format(key)) 164 | coursesList.remove(course) 165 | continue 166 | 167 | # simulte click button 168 | html = self.session.post(self.courseListUrl, data= self.selectPayLoad[dept]) 169 | parser = BeautifulSoup(html.text, 'lxml') 170 | 171 | selectPayLoad = { 172 | '__EVENTTARGET': '', 173 | '__EVENTARGUMENT': '', 174 | '__LASTFOCUS': '', 175 | '__VIEWSTATE': parser.select("#__VIEWSTATE")[0]['value'], 176 | '__VIEWSTATEGENERATOR': parser.select("#__VIEWSTATEGENERATOR")[0]['value'], 177 | '__VIEWSTATEENCRYPTED': '', 178 | '__EVENTVALIDATION': parser.select("#__EVENTVALIDATION")[0]['value'], 179 | 'Hidden1': '', 180 | 'Hid_SchTime': '', 181 | 'DPL_DeptName': dept, 182 | 'DPL_Degree': '6', 183 | self.coursesDB[key]['mUrl'] + '.x': '0', 184 | self.coursesDB[key]['mUrl'] + '.y': '0' 185 | } 186 | self.session.post(self.courseListUrl, data= selectPayLoad) 187 | 188 | # select course 189 | html = self.session.get(self.courseSelectUrl + self.coursesDB[key]['mUrl'] + ' ,B,') 190 | 191 | # check if successful 192 | parser = BeautifulSoup(html.text, 'lxml') 193 | alertMsg = parser.select("script")[0].string.split(';')[0] 194 | self.log('{} {}'.format(self.coursesDB[key]['name'], alertMsg[7:-2])) 195 | 196 | if "加選訊息:" in alertMsg or "已選過" in alertMsg: 197 | coursesList.remove(course) 198 | elif "please log on again!" in alertMsg: 199 | self.login() 200 | 201 | time.sleep(delay) 202 | 203 | def log(self, msg): 204 | print(time.strftime("[%Y-%m-%d %H:%M:%S]", time.localtime()), msg) 205 | 206 | if __name__ == '__main__': 207 | configFilename = 'accounts.ini' 208 | if not os.path.isfile(configFilename): 209 | with open(configFilename, 'a') as f: 210 | f.writelines(["[Default]\n", "Account= your account\n", "Password= your password"]) 211 | print('input your username and password in accounts.ini') 212 | exit() 213 | # get account info fomr ini config file 214 | config = configparser.ConfigParser() 215 | config.read(configFilename) 216 | Account = config['Default']['Account'] 217 | Password = config['Default']['Password'] 218 | 219 | # the courses you want to select, format: '`deptId`,`courseId``classId`' 220 | coursesList = [ 221 | '304,CS250B', 222 | # '304,CS310A', 223 | # '901,LS239A', 224 | ] 225 | 226 | # Time Parameter, sleep n seconds 227 | delay = 1 228 | 229 | depts = set([i.split(',')[0] for i in coursesList]) 230 | 231 | myBot = CourseBot(Account, Password) 232 | myBot.login() 233 | myBot.getCourseDB(depts) 234 | myBot.selectCourses(coursesList, delay) 235 | --------------------------------------------------------------------------------