Repository: GhostFrankWu/SUSTech_Tools
Branch: master
Commit: c066cba09abb
Files: 4
Total size: 14.7 KB
Directory structure:
gitextract_8nh6dn6a/
├── LICENSE
├── README.md
├── class.txt
└── main.py
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Frank
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 南科大Tis选课助手
**2024 Sep 更新:Tis 后台在用户级别做了流量限制**
每个有效请求触发新的限制,因此大量发送数据不一定再是最优选择
因此当前版本同时只选一门课(最重要的),选课队列优先级请参考使用说明
**该程序未对流量特征做任何的混淆,追求隐蔽性请自行修改** (UA,请求参数,其他流量等)
> 维护者不一定会在每学期开学前检查脚本有效性,欢迎有兴趣的同学提前几天测试脚本(以避免 TIS 偷偷修改接口导致脚本失效)
如遇到学期服务器证书配置出错,兼容修改请参照issue手动修复
如遇CAS不明原因500,请**提前多次尝试登录**
## 使用说明
- main.py为Python3编写的主程序,运行即可
- class.txt为需要选择的课程列表,一行一个数据,因编码问题,第一行请留空或不要编辑。
**选课顺序会严格按照class.txt中录入的先后顺序进行,在高优先级课程被选完/冲突前不会选低优先级课程,除非手动输入任意值字符回车跳过**
- 按下回车之后会进入一个3s的循环,每秒选当前最优先的课程一次。(可以按多次开启多个计时循环)
课程列表添加说明(图片加载不出请科学加载):

脚本运行界面:

## 更新
最后一次**检测可用**是2024-09-05,检测结果是 **可用**
- 如果您本地有Python3.6+环境并希望手动运行/修改代码[访问源代码](https://github.com/GhostFrankWu/SUSTech_Tools/blob/master/main.py)
- 如果您本地没有Python环境,您可以[使用windows打包版](https://github.com/GhostFrankWu/SUSTech_Tools/releases/tag/v5.1.0)
## 免责声明
该脚本诞生的目的是研究节省流量,减轻选课系统负担的方法,经过测试确实能达到该效果,因人为修改脚本产生的超过手工频率的流量,由修改者承担责任
>虽然Tis经过若干更新,网页端选课产生的流量已经大大减少,但该脚本仍产生更少的流量
<!--
### 免责声明
该脚本是通过抽象人对计算机的操作方法,提供节省流量并方便选课的功能。脚本为非营利性开源脚本,仅供个人学习、研究或欣赏使用,采用MIT协议,不具有任何市场价值。开发和使用过程不涉及对任何系统进行逆向破解/反汇编/反编译,本脚本编写使用的一切数据都来源于公开在互联网上的内容。
本脚本不提供任何明示或暗示的保证,包括但不限于对适销性和特定用途的适用性的暗示保证。 在任何情况下,版权所有人或贡献者均不对任何直接,间接,偶发,特殊,示范性或后果性的损害负责。
本脚本仅限用于合理合法的学习用途如网络环境测试,因本脚本而产生的各种后果由使用者自行承担,作者对此不负任何责任。
>## TL;DR
>有人要向老师举报:"是脚本导致了教务系统瘫痪"。
>经过测试,学生正常使用TIS选课和使用脚本选课的请求情况如下表所示
>
>项目(三次取平均) | 请求总数(个) | 流量总计(kB) | 总用时(ms)
>-- | -- | -- | --
>TIS登录 | 17 | 188 | 680
>脚本登录 | 0 | 0 | 0
>TIS登录CAS认证 | 22 | 745 | 1410
>脚本登录CAS认证 | 1 | 11 | 96
>TIS进入 | 141 | 2487 | 8760
>脚本进入 | 4 | 223 | 692
>TIS选课+刷新 | 119 | 1299 | 取决于查询内容1-10秒不等
>脚本选课 | 1 | 0.6 | 177
>TIS总计(刷新n次) | 180+119n | 3350+1299n | 10秒+每次刷新耗时
>脚本总计(选课m次) | 5+m | 234+0.6m | 0.8秒+每次请求144ms
>
>可见在目前的TIS设计下,脚本一秒发送100次请求都不及一位正常学生刷新页面看选课按钮有没有激活产生的请求/流量多。
>- 所以如果TIS崩了,那最不应该指责是就是如此节省流量脚本用户了(吧?)
>
>本人寄网挂科水平,欢迎大佬对以上论述批评指正。
-->
================================================
FILE: class.txt
================================================
计算机程序设计基础A-01班-英文
计算机安全-01班-英文
================================================
FILE: main.py
================================================
#!/usr/bin/env python3 # -*- coding: utf-8 -*
"""
main.py 南科大TIS喵课助手
@CreateDate 2021-1-9
@UpdateDate 2024-9-9
"""
import _thread
import time
import os
from getpass import getpass
from json import loads, dumps
from re import findall
import requests
from colorama import init
import sys
import warnings
from urllib3.exceptions import InsecureRequestWarning
def warn(message, category, filename, lineno, _file=None, line=None):
if category is not InsecureRequestWarning:
sys.stderr.write(warnings.formatwarning(message, category, filename, lineno, line))
CLASS_CACHE_PATH = "class.txt"
COURSE_INFO_PATH = "course.txt"
USER_INFO_PATH = "user.txt"
warnings.showwarning = warn
SUCCESS = "[\x1b[0;32m+\x1b[0m] "
STAR = "[\x1b[0;32m*\x1b[0m] "
ERROR = "[\x1b[0;31mx\x1b[0m] "
INFO = "[\x1b[0;36m!\x1b[0m] "
FAIL = "[\x1b[0;33m-\x1b[0m] "
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
head = {
"user-agent": UA,
"x-requested-with": "XMLHttpRequest"
}
COURSE_TYPE = {'bxxk': "通识必修选课", 'xxxk': "通识选修选课", "kzyxk": '培养方案内课程',
"zynknjxk": '非培养方案内课程', "cxxk": '重修选课', "jhnxk": '计划内选课新生'}
TIMEOUT = 1.2 # 线程喵课间隔
course_list = [] # 需要喵的课程队列
# 由于Tis的新限制,逻辑改为同时只选一门课
def load_course():
""" 用于加载本地要喵的课程
如果存在文件就读文件里的,不存在就手动录入
有些(我忘了是哪些了)情况会在文件头会有几个不可见字符,但是会被python读进来,所以第一行建议忽略留空"""
courses = []
if os.path.exists(CLASS_CACHE_PATH) and os.path.isfile(CLASS_CACHE_PATH):
print(INFO + "读取规划课表...")
with open(CLASS_CACHE_PATH, "r", encoding="utf8") as f:
courses = f.readlines()
print(SUCCESS + "规划课表读取完毕")
else:
print(FAIL + "没有找到规划课表,请手动输入课程信息,输入-1结束录入")
s = "===本文件是待喵课程的列表,一行输入一个课程名字==请勿删除本行==="
while s != "-1":
courses.append(s)
s = input()
s = input(INFO + "是否保存录入的信息(y/N)?")
if s in "yY":
with open(CLASS_CACHE_PATH, "w", encoding="utf8") as f:
f.writelines('\n'.join(courses))
return courses
def cas_login(sid, pwd):
""" 用于和南科大CAS认证交互,拿到tis的有效cookie
输入用于CAS登录的用户名密码,输出tis需要的全部cookie内容(返回头Set-Cookie段的route和jsessionid)
我的requests的session不吃CAS重定向给到的cookie,不知道是代码哪里的问题,所以就手动拿了 """
print(INFO + "测试CAS链接...")
try: # Login 服务的CAS链接有时候会变
login_url = "https://cas.sustech.edu.cn/cas/login?service=https%3A%2F%2Ftis.sustech.edu.cn%2Fcas"
req = requests.get(login_url, headers=head, verify=False)
assert (req.status_code == 200)
print(SUCCESS + "成功连接到CAS...")
except Exception as ex:
print(ERROR + f"不能访问CAS, 请检查您的网络连接状态 ({ex})")
return "", ""
print(INFO + "登录中...")
data = { # execution大概是CAS中前端session id之类的东西
'username': sid,
'password': pwd,
'execution': str(req.text).split('''name="execution" value="''')[1].split('"')[0],
'_eventId': 'submit',
'geolocation': '' # 新字段
}
while True:
req = requests.post(login_url, data=data, allow_redirects=False, headers=head, verify=False)
if req.status_code == 500:
print(ERROR + "CAS服务出错,重试中")
break
if "Location" in req.headers.keys():
print(SUCCESS + "登录成功")
else:
print(ERROR + "用户名或密码错误,请检查")
return "", ""
req = requests.get(req.headers["Location"], allow_redirects=False, headers=head, verify=False)
_route = findall('route=(.+?);', req.headers["Set-Cookie"])[0]
_jsessionid = findall('JSESSIONID=(.+?);', req.headers["Set-Cookie"])[0]
return _route, _jsessionid
def getinfo(semester_data):
""" 用于向tis请求当前学期的课程ID,得到的ID将用于选课的请求
输入当前学期的日期信息,返回的json包括了课程名和内部的ID """
if os.path.exists(COURSE_INFO_PATH) and os.path.isfile(COURSE_INFO_PATH):
print(INFO + f"读取本地缓存的课程信息,如果需要更新请删除{COURSE_INFO_PATH}文件")
with open(COURSE_INFO_PATH, "r", encoding="utf8") as f:
cached_course_list = f.readlines()
try:
cached_time = cached_course_list[0].strip()
if cached_time == semester_data['p_xnxq']:
_course_info = loads(cached_course_list[1])
print(SUCCESS + f"课程信息读取完毕,共读取{str(len(_course_info))}门课程信息\n")
return _course_info
else:
print(INFO + "缓存文件已过期,重新获取课程信息")
except Exception as ex:
print(ERROR + f"缓存文件损坏,重新获取课程信息,{ex}")
print(INFO + "从服务器下载课程信息,请稍等...")
_course_info = {}
for c_type in COURSE_TYPE.keys():
data = {
"p_xn": semester_data['p_xn'], # 当前学年
"p_xq": semester_data['p_xq'], # 当前学期
"p_xnxq": semester_data['p_xnxq'], # 当前学年学期
"p_pylx": 1,
"mxpylx": 1,
"p_xkfsdm": c_type,
"pageNum": 1,
"pageSize": 1000 # 每学期总共开课在1000左右,所以单分类可以包括学期的全部课程
}
print("[\x1b[0;36m*\x1b[0m] " + f"获取 {COURSE_TYPE[c_type]} 列表...")
req = requests.post('https://tis.sustech.edu.cn/Xsxk/queryKxrw', data=data, headers=head, verify=False)
raw_class_data = loads(req.text)
if raw_class_data.get('kxrwList'):
for i in raw_class_data['kxrwList']['list']:
_course_info[i['rwmc']] = (i['id'], c_type)
print(SUCCESS + f"课程信息读取完毕,共读取{str(len(_course_info))}门课程信息")
s = input(INFO + "是否保存读取的课程信息(y/N)?")
if s in "yY":
with open(COURSE_INFO_PATH, "w", encoding="utf8") as f:
f.write(str(semester_data['p_xnxq']) + "\n")
f.write(dumps(_course_info, ensure_ascii=False))
return _course_info
def submit(semester_data, loop=3):
""" 用于向tis发送喵课的请求
这里假设主要耗时在网络IO上,本地处理时间几乎可以忽略
(什么,购物车是怎么回事?那首先排除教务系统是个魔改的电商项目)"""
for _ in range(loop):
if not course_list:
print(SUCCESS + "⌯'ㅅ'⌯所有课程已喵完,再见😾")
exec("os._exit(0)") # lint hack
c_id, c_type, c_name = course_list[0]
data = {
"p_pylx": 1,
"p_xktjz": "rwtjzyx", # 提交至,可选任务,rwtjzgwc提交至购物车,rwtjzyx提交至已选 gwctjzyx购物车提交至已选
"p_xn": semester_data['p_xn'],
"p_xq": semester_data['p_xq'],
"p_xnxq": semester_data['p_xnxq'],
"p_xkfsdm": c_type, # 选课方式
"p_id": c_id, # 课程id
"p_sfxsgwckb": 1, # 固定
}
req = requests.post('https://tis.sustech.edu.cn/Xsxk/addGouwuche', data=data, headers=head, verify=False)
res = loads(req.text)['message']
if "成功" in req.text:
print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True)
print("[\x1b[0;34m█\x1b[0m]\t\t\t" + res, flush=True)
print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True)
course_list.pop(0)
else:
print("[\x1b[0;30m-\x1b[0m]\t\t\t" + res, flush=True)
if any(map(lambda x: x in req.text, ["冲突", "已选", "已满"])):
print(f"[\x1b[0;31m!\x1b[0m] ({c_name})因为({res})跳过", flush=True)
course_list.pop(0)
time.sleep(TIMEOUT)
def submit_sequential(semester_data):
""" 按照输入课程顺序向tis发送喵课请求 """
if not course_list:
print(SUCCESS + "⌯'ㅅ'⌯所有课程已喵完,再见😾")
exec("os._exit(0)") # lint hack
course_list_copy = course_list.copy()
for course in course_list_copy:
c_id, c_type, c_name = course
if course in course_list:
data = {
"p_pylx": 1,
"p_xktjz": "rwtjzyx", # 提交至,可选任务,rwtjzgwc提交至购物车,rwtjzyx提交至已选 gwctjzyx购物车提交至已选
"p_xn": semester_data['p_xn'],
"p_xq": semester_data['p_xq'],
"p_xnxq": semester_data['p_xnxq'],
"p_xkfsdm": c_type, # 选课方式
"p_id": c_id, # 课程id
"p_sfxsgwckb": 1, # 固定
}
req = requests.post('https://tis.sustech.edu.cn/Xsxk/addGouwuche', data=data, headers=head, verify=False)
res = loads(req.text)['message']
if "成功" in req.text:
print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True)
print("[\x1b[0;34m█\x1b[0m]\t\t\t" + res, flush=True)
print("[\x1b[0;34m{}\x1b[0m]".format("=" * 50), flush=True)
course_list.remove(course)
else:
print("[\x1b[0;30m-\x1b[0m]\t\t\t" + res, flush=True)
if any(map(lambda x: x in req.text, ["冲突", "已选", "已满"])):
print(f"[\x1b[0;31m!\x1b[0m] ({c_name})因为({res})跳过", flush=True)
course_list.remove(course)
time.sleep(TIMEOUT)
def exit():
""" 退出函数 """
print(INFO + "退出喵课助手,再见😾")
exec("os._exit(0)") # lint hack
if __name__ == '__main__':
init(autoreset=True) # 某窗口系统的优质终端并不直接支持如下转义彩色字符,所以需要一些库来帮忙
course_name_list = load_course() # 读取本地待喵的课程
# 下面是CAS登录
route, jsessionid = "", ""
if os.path.exists(USER_INFO_PATH): # 如果有保存的用户信息,尝试从文件自动登录
try:
with open(USER_INFO_PATH, "r", encoding="utf8") as f:
lines = f.read().splitlines()
if len(lines) >= 2:
user_name, pass_word = lines[0], lines[1]
route, jsessionid = cas_login(user_name, pass_word)
except Exception as e:
print(FAIL + f"自动登录出现异常: {e}")
if route == "" or jsessionid == "":
print(FAIL + "自动登录失败,需要手动登录")
while route == "" or jsessionid == "":
user_name = input("请输入您的学号:") # getpass在PyCharm里不能正常工作,请改为input或写死
pass_word = getpass("请输入CAS密码(密码不显示,输入完按回车即可):")
route, jsessionid = cas_login(user_name, pass_word)
if route == "" or jsessionid == "":
print(FAIL + "请重试...")
else: # 登录成功后询问保存
s = input(INFO + "是否保存用户信息(y/N)?")
if s.lower() in {"y", "yes"}:
with open(USER_INFO_PATH, "w", encoding="utf8") as f:
f.write(f"{user_name}\n{pass_word}")
head['cookie'] = f'route={route}; JSESSIONID={jsessionid};'
# 下面先获取当前的学期
print(INFO + "从服务器获取当前喵课时间...")
semester_info = loads(
requests.post('https://tis.sustech.edu.cn/Xsxk/queryXkdqXnxq',
data={"mxpylx": 1}, headers=head, verify=False).text) # 这里要加mxpylx才能获取到选课所在最新学期
print(SUCCESS + f"当前学期是{semester_info['p_xn']}学年第{semester_info['p_xq']}学期,为"
f"{['', '秋季', '春季', '小'][int(semester_info['p_xq'])]}学期")
# 然后获取本学期全部课程信息
print(INFO + "读取课程信息...")
course_info = getinfo(semester_info)
# 分析要喵课程的ID
for name in course_name_list:
name = name.strip()
if name in course_info.keys():
course_id, course_type = course_info[name]
course_list.append([course_id, course_type, name])
print("[\x1b[0;34m{}\x1b[0m]".format("=" * 25))
for course in course_list:
print(f"{COURSE_TYPE[course[1]]} : {course[2]}\t\tID为: {course[0]}")
print("[\x1b[0;34m{}\x1b[0m]".format("=" * 25))
print(SUCCESS + "成功读入以上信息\n")
# 喵课主逻辑
if not course_list:
print("没有读取到要喵的课程,请检查课程名称是否正确")
exit()
mode = input("请输入喵课模式:[1] -- 优先按照输入课程顺序喵课,2 -- 所有课程循环喵课,0 -- 退出\n") or "1"
while True:
if mode == "1":
print(INFO + "当前模式: 优先按照输入课程顺序喵课")
while course_list:
if input(STAR + "按一下回车喵三次,多按同时喵多次,任意字符跳过当前课程\n"):
course_list.pop(0)
if not course_list:
print(SUCCESS + "⌯'ㅅ'⌯所有课程已喵完,再见😾")
exec("os._exit(0)")
try:
_thread.start_new_thread(submit, (semester_info, 3))
except Exception as e:
print(f"[{e}] 线程异常")
if mode == "2":
print(INFO + "当前模式: 所有课程循环喵课")
while course_list:
if input(STAR + "按一下回车对所有课程喵一次,多按同时喵多次,任意字符退出\n"):
exit()
try:
_thread.start_new_thread(submit_sequential, (semester_info,))
except Exception as e:
print(f"[{e}] 线程异常")
if mode == "0":
exit()
gitextract_8nh6dn6a/ ├── LICENSE ├── README.md ├── class.txt └── main.py
SYMBOL INDEX (7 symbols across 1 files) FILE: main.py function warn (line 25) | def warn(message, category, filename, lineno, _file=None, line=None): function load_course (line 52) | def load_course(): function cas_login (line 75) | def cas_login(sid, pwd): function getinfo (line 112) | def getinfo(semester_data): function submit (line 157) | def submit(semester_data, loop=3): function submit_sequential (line 191) | def submit_sequential(semester_data): function exit (line 225) | def exit():
Condensed preview — 4 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (21K chars).
[
{
"path": "LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2021 Frank\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "README.md",
"chars": 1889,
"preview": "# 南科大Tis选课助手 \n**2024 Sep 更新:Tis 后台在用户级别做了流量限制** \n每个有效请求触发新的限制,因此大量发送数据不一定再是最优选择 \n因此当前版本同时只选一门课(最重要的),选课队列优先级请参考使用说明 "
},
{
"path": "class.txt",
"chars": 33,
"preview": "\n计算机程序设计基础A-01班-英文\n计算机安全-01班-英文\n"
},
{
"path": "main.py",
"chars": 12115,
"preview": "#!/usr/bin/env python3 # -*- coding: utf-8 -*\n\n\"\"\"\nmain.py 南科大TIS喵课助手\n\n@CreateDate 2021-1-9\n@UpdateDate 2024-9-9\n\"\"\"\n"
}
]
About this extraction
This page contains the full source code of the GhostFrankWu/SUSTech_Tools GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 4 files (14.7 KB), approximately 5.6k tokens, and a symbol index with 7 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.