Skip to content

Repository files navigation

gxu-jwxt

广西大学(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 -v

test_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 条)。

许可

MIT

仓库:https://github.com/halfaradish/GxuJwxtPythonSDK

About

这是一个广西大学教务系统python sdk

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages