☰
Python抢课自动化:5行代码实现接口轮询与自动提交
2026/10/20 9:06:25 网站建设 项目流程

抢课这件事,年年开抢,年年有人吐槽。一到教务系统放课的时间点,页面上的“刷新”按钮都快被按出火星子,等你看到“可选”两个字,鼠标还没挪到“提交”,弹窗已经变成“人数已满、无可选名额”。这个Python自动化脚本,解决的正是从“发现名额”到“完成提交”中间那几秒钟的延迟——它用一个5行代码组成的探测循环,在后台按固定频率轮询选课接口,一旦检测到余量,立刻自动提交,整个过程不用你一直守在屏幕前。适合已经学过一点Python基础、想用代码解决实际问题的在校同学,也适合把练手项目落到真实场景的初学者。

1. 抢课场景分析与自动化思路

1.1 选课系统的真实瓶颈:不是手速而是反射弧

很多人以为抢课难是因为网速差、手速慢,实际做过几次抓包分析就会发现,大部分教务系统在放课那一刻的逻辑特别简单:前端页面每隔几秒轮询一次服务器,服务器返回当前课程余量,前端把“可选/已满”渲染成按钮状态。真正卡住你的,是人的反射弧——从服务器返回“可选”,到你眼睛看到“可选”,到你大脑决定点哪个按钮,再到手指移动过去点击,最后等页面刷新出确认结果,这一整圈快则一两秒,慢则四五秒。

在放课的前几分钟,热门课的名额往往在几十秒内就被抢完。服务器这边,每秒能处理的选课请求有限,谁先提交成功,谁就占住这个名额。人肉抢课相当于你每两秒钟才能探测一次状态,而脚本可以把探测频率压缩到每秒钟几次甚至十几次。用抢红包来类比你就明白了:整点秒杀的红包,手动点进去到页面弹出来,红包早就没了;脚本相当于提前把页面接口打开,在那个瞬间自动帮你点。

所以这个项目的核心,不是把提交动作变得有多花哨,而是把“探测状态”和“提交选课”这两个动作焊成一个高频循环。理解了这一点,你后面写码的时候就不会被各种花里胡哨的功能带偏。

1.2 为什么选Python + requests而不是浏览器自动化

市面上也能看到很多人用Selenium控制浏览器自动点击,效果确实直观——它模拟真实用户操作。但我自己实测下来,请求级别的脚本在这种场景里优势明显得多。

  • 轻量:一个requests请求只占几KB内存,Selenium要拉起整个浏览器进程,启动慢、吃内存、还容易被系统识别。
  • 速度快:浏览器自动化要渲染页面、加载JS、执行回调,请求脚本直接打接口,省掉中间所有环节。
  • 可控性强:你可以精确控制探测频率、超时时间、重试逻辑,改一个参数就能调整整体行为。
  • 依赖少:只需要一个第三方库,不需要下载浏览器驱动,部署环境要求极低。

大部分教务系统虽然是网页界面,但底层数据交换依然是HTTP接口。你用浏览器开发者工具打开网络面板,能看到选课按钮背后真实发生的事:一个GET请求查状态,一个POST请求提交选课。脚本的本质就是绕过页面这个“壳”,直接跟接口对话。

自然,这里会有一个疑问:为什么不是每次选课都必然成功?因为教务系统的名额往往在数据库里有唯一约束,谁的事务先提交,谁就占住对应记录。脚本能提高的是你的“探测到名额”的速度,不能绕开服务器本身的公平筛选逻辑。也正因如此,使用这类脚本时,务必要先确认学校选课规则是否允许——在规则允许的范围内,脚本是帮你省掉重复刷新动作的辅助工具,而不是用来恶意抢占名额的外挂。

2. 5行核心代码拆解:原理与灵魂

2.1 5行代码一次看完

先上大家最想看的东西,5行核心的探测加提交循环:

