ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python自动选课脚本实战:从抢课崩溃到11秒成功

Python自动选课脚本实战:从抢课崩溃到11秒成功 简介这份资源专门为北京邮电大学本科生设计解决教务系统选课高峰期手动抢课慢、易错过课程的问题。脚本基于JavaScript编写登录教务系统后即可在浏览器Console控制台运行无需进入选课界面支持按课程名自动选课必修、选修和公选课均可通用能显著提升抢课效率。压缩包仅2个文件其中README说明文档与核心脚本bupt-take-specific-course.js配套提供整体大小仅2KB轻量且易上手。文档中明确给出了课程列表和抢课间隔的配置方法——抢课模式建议50ms捡漏模式建议300ms并附有stop()停止、刷新页面等安全提醒方便读者根据实际场景灵活调整。已有2579人学习/下载适合不熟悉抢课流程或希望提高选课效率的北邮本科在校生尤其适合选课季需要快速反应的同学只需修改courses数组添加目标课程、调整interval参数即可快速开始使用对于有JavaScript基础的学生还能借此理解自动选课的基本实现思路。1. 选课季的绝望与自救我为什么要写这个自动选课脚本每到学期末高校选课系统总会上演一场没有硝烟的战争。教务系统开放选课的那一瞬间成千上万学生同时点击刷新页面转圈五分钟等反应过来的时候热门课程的名额早已归零。我在北邮读本科这几年深受其苦。公共选修课、体育课、热门专业课每学期的抢课难度不亚于春运抢票。经历过几次“眼睁睁看着课程从可选变成不可选”的崩溃后我决定不再手动拼手速而是写一个能自动执行选课流程的脚本。这就是BUPTtakeCourse项目的由来。这个脚本的核心逻辑其实并不复杂通过程序模拟浏览器行为在指定时间点自动登录教务系统根据预设的课程名称去查找课程、判断余量、提交选课请求。它解决的是三个痛点一是摆脱人工反复刷新页面的疲劳二是把点击延迟从人的几百毫秒压缩到程序的几十毫秒三是可以同时监控多个备选课程哪个有名额就抢哪个。适合的同学包括对编程有一定基础、想用自动化方式解决重复操作问题的本科生尤其是那些不想把选课命运完全交给运气的人。需要先说清楚这个脚本并不是什么黑客工具。它不发爆破请求不绕过权限校验更不攻击教务系统。它做的事和你在浏览器里手动选课完全相同只是把操作速度提升到了人类无法达到的水平。项目的全部技术栈就是Python加requests库再加上一点正则表达式和循环重试机制整个项目的代码量不大但思路很值得拆解。下面我从头梳理一遍希望能给同样想写选课工具的同学一些启发。2. 选课系统的技术形态与脚本的突破口2.1 教务系统的请求链路从登录到选课的四步交互在我开始写脚本之前第一件事是搞懂教务系统到底是怎么工作的。绝大多数高校的教务系统使用的是B/S架构浏览器作为客户端通过HTTP请求与服务器交互。以北邮的教务系统为例完整的选课流程大致分为四步第一步访问登录页面提交学号和密码第二步服务器返回一个包含会话标识的Cookie第三步携带这个Cookie请求课程列表接口获取当前可选的课程数据第四步提交选课请求把具体的课程ID和选课人信息发送给服务器。这四步里面最关键的就是会话保持。登录后的Cookie相当于你的身份凭证后续所有操作都必须带着它。很多初学者写脚本时容易忽略这一点导致请求课程列表时被重定向到登录页。我的做法是用requests.Session()来维持会话因为Session对象会自动保存服务器返回的Cookie并在后续请求中自动携带省去了手动维护Cookie的麻烦。import requests session requests.Session() login_url https://jwb.bupt.edu.cn/login data { username: 你的学号, password: 你的密码 } resp session.post(login_url, datadata)登录接口的返回内容需要仔细看有些系统会返回JSON格式的token有些则是直接种Cookie并返回302重定向。如果你遇到重定向需要关闭自动重定向或者手动处理否则可能丢失关键信息。这一步我建议通过浏览器的开发者工具观察真实请求把请求头、表单字段、Cookie逻辑彻底搞清楚。2.2 课程列表与余量判断JSON解析还是HTML解析登录之后的课程数据通常有两种形式一种是服务端直接渲染成HTML表格另一种是前端通过Ajax请求获取JSON数据后动态渲染。北邮的系统两种都有过不同校区、不同学期的接口可能不一样。我的脚本里做了一个兼容设计先尝试请求JSON接口如果拿不到再回退到HTML解析。JSON接口的数据结构一般是这样的{ code: 0, data: [ { course_id: B0500230, course_name: 大学生心理健康, teacher: 张老师, selected_count: 68, capacity: 100, status: 可选 } ] }拿到这个结构之后判断课程是否可选就非常简单了比较selected_count和capacity如果当前已选人数小于容量并且status字段不是“不可选”就可以尝试提交选课。如果系统返回的是HTML那就需要用正则表达式或者BeautifulSoup从表格里提取课程名称、课序号和余量信息。这里有个重要的经验不要直接在脚本里硬编码课程ID因为每学期的课程ID可能会变更好的方式是先用课程名做模糊匹配再动态获取对应的课程ID。2.3 选课请求的提交与结果确认选课的核心动作就是向服务器的选课接口发送一个POST请求参数通常包括课程ID、学生学号、选课学期等。提交后服务器会返回一个结果可能是JSON也可能是一段HTML里面写着“选课成功”或者“选课失败”。脚本需要解析这个结果判断是否真正抢到。这里有个容易忽略的点返回“选课成功”并不代表最终就一定稳了。有些学校在选课后还有“抽签”环节或者限制退课次数。所以我在脚本里保留了结果确认接口的调用在提交选课请求后再查询一次已选课程列表确认目标课程确实出现在里面才算真正成功。这个二次确认能避免很多误判。3. 脚本实现中的核心模块从粗糙到能用的迭代过程3.1 登录模块验证码、加密密码与会话保持的权衡登录模块是所有环节里最容易出问题的。我最初的一个版本只有简单的用户名密码提交但测试时发现学校教务系统在某些时间段会弹出验证码或者要求密码先经过一次JavaScript加密再提交。这两种情况都会让脚本直接失败。对于密码加密我通过浏览器开发者工具找到了一段JS代码发现所谓加密其实就是把密码加上一个固定salt后做SHA256散列。Python自带的hashlib库可以轻松复现import hashlib def encrypt_password(raw_password): salted raw_password fixed_salt # 具体salt从JS里提取 return hashlib.sha256(salted.encode()).hexdigest()对于验证码我建议不要硬破解。现在很多教务系统的验证码虽然简单但识别它们涉及图像处理而且频繁尝试失败可能触发账号锁定。我的方案是脚本检测到需要验证码时立即暂停并发送提醒由用户手动在浏览器里完成登录然后脚本接管已有的会话Cookie。这样做虽然牺牲了全自动但胜在稳定可靠不会因为验证码导致账号被拉黑。登录模块写完之后我自己加了个保存登录状态的逻辑把登录后的Cookie序列化到本地文件下次运行脚本时如果Cookie没过期就直接复用省去重复登录的时间。这个优化在抢课开始前的几秒钟特别有用。3.2 课程匹配模块课程名模糊匹配与优先级排序选课的真正需求往往是“我想上某门课但不知道它在系统里的准确名称”。比如用户输入“心理健康”系统里的全称可能是“大学生心理健康教育”。因此我设计了一个模糊匹配函数基于编辑距离和包含关系来做判断。def match_course(course_list, target_name): matched [] for course in course_list: name course.get(course_name, ) if target_name in name or name in target_name: matched.append(course) else: # 计算简单的编辑距离小于等于2视为匹配 distance levenshtein_distance(target_name, name) if distance 2: matched.append(course) # 按已选人数从少到多排序优先抢余量大的 matched.sort(keylambda x: x[selected_count]) return matched优先级排序这块值得多说一句。我一开始是按照课程ID顺序去抢后来发现有些课程虽然匹配上了但余量只剩一两个请求发送的瞬间正好被其他人抢走白白浪费一次机会。我把所有匹配到的课程都拿过来按“已选人数/容量”的比例从低到高排序优先选余量最充裕的。这样即使第一门课抢不到也能快速切到第二门提高整体成功率。3.3 抢课主循环定时触发、多线程与“心跳”机制抢课脚本的核心是一个循环每秒钟执行一次“查询课程-判断余量-提交选课-确认结果”的流程。这个循环需要一个精准的定时启动器。我的做法是把目标时间解析成时间戳然后程序在执行前计算差值用sleep毫秒级唤醒。import time def wait_until(target_time): target_ts time.mktime(time.strptime(target_time, %Y-%m-%d %H:%M:%S)) while True: now_ts time.time() if now_ts target_ts: return time.sleep(0.01)多线程方面我用了Python的threading库开了多个线程每个线程负责一个课程组。但这里我踩过一个坑如果线程数太多请求频率会激增反而容易被服务器限流或者触发验证码。经过测试3到5个线程是一个比较稳妥的范围。每个线程在提交选课后设置一个短随机休眠时间在0.5秒到1.5秒之间模拟人类操作节奏降低被识别的风险。“心跳”机制是指在整个循环过程中定期发送一个轻量级的会话保活请求避免Cookie超时失效。教务系统的会话超时时间通常在10到30分钟而抢课高峰期可能持续很久如果没有保活机制可能在最关键的时刻突然被踢下线。我设置了每5分钟刷新一次个人页面如果发现跳转回登录页就重新登录。4. 踩坑实录验证码、并发限制与“静默失败”问题4.1 验证码是个分水岭不是技术问题是策略问题前面提到过验证码这里展开讲讲我最初的错误做法。我曾尝试用OCR库识别简单数字验证码用Pillow做灰度化、二值化再用pytesseract识别。实验阶段成功率有70%左右但到了抢课高峰期验证码图片的样式会切换成带干扰线的彩色图识别率直接降到30%以下。更麻烦的是识别失败后系统会刷新验证码而每次错误尝试都会在服务端留下记录。连续错几次账号被临时锁定半小时。我差点把一个学期的重要选课机会搭进去。后来的策略就是彻底放弃自动识别验证码改成人机协作。抢课开始前我先通过浏览器手动登录并拿到Cookie保存到本地。抢课开始时脚本用这个Cookie跑流程不涉及验证码。如果中途遇到验证码请求脚本会在日志目录生成提醒文件同时播放一段提示音我手动在浏览器里处理完验证码后脚本自动读取新Cookie继续执行。这个方案虽然引入了人工参与但把失败率降到了几乎为零而且不影响抢课速度。4.2 请求频率的隐形雷区为什么不能无脑“快”很多人写抢课脚本的第一反应是“越快越好”把循环间隔压到0.01秒开20个线程疯狂请求。这种思路其实很危险。服务器端通常有反向代理和WAF对异常高频的请求会直接返回429状态码或者下发一个前端挑战页面导致后续请求全部失效。更严重的如果某个IP在短时间内产生大量请求可能会被拉入黑名单影响整个宿舍楼的网络出口。我在测试中摸索出的安全阈值是单个线程的请求间隔不低于1秒整个脚本的全局请求速率控制在每秒10次以内。这样虽然看起来“慢”但能保证请求全程有效。选课抢的是成功率不是请求数量。即便每秒只能发10个请求也远超手动操作的速度足以在热门课程开放的前几秒内完成一次完整流程。为了验证这个结论我做过对比实验一组用每秒50次请求的高频模式另一组用每秒5次请求的低频模式在相同网络环境下抢同一门课程。结果高频组在第3秒就触发了验证码后续基本停摆低频组稳定运行在第7秒成功抢到课程。快不一定赢稳才是核心。4.3 “静默失败”问题返回200并不代表选课成功最让我头疼的一个Bug是“静默失败”。脚本提交选课请求后服务器返回了HTTP 200日志里看起来一切正常但实际查询已选课程列表时目标课程根本不在里面。后来排查发现教务系统在选课高峰期会对请求做异步处理返回200可能只是代表“接收成功”真正的结果得等异步队列处理完才能看到。如果只判断HTTP状态码就会漏掉大量失败情况。我的解决方案是三步确认法第一步判断接口返回的JSON字段看code是否为0第二步延时2秒后查询该课程的最新余量看是否比提交前减少第三步调用已选课程接口确认课程出现在列表中。只有三步全部满足才最终认定为选课成功。如果三步中任意一步不满足脚本会重新进入循环尝试下一条备选课程。这套确认逻辑把误报率降到了极低也让日志变得真正可信。5. 合规使用与长期维护别让工具变成风险源5.1 脚本的边界自动化不等于越权这是我认为最重要的一部分。写抢课脚本的过程中我一直提醒自己一个原则自动化操作的范围不能超过一个学生手动操作所允许的范围。什么叫超范围比如通过接口直接修改课程容量、绕过选课时间限制、读取他人信息这些绝对不行。我的脚本只做两件事查询公开的课程数据提交自己的选课请求。这和你在浏览器里点鼠标没有任何区别只是更快了。此外不同学校的教务系统都有相应的用户协议虽然大部分没有明确禁止自动化脚本但作为学生应该有基本的自律。我建议只在正式选课时间窗口内使用脚本不要拿它去做“预探测”“压力测试”之类的事情。选课系统是公共资源过度请求会影响到其他同学这就违背了工具的初衷。5.2 拿我的项目做二次开发时你需要注意什么如果你也想写类似的项目我的建议是不要直接复制别人的登录代码。每所学校的教务系统结构不同甚至同一所学校不同年级用的系统都不一样。拿着北邮的脚本去抢另一个学校的课大概率会因为接口路径、参数名、加密方式的差异而失败。更有用的是参考这套设计思路登录会话保持、课程模糊匹配、余量判断、定时循环、多线程限速、结果确认这六个模块是通用的具体细节必须自己围绕目标系统定制。另外一个容易被忽略的点是学号密码的安全性。脚本读取配置时建议使用环境变量或独立的配置文件不要在代码里硬编码密码更不要把含密码的文件随便传到公共代码仓库。我见过不少同学把登录凭据直接写在开源脚本里提交到GitHub这是非常危险的习惯等于把账号权限公开了。我的做法是使用config.ini文件并在.gitignore里忽略它。5.3 实测效果与后续的优化方向在2024年春季学期我用这个脚本帮自己和三个室友成功抢到了两门热门选修课和一门体育课。整个抢课过程从选课开放到全部确认成功耗时约11秒。同时间手动操作的同学大部分还停留在刷新页面阶段。这个成绩验证了核心思路的可行性。后续我打算扩展几个方向一是增加对移动端教务小程序的支持因为现在很多选课操作会迁移到微信小程序抓包方式和web端差别很大二是加入“捡漏模式”在正式选课结束后持续监控目标课程一旦有人退课就自动补位三是把日志和提醒接入邮件或微信推送这样不用一直盯着控制台。这些功能本质上还是围绕“自动化选课”这一件事做深而不是搞花架子。最后分享一个小经验任何自动化的东西都可能在关键时刻失灵。脚本写好后一定要在非选课时段做完整的模拟测试把登录、查询、提交、确认整条链路跑通一遍然后清掉测试产生的选课记录。我见过有人在正式抢课时才发现登录接口改了结果整个脚本瘫痪连手动操作的时间都被耽误了。工具是辅助不要让它成为唯一的指望。本文还有配套的精品资源点击获取
返回列表