Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bitxk

北京理工大学本科生选课系统(金智 wisedu xsxkappxk.bit.edu.cn/xsxkapp课程余量轮询 + 自动选课命令行工具。

适用场景:补选 / 退换课 / 试听退换课阶段,某门课已经选满,你希望有人退课时自动帮你补上

两种用法,随便挑:

  • 图形界面(推荐给不熟悉命令行的同学):bitxk gui —— 原生桌面窗口,点点鼠标就能抢课。
  • 命令行bitxk grab —— 适合挂服务器 / 写脚本。

登录方式也独立于 SSO 实现:用你自己的 Chromium 浏览器登录一次, 工具只负责把登录态取出来。学校无论怎么改登录页都不影响使用, 短信验证码、2FA、密码管理器也都能正常工作。

适用对象仅限本科生。研究生教务系统(grdms.bit.edu.cn)是另一套 DWR 协议的老系统, 接口完全不同,本工具不适用。


目录

免责声明

请先读完再用。

  1. 本工具为个人学习与自用目的编写,自动化操作的是你自己的账号
  2. 选课系统是学校的生产系统。请勿把轮询间隔调到 1 秒以下,那会对服务器造成不必要的压力,也更容易触发风控导致账号被限制。默认的 2 秒是刻意保守的取值。
  3. 本工具不破解验证码、不绕过任何风控、不伪造身份。它做的是「用你自己的凭据,按正常接口查询余量并提交选课」,与你在浏览器里手动刷新点按钮是同一件事,只是更快更持久。
  4. 抢课本身是个公平性问题:你能抢到,通常意味着有人退课。请不要用本工具批量囤课或倒卖课程
  5. 使用本工具产生的任何后果由使用者自负。请遵守北京理工大学的教学管理规定。

功能

功能 说明
图形界面 bitxk gui:原生桌面窗口,课程管理 + 实时余量表 + 运行日志
浏览器登录 用本机 Chromium 登录一次即可,不依赖 SSO 实现细节(推荐)
自动 SSO 登录 也支持直接模拟统一身份认证(含密码 AES 加密),无需开浏览器
登录态持久化 浏览器 profile + 会话缓存(.bitxk_session.json,权限 600),重启后自动复用,失效自动重登
课程余量轮询 按配置间隔查询教学班余量,带随机抖动
自动选课 一旦出现余量立即提交选课,按课程优先级排序
异步结果确认 选课接口是「受理制」,提交后会轮询状态接口确认真实成败
多课程并发 多门课共享一个全局限速器,不会因为课多就把请求打爆
智能退避 被限流时冷却并自动放大间隔;网络抖动指数退避重试
失效自愈 Token 过期自动重新登录并继续,无需人工干预
冲突去重 已确认时间冲突的教学班不再重复提交
手动登录态兜底 复制浏览器 Cookie + Token 即可导入,三种登录方式逐级兜底
安全试跑 --dry-run 只观察余量、绝不提交选课
到点提醒 抢到课响铃 + 系统通知(macOS / Linux / Windows)
环境自检 bitxk check 一条命令确认网络与页面结构是否仍适配

它到底是怎么工作的

1. 登录方式

工具支持三种登录方式,按推荐程度排序

方式 命令 依赖 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 域名(实测踩过)

一个很容易写错的地方:不要用"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 去换票。

现在的判据只有两类,且都要求实证

  1. sessionStorage 里出现 token(前端登录成功后一定会写),或
  2. 回跳地址里出现 bitXsxkLogin=<key>(CAS 换票成功),

并且会用 student/<学号>.do 独立复验一次会话真的可用 (未登录时该端点返回 302、登录后返回学生信息)。

cookie 必须全量取,不能按 URL 过滤(最容易踩的坑)

用 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(学号)、 namecampusNamecurrentBatch 里有批次 code;currentCampus 里有校区。 工具会把这些直接读出来,不需要手工传 --student-code

(顺带一提:传错学号会得到含糊的 code="2" "非法请求",所以自动读取比手填更可靠。)

残留的失效登录态会被清除

Chrome 的持久 profile 会保留上次登录留下的 sessionStorage.token, 页面一打开 URL 就可能带着旧的 bitXsxkLogin这份登录态往往已经失效

工具的处理:复验不过就先给 30 秒宽限期(服务端可能尚未就绪), 仍然不过就判定为陈旧状态 —— 清除它、提示重新登录,而不会当成成功 缓存下来。否则用户会看到假的"登录成功",等到抢课时才发现用不了。

1.1 自动 SSO 登录的链路

如果你选择不开浏览器、直接填账号密码,走的是下面这条链路。

本科选课系统首页里注入的常量(直接从页面源码读到,非推测):

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-ECBkey = base64decode(login-croypto) (恰好 16 字节),无 IV明文就是口令原文(不加随机前缀)、PKCS#7 填充,密文再 base64。 该结论来自对生产前端 bundle 的源码级确认,非猜测。
  • cbc(旧版 wisedu,部分学校仍在用):AES-128-CBC,明文 = 64 位随机串 + 密码,iv = 16 位随机串。 若登录页出现 pwdEncryptSalt 而非 login-croypto,工具会自动切到该模式;也可用 --encrypt-mode cbc 手动指定。

1.2 关于网址的写法

选课系统的入口是:

http://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do

工具内部使用的是它的 HTTPS 形式:

https://xk.bit.edu.cn/xsxkapp/sys/xsxkapp

这不是偏好问题,而是必须的:选课系统全站强制 HTTPShttp:// 的 任何路径(首页、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"

2. 业务接口

所有业务请求都在 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;写死会把其他校区的课查漏。

2.1 针对本科系统的核对结论

以下每一条都在本科前端源码里逐字确认过,不是按 wisedu 通用形态推测的:

核对项 结果
8 种 teachingClassType 8 个值全部出现在本科 grablessons.js,且对应 8 个页面 Tab
Tab DOM id aRecommendCourse / aProgramCourse / aUnProgramCourse / aPublicCourse / aRetakeCourse / aSportCourse / aMinorCourse / aSchoolCourse —— 与枚举一一对应
选课是异步的 本科前端确实调用 addVolunteerinitProcessInterval 轮询确认
学生信息端点 本科 index.js 里是 GET student/<学号>.do?timestamp=<ms>(不是 POST)
批次判定 electiveBatchList[i].canSelect == '1';另需 needConfirm != '1'isConfirmed == '1'
换 token 本科 loginInUserRegister.jsstudent/register.do?number=<uid>,取 data.token
campus 不是学生信息字段 —— 前端是按每个课程行的 data.campus 取值。故本工具不臆造,留空由服务端兜底
账号类型校验 data.code 为空即"无学籍信息"(例如用研究生账号登本科系统),工具会明确报错

另外,本科系统在高峰期会用信封 code == "4" 拒绝新会话 (文案「在线人数超过上限,请稍后再试!」)。这是很常见的情况, 工具把它当作可重试状态退避处理,不计入连续错误—— 否则会在选课高峰刚开放时就把程序误判退出。

2.2 选课是异步的

这是最容易实现错的一点:

POST elective/volunteer.do           → code == "1"  仅表示「已受理」,结果未定
  └─ 轮询 elective/studentstatus.do  → code == "1"   ✅ 选课成功
                                       code == "-1"  ❌ 选课失败(真实原因在 msg)
                                       其它          ⏳ 仍在处理,1 秒后重试,最多 10 次

前端就是这么做的(initProcessInterval + queryOperateProcess)。所以本工具的 SelectionResult 区分了三种"还没定论"的状态:ACCEPTED(已受理)、 PENDING(轮询超时,不等于失败)、SUCCESS(已确认)。

2.3 token 失效有三种形态

实测都得处理,只看状态码会漏:

形态 场景
HTTP 302Location: .../*default/index.do 带着首页 cookie 请求时最常见
HTTP 401 + text/html Not login! 无 cookie 时的网关响应
HTTP 200 + 应用首页 HTML requests 自动跟完 302 后的落点

因此客户端一律先判状态码、再按内容特征判定,绝不直接 resp.json()

3. 轮询决策

每轮:
  刷新所有未完成课程的教学班状态(全局限速,串行)
  挑出「有余量 且 满足老师/教学班筛选 且 未确认冲突」的教学班
  按 (课程优先级, 教学班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 种类型 (校公选课 / 体育 / 方案内 / 方案外 / 重修 / 辅修 / 推荐 / 全校), 数据量大时可能要十几秒,随时可以点「停止查询」。

筛选(可多选,是"或"的关系):

  • 只看有余量 —— 只想抢能马上选上的
  • 只看已满 —— 想盯着等别人退课(抢课场景最常用
  • 只看冲突 —— 看看哪些课和已选课撞了
  • 只看已选 —— 确认自己已经选上了什么

关键词框输入即筛选(匹配课程名 / 教师 / 教学班号),是本地过滤、 不会每敲一个字就发请求。类型下拉框同理。

点列标题可排序(课程名 / 教师 / 余量 / 容量),再点一次切升降序。 默认把有余量的排在最前

中间栏会跟着轮询自动刷新:正在轮询的那门课每次查到新数据, 中间栏的容量与已选人数同步更新。

界面上能做的事:

  1. 用浏览器登录 —— 弹出浏览器窗口,登录一次后自动取回登录态 (学号、姓名、批次都从页面里自动读出,不用手填);
  2. 添加 / 编辑 / 删除要盯的课程(双击课程行也能编辑);
  3. 查询全部备选课程并筛选,双击结果行直接加进任务清单;
  4. 查看某门课的每个教学班的容量与已选人数;
  5. 开始抢课 / 停止 —— 抢课在后台线程跑,界面不会卡住;
  6. 保存配置 —— 课程列表写回 config.toml,下次打开还在。

抢课是长任务,所有网络请求都在工作线程里执行,事件通过队列回到主线程渲染, 所以点「开始」之后界面依然可以正常操作。

第一次使用建议保持勾选 试跑:它只观察余量、绝不提交选课, 确认课程匹配正确后再取消勾选正式开抢。

安装

方式一:下载现成的发行版(推荐,不用装 Python)

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 —— 校验登录态、环境自检、开始抢课这几条路径。 于是症状变成"软件能正常打开,一失效就崩"。

有两个因素让它在开发机上极难复现:

  1. 只有 windowed 打包才复现。从终端启动 exe 时子进程会继承控制台句柄, stdout 不为 None,所以开发时怎么点都是好的。
  2. 会被 NO_COLOR 掩盖。颜色探测的第一行就是 if os.environ.get("NO_COLOR"), 只要这个变量存在就提前返回,根本走不到 isatty()

所以代码里做了这些约束:

  • 所有流的读取必须走 cli._stdout() / cli._stderr(),它们在无控制台时返回 None
  • 所有输出走 cli._echo(),没有控制台就静默丢弃;
  • tests/test_windowed_streams.py子进程里把两个流置为 None 并显式 popNO_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

依赖只有三个:requestspycryptodomewebsockets (最后一个用来通过 CDP 驱动浏览器,只需要几十 KB)。

方式三:自己打包

一条命令走完全流程(建虚拟环境 → 装依赖 → 打包全部产物 → 生成压缩包与校验和):

# macOS / Linux —— 产物在 dist-release/
bash packaging/build-release.sh

# Windows —— 产物在 dist-release/
powershell -ExecutionPolicy Bypass -File packaging\build-release.ps1

Windows 上也可以直接右键 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           # 打开图形界面

然后在窗口里:

  1. 「用浏览器登录」,在弹出的浏览器窗口里登录一次 → 关掉浏览器;
  2. 「添加」 加上你想盯的课(课程名要和选课系统里显示的完全一致);
  3. 保持 试跑 勾选,点 「开始抢课」,观察余量是否正确;
  4. 确认无误后取消 试跑 勾选,正式开抢。

命令行

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            # 更想上体育课,所以优先级更高

type 取值

共 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 客户端时,有两条路:

  1. 先在浏览器登录,再手动导入登录态(推荐,最省事)—— 见下一节的 --token / --cookie 用法。 工具本身仍能直连 xk.bit.edu.cn,缺的只是登录态。

  2. 走本地代理——如果你有可用的 HTTP 代理,填到配置里:

    [http]
    proxy = "http://127.0.0.1:7890"

    注意 webvpn.bit.edu.cn网页版网关(它把目标站点嵌在路径里), 不是 HTTP 代理,不能直接填进 proxy。 浏览器里通过 WebVPN 登录后复制 Cookie 用第 1 种方式导入,反而更稳。

顺带一提:能连通学校服务器本身就说明网络没问题; bitxk check 会告诉你当前能否直连。

兜底方案:手动导入登录态

如果学校改了 SSO 登录页结构,自动登录会失效(bitxk check 会提前告诉你)。此时用浏览器里的登录态兜底:

  1. 浏览器登录 https://xk.bit.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do
  2. F12Network → 随便点一门课的「选课」按钮
  3. 在请求头里找到:
    • Token: xxxxx ← 整个值复制出来
    • Cookie: JSESSIONID=... ← 整行复制出来
  4. 运行:
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,这个量级对现代教务系统完全可接受。请不要再往下压。


故障排查

提示「没有在本机找到 Chromium 系浏览器」

装一个 Google Chrome 就行;或者告诉工具它在哪:

export BITXK_BROWSER="/path/to/chrome"      # 环境变量
bitxk grab --browser "/path/to/chrome"       # 单次指定

bitxk --list-browsers 看检测结果。

浏览器登录卡住 / 一直等不到登录完成

  1. 确认在弹出的浏览器窗口里确实登录成功了(页面应该跳到选课系统首页);
  2. 窗口被其他窗口挡住时脚本仍能检测到,但你看不到,容易被误以为卡住;
  3. 超时默认 5 分钟,可用 --browser-timeout 600 延长(比如要等短信验证码)。

GUI 打不开 / 提示「无法启动图形界面」

纯命令行环境(SSH、Docker)没有显示服务,用命令行版本:

bitxk grab

提示「配置文件不是 UTF-8 编码」

Windows 上用记事本另存为 ANSI(GBK) 就会这样。用 VS Code / Notepad++ 打开, 另存为 UTF-8 即可。会话文件遇到同样的编码问题会自动忽略并重新登录, 不会崩。

'NoneType' object has no attribute 'isatty'(0.1.0 旧构建)

Windows 图形界面版在登录态失效时会崩,报这个错。

原因是图形界面版打包成了「无控制台」程序,此时 sys.stdoutNone, 而校验登录态的代码路径会顺带用到命令行模块,那里直接调了 sys.stdout.isatty()

已修复,请重新下载 2026-09-17 及之后的构建。详见 无控制台打包

每次打开都要重新登录 / 登录态留不住

先确认是哪种情况:

程序优先复用已缓存的登录态,正常情况下不必每次登录。所以先看日志里 启动时的那两行:

  1. 「登录态仍然有效」 —— 那就是可以用的状态。若你仍被要求重新登录, 说明点的是「用浏览器登录」而不是直接查询;直接查询即可。
  2. 「上次的登录态已失效,请点「用浏览器登录」重新登录」 —— 服务端明确说过期了(token 只有 15~20 分钟)。这是正常的,重新登录一次即可。
  3. 「暂时连不上选课系统,无法校验登录态。登录信息已保留」 —— 网络问题,不是登录问题。登录态没有被丢弃,校园网恢复后点「验证登录」。
  4. 「登录态保存失败」 —— 程序装在只读目录(比如 C:\Program Files 下的解压版),写不了 .bitxk_session.json。把配置换到可写目录即可。

另外,"打开浏览器窗口后是未登录的"本身不一定是故障:SESSION / JSESSIONID / GS_SESSIONID / _WEU 都是会话 cookie,Chrome 关闭时按 设计清除。真正的免登录靠的是 .bitxk_session.json,与浏览器是否还登着无关。

抢到课后浏览器 profile 想清掉

浏览器登录的痕迹都在 ~/.bitxk/

rm -rf ~/.bitxk            # 清掉浏览器 profile 与缓存
rm -f .bitxk_session.json  # 清掉当前目录的会话缓存

bitxk check 报「登录页结构已变更」

学校改版了 SSO 页面,只影响"直接模拟登录"这一种方式。 改用浏览器登录即可bitxk browser-login)——那条路不依赖登录页结构。

登录返回 401 / 「账号或密码错误」

如果你用的是浏览器登录,不会遇到这一类问题——换那条路即可。

  1. 先在浏览器里手动登录一次,确认密码没记错;
  2. 若浏览器能登而脚本不能,试 bitxk grab --encrypt-mode cbc
  3. 连续失败会触发验证码,工具检测到后会主动停止而不是继续硬试(避免锁账号)。

提示「当前不在可选课时间内」

正常行为,直接退出(退出码 3)。选课批次没开放时,接口不会返回任何 canSelect=1 的批次。用 bitxk login 可以看到所有批次及其时间窗。

一直显示「已满」但浏览器里明明有余量

大概率是容量字段命名与预期不同。用 -v 跑一次,日志里会打印原始响应字段;TeachingClass 会记录 capacity_source 说明它是从哪个字段推出来的。请带日志提 issue。

提示「在线人数已达上限」

选课系统限制并发在线人数,高峰期限流。这是正常现象,工具会自动退避重试, 不需要你做任何事;也不会因此退出。想减少撞上的概率,可以把 interval 调大一些。

提示「未查询到学籍信息」

说明登录成功了,但这个账号在本科选课系统里没有学籍 —— 最常见的原因是用研究生账号登录本科系统。请确认用的是本科学号。

一直卡在「已受理待确认」

正常现象,说明选课请求已被系统受理、后台正在排队处理。工具会自动轮询 studentstatus.do 确认结果。若最终显示「结果未知」,不代表失败 —— 后台可能仍在处理,下一轮查询课程列表时看到「已选」就说明成功了。

提交后返回「时间冲突」但我不觉得冲突

以服务端的判定为准(它掌握你的完整课表,包括其他批次的课)。 也可以运行 bitxk list 看该教学班的 status 是否已被标为「冲突」。

抢到了但我不想要

本工具不做退课,请去选课系统页面手动退。另外建议先 --dry-run 试跑确认匹配逻辑符合预期。

想只抢第一门就停

[notify]
stop_on_success = true

项目结构

bitxk/
├── __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  # 只跑轮询引擎测试

设计上的两个原则:

  1. 业务失败不是异常。 「容量已满」是轮询中的正常状态,用 SelectionResult.outcome 表达;只有「登录失效」「被限流」「网络故障」这类需要上层改变行为的才抛异常。
  2. 容量字段不写死。 选课系统不同版本的字段名不一致(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

参考资料

本项目在实现前调研了以下开源项目与资料,特此致谢:

许可证

MIT

About

北京理工大学本科生选课助手:课程余量轮询 + 自动选课,支持图形界面与浏览器登录 | BIT course selection helper with GUI and browser-based login

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages