广西大学(GXU)正方教务管理系统 V9.0 的 Python SDK,封装鉴权、成绩、课表、选课、考试、个人信息、课程查询、评教、调课通知与级联字典等业务域,共整理 613 个接口(见 api.md)。
它最值钱的部分是鉴权。 正方的登录链路要求把密码用 RSA(PKCS1_v1_5) 加密、带 csrftoken 提交、跟随 302 落到菜单页,会话过期时还会用「HTTP 200 + 一整页登录页」伪装成功——这些都由 SDK 兜住,登录会话默认落盘复用。所以除了用现成的业务域,你也可以只借它的登录,之后用 client.get() / client.post() 自己构造请求,见下文「只借道鉴权」。
- 纯 Python,运行时只依赖
httpx与pycryptodome - 只需一个同步客户端,无异步分支
- 登录会话默认落盘复用,批量抓取不必每轮重新登录
- 自带命令行工具
gxu-jwxt - 所有接口调用方式都经过对真实部署的实测校验,并由
tests/test_live_endpoints.py回归守卫
学期参数:所有域的
semester=都接受"2026-2027-1"、"2026"、("2026", "3")等写法,留空表示当前学期。正方内部要的是xnm(学年) +xqm(3=秋/12=春/16=小学期), 转换由_base.resolve_term()统一完成。
本项目仅供个人学习与查询本人教务数据使用。请遵守学校相关规定,不要用于高频请求、代刷或其他违规用途。
pip install gxu-jwxt开发安装(含测试依赖):
make dev-install # 等价于 pip install -e ".[dev]"要求 Python >= 3.10。
from gxu_jwxt import JwxtClient
with JwxtClient(username="学号", password="密码") as client:
client.auth.login()
scores = client.score.get_scores(semester="2025-2026-1")
gpa = client.score.get_gpa_stats()
schedule = client.schedule.get_personal_schedule(week=5)
exams = client.exam.get_exam_schedule()
profile = client.profile.get_profile()凭据与站点配置的优先级:构造参数 > 环境变量 > 配置文件 > 默认值。
export JWXT_USERNAME=学号
export JWXT_PASSWORD=密码from gxu_jwxt import JwxtClient, JwxtConfig
config = JwxtConfig.from_file("config.json") # {"username": ..., "password": ...}
client = JwxtClient(config=config)正方这套系统的重头戏在鉴权,业务接口本身都是普通 HTTP,只是坑不少:
- 密码要先用
/xtgl/login_getPublicKey.html下发的公钥做 RSA(PKCS1_v1_5) 加密,再带着csrftoken提交; - 登录前要先 POST 一次
login_logoutAccount.html清掉旧会话,登录后要跟随 302 落到菜单页,才算真正拿到 JSESSIONID; - 会话过期不会给你 401:服务端对任何接口都回 200,body 是一整页登录页;
- 个别接口(如平时分明细)少传
gnmkdm会被当成非法请求,直接把整个会话踢下线。
client.auth.login() 把这些全做完了,会话还会落盘复用(见下文「会话持久化」)。所以哪怕只想查一个 SDK 还没封装的接口,也不必自己再写一遍登录:
from gxu_jwxt import (
JwxtClient, resolve_term,
parse_json_response, is_success_response, response_message,
)
with JwxtClient(username="学号", password="密码") as client:
client.auth.login() # 只有这一句和鉴权有关
xnm, xqm = resolve_term("2025-2026-1") # 正方要的是 xnm="2025" + xqm="3"
# 相对路径自动补 /jwglxt 前缀;会话过期会自动重登录后重试一次
resp = client.post(
"/query/query_cxEjjxcdlbList.html", # 例:空闲教室查询(SDK 未封装),参数请自行抓包核对
data={"xnm": xnm, "xqm": xqm, "jc": "3"},
)
data = parse_json_response(resp.text) # 兼容裸 JSON / HTML 内嵌 JSON / 登录页
if not is_success_response(data): # 认 flag/status/code/success 四种成功标记
print("失败:", response_message(data))几点说明:
client.get()/client.post()接受相对路径或完整 URL(自动拼base_url+path_prefix),自动带上当前 cookie,自动处理会话过期并重登录一次;网络错误抛NetworkError,会话彻底失效抛SessionExpiredError。需要csrftoken的写操作取client.csrf_token。- 上面 4 个助手函数从包根导出;
gxu_jwxt._base里还有更多实测用过的纯函数(如current_term()、parse_week_range()、extract_csrf_token())。 - 参数形态要自己核对:正方的很多参数写错不会报错,只会静默返回空数据。各业务域实测确认过的形态见 AGENTS.md 的接口表,全量接口清单见 api.md。
- 不想用 httpx 的话,会话以 JSON 形式存在
client.session_file(含 cookies 与 csrftoken);进程内也可以走client._http.cookies(私有属性,不保证稳定)。
常规查询直接用下面的业务域;接口还没封装时,按上一节用 client.get() / client.post() 自己发。
| 域 | 入口 | 说明 |
|---|---|---|
| 认证 | client.auth |
登录、注销、验证码、csrftoken |
| 成绩 | client.score |
成绩列表、平时分构成、绩点统计、学分统计、分页遍历 |
| 课表 | client.schedule |
个人/班级课表、节次时间表、当前学期与年级、单双周解析 |
| 选课 | client.selection |
选课开关发现、可选课程、quick_enroll()、enroll_with_retry()、退课 |
| 考试 | client.exam |
考试安排、准考证 |
| 个人信息 | client.profile |
学籍信息(档案页 HTML 解析)、照片、学年列表 |
| 级联字典 | client.dictionaries |
学院、专业、专业方向、班级 |
| 课程查询 | client.courses |
开课课程库查询(课程号/教师/时间地点/开课状态) |
| 评教 | client.evaluation |
评教任务、问卷解析、请求体生成(提交默认 dry-run) |
| 调课通知 | client.notification |
调课/停课通知,支持只看未读 |
scores = client.score.get_scores(semester="2025-2026-1")
scores = client.score.get_scores(course_type="必修") # 本地按课程性质筛选
gpa = client.score.get_gpa_stats() # 加权绩点、平均分、通过/未通过
stats = client.score.get_credit_stats() # 服务端按课程类别的学分统计
# 平时分构成(分项 / 占比 / 成绩);未录入时返回空列表
schedule = client.schedule.get_personal_schedule()
items = client.score.get_usual_score(schedule.entries[0].teaching_class_id)
for i in items:
print(i.name, i.ratio, i.score) # 平时成绩 40 97.3 / 期末成绩 60 78 / 总评 None 86# 选课开关(xkkz_id)与 do_jxb_id 都从「已选课程」里下发,SDK 会自动取用
switches = client.selection.get_selection_switches()
print(switches[0].course_type_name, switches[0].switch_id)
courses = client.selection.get_available_courses(course_type_code="01", keyword="数学")
result = client.selection.quick_enroll("1071206") # 一键选课(自动挑教学班 + 冲突检查)
if result.success:
print(result.course_name)
client.selection.enroll_with_retry("1071206", max_retries=10, interval=2) # 名额满时重试
client.selection.quick_drop("1071206")选课/退课只在选课窗口开放期间有效,窗口外服务端返回
无操作权限!。get_available_courses()在窗口外会抛RuntimeError并带上服务端原话, 而不是静默返回空列表。
tasks = client.evaluation.get_pending_evaluations() # 只列未评的
for task, payload in client.evaluation.auto_evaluate(option_index=0, dry_run=True):
print(task.course_name, task.teacher_name, len(payload["modelList"][0]["xspjList"]))
# 确认无误后再真正提交(不可逆)
client.evaluation.auto_evaluate(option_index=0, comment="讲解清晰", dry_run=False)notices = client.notification.get_notifications(only_unread=True)
for n in notices:
print(n.published_at, n.title)colleges = client.dictionaries.get_colleges() # 32 个学院
majors = client.dictionaries.get_majors() # 281 个专业(全量)
dirs = client.dictionaries.get_major_directions() # 181 个专业方向
classes = client.dictionaries.get_classes() # 3455 个班级(全量)
# 按学院筛选请在本地过滤,不要传 jg_id
mech = next(c for c in colleges if c.name == "机械工程学院")
my_classes = [c for c in classes if c.college_id == mech.college_id]为什么不能用 jg_id 过滤:服务端只会返回仍存在于学院列表里的数据。实测 3455 个班级中有 74 个挂在 6 个已撤销学院下(中加国际学院、资源与冶金学院、材料科学与工程学院、环境学院、教育学院、研究生),逐学院查询并集只有 3381 个,这 74 个永远查不到。全量结果自带 college_id/college_name、major_id/major_name、grade 等归属字段,本地筛选无损。
登录成功后 cookie 会写入磁盘,下次启动时 client.auth.login() 先尝试复用,有效就只发 1 次探测请求(而不是 4~5 次登录请求),失效则自动回退完整登录。批量抓取脚本无需改动即可受益。
client.session_file # 当前会话文件路径
client.save_session() # 手动写盘
client.restore_session() # 手动恢复(validate=False 为乐观恢复,不发请求)
client.clear_saved_session() # 删除会话文件| 配置 | 默认 | 说明 |
|---|---|---|
persist_session |
True |
置 False 完全关闭(不读也不写) |
session_file |
./.jwxt_session.json |
相对路径以客户端构造时的工作目录为基准 |
对应环境变量 JWXT_PERSIST_SESSION(0/false 关闭)与 JWXT_SESSION_FILE。
安全提示:会话文件包含等同于账号凭据的 JSESSIONID。POSIX 下写入权限为 0600(Windows 无法限制),请把它加入你的
.gitignore,不要提交到版本库。注销时文件会被自动删除。
服务端仍会按自身策略让空闲会话过期(正方通常 30 分钟~2 小时),持久化消除的是"每轮重新登录",不是让会话永生。
gxu-jwxt -u 学号 -p 密码 score list --semester "2025-2026-1"
gxu-jwxt score gpa # 绩点 + 按类别的学分统计
gxu-jwxt score usual --semester "2025-2026-1" --keyword 面向对象
gxu-jwxt schedule show --week 5
gxu-jwxt schedule periods # 节次时间表
gxu-jwxt schedule term # 当前学期与学生年级
gxu-jwxt selection list --type 01
gxu-jwxt selection switches # 选课开关(xkkz_id)
gxu-jwxt selection quick-enroll 1071206
gxu-jwxt courses query --keyword 编译
gxu-jwxt notification list --unread
gxu-jwxt evaluation list
gxu-jwxt evaluation auto --option 0 # 预演,不加 --submit 不会提交
gxu-jwxt exam list
gxu-jwxt profile show -v # 档案页全部字段
gxu-jwxt profile years # 学年列表
gxu-jwxt dictionaries colleges
gxu-jwxt dictionaries classes --njdm-id 2026
gxu-jwxt session show # 查看已保存的会话(不显示 cookie 值)
gxu-jwxt session clear # 删除已保存的会话凭据优先级:-u/-p > 环境变量 > 当前目录的 config.json。全局参数:--base-url、--timeout、--session-file、--no-session。
- api.md —— 613 个接口按业务分类的清单,含登录流程与命名约定
- discovered_apis.json —— 原始爬取结果(菜单 + 接口)
两者由 crawler.py 生成,重新生成需要真实账号:
python crawler.py # 登录 → 遍历 32 个模块页 → 扫描全部 JS → 提取接口爬虫只依赖 httpx(SDK 已有依赖),无需额外安装。接口目录中的会话令牌(如照片接口的 encodeXhid)会自动脱敏。
make dev-install # 安装到项目 venv
make test # python -m pytest tests/ -v
# 没装依赖时的替代方式
PYTHONPATH=src python -m pytest tests/ -q- 单元测试不联网:
tests/test_session_e2e.py会在本地起一个假教务服务器验证 cookie 全链路 - 联调用例需要环境变量,未设置则自动跳过(都是只读的,不会选课、退课或提交评教):
JWXT_USERNAME=学号 JWXT_PASSWORD=密码 python -m pytest tests/test_choose.py -v
JWXT_USERNAME=学号 JWXT_PASSWORD=密码 python -m pytest tests/test_live_endpoints.py -vtest_live_endpoints.py 把每个接口的参数形态都固化成了回归守卫。改完任何域之后
跑一次:正方的很多参数写错不会报错,只会静默返回空数据,这些用例就是为了让那种
回归立刻红灯。
架构、模块划分与各接口的实测踩坑记录见 AGENTS.md。
- 选课/退课的写入路径未实测:选课窗口关闭时服务端一律返回「无操作权限」,
成功路径的参数形态来自可工作的同类实现与
test_app/在分批次选课时提前选课.md, 需在选课开放期自行验证。 - 评教问卷结构未实测:实测账号的评教已全部完成、问卷页返回「已过评价时间!」, 解析器按同类实现的字段名编写,识别不到时会保留原始 HTML 供人工查看。
- 没有「当前周」接口:
get_current_week(start_date=...)按校历首日本地推算, 不传首日返回 0。 - 没有学期列表接口:
get_current_term()从课表响应的xsxx块读取; 学年列表可以退而求其次,从档案页的下拉框读(client.profile.get_available_years())。 - 成绩列表的学期筛选正常,但某学期可能确实没有数据:实测该部署上 2025-2026-1 在成绩查询模块返回 0 条,尽管同一门课的平时分明细里有总评成绩。 这是学校的成绩发布状态,不是参数问题(2024-2025 学年两个学期分别返回 14 / 13 条)。
仓库:https://github.com/halfaradish/GxuJwxtPythonSDK