import requests, time sess = requests.Session() while True: info = sess.get("https://example.edu/api/course/status", params={"cid": "CS101"}).json() if info["data"]["available"] > 0: sess.post("https://example.edu/api/course/select", json={"cid": "CS101"}) break time.sleep(0.2)

这5行代码的逻辑一句话说清楚:每隔0.2秒问一次服务器“这门课还有没有名额”,有名额就立刻提交选课,成功后退出循环。整个过程没有多余动作。

  • 第1行:引入两个库,requests负责发HTTP请求,time负责控节奏。
  • 第2行:创建Session对象,用来保持登录状态,后面所有的请求都走它。
  • 第3行:进入死循环,这是整个脚本的引擎,只要没抢到就接着探测。
  • 第4行:GET请求查询课程状态,拿到JSON后判断available字段,大于0说明有剩余名额。
  • 第5行:有条件时POST提交选课,提交完用break跳出循环,脚本结束。

很多抢课脚本写了几百行,核心逻辑抽出来就是上面这段。其他额外功能,比如多门课同时监控、通知提醒、自动重试,都是在这个循环外围增加的东西。所以标题说“5行核心代码”,不是夸张,而是这5行就是整个自动化流程的心脏。

2.2 隐藏在5行背后的三件套:登录态、接口、请求头

如果你把上面的代码直接复制运行,大概率跑不通。原因很简单,5行代码成立的前提是前面三件事已经完成。

第一,登录态。绝大多数教务系统的选课接口需要身份认证。你的浏览器之所以能直接选课,是因为登录之后Cookie里有你的会话凭证。requests的Session对象会自动保存服务器返回的Cookie,但前提是你得先在脚本里完成一次模拟登录。登录这一层每个学校实现方式不一样,有的走账号密码表单,有的走统一身份认证平台,还有的需要先GET一次登录页拿一个动态token再POST。这些都属于“预处理”,不在5行核心代码范围内。

第二,准确的后端接口地址。你需要在浏览器开发者工具的Network面板里,找到选课页面发出的真实请求。很多时候网页上显示的按钮文案是“选课”,但背后的POST地址可能长得跟课程ID、学期ID、学院代码纠缠在一起。别猜,直接用抓包拿到的真实地址。

第三,请求头伪装。服务器会检查请求的来源是否像一个正常的浏览器。最常见的校验就是User-Agent,你得带上浏览器会发送的那一串UA;有些系统还会校验Referer,要求请求必须从选课页面发过来,否则直接拒绝。这些请求头字段同样要在5行代码之外先准备好。

所以,如果你问“这5行代码是不是有手就行”,答案是:5行是结果,不是起点。真正的难点在于环境准备和参数分析,这也是我把第三部分单独拿出来讲的原因。

2.3 频率、超时与退出条件:这3个参数决定了成败

5行代码里有3个关键参数,很多人跑起来之后发现效果不好,八成是这3个没调明白。

第一个是time.sleep(0.2),也就是每次探测的间隔。0.2秒意味着每秒钟发5次请求,对大多数学校的服务器来说算比较稳的健康频率。如果你设成0.05,一秒20次,短时间内没问题,但持续几分钟很可能触发风控。反过来设成1,虽然绝对安全,但热门课很可能在你两次探测之间的空档里被抢完。经验值一般取0.1到0.5秒之间,具体看学校服务器抗造程度。

第二个是timeout参数。我在上面的示例里没写,但实际跑一定加上,比如timeout=3。它的意义是:如果服务器挂了或者网卡了,请求不会一直挂在那里卡死整个循环。没有超时设置,一旦网络抖动,脚本就像人盯着一个永远不加载的页面傻等。

第三个是break退出条件。一定要在提交成功后退出,否则它提交完还会继续循环,要么重复提交报错,要么把自己账号搞进异常状态。更稳妥的写法是判断POST返回的JSON里的业务码,比如code == 0代表成功,否则继续等待。

