北京理工大学本科生选课系统(金智 wisedu xsxkapp,xk.bit.edu.cn/xsxkapp)
课程余量轮询 + 自动选课命令行工具。
适用场景:补选 / 退换课 / 试听退换课阶段,某门课已经选满,你希望有人退课时自动帮你补上。
两种用法,随便挑:
- 图形界面(推荐给不熟悉命令行的同学):
bitxk gui—— 原生桌面窗口,点点鼠标就能抢课。 - 命令行:
bitxk grab—— 适合挂服务器 / 写脚本。
登录方式也独立于 SSO 实现:用你自己的 Chromium 浏览器登录一次, 工具只负责把登录态取出来。学校无论怎么改登录页都不影响使用, 短信验证码、2FA、密码管理器也都能正常工作。
适用对象仅限本科生。研究生教务系统(
grdms.bit.edu.cn)是另一套 DWR 协议的老系统, 接口完全不同,本工具不适用。
请先读完再用。
- 本工具为个人学习与自用目的编写,自动化操作的是你自己的账号。
- 选课系统是学校的生产系统。请勿把轮询间隔调到 1 秒以下,那会对服务器造成不必要的压力,也更容易触发风控导致账号被限制。默认的 2 秒是刻意保守的取值。
- 本工具不破解验证码、不绕过任何风控、不伪造身份。它做的是「用你自己的凭据,按正常接口查询余量并提交选课」,与你在浏览器里手动刷新点按钮是同一件事,只是更快更持久。
- 抢课本身是个公平性问题:你能抢到,通常意味着有人退课。请不要用本工具批量囤课或倒卖课程。
- 使用本工具产生的任何后果由使用者自负。请遵守北京理工大学的教学管理规定。
| 功能 | 说明 |
|---|---|
| 图形界面 | bitxk gui:原生桌面窗口,课程管理 + 实时余量表 + 运行日志 |
| 浏览器登录 | 用本机 Chromium 登录一次即可,不依赖 SSO 实现细节(推荐) |
| 自动 SSO 登录 | 也支持直接模拟统一身份认证(含密码 AES 加密),无需开浏览器 |
| 登录态持久化 | 浏览器 profile + 会话缓存(.bitxk_session.json,权限 600),重启后自动复用,失效自动重登 |
| 课程余量轮询 | 按配置间隔查询教学班余量,带随机抖动 |
| 自动选课 | 一旦出现余量立即提交选课,按课程优先级排序 |
| 异步结果确认 | 选课接口是「受理制」,提交后会轮询状态接口确认真实成败 |
| 多课程并发 | 多门课共享一个全局限速器,不会因为课多就把请求打爆 |
| 智能退避 | 被限流时冷却并自动放大间隔;网络抖动指数退避重试 |
| 失效自愈 | Token 过期自动重新登录并继续,无需人工干预 |
| 冲突去重 | 已确认时间冲突的教学班不再重复提交 |
| 手动登录态兜底 | 复制浏览器 Cookie + Token 即可导入,三种登录方式逐级兜底 |
| 安全试跑 | --dry-run 只观察余量、绝不提交选课 |
| 到点提醒 | 抢到课响铃 + 系统通知(macOS / Linux / Windows) |
| 环境自检 | bitxk check 一条命令确认网络与页面结构是否仍适配 |
工具支持三种登录方式,按推荐程度排序:
| 方式 | 命令 | 依赖 SSO 实现? | 说明 |
|---|---|---|---|
| 浏览器登录 | bitxk gui / bitxk browser-login |
❌ 不依赖 | 开一个 Chromium 窗口,你登录一次,工具把 Cookie + Token 取出来 |
| 自动 SSO 登录 | bitxk grab(填账号密码) |
✅ 依赖 | 直接模拟统一身份认证,不用开浏览器 |
| 手动导入 | --cookie + --token |
❌ 不依赖 | 从 F12 里复制,任何情况下都能用 |
为什么推荐浏览器登录:模拟 SSO 需要复刻密码加密、执行串、风控字段, 学校一改版就失效。而"让真人在真浏览器里登录一次,脚本只负责取出登录态" 不受任何改版影响,短信验证码 / 2FA / 密码管理器也都能正常工作。
实现上用的是 Chromium 自带的 DevTools Protocol:
发现本机浏览器 → 以 --remote-debugging-port 启动一个独立 profile
→ 导航到选课系统(自动跳统一身份认证)
→ 你在窗口里正常登录
→ 脚本轮询页面,发现登录成功
→ Network.getCookies 取 Cookie + 读 sessionStorage 的 token
→ 关闭浏览器,登录态缓存到本地
刻意不用 Playwright / Selenium:本机已有的 Chrome / Edge / Chromium
直接就能用,而 Playwright 要额外下 200MB 浏览器。用到的
websockets 库只有几十 KB。
浏览器发现顺序:--browser 路径 → 环境变量 BITXK_BROWSER →
系统安装的 Chrome / Edge / Chromium / Brave → Playwright 缓存里的完整 Chromium
(自动跳过没有窗口的 headless_shell)。
查看本机检测结果:
bitxk --list-browsers登录态会缓存在 ~/.bitxk/chrome-profile(浏览器 profile)和
config.toml 同目录的 .bitxk_session.json(会话)。删掉它们就相当于退出登录。
这两份缓存解决的问题不同,缺一不可:
| 位置 | 存什么 | 作用 |
|---|---|---|
~/.bitxk/chrome-profile/ |
浏览器 cookie、CAS 侧的会话 | 浏览器侧的登录痕迹 |
.bitxk_session.json |
选课系统的 Cookie + Token | 重开程序直接复用登录态,不必重新登录 |
正常使用下你不需要每次都登录。 程序启动时会自动恢复
.bitxk_session.json,并在后台打一次真实接口校验,然后:
| 校验结果 | 程序行为 |
|---|---|
| 有效 | 登录态直接可用,可以查询和抢课 |
| 失效(服务端明确说登录过期) | 清掉并提示「用浏览器登录」重新登录 |
| 连不上(网络故障 / 被限流) | 保留登录态,提示校园网恢复后点「验证登录」 |
最后一条是刻意的:只有服务端明确说失效才丢会话。实测踩过一次 SSL EOF 被当成"登录失效",把好好的登录态扔掉并要求重新输密码,而真正的问题只是网络。
至于"打开浏览器为什么可能是未登录态",要区分两件事:
SESSION/route/clientThemeKey/sidTheme/JSESSIONID/GS_SESSIONID/_WEU—— 这些全是会话 cookie(无过期时间), 关闭浏览器时按设计清除。所以 profile 记住了,浏览器侧也未必还登着。- 程序侧的免登录靠的是
.bitxk_session.json(有效期约 15~20 分钟), 与浏览器是否还登着无关。
浏览器 profile 这块有个很容易踩的坑:关闭浏览器必须走 CDP 的
Browser.close 优雅退出,不能直接 terminate()。Windows 上 Popen.terminate()
等于 TerminateProcess,Chrome 来不及把 profile 写到磁盘就被杀了。实测:强杀后
Default\Network\Cookies 的 mtime 长期不变,种一个 7 天有效期的 cookie
重启后照样消失;改走优雅退出后同一文件立刻被写入。
一个很容易写错的地方:不要用"URL 是否离开 sso.bit.edu.cn"来判断登录完成。
SSO 登录页自身的地址里就带着 service 参数:
https://sso.bit.edu.cn/cas/login?service=https%3A%2F%2Fxk.bit.edu.cn%2Fxsxkapp%2F...
↑ 这里就含 bit.edu.cn
于是"离开 SSO 域"这个判据在用户还没输密码时就会成立,脚本会误报 "已登录"并拿一个空 token 去换票。
现在的判据只有两类,且都要求实证:
sessionStorage里出现token(前端登录成功后一定会写),或- 回跳地址里出现
bitXsxkLogin=<key>(CAS 换票成功),
并且会用 student/<学号>.do 独立复验一次会话真的可用
(未登录时该端点返回 302、登录后返回学生信息)。
用 CDP 的 Network.getCookies(urls=[...]) 取 cookie 时,它会按路径匹配,
只返回能作用于 / 的那些。而选课系统三个关键 cookie 的 path 是 /xsxkapp:
| cookie | path | 作用 |
|---|---|---|
JSESSIONID |
/xsxkapp |
会话 |
GS_SESSIONID |
/xsxkapp/ |
会话 |
_WEU |
/xsxkapp/ |
鉴权必需 |
Network.getCookies(urls=["https://xk.bit.edu.cn"]) → 只有 route ✗ 漏了三个
Network.getCookies()(不带 urls) → 4 个全都有 ✓
只带 route 去请求接口,一律返回 302 —— 表现出来就是"浏览器里明明登着,
工具却说未登录"。正确做法是用 Storage.getCookies 全量取回,再按域名本地过滤。
登录成功后前端会把 studentInfo 写进 sessionStorage,里面有 code(学号)、
name、campusName;currentBatch 里有批次 code;currentCampus 里有校区。
工具会把这些直接读出来,不需要手工传 --student-code。
(顺带一提:传错学号会得到含糊的 code="2" "非法请求",所以自动读取比手填更可靠。)
Chrome 的持久 profile 会保留上次登录留下的 sessionStorage.token,
页面一打开 URL 就可能带着旧的 bitXsxkLogin。这份登录态往往已经失效。
工具的处理:复验不过就先给 30 秒宽限期(服务端可能尚未就绪), 仍然不过就判定为陈旧状态 —— 清除它、提示重新登录,而不会当成成功 缓存下来。否则用户会看到假的"登录成功",等到抢课时才发现用不了。
如果你选择不开浏览器、直接填账号密码,走的是下面这条链路。
本科选课系统首页里注入的常量(直接从页面源码读到,非推测):
BaseUrl = "https://xk.bit.edu.cn/xsxkapp";
casUrl = "https://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/bitXsxkLogin/casLogin.do";
loginType = "cas"; // 走统一身份认证登录链路:
GET https://sso.bit.edu.cn/cas/login?service=<上面的 casUrl>
HTML 里藏着:
login-croypto -> base64 字符串,作为 AES 密钥
login-page-flowkey -> 作为 execution(服务端加密 JWT,必须逐字节原样回填)
POST 同一 URL(application/x-www-form-urlencoded)
username / password(密文) / execution / croypto /
captcha_code / captcha_payload / _eventId=submit / type=UsernamePassword
└─ 成功返回 302,Location 带 ?ticket=ST-xxx
GET Location(回打选课系统的 casLogin.do 完成换票)
└─ 跳转链某一跳带 ?bitXsxkLogin=<key>
GET .../student/register.do?number=<key>
└─ {"data": {"token": "...", "name": "..."}} ← 之后所有请求带全小写 header `token`
实现注意:POST 必须用
allow_redirects=False—— ticket 就在 302 的Location里, 自动跟随会把这个响应丢掉。另外一个 SESSION 只能发一次登录 POST, 失败必须丢弃整个会话重建(第二次会得到1320007)。
密码加密(bitxk/auth.py):
ecb(默认,当前 BIT 在用):AES-128-ECB,key = base64decode(login-croypto)(恰好 16 字节),无 IV、明文就是口令原文(不加随机前缀)、PKCS#7 填充,密文再 base64。 该结论来自对生产前端 bundle 的源码级确认,非猜测。cbc(旧版 wisedu,部分学校仍在用):AES-128-CBC,明文 = 64 位随机串 + 密码,iv= 16 位随机串。 若登录页出现pwdEncryptSalt而非login-croypto,工具会自动切到该模式;也可用--encrypt-mode cbc手动指定。
选课系统的入口是:
http://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do
工具内部使用的是它的 HTTPS 形式:
https://xk.bit.edu.cn/xsxkapp/sys/xsxkapp
这不是偏好问题,而是必须的:选课系统全站强制 HTTPS,http:// 的
任何路径(首页、CAS 回跳、业务接口)都会返回 302 跳转到 https://。
这对 GET 无害,但对 POST 是致命的 —— HTTP 客户端跟随 302 时会把 POST 降级成 GET 并丢掉请求体,服务端收到的根本不是查询请求,而是 一张选课首页的 HTML,表现出来就像"登录态失效",非常难排查:
POST http://…/elective/publicCourse.do
→ 302 → https://…
→ 实际发出 GET(无 body)
→ 200 + 选课首页 HTML ← 参数没了
所以如果你在配置里填了 http:// 的地址,工具会自动升级成 https://:
配置加载时归一化一次,运行时若真收到 http→https 的 302 还会就地升级
基址并重发原请求(而不是跟着跳转)。
配置项(一般不用改):
# 可以整条粘进来,会自动归一化
api_base = "http://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do"所有业务请求都在 https://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/ 下,鉴权靠 全小写 header token(另发 language)。
| 用途 | 端点 |
|---|---|
| 换取 Token | student/register.do?number=<key> |
| 学生信息 / 批次列表 / 校区码 | student/<学号>.do |
| 校公选课查询 | elective/publicCourse.do |
| 推荐课程查询 | elective/recommendedCourse.do |
| 方案内 / 方案外 / 重修 / 体育 / 辅修 | elective/programCourse.do |
| 全校课程 | elective/course.do |
| 提交选课 / 退选 | elective/volunteer.do |
| 查询处理结果 | elective/studentstatus.do |
| 服务端权威的"能不能选" | util/canchoose.do |
参数以 POST 表单体发送,值是一个 JSON 字符串:
POST .../elective/publicCourse.do
Content-Type: application/x-www-form-urlencoded
token: <全小写>
language: zh_cn
querySetting={"data":{"studentCode":"...","campus":"...","electiveBatchCode":"...",
"isMajor":"1","teachingClassType":"XGXK","checkCapacity":"2",
"queryContent":"科幻文学"},"pageSize":"10","pageNumber":"0","order":""}
三个容易踩的细节:
checkCapacity必须用"2"。"1"会把满员课直接过滤掉,列表里根本看不到目标课, 也就永远发现不了"有人退课"这件事;"2"是"校验但不过滤",满员课仍在列表里带isFull='1'。- 余量没有现成字段。接口只给
classCapacity(容量)和已选人数,剩余量要自己算:剩余 = classCapacity - 已选人数。dataList[]顶层用numberOfFirstVolunteer(只算第一志愿),tcList[]子项用numberOfSelected(已选总数)。判满优先信任服务端的isFull == '1'。 campus不是常量,取自student/<学号>.do;写死会把其他校区的课查漏。
以下每一条都在本科前端源码里逐字确认过,不是按 wisedu 通用形态推测的:
| 核对项 | 结果 |
|---|---|
8 种 teachingClassType |
8 个值全部出现在本科 grablessons.js,且对应 8 个页面 Tab |
| Tab DOM id | aRecommendCourse / aProgramCourse / aUnProgramCourse / aPublicCourse / aRetakeCourse / aSportCourse / aMinorCourse / aSchoolCourse —— 与枚举一一对应 |
| 选课是异步的 | 本科前端确实调用 addVolunteer → initProcessInterval 轮询确认 |
| 学生信息端点 | 本科 index.js 里是 GET student/<学号>.do?timestamp=<ms>(不是 POST) |
| 批次判定 | electiveBatchList[i].canSelect == '1';另需 needConfirm != '1' 或 isConfirmed == '1' |
| 换 token | 本科 loginInUserRegister.js 调 student/register.do?number=<uid>,取 data.token |
campus |
不是学生信息字段 —— 前端是按每个课程行的 data.campus 取值。故本工具不臆造,留空由服务端兜底 |
| 账号类型校验 | data.code 为空即"无学籍信息"(例如用研究生账号登本科系统),工具会明确报错 |
另外,本科系统在高峰期会用信封 code == "4" 拒绝新会话
(文案「在线人数超过上限,请稍后再试!」)。这是很常见的情况,
工具把它当作可重试状态退避处理,不计入连续错误——
否则会在选课高峰刚开放时就把程序误判退出。
这是最容易实现错的一点:
POST elective/volunteer.do → code == "1" 仅表示「已受理」,结果未定
└─ 轮询 elective/studentstatus.do → code == "1" ✅ 选课成功
code == "-1" ❌ 选课失败(真实原因在 msg)
其它 ⏳ 仍在处理,1 秒后重试,最多 10 次
前端就是这么做的(initProcessInterval + queryOperateProcess)。所以本工具的
SelectionResult 区分了三种"还没定论"的状态:ACCEPTED(已受理)、
PENDING(轮询超时,不等于失败)、SUCCESS(已确认)。
实测都得处理,只看状态码会漏:
| 形态 | 场景 |
|---|---|
HTTP 302 → Location: .../*default/index.do |
带着首页 cookie 请求时最常见 |
HTTP 401 + text/html Not login! |
无 cookie 时的网关响应 |
| HTTP 200 + 应用首页 HTML | requests 自动跟完 302 后的落点 |
因此客户端一律先判状态码、再按内容特征判定,绝不直接 resp.json()。
每轮:
刷新所有未完成课程的教学班状态(全局限速,串行)
挑出「有余量 且 满足老师/教学班筛选 且 未确认冲突」的教学班
按 (课程优先级, 教学班ID) 排序,依次提交选课
睡眠 interval + random(0, jitter)
异常:
Token 过期 → 立即重登,不浪费一整轮
被限流 → 冷却 N 秒,并把间隔 ×1.5(上限 8 倍)
网络抖动 → 指数退避重试,连续失败超过阈值才退出
时间冲突 → 该教学班进黑名单,不再重复提交
抢到课 → 记录 + 响铃 + 系统通知
bitxk gui打开一个原生桌面窗口,三栏布局,界面只保留必要信息:
┌──────────────────────────────────────────────────────────────────────────┐
│ BIT 选课助手 登录态:彭煜涵(1120252751) [用浏览器登录][验证][自检] │
├──────────────────┬────────────────────┬──────────────────────────────────┤
│ 要盯的课程 │ 该任务的备选教学班 │ 课程查询(全部备选) │
│ ┌──────────────┐ │ ┌────────────────┐ │ 关键词[___] 类型[全部] [x]查全部 │
│ │科幻文学 │ │ │教学班 老师 时间 │ │ [查询][停止] │
│ │体育/羽毛球 │ │ │ 剩余 │ │ 筛选 [ ]有余量 [x]已满 [ ]冲突 │
│ │数据结构与算法│ │ ├────────────────┤ │ [ ]已选 [清除筛选] │
│ └──────────────┘ │ │…003501 马明 │ │ ┌──────────────────────────────┐ │
│ [添加][编辑][删除]│ │ 周三3-4节 │ │ │课程 老师 上课时间 剩余 │ │
│ │ │ 0 │ │ ├──────────────────────────────┤ │
│ │ └────────────────┘ │ │AI赋能… 张旋 星期二… 28 │ │
│ │ [刷新] │ │体育/柔道 刘秀平 星期三… 1 │ │
│ │ │ └──────────────────────────────┘ │
├──────────────────┴────────────────────┴──────────────────────────────────┤
│ 间隔[2.0]秒 [x]试跑(只观察不提交) [ ]抢到一门就停 [开始抢课] [停止] │
├──────────────────────────────────────────────────────────────────────────┤
│ 运行日志 │
└──────────────────────────────────────────────────────────────────────────┘
表格只保留四列:课程 / 老师 / 上课时间 / 剩余。
上课时间是压缩过的「周几第几节」(如 星期三 3-4节)—— 完整的
「1-16周 星期三 3-4节 良乡校区文萃楼」有 188px,在列宽紧张的表格里
会把其他内容挤掉,而周次与教室不是选课时最关心的信息。
类型、教学班号、容量分母、已选人数、更新时间这些都没有单独占列;
需要细节时看命令行输出(bitxk list 会打印完整容量),
或双击课程加进任务后看中间栏。
左栏只列课程名;类型与优先级双击行即可编辑(禁用的课显示为灰色)。
| 栏 | 作用 |
|---|---|
| 左:要盯的课程 | 抢课任务清单。选中的那门课会驱动中间栏 |
| 中:该任务的备选教学班 | 这门课有几个班、每个班的教学班号 / 老师 / 上课时间 / 剩余名额 |
| 右:全部课程查询 | 检索所有备选课程,带筛选与排序;双击任意一行可加进左边的任务清单 |
点「查询」会按需拉取课程:勾选「查全部类型」则遍历全部 8 种类型 (校公选课 / 体育 / 方案内 / 方案外 / 重修 / 辅修 / 推荐 / 全校), 数据量大时可能要十几秒,随时可以点「停止查询」。
筛选(可多选,是"或"的关系):
只看有余量—— 只想抢能马上选上的只看已满—— 想盯着等别人退课(抢课场景最常用)只看冲突—— 看看哪些课和已选课撞了只看已选—— 确认自己已经选上了什么
关键词框输入即筛选(匹配课程名 / 教师 / 教学班号),是本地过滤、 不会每敲一个字就发请求。类型下拉框同理。
点列标题可排序(课程名 / 教师 / 余量 / 容量),再点一次切升降序。 默认把有余量的排在最前。
中间栏会跟着轮询自动刷新:正在轮询的那门课每次查到新数据, 中间栏的容量与已选人数同步更新。
界面上能做的事:
- 用浏览器登录 —— 弹出浏览器窗口,登录一次后自动取回登录态 (学号、姓名、批次都从页面里自动读出,不用手填);
- 添加 / 编辑 / 删除要盯的课程(双击课程行也能编辑);
- 查询全部备选课程并筛选,双击结果行直接加进任务清单;
- 查看某门课的每个教学班的容量与已选人数;
- 开始抢课 / 停止 —— 抢课在后台线程跑,界面不会卡住;
- 保存配置 —— 课程列表写回
config.toml,下次打开还在。
抢课是长任务,所有网络请求都在工作线程里执行,事件通过队列回到主线程渲染, 所以点「开始」之后界面依然可以正常操作。
第一次使用建议保持勾选 试跑:它只观察余量、绝不提交选课, 确认课程匹配正确后再取消勾选正式开抢。
到 Releases 页面按需下载:
Windows
| 文件 | 大小 | 适合谁 |
|---|---|---|
BIT-Course-Helper-0.1.0-setup.exe |
40 MB | 推荐。 双击安装,自动建开始菜单与桌面快捷方式;按用户安装,不需要管理员权限 |
...-windows-portable-gui.exe |
14 MB | 不想安装:单个 exe,双击即用图形界面 |
...-windows-portable-cli.exe |
14 MB | 不想安装、要用命令行:单个 exe,输出能正常打印 |
...-windows-x64.zip |
46 MB | 需要频繁跑命令行:解压后启动最快(0.1 秒,单文件版要 1.4 秒) |
macOS
| 文件 | 大小 | 适合谁 |
|---|---|---|
BIT-Course-Helper-0.1.0-macos.tar.gz |
41 MB | 完整包:.app(双击开界面)+ 命令行版 |
...-macos-portable-gui |
13 MB | 单个可执行文件,直接双击开界面 |
...-macos-portable-cli |
13 MB | 单个可执行文件,命令行用(启动 0.2 秒) |
单文件 vs 文件夹版:单文件版每次运行都要把内置运行时解压到临时目录, 所以启动慢一些(Windows 约 1.4 秒,macOS 约 0.2 秒)。图形界面无所谓, 但如果你要频繁调用命令行(比如写脚本),用文件夹版或安装版更合适。
为什么 Windows 的单文件版分两个:Windows 上"一个 exe 同时干 GUI 和 CLI" 做不到 —— PE 头只能有一个子系统。
console=False的 exe(Subsystem=GUI) 双击不弹黑框,但它的 stdout 会被丢弃,命令行拿不到输出; 所以分别产出 GUI 版(Subsystem=2)和 CLI 版(Subsystem=3)。
发行包 40MB 上下,大头是 Python 运行时和图形界面库,不含浏览器内核。
「用浏览器登录」这个功能驱动的是你本机已经装好的 Chrome / Edge / Chromium / Brave, 工具通过 DevTools Protocol 连过去取登录态。所以:
| 方案 | 包体积 |
|---|---|
| 打包 Playwright / Selenium(自带内核) | ~250MB |
| 本工具(复用系统浏览器) | ~40MB |
打包时还做了这些裁剪:
- 排除了
cryptography/bcrypt/cffi—— 它们不是本项目的依赖, 只是恰好装在构建机上被 PyInstaller 顺手收了进来(省约 11MB); - 排除了
numpy/pandas/PIL/ 其它 GUI 框架 / 开发期工具; - macOS 上二进制符号表已 strip(Windows 上不能 strip,spec 里按平台做了判断)。
包内结构:
bitxk-gui / bitxk-gui.exe 图形界面,双击这个
bitxk / bitxk.exe 命令行版 —— 和图形界面**共用同一份运行库**
portable/ 单文件便携版,免解压
config.example.toml README.md LICENSE
两个程序共用一份 _internal/,所以同时提供图形界面和命令行不额外占体积。
打包时不会再塞一份精简命令行版(
bitxk-cli.spec仍会构建,但不进发行包)。 它要再带一整套运行库(约 22MB),而功能上没有任何增量 ——bitxk命令行程序本来就在文件夹版里。这一条让压缩包从 51MB 降到 40MB。单文件便携版是可选的,觉得大就加
--skip-portable/-SkipPortable, 发行包能再小约 14MB,功能不受影响。
macOS 首次打开提示"无法验证开发者":这是未签名应用的正常提示。 右键点图标 → 选「打开」→ 再确认一次即可;或执行
xattr -dr com.apple.quarantine /Applications/BIT-Course-Helper.app。Windows SmartScreen 拦截:点「更多信息」→「仍要运行」。
开发这个项目时踩过一次,记在这里免得重蹈覆辙。
Windows 的 PE 文件只能有一个子系统,所以图形界面版打包时设 console=False
(双击不弹黑框)。代价是这个程序没有控制台,sys.stdout / sys.stderr
就是 None,任何 sys.stdout.isatty() 之类的调用都会直接抛
AttributeError: 'NoneType' object has no attribute 'isatty'。
问题在于这个错误不会在启动时暴露。gui.py 只在真正需要时才
from .cli import _build_http —— 校验登录态、环境自检、开始抢课这几条路径。
于是症状变成"软件能正常打开,一失效就崩"。
有两个因素让它在开发机上极难复现:
- 只有 windowed 打包才复现。从终端启动 exe 时子进程会继承控制台句柄,
stdout不为None,所以开发时怎么点都是好的。 - 会被
NO_COLOR掩盖。颜色探测的第一行就是if os.environ.get("NO_COLOR"), 只要这个变量存在就提前返回,根本走不到isatty()。
所以代码里做了这些约束:
- 所有流的读取必须走
cli._stdout()/cli._stderr(),它们在无控制台时返回None; - 所有输出走
cli._echo(),没有控制台就静默丢弃; tests/test_windowed_streams.py在子进程里把两个流置为None并显式pop掉NO_COLOR来还原用户环境 —— 子进程是必须的,因为Style.enabled在导入时求值一次,同进程内改sys.stdout抓不到那个时刻;- 还有一条静态检查,禁止源码里再出现未判空的
sys.stdout.<attr>。
同一类"macOS 开发、Windows 使用"的盲区还导致了另一个崩溃:会话文件和配置文件
不是合法 UTF-8 时(Windows 记事本很容易存成 GBK),read_text 抛的是
UnicodeDecodeError,而它不是 json.JSONDecodeError 的子类,
原来的 except 抓不到。现在统一捕获 ValueError 并给出可操作的提示。
需要 Python ≥ 3.11(用到标准库 tomllib)。图形界面用 Python 自带的
tkinter,不需要额外安装任何东西。
git clone https://github.com/Sirius-Peng/bitxk.git && cd bitxk
# 装成命令(推荐)
pip install -e .
# 或只装依赖,用 python -m bitxk 运行
pip install -r requirements.txt
python -m bitxk --help依赖只有三个:requests、pycryptodome、websockets
(最后一个用来通过 CDP 驱动浏览器,只需要几十 KB)。
一条命令走完全流程(建虚拟环境 → 装依赖 → 打包全部产物 → 生成压缩包与校验和):
# macOS / Linux —— 产物在 dist-release/
bash packaging/build-release.sh
# Windows —— 产物在 dist-release/
powershell -ExecutionPolicy Bypass -File packaging\build-release.ps1Windows 上也可以直接右键 packaging/build-release.ps1 → 「使用 PowerShell 运行」。
脚本会自动处理几件容易踩坑的事:缺少根证书导致的 pip SSL 报错、上次半途失败的
虚拟环境、Inno Setup 是否安装、以及发行包里不该出现的配置文件。
详见 packaging/README.md。
只想单独打某个产物,也可以直接用 PyInstaller:
pip install pyinstaller
pyinstaller --clean --noconfirm packaging/bitxk.spec # 完整版(GUI + 命令行)
pyinstaller --clean --noconfirm packaging/bitxk-cli.spec # 精简命令行版(去掉 tkinter)
pyinstaller --clean --noconfirm packaging/bitxk-onefile.spec # 单文件版(两个 exe)
# 产物在 dist/ 下bitxk init # 生成配置模板(只需一次)
bitxk gui # 打开图形界面然后在窗口里:
- 点 「用浏览器登录」,在弹出的浏览器窗口里登录一次 → 关掉浏览器;
- 点 「添加」 加上你想盯的课(课程名要和选课系统里显示的完全一致);
- 保持 试跑 勾选,点 「开始抢课」,观察余量是否正确;
- 确认无误后取消 试跑 勾选,正式开抢。
bitxk init # 1. 生成配置模板
$EDITOR config.toml # 2. 填学号密码和想选的课
bitxk check # 3. 自检:网络 + 登录页结构
bitxk --list-browsers # 4. 看看本机有哪些浏览器可用
# 5. 登录(二选一)
bitxk browser-login # a) 浏览器登录(推荐,不受 SSO 改版影响)
bitxk login # b) 直接填账号密码登录
bitxk list # 6. 看一眼当前余量
bitxk grab --dry-run # 7. 安全试跑:轮询但绝不提交
bitxk grab # 8. 正式开抢密码安全:不建议把密码写进
config.toml。用环境变量更稳妥:export BITXK_USERNAME=1120200001 export BITXK_PASSWORD='你的密码' bitxk grab或者干脆用
bitxk browser-login—— 那样工具根本不需要你的密码。
config.toml 全部字段(bitxk init 生成的模板里都带注释):
[account]
username = "" # 也可用环境变量 BITXK_USERNAME
password = "" # 也可用环境变量 BITXK_PASSWORD
[poll]
interval = 2.0 # 轮询间隔(秒),下限 0.5,建议 1.5~3
min_request_interval = 0.8 # 任意两次请求的最小间隔(秒),全局限速
jitter = 0.5 # 随机抖动上限(秒)
max_duration = 14400 # 最长运行时长(秒),0 = 不限
max_consecutive_errors = 30 # 连续失败多少次后退出
rate_limit_cooldown = 30.0 # 被限流后的冷却秒数
relogin_after = 600 # 会话使用多久后主动重登(秒)
[http]
timeout = 10.0
max_retries = 3
proxy = "" # 校外可填校园 WebVPN 代理
verify_ssl = true
[notify]
sound = true # 响铃
stop_on_success = false # true = 抢到第一门就退出
# 想盯几门课就写几个 [[courses]]
[[courses]]
name = "科幻文学" # 必须与选课系统里显示的课程名完全一致
type = "XGXK" # 教学班类型,见下表
priority = 100 # 数字越小越先选
enabled = true
# teachers = ["张三"] # 可选:只选指定老师的课
# classes = ["1234567"] # 可选:只选指定教学班 ID
[[courses]]
name = "体育/羽毛球"
type = "TYKC"
priority = 50 # 更想上体育课,所以优先级更高共 8 种,与选课系统页面上的 Tab 一一对应:
| 值 | 含义 | 查询接口 |
|---|---|---|
XGXK |
校公选课 | publicCourse.do |
TYKC |
体育课程 | programCourse.do |
FANKC |
方案内课程 | programCourse.do |
FAWKC |
方案外课程 | programCourse.do |
CXKC |
重修课程 | programCourse.do |
FXKC |
辅修课程 | programCourse.do(唯一 isMajor='0') |
TJKC |
推荐课程 | recommendedCourse.do |
QXKC |
全校课程 | course.do |
注意体育课没有独立端点 —— 它和方案内/方案外/重修/辅修共用
programCourse.do, 由服务端按 body 里的teachingClassType分流。
写错 type 会导致查不到课却看不出原因,所以工具在启动时会校验并直接报错。
bitxk gui 打开图形界面(推荐)
bitxk init 生成 config.toml 模板
bitxk check 环境自检(无需账号)
bitxk --list-browsers 列出本机可用的 Chromium 系浏览器
bitxk browser-login 用浏览器登录并缓存登录态(推荐)
bitxk login 直接模拟 SSO 登录并列出可选批次
bitxk list 打印当前各教学班余量
bitxk grab 轮询并自动选课
bitxk grab --once 只查一轮(等同 list)
bitxk grab --dry-run 轮询但绝不提交选课(安全试跑)
bitxk grab --browser 抢课前先用浏览器登录一次
bitxk grab --interval 3 覆盖轮询间隔
bitxk grab --duration 7200 覆盖最长运行时长
bitxk grab -v 输出调试日志
全局参数:
-c/--config <路径> 指定配置文件(默认 ./config.toml)
-u/--username 学号(覆盖配置)
-p/--password 密码(覆盖配置)
--encrypt-mode ecb|cbc SSO 密码加密模式
--cookie '...' 手动导入 Cookie
--token '...' 手动导入 Token
--student-code <学号> 手动导入模式必填(批次接口形如 student/<学号>.do)
--browser [路径] 用 Chromium 浏览器登录;可选路径指定浏览器
--browser-timeout 300 等待浏览器登录的超时秒数
--list-browsers 列出检测到的浏览器后退出
退出码:0 成功 / 1 未抢到 / 2 登录失败 / 3 不在选课时间 / 4 配置错误 / 5 其它错误。
在校内网络(校园网 / 学校 VPN 客户端)下直接运行即可,无需额外配置。
在校外且不想装 VPN 客户端时,有两条路:
-
先在浏览器登录,再手动导入登录态(推荐,最省事)—— 见下一节的
--token/--cookie用法。 工具本身仍能直连xk.bit.edu.cn,缺的只是登录态。 -
走本地代理——如果你有可用的 HTTP 代理,填到配置里:
[http] proxy = "http://127.0.0.1:7890"
注意
webvpn.bit.edu.cn是网页版网关(它把目标站点嵌在路径里), 不是 HTTP 代理,不能直接填进proxy。 浏览器里通过 WebVPN 登录后复制 Cookie 用第 1 种方式导入,反而更稳。
顺带一提:能连通学校服务器本身就说明网络没问题;
bitxk check会告诉你当前能否直连。
如果学校改了 SSO 登录页结构,自动登录会失效(bitxk check 会提前告诉你)。此时用浏览器里的登录态兜底:
- 浏览器登录
https://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do - 按
F12→ Network → 随便点一门课的「选课」按钮 - 在请求头里找到:
Token: xxxxx← 整个值复制出来Cookie: JSESSIONID=...← 整行复制出来
- 运行:
bitxk grab --student-code '1120200001' \
--token 'xxxxx' \
--cookie 'JSESSIONID=...; route=...; _WEU=...'--student-code 在手动导入模式下是必需的:批次接口是 student/<学号>.do,
没有学号就无法确定该请求哪个地址。
--token 也支持直接粘贴 CAS 回跳后地址栏里带 bitXsxkLogin= 的完整 URL,工具会自动提取。
轮询频率是这个工具唯一可能"伤害别人"的地方,所以设计上刻意保守:
- 默认 2 秒一轮,且
PollConfig在代码层拒绝interval < 0.5,改配置也绕不过去。 - 全局限速器保证任意两次请求至少间隔
min_request_interval(默认 0.8 秒),即使你配了 10 门课也不会并发轰炸。 - 随机抖动让请求不会卡在整秒上,避免和别人的脚本形成同步尖峰。
- 被限流就退让:冷却 30 秒,并把间隔按 1.5 倍逐步放大(最多 8 倍),而不是硬顶。
- 连续失败即退出,避免在系统维护期间无意义地刷。
一个 2 秒间隔的实例,一小时约 1800 次请求 —— 相比浏览器里手动 F5,这个量级对现代教务系统完全可接受。请不要再往下压。
装一个 Google Chrome 就行;或者告诉工具它在哪:
export BITXK_BROWSER="/path/to/chrome" # 环境变量
bitxk grab --browser "/path/to/chrome" # 单次指定用 bitxk --list-browsers 看检测结果。
- 确认在弹出的浏览器窗口里确实登录成功了(页面应该跳到选课系统首页);
- 窗口被其他窗口挡住时脚本仍能检测到,但你看不到,容易被误以为卡住;
- 超时默认 5 分钟,可用
--browser-timeout 600延长(比如要等短信验证码)。
纯命令行环境(SSH、Docker)没有显示服务,用命令行版本:
bitxk grabWindows 上用记事本另存为 ANSI(GBK) 就会这样。用 VS Code / Notepad++ 打开, 另存为 UTF-8 即可。会话文件遇到同样的编码问题会自动忽略并重新登录, 不会崩。
Windows 图形界面版在登录态失效时会崩,报这个错。
原因是图形界面版打包成了「无控制台」程序,此时 sys.stdout 是 None,
而校验登录态的代码路径会顺带用到命令行模块,那里直接调了 sys.stdout.isatty()。
已修复,请重新下载 2026-09-17 及之后的构建。详见 无控制台打包。
先确认是哪种情况:
程序优先复用已缓存的登录态,正常情况下不必每次登录。所以先看日志里 启动时的那两行:
- 「登录态仍然有效」 —— 那就是可以用的状态。若你仍被要求重新登录, 说明点的是「用浏览器登录」而不是直接查询;直接查询即可。
- 「上次的登录态已失效,请点「用浏览器登录」重新登录」 —— 服务端明确说过期了(token 只有 15~20 分钟)。这是正常的,重新登录一次即可。
- 「暂时连不上选课系统,无法校验登录态。登录信息已保留」 —— 网络问题,不是登录问题。登录态没有被丢弃,校园网恢复后点「验证登录」。
- 「登录态保存失败」 —— 程序装在只读目录(比如
C:\Program Files下的解压版),写不了.bitxk_session.json。把配置换到可写目录即可。
另外,"打开浏览器窗口后是未登录的"本身不一定是故障:SESSION /
JSESSIONID / GS_SESSIONID / _WEU 都是会话 cookie,Chrome 关闭时按
设计清除。真正的免登录靠的是 .bitxk_session.json,与浏览器是否还登着无关。
浏览器登录的痕迹都在 ~/.bitxk/:
rm -rf ~/.bitxk # 清掉浏览器 profile 与缓存
rm -f .bitxk_session.json # 清掉当前目录的会话缓存学校改版了 SSO 页面,只影响"直接模拟登录"这一种方式。
改用浏览器登录即可(bitxk browser-login)——那条路不依赖登录页结构。
如果你用的是浏览器登录,不会遇到这一类问题——换那条路即可。
- 先在浏览器里手动登录一次,确认密码没记错;
- 若浏览器能登而脚本不能,试
bitxk grab --encrypt-mode cbc; - 连续失败会触发验证码,工具检测到后会主动停止而不是继续硬试(避免锁账号)。
正常行为,直接退出(退出码 3)。选课批次没开放时,接口不会返回任何 canSelect=1 的批次。用 bitxk login 可以看到所有批次及其时间窗。
大概率是容量字段命名与预期不同。用 -v 跑一次,日志里会打印原始响应字段;TeachingClass 会记录 capacity_source 说明它是从哪个字段推出来的。请带日志提 issue。
选课系统限制并发在线人数,高峰期限流。这是正常现象,工具会自动退避重试,
不需要你做任何事;也不会因此退出。想减少撞上的概率,可以把 interval 调大一些。
说明登录成功了,但这个账号在本科选课系统里没有学籍 —— 最常见的原因是用研究生账号登录本科系统。请确认用的是本科学号。
正常现象,说明选课请求已被系统受理、后台正在排队处理。工具会自动轮询
studentstatus.do 确认结果。若最终显示「结果未知」,不代表失败 ——
后台可能仍在处理,下一轮查询课程列表时看到「已选」就说明成功了。
以服务端的判定为准(它掌握你的完整课表,包括其他批次的课)。
也可以运行 bitxk list 看该教学班的 status 是否已被标为「冲突」。
本工具不做退课,请去选课系统页面手动退。另外建议先 --dry-run 试跑确认匹配逻辑符合预期。
[notify]
stop_on_success = truebitxk/
├── __init__.py 包入口与公共 API
├── __main__.py python -m bitxk
├── cli.py 命令行界面(gui/init/check/browser-login/login/list/grab)
├── gui.py tkinter 桌面界面(配置面板 + 余量表 + 日志)
├── browser.py Chromium 发现 / CDP 驱动 / 浏览器登录取登录态
├── auth.py SSO 登录、密码加密、会话持久化、手动导入
├── http.py HTTP 传输层:限速、重试、限流识别
├── client.py 选课系统业务接口(查询/选课/批次)
├── poller.py 轮询引擎(本项目核心)
├── models.py 数据模型与容量推断
├── config.py TOML 配置加载与校验
├── notify.py 响铃与系统通知
└── exceptions.py 分层异常
packaging/
├── README.md 打包说明(怎么编、产物是什么、怎么排错)
├── build-release.sh macOS / Linux 一键打包
├── build-release.ps1 Windows 一键打包
├── entry.py 打包入口:无参数开 GUI,带参数走 CLI
├── bitxk.spec 完整版打包配置
├── bitxk-cli.spec 精简命令行版打包配置
├── bitxk-onefile.spec 单文件便携版打包配置
├── installer.iss Inno Setup 安装包脚本
└── config.example.toml 发行包里的配置模板
tests/ 452 个测试;网络与浏览器默认全部打桩,
另有 3 项真实启动浏览器的用例(无浏览器时自动跳过)。
test_windowed_streams.py 专门还原「双击无控制台的 exe」
这一真实场景(见「无控制台打包」一节)
pip install -e ".[dev]"
python -m pytest -q # 跑测试
python -m pytest -q -k poller # 只跑轮询引擎测试设计上的两个原则:
- 业务失败不是异常。 「容量已满」是轮询中的正常状态,用
SelectionResult.outcome表达;只有「登录失效」「被限流」「网络故障」这类需要上层改变行为的才抛异常。 - 容量字段不写死。 选课系统不同版本的字段名不一致(
remainCapacity/rl/remainNumber…),models.py用候选键匹配 + 多路推断,并通过capacity_source暴露推断依据,便于排错。
改动与选课系统交互的代码(client.py / auth.py / models.py)时,请务必让
tests/ 里对应的契约测试保持通过 —— 它们逐条固化了实测到的接口行为,
是防止改坏的主要防线:
python -m pytest tests/test_client.py tests/test_auth_contract.py -v本项目在实现前调研了以下开源项目与资料,特此致谢:
- vc6-1998/BIT-CourseKiller —— 北理工本科抢课脚本,提供了 SSO 登录与
xsxkapp接口的可用链路,是本次实现的主要参考。 - lijyve/Fuck-BITcourse —— 早期选课系统
volunteer.do调用方式。 - mhllwmt/Spider_Bit —— 研究生院 DWR 老系统爬虫(与本科系统不同,仅作历史参考)。
- only9464/HEU-Wisedu —— 哈工程同款 wisedu 选课系统的桌面客户端,印证了
xsxkapp接口的通用形态。 - friskit-china/I_NEED_COURSE、MichaelToLearn/oh_my_course —— 北理工抢课工具,其免责声明与「延长时间间隔」的建议值得每个人读一遍。
- FoskyM: php curl 登录金智统一认证平台 —— wisedu 统一认证加密流程的逆向分析。
MIT