├── .gitignore ├── LICENSE ├── README.md ├── code.bmp ├── info.py ├── main.py ├── requirements.txt ├── scs.py └── setting.py /.gitignore: -------------------------------------------------------------------------------- 1 | #log file 2 | log3.log 3 | #capcha code img 4 | code.bmp 5 | #personal info 6 | info.py 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sirius See 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 中山大学(SYSU)选课系统的抢课软件 2 | 3 | ## 这是什么? 4 | 5 | SYSU Course Selector(SCS),是一个基于 python3 的抢课软件,系我在贵校就读三年来,从未选上理想的课程,愤然而作。 6 | 7 | 实践证明,有了 SCS 的帮助,我的确选到了我想要的课程。 8 | 9 | ## 如何使用? 10 | 11 | 0. 你必须把源码拉到本地,执行 `git clone https://github.com/Siriussee/sysu-course-selector.git`; 12 | 1. 我只提供了源代码,所以首先你得准备一个 [python 3.7.0](https://www.python.org/downloads/); 13 | 2. 安装对应的依赖 `pip install -r requirements.txt`; 14 | 3. 根据你的实际情况修改 `info.py`; 15 | 4. 运行主程序 `python main.py`; 16 | 5. 根据 `code.bmp` 中的验证码,输入验证码; 17 | 6. 输入你想选的课的课程号; 18 | 7. 别关掉它,耐心等待。 19 | 20 | 需要注意的是,这个抢课软件也仅仅是在有人退课的情况下为你尽快抢选这门课程。因此,它不能保证你一定能够选上这门课——说不定大家都不退课,或者有别人也用了这个软件捷足先登了呢。 21 | 22 | ## 我是真的不会用怎么办? 23 | 24 | 请带上一杯奶茶找我,我帮你。如果我们素未谋面,请在 issue 留下您的邮箱。 25 | 26 | ## 出现了bug? 27 | 28 | 请在提交 issue 时附带运行时生成的 `log3.log`,这样能方便我定位错误。 29 | 30 | ## 更新计划 31 | 32 | - 打包成二进制可执行文件(没什么动力)。 33 | - 我的代码只在SE专业的专选课程进行过测试,希望能够将适用性扩展到更多专业和更多课程类别。 34 | - 二维码的机器识别。 35 | - TBD 36 | -------------------------------------------------------------------------------- /code.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Siriussee/sysu-course-selector/631233df0b75f6e771a756f676cffe02d45c8229/code.bmp -------------------------------------------------------------------------------- /info.py: -------------------------------------------------------------------------------- 1 | # please change this demo to your real netid and password 2 | 3 | name = 'YOUR_NET_ID' 4 | pwd = 'YOUR_PASSWORD' 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from scs import course_selector 2 | from info import name, pwd 3 | 4 | def main(): 5 | cs = course_selector() 6 | img = cs.pre_login() 7 | with open('code.bmp', 'wb') as f: 8 | f.write(img) 9 | captcha_str = input('what is captcha?') 10 | cs.in_login(name, pwd, captcha_str) 11 | course_data = cs.course_query() 12 | # print course_data 13 | print('{:10}{:30}{:10}{:10}{:10}'.format('Course ID', 'Course Name', 'Lecturer', 'Seleted/All', 'Chosen')) 14 | for cd in course_data: 15 | print('{:10}{:30}{:10}{:10}{:10}'.format(cd['cid'], cd['cname'], cd['lecturer'], cd['snum'] ,cd['status'])) 16 | target_course_list_str = input('Enter Course ID, separate by ENGILSH comma , :') 17 | cs.course_select_wrapper(target_course_list_str) 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | asn1crypto==0.24.0 3 | astroid==2.0.4 4 | atomicwrites==1.1.5 5 | attrs==18.1.0 6 | Automat==0.7.0 7 | autopep8==1.3.5 8 | beautifulsoup4==4.6.3 9 | bs4==0.0.1 10 | certifi==2018.11.29 11 | cffi==1.11.5 12 | colorama==0.3.9 13 | constantly==15.1.0 14 | cryptography==2.4.2 15 | cryptography-vectors==2.3 16 | cssselect==1.0.3 17 | cycler==0.10.0 18 | fake-useragent==0.1.10 19 | hyperlink==18.0.0 20 | idna==2.7 21 | incremental==17.5.0 22 | isort==4.3.4 23 | json2table==1.1.5 24 | kiwisolver==1.0.1 25 | lazy-object-proxy==1.3.1 26 | lxml==4.2.4 27 | mccabe==0.6.1 28 | mkl-fft==1.0.6 29 | mkl-random==1.0.1 30 | more-itertools==4.3.0 31 | olefile==0.46 32 | parsel==1.4.0 33 | Pillow==5.2.0 34 | pluggy==0.7.1 35 | py==1.5.4 36 | pyasn1==0.4.4 37 | pyasn1-modules==0.2.2 38 | pycodestyle==2.4.0 39 | pycparser==2.18 40 | PyDispatcher==2.0.5 41 | pylint==2.1.1 42 | pyparsing==2.2.2 43 | PySocks==1.6.8 44 | pytesseract==0.2.6 45 | pytest==3.7.1 46 | pytest-runner==4.2 47 | python-dateutil==2.7.5 48 | pytz==2018.9 49 | pywin32==223 50 | queuelib==1.5.0 51 | six==1.11.0 52 | w3lib==1.19.0 53 | wincertstore==0.2 54 | wrapt==1.10.11 55 | zope.interface==4.5.0 56 | -------------------------------------------------------------------------------- /scs.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import http.cookiejar 3 | import json 4 | import logging 5 | import socket 6 | import threading 7 | import time 8 | import urllib.parse 9 | import urllib.request 10 | 11 | #import pandas as pd 12 | import socks 13 | from bs4 import BeautifulSoup 14 | 15 | from info import name, pwd 16 | from setting import CONCURRENT_REQUEST, TIMEOUT, DELAY, USE_SOCKS5_PROXY, SOCKS5_PROXY_PORT 17 | 18 | class course_selector: 19 | user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' 20 | cas_url = 'https://cas.sysu.edu.cn/cas/login?service=https://uems.sysu.edu.cn/jwxt/api/sso/cas/login%3Fpattern=student-login' 21 | captcha_url = 'https://cas.sysu.edu.cn/cas/captcha.jsp' 22 | selection_url = 'https://uems.sysu.edu.cn/jwxt/mk/courseSelection/' 23 | courselist_url = 'https://uems.sysu.edu.cn/jwxt/{}?_t={}' 24 | course_select_url = 'https://uems.sysu.edu.cn/jwxt/choose-course-front-server/classCourseInfo/course/choose?_t={}' 25 | headers = {'User-Agent' : user_agent} 26 | info_para = ('student-status/student-info/detail', 27 | 'choose-course-front-server/classCourseInfo/selectCourseInfo', 28 | 'choose-course-front-server/stuCollectedCourse/getYearTerm') 29 | 30 | def __init__(self): 31 | logging.basicConfig( 32 | filename='log3.log', 33 | level=logging.DEBUG, 34 | format='%(asctime)s %(levelname)-8s %(message)s', 35 | datefmt='%Y-%m-%d %H:%M:%S' 36 | ) 37 | cj = http.cookiejar.CookieJar() 38 | self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) 39 | self.name = None 40 | self.pwd = None 41 | self.course_list = None 42 | self.exec_code = None 43 | if USE_SOCKS5_PROXY: 44 | socks.set_default_proxy(socks.SOCKS5, "localhost", SOCKS5_PROXY_PORT) 45 | socket.socket = socks.socksocket 46 | 47 | # a safe opener.open(cas_get) 48 | def __open_s(self, req): 49 | try: 50 | response = self.opener.open(req, timeout=TIMEOUT) 51 | content = response.read() 52 | try: 53 | content = content.decode(encoding='UTF-8') 54 | except: 55 | pass # kepp it what it was, like capcha img 56 | finally: 57 | response_dict = { 58 | 'read' : content, 59 | 'url' : response.geturl(), 60 | 'info' : response.info() 61 | } 62 | logging.debug(response_dict['read']) 63 | return response_dict 64 | except urllib.error.HTTPError as e: 65 | err_dict = { 66 | 'read' : e.read().decode(encoding='UTF-8'), 67 | 'code' : e.code, 68 | 'reason' : e.reason 69 | } 70 | logging.debug('err_code={}, err_reason={}, err_msg={}'.format(err_dict['code'], err_dict['reason'], err_dict['reason'])) 71 | return err_dict 72 | except (socket.timeout, urllib.error.URLError): 73 | logging.debug('timeout') 74 | return None 75 | 76 | def __current_time(self): 77 | return int(time.time()) 78 | 79 | def __courselist_headers(self, t): 80 | cl_header = { 81 | 'User-Agent' : self.user_agent, 82 | 'lastAccessTime': t * 1000 + 213, 83 | 'Content-Type': 'application/json;charset=UTF-8', #MUST have 84 | 'Origin': 'https://uems.sysu.edu.cn', 85 | 'Referer': 'https://uems.sysu.edu.cn/jwxt/mk/courseSelection/' 86 | } 87 | return cl_header 88 | 89 | # get exec code and return captcha img 90 | def pre_login(self): 91 | # find CAS execution code 92 | cas_get = urllib.request.Request(self.cas_url, headers=self.headers) 93 | response = self.__open_s(cas_get) 94 | if response != None and 'code' not in response: 95 | html = response['read'] 96 | else: 97 | raise NameError('visit cas failed') 98 | soup = BeautifulSoup(html, features="lxml") 99 | execution = soup.find_all('input', attrs={'name': 'execution'})[0]['value'] 100 | self.exec_code = execution 101 | # get captcha img 102 | captcha_get = urllib.request.Request(self.captcha_url, headers=self.headers) 103 | response = self.__open_s(captcha_get) 104 | if response != None and 'code' not in response: 105 | img = response['read'] 106 | return img 107 | else: 108 | raise NameError('get captcha failed') 109 | 110 | # build login post and login 111 | def in_login(self, username, pwd, captcha_str): 112 | # build login post and login to CAS, update cookie jar 113 | value = { 114 | 'username' : username, 115 | 'password' : pwd, 116 | 'captcha': captcha_str, 117 | 'execution' : self.exec_code, 118 | '_eventId' : 'submit', 119 | 'geolocation': '' 120 | } 121 | data = urllib.parse.urlencode(value).encode() 122 | req_post = urllib.request.Request(self.cas_url, headers=self.headers, data=data) 123 | response = self.__open_s(req_post) 124 | # TODO:login to CAS error handling, wrong captcha or something else 125 | #html = response.read() 126 | #soup = BeautifulSoup(html, features="lxml") 127 | #title = soup.find_all('title')[0] 128 | #logging.debug(title) 129 | 130 | # simulate what SYSU course selection system do 131 | # get session cookie 132 | def post_login(self): 133 | # login to course selection system, update cookie jar 134 | selection_get = urllib.request.Request(self.selection_url, headers=self.headers) 135 | response = self.__open_s(selection_get) 136 | # TODO: error handling 137 | # get personal info 138 | for para in self.info_para: 139 | courselist_req_post = urllib.request.Request(self.courselist_url.format(para, self.__current_time()), headers=self.headers) 140 | response = self.__open_s(courselist_req_post) 141 | if response != None and 'code' not in response: 142 | pass 143 | else: 144 | raise NameError('query course failed') 145 | logging.debug(response['read']) 146 | 147 | def course_query(self): 148 | #payload config: 149 | # pageSize: the maximun number of course the server will return 150 | # 专必 specialty compulsory: selectedCate=11;selectedType=1; 151 | # 专选 specialty Elective: selectedCate=21;selectedType=1; 152 | # 体育 PE: selectedCate=10;selectedType=3; 153 | # TODO: expand query to all catagories 154 | query_payload = '{"pageNo":1,"pageSize":20,"param":{"semesterYear":"2018-2","selectedType":"1","selectedCate":"21","hiddenConflictStatus":"0","hiddenSelectedStatus":"0","collectionStatus":"0"}}' 155 | data = query_payload.encode() 156 | current_time = self.__current_time() 157 | courselist_req_post = urllib.request.Request( 158 | self.courselist_url.format('choose-course-front-server/classCourseInfo/course/list', 159 | current_time), data=data, headers=self.__courselist_headers(current_time)) 160 | response = self.__open_s(courselist_req_post) 161 | if response != None and 'code' not in response: 162 | courselist_str = response['read'] 163 | else: 164 | raise NameError('query course failed') 165 | courselist_json = json.loads(courselist_str) 166 | course_data = courselist_json['data']['rows'] 167 | 168 | simplified_course_data_dict = [{ 169 | 'cid' : x['courseNum'], 170 | 'cname' : x['courseName'], 171 | 'lecturer' : x['teachingTimePlace'].split(';')[0], 172 | 'sid' : x['teachingClassId'], 173 | 'snum' : '{}/{}'.format(x['courseSelectedNum'], x['baseReceiveNum']), 174 | 'status' : True if x['selectedStatus'] == '4' else False 175 | } for x in course_data] 176 | 177 | simplified_course_data = [[x['courseNum'], x['courseName'],x['teachingTimePlace'].split(';')[0], x['teachingClassId'], '{}/{}'.format(x['courseSelectedNum'], x['baseReceiveNum']), x['selectedStatus']] for x in course_data] 178 | self.course_list = simplified_course_data 179 | 180 | #print(pd.DataFrame(simplified_course_data, columns=['Course ID', 'Course Name', 'Lecturer', 'Selete ID', 'Seleted/All', 'Chosen'])) 181 | 182 | return simplified_course_data_dict 183 | 184 | class course_select_thread (threading.Thread): 185 | def __init__(self, course_selector, select_id, selete_type, select_cate): 186 | threading.Thread.__init__(self) 187 | self.course_selector = course_selector 188 | self.select_id = select_id 189 | self.selete_type = selete_type 190 | self.select_cate = select_cate 191 | 192 | def run(self): 193 | self.course_selector.course_select(self.select_id, self.selete_type, self.select_cate) 194 | 195 | # clazzId=teachingClassId 196 | # 专必: selectedCate=11;selectedType=1; 197 | # 专选: selectedCate=21;selectedType=1; 198 | # 体育: selectedCate=10;selectedType=3; 199 | def course_select(self, select_id, selete_type, select_cate): 200 | choose_payload = {"clazzId":str(select_id),"selectedType":str(selete_type),"selectedCate":str(select_cate),"check":True} 201 | choose_payload = json.dumps(choose_payload) 202 | data = choose_payload.encode() 203 | while True: 204 | current_time = self.__current_time() 205 | choose_req_post = urllib.request.Request( 206 | self.course_select_url.format(current_time), data=data, headers=self.__courselist_headers(current_time)) 207 | response = self.__open_s(choose_req_post) # will get an json in res['read'] 208 | if response != None: 209 | response_data = json.loads(response['read']) 210 | if response_data['code'] == 200 or response_data['code'] == 52021104: # success or already selected 211 | print('success, exiting tread') 212 | break 213 | time.sleep(DELAY) 214 | 215 | def course_select_wrapper(self, target_course_list_str): 216 | target_course_id_list = [x.strip() for x in target_course_list_str.split(',')] 217 | target_select_id_list = [] 218 | for cid in target_course_id_list: 219 | for x in self.course_list: 220 | if x[0] == cid: 221 | target_select_id_list.append(x[3]) 222 | if len(target_course_id_list) != len(target_select_id_list): 223 | pass 224 | #TODO:error input handling 225 | else: 226 | thread_pool = [] 227 | for sid in target_select_id_list: 228 | for i in range(CONCURRENT_REQUEST): 229 | thread = self.course_select_thread(self, sid, 1, 21) #TODO:add different categories support 230 | thread.start() 231 | thread_pool.append(thread) 232 | for t in thread_pool: 233 | t.join() 234 | -------------------------------------------------------------------------------- /setting.py: -------------------------------------------------------------------------------- 1 | # number of concurrent request threads of each target lesson 2 | # avoid too large CONCURRENT_REQUEST, or it will hit the server too hard 3 | # OUGHT TO be int and 1 <= CONCURRENT_REQUEST <= 10 4 | CONCURRENT_REQUEST = 1 5 | 6 | # after TIMEOUT, course_selector will drop current request and try again 7 | # OUGHT TO be int and 2 <= TIMEOUT <= 60 8 | TIMEOUT = 5 9 | 10 | # time interval between 2 successful request 11 | # avoid too small DELAY, or it will hit the server too hard 12 | # OUGHT TO be int and 1 <= DELAY <= 60 13 | DELAY = 5 14 | 15 | # if you use ocproxy and openconnect to VPN to SYSU's intranet, 16 | # set USE_SOCKS5_PROXY = True and SOCKS5_PROXY_PORT to the port you set in ocproxy; 17 | # otherwise(like using openconnect without ocproxy), set USE_SOCKS5_PROXY = False and ommit SOCKS5_PROXY_PORT 18 | USE_SOCKS5_PROXY = False 19 | SOCKS5_PROXY_PORT = 1080 --------------------------------------------------------------------------------