这里还有一个反向思维:名额定点放出的那一刻,服务器压力最大,响应可能会变慢。你的探测请求不必抢在放课前一秒疯狂打满,因为学校通常是整点放量,你提前10秒进入循环就已经够快了。真正能决定成败的,是提交那一下的准确性和稳定性,而不是多刷那几次状态查询。

3. 完整可运行的脚本与实操步骤

3.1 环境准备:Python、虚拟环境与抓包工具

在动手写代码之前,先把环境理顺。建议你用Python 3.8以上版本,太老的版本对requests的支持有细微差异,没必要给自己埋坑。

mkdir grab-course cd grab-course python -m venv venv source venv/bin/activate pip install requests

用虚拟环境是个好习惯,虽然一个requests库不太会跟系统里的其他包冲突,但养成了这个习惯,以后搞稍微复杂点的项目会省很多心。抓包工具我用的是浏览器自带的开发者工具,按F12打开,切到Network面板,勾选Preserve log(保留日志),然后在选课页面手动点一次刷新和一次选课,把这两个请求记录下来。记住里面的请求地址、请求方法、请求头和提交的请求体,这就是后面脚本要复刻的样本。

如果你所在的学校选课系统是桌面客户端而非网页版,那这个脚本就不适用了,桌面客户端的接口通常做了加密和签名,难度直线上升。本文的整套思路只针对网页版教务系统,适用范围你提前判断好。

3.2 从模拟登录到会话保持的完整流程

登录是脚本第一道关卡。我见过很多学校有统一身份认证平台,录入账号密码后POST到一个登录接口,返回一个加密token。这里用一段通用的模拟登录示例,具体字段名以你抓包看到的为准:

import requests sess = requests.Session() sess.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36" }) # 第一步:GET登录页,取动态token(如果登录接口需要) login_page = sess.get("https://example.edu/login", timeout=5) csrf_token = extract_token(login_page.text) # 需要根据实际页面写提取逻辑 # 第二步:提交账号密码 login_resp = sess.post( "https://example.edu/api/login", json={ "username": "your_student_id", "password": "your_password", "csrf_token": csrf_token, }, timeout=5 ) print(login_resp.status_code, login_resp.json())

登录成功之后,sess里面保存了服务器下发的Cookie,后续所有请求只要继续用这个sess,服务器就会认为你是一个已经登录的用户。整个过程相当于你进体育馆时刷了一次身份证,后面各个闸机只要看到你的脸就直接放行。

这里有个细节值得单独说:sess.headers.update()设置的请求头会应用到该Session对象发出的所有请求上,所以User-Agent这种每个请求都要带的字段,放到Session级别统一设置就行。别每个请求手动带一遍,代码一长容易漏。

3.3 完整脚本分块解读

现在把核心循环和登录拼接起来,形成一份可以直接跑的脚本:

import requests import time LOGIN_URL = "https://example.edu/api/login" COURSE_URL = "https://example.edu/api/course/status" SUBMIT_URL = "https://example.edu/api/course/select" USERNAME = "your_student_id" PASSWORD = "your_password" COURSE_ID = "CS101" sess = requests.Session() sess.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36", "Referer": "https://example.edu/select-course" }) def login(): resp = sess.post(LOGIN_URL, json={ "username": USERNAME, "password": PASSWORD }, timeout=5) resp.raise_for_status() data = resp.json() if data.get("token"): sess.headers.update({"Authorization": "Bearer " + data["token"]}) print("登录成功") def grab_course(): while True: try: info = sess.get( COURSE_URL, params={"courseId": COURSE_ID}, timeout=3 ).json() if info["data"]["available"] > 0: result = sess.post( SUBMIT_URL, json={"courseId": COURSE_ID}, timeout=3 ).json() if result.get("code") == 0: print("选课成功", time.strftime("%Y-%m-%d %H:%M:%S")) break else: print("提交失败,返回:", result) time.sleep(0.2) except Exception as exc: print("网络异常,重试中:", exc) time.sleep(0.5) if __name__ == "__main__": login() grab_course()

