简介:大学教务管理系统抢课脚本,利用Python模拟对抢课页面进行持续点击,主要面向在校学生、Python初学者以及需要网页自动化的开发者,帮助替代手动刷新、反复点击,解决选课高峰期抢不到课程的问题。压缩包共5个文件,含2个Python代码文件、2个Markdown说明文档和1个License许可证,整包仅8KB,体量轻巧、结构精简,适合快速阅读源码逻辑。两个py脚本分别是两所高校教务系统的适配版本,可直接运行,也可对比学习不同页面下的模拟点击与元素定位方式;两个md文件分别负责环境准备、运行步骤与脚本原理说明,License则标明使用许可范围。通过研读这份脚本,读者能掌握用Python模拟页面操作、定位点击目标、循环检查选课状态等自动化思路,并根据自己学校的教务系统页面定制修改。目前已有1050人学习,适合有一定Python语法基础、希望优化抢课效率或入门网页自动化的学生开发者参考调试。
1. 抢课脚本在解决什么问题:手速永远追不上定时任务的自动化点击
每学期选课刚开始的几分钟,热门课的名额基本就清零了。做过这种事的同学都有同一个经验:双手一直按着刷新,结果页面还没渲染完,课程状态已经从“可选”变成“已满”。这个“大学教务管理系统抢课脚本”要解决的正是这个场景:用 Python 模拟登录教务系统、保持登录态、轮询抢课页面,在课程可提交的瞬间自动发送选课请求,把人工“刷新—看状态—点击选课”压缩成一个毫秒级的判断循环。它本质上是一个面向特定页面的自动点击脚本,并不是什么灰产工具,换一个非抢课页面,它也能当通用的定时轮询器来用。适合做过一点 Python 基础、想学 requests 和 Session 用法的同学拿去练手,也适合确实被选课折磨过的人改改参数直接跑。
2. 抢课请求链路拆解:登录态、查询接口、选课提交三个环节
2.1 整体认识:这个脚本不是按键精灵,而是会“说话”的 HTTP 客户端
很多人一听“模拟点击”,第一反应是找一个屏幕坐标脚本,把鼠标移动到某个按钮上再点两下。这个项目完全不是这个思路。它本质上是用 requests 库伪造浏览器发出的 HTTP 请求,直接跟教务系统服务器通信。这样做最大的好处是响应速度远快于真实浏览器渲染,省去加载 CSS、JavaScript 的时间,在开抢后的前几秒里,每省下几十毫秒都有意义。
整个脚本在逻辑上可以分为三层,从下到上分别是:登录层负责拿 Session 并维持 Cookie;查询层负责拉取当前可选课程列表,判断目标课程的状态;提交层负责发送选课请求。三层之间靠同一个 Session 实例串联,因为教务系统的登录态靠 Cookie 维持,只要 Session 还拿着正确的 Cookie,后续的所有查询和提交都不需要重复登录。
配套的项目目录结构通常长这样:
grabber/ ├── config.py # 账号、课程ID、请求频率配置 ├── login.py # 登录封装,返回带Cookie的Session ├── api.py # 查询接口与提交接口的封装 ├── grab.py # 抢课主循环 ├── notify.py # 抢课结果日志与通知 └── requirements.txt # 依赖列表这个结构足够常规,但也足够说明问题:登录、查询、提交、通知各管一摊,后续要改成多课程或者捡漏模式,不需要动主循环,只改 API 层和调度层就行。实际下载到的项目文件名未必完全一致,但职责分布大体是类似的,拿到手先按这个思路读代码,不容易迷路。
2.2 登录环节:从教务系统手里拿到那张“入场券”
先打开教务系统登录页,按 F12 切到 Network 面板,勾选 Preserve log,然后手动完成一次登录。你会看到登录这个过程在浏览器背后至少发出两个请求:第一个 GET 登录页,返回一个带隐藏字段的 HTML 表单;第二个 POST 登录接口,提交用户名、密码和隐藏字段。服务器校验通过后,响应头里会带着 Set-Cookie,浏览器保存下这个 Cookie,之后的所有请求都会自动带上。
脚本要模拟的就是这个过程。requests 提供了 Session 对象,它会自动保存服务器发来的 Cookie,并在后续请求里自动携带,省去手工管理 Cookie 的麻烦。不过现在的教务系统普遍加了 CSRF Token,登录页里藏着一个隐藏的 input,名字可能是 token、csrf_token 或者 authenticity_token,不带进去,POST 直接会被拒绝。脚本里需要先用 BeautifulSoup 解析出这个值,再拼进表单数据里提交。
常见做法是登录成功后立刻访问一个“个人信息”或者“学期课表”页面测试登录态,能拿到 200 且页面里出现学号字样,就说明 Session 已经可用了。这一步很多人会跳过,结果抢课请求发出去才发现没有登录态,返回的全是登录页 HTML,白等一整轮。
2.3 查询与提交:所谓“不断点击”其实就是两段 HTTP 请求
登录之后,抢课界面在同一学期下会展示两个关键区域:可选课程列表和待选课程列表。浏览器里看到的是列表,网络层看到的其实是两个接口。
- 查询接口:GET
/course/enrollable?semester=2024-2025-1,返回可选课程的 JSON 数组; - 提交接口:POST
/course/enroll,表单里带 course_id、semester 等字段,返回值表示成功或失败。
“对抢课页面进行不断点击”翻译成代码,就是一个循环里做了三件事:请求查询接口、检查目标课程是否还有名额、如果有就调用提交接口;没有就睡几百毫秒再来一遍。所以脚本性能的瓶颈通常不在提交那一下,而在查询接口的响应速度和轮询间隔的设计,这也是后面第四章调参数时要重点盯的部分。
3. 核心代码实现:用 requests 把“刷新页面”变成一条循环
3.1 登录模块:封装 Session,让后续请求都带着身份
先写登录。项目中 login.py 的核心逻辑是这样一段:
# login.py import requests from bs4 import BeautifulSoup LOGIN_URL = "https://xxx.example.edu.cn/login" # 登录页地址,按实际改动 def extract_token(html: str) -> str: """从登录页HTML中解析出隐藏的token""" soup = BeautifulSoup(html, "html.parser") token_input = soup.find("input", {"name": "token"}) return token_input.get("value", "") def build_session(username: str, password: str) -> requests.Session: session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", }) # 先GET登录页,拿到初始Cookie和隐藏token再POST resp = session.get(LOGIN_URL, timeout=10) token = extract_token(resp.text) form = { "username": username, "password": password, "token": token, } login_resp = session.post(LOGIN_URL, data=form, timeout=10) if login_resp.status_code != 200: raise RuntimeError("登录请求失败") return session逻辑说明:先初始化一个 Session 并伪装 User-Agent,因为有些教务系统会拦截默认的 python-requests 标识;然后 GET 一次登录页,目的不是看页面内容,而是拿到隐藏 token 和初始 Cookie;最后 POST 表单完成登录。函数返回一个带着身份信息的 Session,后续所有查询和提交都传这个对象进来。
参数说明:timeout=10表示单次请求最长等 10 秒。抢课场景下宁可快速失败重试,也不要卡在一个慢请求上不动。extract_token里查找的 input name 以实际抓包为准,有的系统叫_token,也有的藏在 Cookie 里,需要按 2.2 节的方法核对。
3.2 查询与提交:把“看课程状态”封装成两个独立函数
# api.py import requests ENROLLABLE_URL = "https://xxx.example.edu.cn/course/enrollable" ENROLL_URL = "https://xxx.example.edu.cn/course/enroll" def get_courses(session: requests.Session, semester: str): """获取当前学期所有可选课程""" resp = session.get( ENROLLABLE_URL, params={"semester": semester}, timeout=5, ) return resp.json().get("data", []) # 接口返回的是JSON数组 def submit_course(session: requests.Session, course_id: str, semester: str): """提交选课请求,返回True表示提交成功""" resp = session.post( ENROLL_URL, data={"course_id": course_id, "semester": semester}, timeout=5, ) data = resp.json() return data.get("success", False)逻辑说明:这两个函数严格划分了“查状态”和“抢课程”的职责。查询用 GET 带 query 参数,提交用 POST 带表单参数,返回值统一从 JSON 里取 success 字段,方便主循环直接判断。
参数说明:requests 里的params会被拼成 URL 查询字符串,data会以application/x-www-form-urlencoded格式提交。有些系统接口要求 JSON Body,那就改成json={"course_id": course_id},同时留意请求头里是否带有Content-Type: application/json,这是最常见的一类翻车点,后面避坑章节会专门展开。
3.3 抢课主循环:轮询 + 条件触发 + 失败退避
# grab.py import time import logging from login import build_session from api import get_courses, submit_course def grab(session, target_course_id, semester, interval=0.5, max_retry=100): for attempt in range(max_retry): try: courses = get_courses(session, semester) target = [c for c in courses if c["id"] == target_course_id] if target and target[0].get("available", False): ok = submit_course(session, target_course_id, semester) if ok: logging.info("第 %s 次轮询抢课成功", attempt + 1) return True else: logging.warning("有名额但提交失败,需要检查是否有验证码") else: logging.info("第 %s 次轮询:课程暂无名额", attempt + 1) except Exception as exc: logging.error("请求异常:%s", exc) time.sleep(interval) return False if __name__ == "__main__": session = build_session("你的学号", "你的密码") grab(session, "CSE-2301", "2024-2025-1")逻辑说明:这个循环就是“不断点击”的落点。每一次先拉最新可选课程列表,然后筛选目标课程判断是否可选,可选就提交。提交失败不会立刻退出,而是继续轮询等待下一次名额释放,因为很多人抢到又退课,下一轮还有机会捡回来。
参数说明:interval是轮询间隔,默认 0.5 秒;max_retry是最大尝试次数。间隔太短容易被服务器限流,太长容易错过名额,结合多数教务系统的处理速度,0.3 到 1 秒是比较合理的经验区间。logging的输出级别也可以调成 INFO,这样每一轮的状态都会打到控制台,方便观察脚本是不是活着。
3.4 入口脚本:把配置穿起来
实际项目里通常会再提供一个 run.py 或 main.py,把 config 里的参数读进来,组装上面的模块。核心代码不长:
# run.py from config import USERNAME, PASSWORD, SEMESTER, COURSE_ID, INTERVAL from login import build_session from grab import grab session = build_session(USERNAME, PASSWORD) grab(session, COURSE_ID, SEMESTER, interval=INTERVAL)到这里,一套可用的抢课脚本基本成型。你会发现代码量很小,难点不在代码本身,而在对教务系统接口的理解和异常流的处理,这正是这个项目想让读者上手练习的点。
4. 把它跑起来:环境准备、配置文件与结果验证
4.1 环境准备:Python 虚拟环境与依赖安装
建议用虚拟环境跑,避免把依赖装到全局环境里。Python 版本建议 3.8 及以上,主要依赖只有 requests 和 beautifulsoup4,非常轻量。
python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install requests beautifulsoup4逻辑说明:先用 venv 建一个隔离环境,然后安装两个核心库。requests负责发 HTTP 请求,beautifulsoup4负责解析登录页里的隐藏 token。如果你的教务系统登录不涉及 token 解析,也可以用正则替代,但 BeautifulSoup 更稳,对 HTML 结构变化容忍度高一些。
参数说明:如果本机同时有 Python 2 和 Python 3,建议用python3 -m venv .venv显式指定版本。Windows 下激活虚拟环境的命令是.venv\Scripts\activate.bat,macOS/Linux 是source .venv/bin/activate。
4.2 配置文件:把账号和课程代码填进去
项目里一般会有一个 config.py,里面集中存放所有可变参数,这样就不需要每次改代码。
# config.py LOGIN_URL = "https://xxx.example.edu.cn/login" USERNAME = "20240001" PASSWORD = "your-password" SEMESTER = "2024-2025-1" COURSE_ID = "CSE-2301" INTERVAL = 0.5 MAX_RETRY = 200 TIMEOUT = 10参数说明:SEMESTER的格式一定要和教务系统里的一致,有的系统用2024-2025-1,有的用2024-2025-01,填错了查询接口会返回空列表;COURSE_ID不是课程名称,是课程在系统中的唯一 ID,一般从抓包的 URL 参数里能看到,比如/course/enroll?course_id=CSE-2301里的CSE-2301就是。INTERVAL和MAX_RETRY控制轮询节奏,默认值在大多数场景下够用。
4.3 启动与验证:先测登录,再查接口,最后开抢
拿到项目后不要直接开着脚本等开抢,先按下面三步过一遍:
# 第一步:测试登录是否成功 python -c "from login import build_session; s = build_session('20240001', '你的密码'); print(s.cookies.get_dict())" # 第二步:手动调一次查询接口,看看返回的数据结构 python -c "from login import build_session; from api import get_courses; s = build_session('20240001', '你的密码'); print(get_courses(s, '2024-2025-1')[:2])"逻辑说明:第一条命令只验证登录链路,能打印出 Cookie 字典就说明 Session 建立成功;第二条命令打印查询接口返回的前两条课程数据,目的是确认 JSON 字段名和脚本里写的是否一致,比如字段是available还是status,直接决定主循环的判断能不能命中。
参数说明:如果第二条返回空列表,大概率是SEMESTER格式不对;如果抛出JSONDecodeError,说明接口返回的不是 JSON,多半是被拦截或者重定向到了登录页,按第五章的排查思路处理。
4.4 用抓包确认请求细节:最硬核的验证方式
脚本跑不通的时候,我一般会回到 F12 抓包对比一次手工操作和脚本发出的请求。重点看三处:请求方法、请求地址、请求头里的 Content-Type 和 Referer。
特别是 Referer,很多教务系统会校验这个头。手工操作时浏览器会自动带上来源页地址,脚本里如果不设置,服务端可能直接拒绝。遇到这种情况,在构造请求时手动补上即可:
session.headers["Referer"] = "https://xxx.example.edu.cn/course/list"这属于最常见的低投入高回报修复手段,多校教务系统都吃这一套。
5. 避坑指南:让脚本从“能点”到“点得稳”的五个细节
5.1 Session 失效:第一个请求就已经被踢回登录页
现象:程序日志看起来一切正常,但提交选课一直返回 false,把返回内容打印出来才发现拿到的全是登录页 HTML。
原因:Cookie 过期,或者 Session 在服务端被销毁。最常见的是登录后隔了很长时间才跑抢课,中途又打开浏览器登录了一次,把同一个账号的服务端会话挤掉了。
解决:在主循环里加一个轻量探测请求,每轮轮询前访问一次课表接口,判断返回内容是以{开头还是以<开头,后者说明登录态已经失效,先重新登录再继续轮询,避免在失效状态上空转几十轮。
5.2 提交接口要求 Referer,手工构造请求被拒
现象:从浏览器复制接口地址到脚本里,查询正常,但提交时返回 403。
原因:提交接口校验了 Referer 请求头,必须来自课程列表页,跨来源直接拒绝。
解决:构造 Session 时统一设置来源头,把 4.4 节那段配置放到登录成功之后的位置,这样提交请求发出时自动带上合法来源。
5.3 查询接口返回的不是 JSON 而是 HTML
现象:调用resp.json()时抛JSONDecodeError,脚本直接中断。
原因:请求被重定向到了登录页或者错误提示页,返回体是 HTML。也有可能是接口地址本身就是返回 HTML 片段的老式列表页。
解决:先把响应体打印前 500 字符,判断实际内容。如果对象是登录页,优先检查 Session 是否过期;如果接口本来就是 HTML,可以改成用 BeautifulSoup 解析表格里的课程状态,再走提交逻辑,不要死磕 JSON。
5.4 本地时间和服务器时间差出“一整节”
现象:本地时间到点就开跑,结果日志显示“课程未开始”;等本地过了一分钟再跑的时候,名额已经被抢完。
原因:教务系统的抢课入口开启时间以服务器时间为准,学生电脑和服务器之间存在秒级甚至分钟级偏差,本地显示到点不代表服务器也到点。
解决:让脚本自己校准。登录成功后读取响应头里的Date字段,解析出服务器时间,和本地时间做差得到 offset,主循环里用time.time() + offset判断是否到达开抢时刻。这样能避免因为电脑时间不准而错过窗口。
5.5 提交时突然要求验证码
现象:日志里写着“有名额但提交失败”,把提交响应打出来,里面嵌着一个验证码页面。
原因:部分教务系统对高频提交启用了二次验证,触发后表单中多出验证码字段,简单的 POST 不再有效。
解决:降级成半自动方案,脚本轮询到可选名额时播放提示音并停住,由人手工完成验证码输入后继续。验证码识别模型属于另一套技术栈,成本高且不稳定,校园小脚本没必要硬上。
6. 二次开发:从单个课程点击器变成通用选课小助手
拿到一个能跑的抢课脚本之后,下一步通常是把它改造成更顺手的样子。最常见的三个改造方向:多课程并发、捡漏模式、结果通知。
多课程并发这里要提醒一个血泪经验:不要多个线程共享同一个 requests.Session。requests 的 Session 对象不是线程安全的,多线程同时调用同一个 Session 发请求,可能出现 Cookie 错乱,服务端把多个线程当成同一会话,莫名其妙把账号踢下线。稳一点的写法是一门课开一个独立 Session,再各跑一个线程:
# multi_grab.py import threading from login import build_session threads = [] for cid in COURSE_IDS: session = build_session(USERNAME, PASSWORD) # 每个线程独立登录 t = threading.Thread(target=grab, args=(session, cid, SEMESTER)) threads.append(t) t.start()逻辑说明:每个线程独享一个登录态,互不干扰,哪怕其中一门课触发了验证码,也不影响其他线程继续轮询。COURSE_IDS可以在 config 里配成一个列表,主程序逐个创建线程即可。
捡漏模式是另一个实用扩展。思路是轮询“自己已选课程”接口,如果目标课程出现在已选列表里,说明抢到了;如果没出现,继续查询课程可退名额。代码上只需要在grab里多加一个状态分支。通知功能则可以在抢课成功时调用微信推送接口或者 Server酱,把结果推到手机,省得一直盯控制台。
最后说一个我自己的教训:有次改动后台式机跑得很稳,换到笔记本上开跑后频繁掉线,排查半天才发现是笔记本休眠把网络断开了,等唤醒回来名额早没了。从那以后,我每次上线前都会强制走一遍全流程检查,关闭系统休眠、锁定电源计划、先在测试课程上跑两轮确认日志正常,再等开抢时刻。希望帮到你。
本文还有配套的精品资源,点击获取