├── class.txt ├── screenShots ├── sc.png └── help.png ├── LICENSE ├── README.md └── main.py /class.txt: -------------------------------------------------------------------------------- 1 | 2 | 计算机程序设计基础A-01班-英文 3 | 计算机安全-01班-英文 4 | -------------------------------------------------------------------------------- /screenShots/sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostFrankWu/SUSTech_Tools/HEAD/screenShots/sc.png -------------------------------------------------------------------------------- /screenShots/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GhostFrankWu/SUSTech_Tools/HEAD/screenShots/help.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Frank 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 | # 南科大Tis选课助手 2 | **2024 Sep 更新:Tis 后台在用户级别做了流量限制** 3 | 每个有效请求触发新的限制,因此大量发送数据不一定再是最优选择 4 | 因此当前版本同时只选一门课(最重要的),选课队列优先级请参考使用说明 5 | **该程序未对流量特征做任何的混淆,追求隐蔽性请自行修改** (UA,请求参数,其他流量等) 6 | 7 | > 维护者不一定会在每学期开学前检查脚本有效性,欢迎有兴趣的同学提前几天测试脚本(以避免 TIS 偷偷修改接口导致脚本失效) 8 | 9 | 如遇到学期服务器证书配置出错,兼容修改请参照issue手动修复 10 | 如遇CAS不明原因500,请**提前多次尝试登录** 11 | 12 | ## 使用说明 13 | - main.py为Python3编写的主程序,运行即可 14 | - class.txt为需要选择的课程列表,一行一个数据,因编码问题,第一行请留空或不要编辑。 15 | **选课顺序会严格按照class.txt中录入的先后顺序进行,在高优先级课程被选完/冲突前不会选低优先级课程,除非手动输入任意值字符回车跳过** 16 | - 按下回车之后会进入一个3s的循环,每秒选当前最优先的课程一次。(可以按多次开启多个计时循环) 17 | 课程列表添加说明(图片加载不出请科学加载): 18 | ![课程名称说明](screenShots/help.png) 19 | 脚本运行界面: 20 | ![脚本运行界面](screenShots/sc.png) 21 | 22 | ## 更新 23 | 最后一次**检测可用**是2024-09-05,检测结果是 **可用** 24 | - 如果您本地有Python3.6+环境并希望手动运行/修改代码[访问源代码](https://github.com/GhostFrankWu/SUSTech_Tools/blob/master/main.py) 25 | - 如果您本地没有Python环境,您可以[使用windows打包版](https://github.com/GhostFrankWu/SUSTech_Tools/releases/tag/v5.1.0) 26 | 27 | ## 免责声明 28 | 29 | 该脚本诞生的目的是研究节省流量,减轻选课系统负担的方法,经过测试确实能达到该效果,因人为修改脚本产生的超过手工频率的流量,由修改者承担责任 30 | >虽然Tis经过若干更新,网页端选课产生的流量已经大大减少,但该脚本仍产生更少的流量 31 | 32 | 62 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 # -*- coding: utf-8 -* 2 | 3 | """ 4 | main.py 南科大TIS喵课助手 5 | 6 | @CreateDate 2021-1-9 7 | @UpdateDate 2024-9-9 8 | """ 9 | 10 | import _thread 11 | import time 12 | import os 13 | from getpass import getpass 14 | from json import loads, dumps 15 | from re import findall 16 | 17 | import requests 18 | from colorama import init 19 | 20 | import sys 21 | import warnings 22 | from urllib3.exceptions import InsecureRequestWarning 23 | 24 | 25 | def warn(message, category, filename, lineno, _file=None, line=None): 26 | if category is not InsecureRequestWarning: 27 | sys.stderr.write(warnings.formatwarning(message, category, filename, lineno, line)) 28 | 29 | CLASS_CACHE_PATH = "class.txt" 30 | COURSE_INFO_PATH = "course.txt" 31 | USER_INFO_PATH = "user.txt" 32 | warnings.showwarning = warn 33 | SUCCESS = "[\x1b[0;32m+\x1b[0m] " 34 | STAR = "[\x1b[0;32m*\x1b[0m] " 35 | ERROR = "[\x1b[0;31mx\x1b[0m] " 36 | INFO = "[\x1b[0;36m!\x1b[0m] " 37 | FAIL = "[\x1b[0;33m-\x1b[0m] " 38 | UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" 39 | head = { 40 | "user-agent": UA, 41 | "x-requested-with": "XMLHttpRequest" 42 | } 43 | 44 | COURSE_TYPE = {'bxxk': "通识必修选课", 'xxxk': "通识选修选课", "kzyxk": '培养方案内课程', 45 | "zynknjxk": '非培养方案内课程', "cxxk": '重修选课', "jhnxk": '计划内选课新生'} 46 | 47 | TIMEOUT = 1.2 # 线程喵课间隔 48 | 49 | course_list = [] # 需要喵的课程队列 50 | # 由于Tis的新限制,逻辑改为同时只选一门课 51 | 52 | def load_course(): 53 | """ 用于加载本地要喵的课程 54 | 如果存在文件就读文件里的,不存在就手动录入 55 | 有些(我忘了是哪些了)情况会在文件头会有几个不可见字符,但是会被python读进来,所以第一行建议忽略留空""" 56 | courses = [] 57 | if os.path.exists(CLASS_CACHE_PATH) and os.path.isfile(CLASS_CACHE_PATH): 58 | print(INFO + "读取规划课表...") 59 | with open(CLASS_CACHE_PATH, "r", encoding="utf8") as f: 60 | courses = f.readlines() 61 | print(SUCCESS + "规划课表读取完毕") 62 | else: 63 | print(FAIL + "没有找到规划课表,请手动输入课程信息,输入-1结束录入") 64 | s = "===本文件是待喵课程的列表,一行输入一个课程名字==请勿删除本行===" 65 | while s != "-1": 66 | courses.append(s) 67 | s = input() 68 | s = input(INFO + "是否保存录入的信息(y/N)?") 69 | if s in "yY": 70 | with open(CLASS_CACHE_PATH, "w", encoding="utf8") as f: 71 | f.writelines('\n'.join(courses)) 72 | return courses 73 | 74 | 75 | def cas_login(sid, pwd): 76 | """ 用于和南科大CAS认证交互,拿到tis的有效cookie 77 | 输入用于CAS登录的用户名密码,输出tis需要的全部cookie内容(返回头Set-Cookie段的route和jsessionid) 78 | 我的requests的session不吃CAS重定向给到的cookie,不知道是代码哪里的问题,所以就手动拿了 """ 79 | print(INFO + "测试CAS链接...") 80 | try: # Login 服务的CAS链接有时候会变 81 | login_url = "https://cas.sustech.edu.cn/cas/login?service=https%3A%2F%2Ftis.sustech.edu.cn%2Fcas" 82 | req = requests.get(login_url, headers=head, verify=False) 83 | assert (req.status_code == 200) 84 | print(SUCCESS + "成功连接到CAS...") 85 | except Exception as ex: 86 | print(ERROR + f"不能访问CAS, 请检查您的网络连接状态 ({ex})") 87 | return "", "" 88 | print(INFO + "登录中...") 89 | data = { # execution大概是CAS中前端session id之类的东西 90 | 'username': sid, 91 | 'password': pwd, 92 | 'execution': str(req.text).split('''name="execution" value="''')[1].split('"')[0], 93 | '_eventId': 'submit', 94 | 'geolocation': '' # 新字段 95 | } 96 | while True: 97 | req = requests.post(login_url, data=data, allow_redirects=False, headers=head, verify=False) 98 | if req.status_code == 500: 99 | print(ERROR + "CAS服务出错,重试中") 100 | break 101 | if "Location" in req.headers.keys(): 102 | print(SUCCESS + "登录成功") 103 | else: 104 | print(ERROR + "用户名或密码错误,请检查") 105 | return "", "" 106 | req = requests.get(req.headers["Location"], allow_redirects=False, headers=head, verify=False) 107 | _route = findall('route=(.+?);', req.headers["Set-Cookie"])[0] 108 | _jsessionid = findall('JSESSIONID=(.+?);', req.headers["Set-Cookie"])[0] 109 | return _route, _jsessionid 110 | 111 | 112 | def getinfo(semester_data): 113 | """ 用于向tis请求当前学期的课程ID,得到的ID将用于选课的请求 114 | 输入当前学期的日期信息,返回的json包括了课程名和内部的ID """ 115 | if os.path.exists(COURSE_INFO_PATH) and os.path.isfile(COURSE_INFO_PATH): 116 | print(INFO + f"读取本地缓存的课程信息,如果需要更新请删除{COURSE_INFO_PATH}文件") 117 | with open(COURSE_INFO_PATH, "r", encoding="utf8") as f: 118 | cached_course_list = f.readlines() 119 | try: 120 | cached_time = cached_course_list[0].strip() 121 | if cached_time == semester_data['p_xnxq']: 122 | _course_info = loads(cached_course_list[1]) 123 | print(SUCCESS + f"课程信息读取完毕,共读取{str(len(_course_info))}门课程信息\n") 124 | return _course_info 125 | else: 126 | print(INFO + "缓存文件已过期,重新获取课程信息") 127 | except Exception as ex: 128 | print(ERROR + f"缓存文件损坏,重新获取课程信息,{ex}") 129 | print(INFO + "从服务器下载课程信息,请稍等...") 130 | _course_info = {} 131 | for c_type in COURSE_TYPE.keys(): 132 | data = { 133 | "p_xn": semester_data['p_xn'], # 当前学年 134 | "p_xq": semester_data['p_xq'], # 当前学期 135 | "p_xnxq": semester_data['p_xnxq'], # 当前学年学期 136 | "p_pylx": 1, 137 | "mxpylx": 1, 138 | "p_xkfsdm": c_type, 139 | "pageNum": 1, 140 | "pageSize": 1000 # 每学期总共开课在1000左右,所以单分类可以包括学期的全部课程 141 | } 142 | print("[\x1b[0;36m*\x1b[0m] " + f"获取 {COURSE_TYPE[c_type]} 列表...") 143 | req = requests.post('https://tis.sustech.edu.cn/Xsxk/queryKxrw', data=data, headers=head, verify=False) 144 | raw_class_data = loads(req.text) 145 | if raw_class_data.get('kxrwList'): 146 | for i in raw_class_data['kxrwList']['list']: 147 | _course_info[i['rwmc']] = (i['id'], c_type) 148 | print(SUCCESS + f"课程信息读取完毕,共读取{str(len(_course_info))}门课程信息") 149 | s = input(INFO + "是否保存读取的课程信息(y/N)?") 150 | if s in "yY": 151 | with open(COURSE_INFO_PATH, "w", encoding="utf8") as f: 152 | f.write(str(semester_data['p_xnxq']) + "\n") 153 | f.write(dumps(_course_info, ensure_ascii=False)) 154 | return _course_info 155 | 156 | 157 | def submit(semester_data, loop=3): 158 | """ 用于向tis发送喵课的请求 159 | 这里假设主要耗时在网络IO上,本地处理时间几乎可以忽略 160 | (什么,购物车是怎么回事?那首先排除教务系统是个魔改的电商项目)""" 161 | for _ in range(loop): 162 | if not course_list: 163 | print(SUCCESS + "⌯'ㅅ'⌯所有课程已喵完,再见😾") 164 | exec("os._exit(0)") # lint hack 165 | c_id, c_type, c_name = course_list[0] 166 | data = { 167 | "p_pylx": 1, 168 | "p_xktjz": "rwtjzyx", # 提交至,可选任务,rwtjzgwc提交至购物车,rwtjzyx提交至已选 gwctjzyx购物车提交至已选 169 | "p_xn": semester_data['p_xn'], 170 | "p_xq": semester_data['p_xq'], 171 | "p_xnxq": semester_data['p_xnxq'], 172 | "p_xkfsdm": c_type, # 选课方式 173 | "p_id": c_id, # 课程id 174 | "p_sfxsgwckb": 1, # 固定 175 | } 176 | req = requests.post('https://tis.sustech.edu.cn/Xsxk/addGouwuche', data=data, headers=head, verify=False) 177 | res = loads(req.text)['message'] 178 | if "成功" in req.text: 179 | print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True) 180 | print("[\x1b[0;34m█\x1b[0m]\t\t\t" + res, flush=True) 181 | print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True) 182 | course_list.pop(0) 183 | else: 184 | print("[\x1b[0;30m-\x1b[0m]\t\t\t" + res, flush=True) 185 | if any(map(lambda x: x in req.text, ["冲突", "已选", "已满"])): 186 | print(f"[\x1b[0;31m!\x1b[0m] ({c_name})因为({res})跳过", flush=True) 187 | course_list.pop(0) 188 | time.sleep(TIMEOUT) 189 | 190 | 191 | def submit_sequential(semester_data): 192 | """ 按照输入课程顺序向tis发送喵课请求 """ 193 | if not course_list: 194 | print(SUCCESS + "⌯'ㅅ'⌯所有课程已喵完,再见😾") 195 | exec("os._exit(0)") # lint hack 196 | course_list_copy = course_list.copy() 197 | for course in course_list_copy: 198 | c_id, c_type, c_name = course 199 | if course in course_list: 200 | data = { 201 | "p_pylx": 1, 202 | "p_xktjz": "rwtjzyx", # 提交至,可选任务,rwtjzgwc提交至购物车,rwtjzyx提交至已选 gwctjzyx购物车提交至已选 203 | "p_xn": semester_data['p_xn'], 204 | "p_xq": semester_data['p_xq'], 205 | "p_xnxq": semester_data['p_xnxq'], 206 | "p_xkfsdm": c_type, # 选课方式 207 | "p_id": c_id, # 课程id 208 | "p_sfxsgwckb": 1, # 固定 209 | } 210 | req = requests.post('https://tis.sustech.edu.cn/Xsxk/addGouwuche', data=data, headers=head, verify=False) 211 | res = loads(req.text)['message'] 212 | if "成功" in req.text: 213 | print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True) 214 | print("[\x1b[0;34m█\x1b[0m]\t\t\t" + res, flush=True) 215 | print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True) 216 | course_list.remove(course) 217 | else: 218 | print("[\x1b[0;30m-\x1b[0m]\t\t\t" + res, flush=True) 219 | if any(map(lambda x: x in req.text, ["冲突", "已选", "已满"])): 220 | print(f"[\x1b[0;31m!\x1b[0m] ({c_name})因为({res})跳过", flush=True) 221 | course_list.remove(course) 222 | time.sleep(TIMEOUT) 223 | 224 | 225 | def exit(): 226 | """ 退出函数 """ 227 | print(INFO + "退出喵课助手,再见😾") 228 | exec("os._exit(0)") # lint hack 229 | 230 | 231 | if __name__ == '__main__': 232 | init(autoreset=True) # 某窗口系统的优质终端并不直接支持如下转义彩色字符,所以需要一些库来帮忙 233 | course_name_list = load_course() # 读取本地待喵的课程 234 | # 下面是CAS登录 235 | route, jsessionid = "", "" 236 | if os.path.exists(USER_INFO_PATH): # 如果有保存的用户信息,尝试从文件自动登录 237 | try: 238 | with open(USER_INFO_PATH, "r", encoding="utf8") as f: 239 | lines = f.read().splitlines() 240 | if len(lines) >= 2: 241 | user_name, pass_word = lines[0], lines[1] 242 | route, jsessionid = cas_login(user_name, pass_word) 243 | except Exception as e: 244 | print(FAIL + f"自动登录出现异常: {e}") 245 | if route == "" or jsessionid == "": 246 | print(FAIL + "自动登录失败,需要手动登录") 247 | 248 | while route == "" or jsessionid == "": 249 | user_name = input("请输入您的学号:") # getpass在PyCharm里不能正常工作,请改为input或写死 250 | pass_word = getpass("请输入CAS密码(密码不显示,输入完按回车即可):") 251 | route, jsessionid = cas_login(user_name, pass_word) 252 | if route == "" or jsessionid == "": 253 | print(FAIL + "请重试...") 254 | else: # 登录成功后询问保存 255 | s = input(INFO + "是否保存用户信息(y/N)?") 256 | if s.lower() in {"y", "yes"}: 257 | with open(USER_INFO_PATH, "w", encoding="utf8") as f: 258 | f.write(f"{user_name}\n{pass_word}") 259 | head['cookie'] = f'route={route}; JSESSIONID={jsessionid};' 260 | # 下面先获取当前的学期 261 | print(INFO + "从服务器获取当前喵课时间...") 262 | semester_info = loads( 263 | requests.post('https://tis.sustech.edu.cn/Xsxk/queryXkdqXnxq', 264 | data={"mxpylx": 1}, headers=head, verify=False).text) # 这里要加mxpylx才能获取到选课所在最新学期 265 | print(SUCCESS + f"当前学期是{semester_info['p_xn']}学年第{semester_info['p_xq']}学期,为" 266 | f"{['', '秋季', '春季', '小'][int(semester_info['p_xq'])]}学期") 267 | # 然后获取本学期全部课程信息 268 | print(INFO + "读取课程信息...") 269 | course_info = getinfo(semester_info) 270 | # 分析要喵课程的ID 271 | for name in course_name_list: 272 | name = name.strip() 273 | if name in course_info.keys(): 274 | course_id, course_type = course_info[name] 275 | course_list.append([course_id, course_type, name]) 276 | print("[\x1b[0;34m{}\x1b[0m]".format("=" * 25)) 277 | for course in course_list: 278 | print(f"{COURSE_TYPE[course[1]]} : {course[2]}\t\tID为: {course[0]}") 279 | print("[\x1b[0;34m{}\x1b[0m]".format("=" * 25)) 280 | print(SUCCESS + "成功读入以上信息\n") 281 | # 喵课主逻辑 282 | 283 | if not course_list: 284 | print("没有读取到要喵的课程,请检查课程名称是否正确") 285 | exit() 286 | 287 | mode = input("请输入喵课模式:[1] -- 优先按照输入课程顺序喵课,2 -- 所有课程循环喵课,0 -- 退出\n") or "1" 288 | 289 | while True: 290 | 291 | if mode == "1": 292 | print(INFO + "当前模式: 优先按照输入课程顺序喵课") 293 | while course_list: 294 | if input(STAR + "按一下回车喵三次,多按同时喵多次,任意字符跳过当前课程\n"): 295 | course_list.pop(0) 296 | if not course_list: 297 | print(SUCCESS + "⌯'ㅅ'⌯所有课程已喵完,再见😾") 298 | exec("os._exit(0)") 299 | try: 300 | _thread.start_new_thread(submit, (semester_info, 3)) 301 | except Exception as e: 302 | print(f"[{e}] 线程异常") 303 | 304 | if mode == "2": 305 | print(INFO + "当前模式: 所有课程循环喵课") 306 | while course_list: 307 | if input(STAR + "按一下回车对所有课程喵一次,多按同时喵多次,任意字符退出\n"): 308 | exit() 309 | try: 310 | _thread.start_new_thread(submit_sequential, (semester_info,)) 311 | except Exception as e: 312 | print(f"[{e}] 线程异常") 313 | 314 | if mode == "0": 315 | exit() 316 | --------------------------------------------------------------------------------