这份脚本的骨架就是前面说的5行核心循环,但外面套了两层防护:try/except用来处理网络抖动,timeout用来避免卡死。注意,info["data"]["available"]这个字段名完全以你抓包拿到的JSON结构为准,有的系统返回的是surplus,有的是num,还有的是字符串"可申请"。写代码之前,先在浏览器里手动请求一次接口,看清楚返回结构再下手。

提交成功之后的处理,不同学校差异很大。有的系统POST提交即生效,有的系统POST之后还要等异步任务跑完才能确认。遇到后者,你硬用code == 0判断就可能误判——它只是“受理成功”而不是“选课成功”。稳妥的做法是提交后再GET一次课程状态,确认你的学号出现在已选名单里,再结束循环。

3.4 性能估算与并发策略:多大频率才安全

作为实践验证,我跑过一组简单估算。假设一次GET请求从发出到拿到响应耗时约0.15秒,脚本里再sleep 0.2秒,那么一轮完整循环大约0.35秒,一分钟约171次探测。单纯从探测频率这一项看,已经远超人工手动刷新。

但是,这个频率不是越高越好,原因在于提交的成功率才是最终目标。如果你用0.05秒间隔疯狂请求状态接口,不仅容易让服务器把你当成攻击行为,而且会占用你网络的请求队列,真正提交选课的那个POST反而可能因为连接池拥塞被延迟。

多线程看起来能提高并发,但我个人不推荐在抢课场景用太多线程。主要原因是大部分学校服务器对单个IP的并发连接数有限,你要是开5个线程同时打,服务器直接拒绝后续请求,反而把自己搞下线。假如真要同时监控几门课,用单个Session顺序循环即可,一门课一个循环,课程之间轮流探测,压力可控得多。

还有一个容易被忽略的点:脚本的时间基准问题。你的电脑时钟和服务器时钟可能有偏差,如果教务系统是固定整点放课,建议在脚本里先通过网络时间校准一下,或者干脆提前30秒进入循环。这样到点时你已经处在高频探测状态,而不是到了整点才启动脚本,白白浪费前面好几秒的启动时间。

4. 常见问题与排查技巧实录

4.1 登录失败:八成是请求头或动态字段问题

登录失败是所有人遇到的第一个坎。常见报错无非几种:账号密码正确但登录接口返回密码错误、一直提示验证码错误、或者登录后请求选课接口返回401。

排查思路按顺序来:

  • 先看登录请求是不是带了完整的Referer和User-Agent,很多系统要求“从登录页面来到登录接口”,Referer缺失直接拒绝。
  • 再看登录请求是不是需要额外的动态参数。打开登录页源码,搜索csrf、token、sign关键词,多半能找到隐藏的表单字段。这类字段每次刷新页面都会变,你要先用GET请求拉下来再拼到POST参数里。
  • 最后看密码的加密方式。有些系统提交前会用JS把密码做一次MD5或RSA加密,你直接POST明文密码当然失败。在开发者工具里找到加密逻辑,用Python复刻一遍。

我见过最坑的情况是,学校把所有静态资源都放在CDN上,登录接口却要求请求头带上一个从CDN响应里计算出来的校验值,不抓包根本发现不了。这时候别硬猜,老老实实把浏览器发出的所有请求头逐项复制到脚本里,一个一个排除。

4.2 始终探测不到名额的排查思路

如果你确认登录成功,代码里的循环也一直在跑,但永远拿不到“有余额”的状态,问题大概率出在接口或参数上。

先把接口地址单独拿出来,在浏览器里手动访问一遍,看看返回的JSON长什么样。如果浏览器里正常、脚本里异常,那说明请求头构造不完整。如果浏览器返回的数据里available本来就是0,那说明放课时间还没到,或者你已经错过了放课窗口。

