简介:面向北京邮电大学本科生的自动抢课工具,基于JavaScript实现,针对教务系统登录后的选课流程设计,无需进入选课界面即可按课程名自动匹配并抢课,覆盖必修、选修与公选课。压缩包共2个文件,包含1个JavaScript脚本和1个Markdown说明文档,整体仅2KB,轻量简洁。脚本支持通过courses变量自定义目标课程,通过interval调整轮询间隔,抢课模式可设50ms,捡漏模式建议300ms,兼顾不同场景下的成功率与服务器压力。已有2579人学习下载,适合熟悉基本浏览器调试操作、希望提升选课效率的在校学生。除核心脚本外,说明文档还梳理了启动时机、异常停止、与手动抢课配合等实用经验,同时提醒勿设置过低间隔、勿恶意抢课,引导合理使用。 每年选课季,北邮的本科生群都会哀嚎一片。热门通识课、体育课,开选即满,刷新、提交、失败、再刷新,这套循环能磨掉人半条命。我写了个叫 BUPTtakeCourse 的小工具,输入课程名,它自动完成登录、检索、提交选课请求,到点自动开抢。这篇博文把我做这个抢课脚本的完整思路、技术拆解和踩坑记录整理出来,希望能帮到同样有选课需求的学弟学妹,也适合想了解校园自动化脚本怎么写的人参考。
1. 项目背景与核心需求拆解
1.1 选课季到底难在哪
北邮的选课系统在高峰期,页面卡顿、接口超时是常态。名额有限的课程,比如热门公选课、体育专项课,往往开放选课的第一分钟就被秒空。手动操作的流程看起来简单:登录、搜索课程、点选课、确认提交。但真正经历过的人都知道,问题出在高频和快速响应的矛盾上——你还在等页面刷新,别人已经提交完了。
抢课的本质,其实是把“查询余量”和“提交选课”这两个动作反复执行,直到成功。这个场景天然适合脚本化:流程固定、频次高、对响应速度要求苛刻。人工操作有两大痛点,一是手速跟不上节奏,二是老得盯屏幕,一盯就是半小时起步。脚本的价值不是作弊,而是把这些重复劳动交给程序,让它在后台持续监控,一旦发现余量就第一时间提交。
1.2 “按课程名自动选课”的真实需求
这个项目最关键的设计点,名字里已经写清楚了:根据课程名自动选课。很多人会问,为什么不直接用课程代码?因为对普通学生来说,课程代码记不住,但“电路分析基础”“羽毛球初级”这类课程名是一眼就能认出来的。
用课程名做主输入,需要额外解决两个问题。第一是课程名可能重复,不同老师开课、不同时段开课,课程名一样;第二是系统里的课程名和选课界面显示的名称可能不完全一致,比如多了“(A班)”这种后缀。所以脚本的核心逻辑,不是简单地拿字符串去比较,而是要有一套匹配策略,把课程名、上课时间、任课教师等信息组合起来,先识别、再确认,最后才发起选课。
1.3 脚本的能力边界
需要先把定位说清楚,BUPTtakeCourse 不是漏洞利用工具,也不是绕过认证的违规程序。它的前提是:学校选课系统本身允许你选这门课,你有选这门课的身份资格,只是名额紧张、需要拼手速。脚本做的事情,和人工抢课时一直在点“提交”按钮没有本质区别,只是把它变成了自动化的高频检测,一旦余量出现就立刻提交。
这个边界很重要。我在设计脚本时就定了几个原则:不碰验证码破解、不尝试绕过登录认证、不滥用高频请求,轮询频率控制在合理范围内,避免对教务系统造成额外压力。脚本只是把你的个人操作变快,而不是破坏规则。
2. 整体方案设计与技术选型
2.1 自动化方案的选型逻辑
做这类校园自动化脚本,有两套主流方案:模拟 HTTP 请求和浏览器自动化。模拟请求就是用 requests 库直接构造登录、查询、选课的请求,优点是轻量、快、资源占用小,能在极小延迟内完成整套操作;缺点是必须先分析出系统的接口规律,还要处理登录态、参数加密、可能的验证码。
浏览器自动化,比如 Selenium 或 Playwright,则是启动一个真实浏览器,模拟人的点击行为。它的优点是几乎不用逆向接口,页面怎么操作,代码就怎么写;缺点是速度慢,每次启动浏览器都要几秒钟,内存开销也大,在抢课这种拼毫秒的场景下,反而吃亏。
BUPTtakeCourse 选用的是以请求模拟为主、浏览器自动化兜底的混合策略。主流程全部走 HTTP 请求,这样速度快;遇到个别必须要验证码或复杂前端逻辑的场景,再临时唤起浏览器,人工介入处理一次,拿到登录态后继续走请求流程。
2.2 技术栈与依赖选择
整个项目基于 Python 3,依赖库非常克制,核心就几个:
- requests:负责所有 HTTP 请求,网络交互的基础
- PyYAML:读取配置文件,把课程名、轮询间隔、账号信息等参数外置
- BeautifulSoup4 + lxml:解析选课页面返回的 HTML 表格
- logging:本地日志记录,方便排查问题
- smtplib / requests:结果通知,邮件或微信推送渠道二选一
选这些库的考虑是稳定、成熟、没有太深的坑。requests 是 Python 社区的基础库,各种加密参数虽然要手动处理,但网上案例多;BeautifulSoup 做 HTML 表格解析足够用,不需要引重型爬虫框架。后续如果接口返回的不是 HTML 而是 JSON,解析模块可以直接替换成 json.loads,改动成本很小。
2.3 目录结构与模块划分
项目拆成几个职责单一的模块,后期维护起来非常舒服:
BUPTtakeCourse/ ├── main.py # 入口,调度逻辑 ├── config.yaml # 配置文件,账号与选课目标 ├── login.py # 统一身份认证登录 ├── course.py # 课程查询与目标匹配 ├── selector.py # 选课请求提交 ├── monitor.py # 轮询监控与状态机 ├── notifier.py # 结果通知 └── logger.py # 日志初始化模块划分的原则是:登录只管登录,查询只管查询,选课只管选课。尤其是登录态,单独放一个模块,通过全局 session 对象共享,其他模块不关心 cookie 怎么来的,只负责在请求时带上它。这样有个好处,后面如果要给脚本加多账号支持,只需要改动 login.py 的调用方式,其他模块不用动。
3. 核心模块实现与细节解析
3.1 统一身份认证登录模块
登录是整套脚本的地基。北邮的账号体系走的是统一身份认证,登录状态拿到手之后,后续的课程查询、选课提交都依赖这个 session 里的 cookie。
我实现登录模块时,遇到了最常见的三个坑。第一个是登录请求中往往带有动态 token 或隐藏字段,需要先从登录页抓取再拼进 POST 表单;第二个是密码传输经常不是明文,有的系统用 RSA 公钥加密,公钥要从某个接口动态获取;第三个是部分环境会弹验证码,这种情况自动识别成本太高,我选择了折衷方案——检测到验证码时,脚本暂停并向终端输出提示,人工输入一次验证码后继续。
登录成功后,把 session 的 cookie 序列化保存到本地文件。这样脚本重启之后,如果 cookie 还在有效期内,可以直接复用,不需要每次都走登录流程。选课系统卡顿的时候,重复登录本身也是一种负担。
3.2 课程查询与目标匹配
课程查询接口通常会返回当前学期所有课程列表,或者按关键字匹配的课程子集。页面一般是一张表格,里面包含课程名、课程号、任课教师、上课时间、剩余名额这些信息。course.py 负责把这张表解析成结构化数据,然后与 config.yaml 里配置的目标课程做匹配。
匹配逻辑我做了三层校验,避免误选同名课程。第一层是课程名包含匹配,比如配置的是“羽毛球”,那“羽毛球初级班”“羽毛球提高班”都能命中;第二层是校区或时段校验,如果你只想选周六上午的课,脚本会过滤掉其他时段的选项;第三层是人工确认机制,如果匹配到的课程多于一条,脚本会先把结果打印出来让用户选一条,避免脚本自作主张。
这一层最关键的是要拿到稳定字段。有些系统的课程名单里,课程名和选课提交时的参数不是一回事,课程名只是给人看的,真正提交时用的是课程号或开课编号。所以 course.py 在匹配到课程后,必须把该课程对应的提交参数也一起提取出来,存成一条“选课目标”。
3.3 自动选课与重试策略
selector.py 负责真正提交选课请求。提交接口接收课程标识、学期等参数,一般返回一个状态码,显示选课成功、课容量已满、时间冲突或者未到选课时间。这些状态码必须逐一映射成可读的中文提示,方便判断失败原因。
重试策略是个值得细讲的地方。这里不能直接用 while True 死循环高频率提交,而是要做状态机:
- 状态一:等待开始,选课时间未到,低频轮询每个 30 秒查一次
- 状态二:监控余量,时间已到,每 3 到 5 秒查一次余量
- 状态三:提交选课,余量大于 0,立即提交
- 状态四:成功结束,收到成功返回后停止该课程的监控
这样设计的好处是,不需要全程高频轰炸,对系统友好,也不容易被风控。每次请求之间我加了随机延迟,不是固定间隔,而是 2 到 5 秒内的随机值,这样请求节奏更接近人的行为,也避免了所有请求在同一时刻发出造成的瞬时拥塞。
3.4 定时触发与结果通知
选课不是全天开放的,系统会在某个时刻统一放名额。monitor.py 里我通过比较本地时间与配置的选课开始时间来切换状态,时间没到就处于等待状态,时间一到自动进入监控状态。这里要注意,服务器时间可能和本地时间不一致,我建议第一次运行时先从教务系统响应头里拿一次服务器时间,以它为准。
通知模块用的方式比较轻,成功后通过邮件或 Server酱 推一条消息到微信。我实际用下来,推送比看日志靠谱得多,选课成功的那一刻你未必在电脑前,但手机通知一定不会错过。失败次数太多时也会推送一条提示,方便及时调整策略。
4. 完整实操流程与运行调优
4.1 环境准备与项目启动
先用 Python 3.8 以上版本跑通依赖,创建虚拟环境后安装这几个包:
pip install requests pyyaml beautifulsoup4 lxml在 config.yaml 里填好学号和密码,配置目标课程信息:
account: username: "你的学号" password: "你的密码" targets: - name: "羽毛球初级" campus: "本部" time: "周六上午" schedule: start_time: "2025-02-20 10:00:00" interval_wait: 30 interval_query: 3 notify: email_enabled: false serverchan_key: ""第一次运行建议只测试登录流程,不开启选课监控。看到日志输出登录成功、课程列表解析正常之后,再带入真实选课时间去跑完整流程。
4.2 轮询间隔与并发参数调优
轮询间隔直接决定脚本的抢课成功率,也决定了脚本给系统带来的压力。我实测下来,interval_query 设为 3 秒比较合适,能保证在名额释放后的几秒内发起提交,又不至于因为请求太密集导致自己的 IP 被临时限制。设置成 1 秒以内反而容易触发风控。
提交选课时使用了线程池限制并发数,默认只支持一个目标课程同时提交。因为多门课同时提交会引发冲突校验,比如同一时间段的课同时选,系统会判定冲突。如果确实想同时抢多门不同时间的课,每个课程跑一个独立监控实例,并且每个请求之间保持 1 秒以上的间隔,优先级从上到下排列。
4.3 日志观测与成功判定
运行过程中最重要的是看日志。我在每个关键环节都加了日志:登录成功、课程匹配到几条、进入监控状态、检测到余量、提交结果、成功通知。日志格式统一为时间、级别、模块、内容四段,方便 grep:
2025-02-20 10:00:03,234 INFO couse.py 目标课程'羽毛球初级'匹配到2条记录 2025-02-20 10:00:06,531 INFO selector.py 第1次提交,返回码200,选课成功成功判定必须看系统返回的状态码,而不是只看 HTTP 200。有些系统 HTTP 返回 200 但内容里写着“选课失败:时间冲突”。我把常见结果码整理成映射表,遇到未定义的状态码直接输出原始报文,方便后续补充。
5. 常见问题与排查技巧实录
5.1 登录状态失效导致后续全部报错
最常见的问题是脚本跑着跑着突然连续报“未登录”或“无权限”。原因是统一身份认证的 session 过期,或者因为多次请求被系统踢下线。排查时先看日志里是不是登录模块才有的错误,确认后就重新登录。
我的处理办法是在 monitor.py 里做一个响应状态拦截器,所有接口返回“未登录”状态码时,自动触发一次重新登录,然后继续任务。这样 session 过期对用户来说几乎无感知,只要账号没掉线,就不会中断监控。
5.2 请求被临时限制
频率设置过高时,接口会返回请求频率超限或直接返回验证码页面。这个问题的本质是请求模式太像机器了。我调整过几个参数后效果很明显:轮询间隔从固定的 2 秒改成 2 到 5 秒之间的随机值;每次提交选课请求之前额外 sleep 0.3 到 0.8 秒;去掉了多线程并发查询同一个接口的逻辑。
真实心塞的是,有段时间我把并发调得过高,IP 被限制了好几个小时,正好错过了当天名额。后来学乖了,抢课脚本不能贪快,稳定比快更重要。
5.3 课程名匹配到多条记录导致选错课
课程名匹配得太宽泛,可能会出现“命中了 5 条记录、脚本默认选了第一条结果”的情况。我踩过这个坑,差点选错课。解决方式前面提到过,匹配到多条记录时脚本不自动选择,先停住输出备选列表,同时询问人工在终端输入序号确认。虽然多了一步交互,但针对“课程名容易重复”的场景,这个安全冗余是值得的。
后来我还给 course.py 增加了缓存功能,把匹配结果按课程名存成 JSON,下次运行同一门课直接复用人工确认过的结果,不用重复操作。
5.4 明明有余量却提交选课失败
这种情况有两种原因。第一是选课条件不满足,比如有先修课程没通过、专业限制不对,或者该课程只对特定年级开放。第二种原因是提交参数缺少必要字段,比如必须勾选的“同意调课”复选框,对应的请求参数没有带上。
排查思路很直接:先用浏览器手工选一次这门课,注意观察网络面板里发出的请求,把里面选课接口的参数和脚本发出去的参数做逐一对比。我按照这个思路解决过两次问题,一次是漏传了学期字段,一次是漏传了选课类型字段,加上之后立刻恢复正常。
6. 合规使用与个人实操心得
6.1 脚本的合规边界和使用原则
关于 BUPTtakeCourse 这类脚本,我想认真地提醒几点。它只能用于自己的账号,只能用于学校允许你选的课程,只能用于正常补选需求。不能拿它去大量挤占名额、替别人刷课、或者以任何方式影响选课系统的公平性。
我明确拒绝了几个功能需求:批量替多个账号抢课、在未开放时段强行请求、绕过课程限制条件。这些功能技术上都能做,但越过安全的边界就意味着给自己找麻烦。选课名额是公共资源,脚本是公平竞争的手速增强器,而不是破坏规则的工具。务必确认自己是否了解并接受学校对自动选课的相关管理规定。
6.2 我在实际调试中踩过的坑
第一次写登录模块时,一直卡在密码加密上,后来通过断点调试,在浏览器开发者工具里查看请求提交的参数名,才发现加密字段名和普通密码字段不同,处理起来就顺畅了。这个经验对任何校内脚本都适用:先用浏览器手动操作一次,在开发者工具的 Network 面板里观察请求和响应,大多数接口规律都能摸清楚。
另一个坑是本地时间与服务器时间不一致,导致监控提前进入高频状态。当时看着日志里到了设定时间但没有余量,空跑了一个小时。后来增加了一次 NTP 校准——从系统响应头里读取服务器时间字段,虽然差异通常只有几秒,但对抢课来说已经足够了。
6.3 后续可以怎么扩展
如果只想解决一门课的问题,现在的代码已经够了。但如果你想把这个项目当成一个长期维护的工具,我会建议做这几件事。第一是增加 Web 配置界面,不用每次改 YAML 文件;第二是把轮询状态机抽象成更通用的选课引擎,适配其他学校的教务系统;第三是增加重试退避策略的单元测试,保证频繁重构时核心逻辑不会被改坏。
如果你只是想安稳选上一门课,不需要把这些扩展全部做出来,把 config.yaml 配好、跑一次看日志就够了。希望我踩过的这些坑,能帮你少熬几个选课季的夜。
本文还有配套的精品资源,点击获取