还有两种容易被忽略的情况:一是你访问的是缓存接口,教务系统为了降低数据库压力,会在状态接口上做几秒的Redis缓存,你探测到的数据可能是延迟的;二是课程ID传错了,同一个课程名称在不同学期对应不同内部ID,你拿着上学期的ID去查这学期的状态,当然永远显示无课。这两种情况的处理方式不同,前者只能接受,后者去选课页面的源码里重新搜课程ID。

4.3 触发限流后的降级与恢复

脚本跑太久被封是个现实问题。被限流的表现一般是从某个时间点开始,所有请求返回403、429,或者返回一个包含验证码的HTML页面而不是JSON。

遇到这种情况,第一件事是立刻停掉脚本,不是换个IP继续跑。停个10到15分钟,再手动用浏览器访问一次选课页面,看看是否恢复正常。如果浏览器都访问不了,说明限流级别比较高,可能需要等更久。

恢复之后,把探测频率调低,可以在代码里加入简单的退避策略——连续失败几次就把sleep时间从0.2秒逐渐提高到1秒。这不是脚本能力的问题,而是对服务器资源的尊重。选课这件事说到底大家都要用系统,别为了自己一门课把整个系统搞瘫,于情于理都说不过去。

4.4 提交成功但没有选上的隐藏原因

比抢不到更难受的,是脚本显示“提交成功”,刷新课表却没有这门课。我排查过几次这种案例,原因主要出在三个层面。

第一,业务上需要二次确认。有的系统为了防止误操作,提交选课后要在一个确认弹窗里再点一次“确定”,这个确认动作对应另一个接口,你只调了第一次提交,等于只是把请求放进了待确认列表。

第二,异步处理延迟。选课接口把请求写进消息队列,后台慢慢落库,你POST返回的“成功”只是代表消息接收成功。这种情况等几秒钟再查询课表,一般就能看到。

第三,并发覆盖。如果你开了多个脚本或多个标签页,同一个课程被提交了多次,最后一次提交的结果可能覆盖了前一次。有时候系统只保留最新的一次选课记录,前面的结果就被顶掉了。解决办法是同时只跑一个脚本进程,提交成功后立刻退出,不要反复提交。

4.5 常见问题速查表

现象常见原因处理思路
登录报密码错误密码被前端JS加密后再传输抓包看提交参数,复刻加密逻辑
登录后选课接口401Cookie或Token未正确传递检查Session对象是否复用、Authorization头是否带上
循环运行但一直显示无名额课程ID传错或接口地址过时从选课页面源码/抓包重新提取参数
请求一段时间后全部403触发服务器限流停止脚本,等待恢复,调低探测频率
返回“提交成功”但课表没课缺少二次确认或异步处理延迟查看提交后的业务码,再查选课结果接口
脚本偶尔卡住不动网络请求卡死给所有请求加timeout参数,配合try/except重试

5. 一点个人经验与后续扩展

我个人这几年用下来,对这类抢课脚本有个明确的原则:所有选课脚本都不做秒级并发,每轮探测稳定在0.2秒左右就够了。原因很简单,你不差那几次请求,学校服务器的处理能力才是真正的瓶颈,把自己账号刷进风控名单,还不如提前把课表看明白、把接口参数核对清楚。

还有一个小技巧:脚本跑完别急着关终端,先看一眼输出里的提交返回码再关,避免出现“以为抢到了,实际没提交上”的错觉。每次运行完把日志留一下,回查的时候能省很多力气。

这个5行代码的思路,稍微改一改还能用到别的地方——抢讲座名额、预约羽毛球场地、定时打卡、监控优惠券库存,本质都是同一套逻辑:探测目标状态,在状态变化的一瞬间完成提交,把人工的反射弧换成程序的毫秒级判断。你把这个循环吃透了,以后看到任何“整点开抢”的需求,都能快速写出属于自己的自动化小工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询