资讯动态

Python教务抢课脚本实战:从模拟登录到轮询捡漏自动提交

发布时间:2026/10/17 18:34:13 来源:尧图企业网站定制
简介这是一份面向同济大学本科生的选课辅助脚本专门用于第三四轮选课阶段的捡漏操作。同济选课机制中第一二轮采取随机筛选第三四轮才是真正的即时抢课此时热门课程虽已满但教务处可能临时增加容量、部分同学退课或深夜有人退选后立即被抢这些机会稍纵即逝。本脚本通过Python爬虫持续监控选课系统捕捉上述空档并自动提交选课请求支持24小时无人值守运行极大降低人工盯屏刷新的精力消耗。资源内含Python2.7与Python3.6两个版本并已打包为exe可执行文件无需配置开发环境即可直接启动整个压缩包仅22KB便携易用。目前已有1522人学习下载实用性获得不少同学认可。同时附带的原理说明还能帮助读者理解教务系统交互逻辑是学习Python自动化爬取与模拟操作的理想示例。1. 同济大学抢课脚本这不是外挂是给三四轮捡漏准备的节流阀先给结论同济大学抢课脚本这套思路真正的价值不在第一二轮拼手速而在三四轮选课的捡漏窗口期。前两轮选课大家挤在同一个时间点教务系统卡的厉害脚本能做的只是把请求发出去能不能抢到很大程度看系统处理顺序。可到了三四轮大量学生退课、换课、调整培养方案每天都会释放出碎片化的课程名额。手动刷新课表去盯这些名额既折磨人又容易错过哪怕你守着页面也抢不过那些开着轮询脚本的人。这个脚本解决的就是这个问题模拟登录、保持会话、高频轮询目标课程状态、一旦发现可选就自动提交。它本质上是一个把“盯梢”和“点击”自动化的工具而不是绕过系统限制的外挂。我最早接触这类脚本是在给某高校的教务系统做选课辅助工具的时候当时看到学生群里有人在深夜两点手动刷新页面等退课名额就觉得这件事完全可以用程序替代。如果你有基础的 Python 经验能看懂 requests 和简单的 JSON 解析就可以照着这套思路自己复现一个。这篇文章不贴任何现成的完整代码包而是把登录、轮询、提交、避坑这四个核心环节拆开讲清楚每段代码你都能直接跑通再根据自己的课程编号和教务系统改参数。读完你会明白抢课脚本不是黑科技只是一个懂 HTTP 协议的“盯盘机器人”。2. 先把登录和会话打通模拟登录的三步拆解2.1 教务系统的认证流程验证码、加密参数和 Cookie绝大多数高校教务系统都采用统一的登录入口你输入学号和密码之后前端会先把密码做一次加密传输然后服务端返回一个会话标识。模拟登录的第一步是搞清楚这个加密过程。常见做法是打开浏览器的开发者工具切到 Network 面板手动登录一次看登录请求的 Form Data 里都有哪些字段。我见过的某高校系统登录接口提交的参数包括用户名、加密后的密码、一个动态的 csrf_token以及一个由前端脚本生成的 timeStamp 拼接串。这里最关键的是搞清楚密码到底是明文还是加密后传输以及 token 从哪里获取。常见做法是先用 GET 请求访问登录页面从 HTML 里提取 token然后构造 POST 请求提交登录信息。我的习惯是先把登录页面的 HTML 存下来搜一下csrf、token、_token之类的关键词看它是藏在隐藏的 input 里还是由 JavaScript 动态生成。如果是动态生成就得找到生成它的那段 JS有时候甚至需要把加密函数用 Python 重写一遍。这一步不能跳因为大多数抢课脚本“翻车”都是死在登录上——不是密码错了而是请求参数少了字段。拿到会话之后requests 的 Session 对象会把 Cookie 自动保存下来后续的选课请求只要带上同一个 Session服务端就会认为你是登录状态。这里有个血泪经验不要在每次请求时手动拼接 Cookie直接用 Session 发请求因为服务端可能在后端更新了会话标识你手写的 Cookie 一旦过期整个脚本就失效了。2.2 模拟登录的代码骨架与参数说明下面这段代码是我处理过的某高校教务系统模拟登录的最小实现你拿到的系统参数可能不一样但流程是通用的。import requests import re import hashlib # 创建一个 Session 对象自动管理 Cookie session requests.Session() # 1. 访问登录页拿到动态 token login_page_url http://jw.example.edu.cn/login headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: login_page_url } resp session.get(login_page_url, headersheaders) # 从 HTML 中提取隐藏字段具体正则按你系统的实际页面结构调整 csrf_token re.search(rnamecsrf_token value([^]), resp.text).group(1) exec_time re.search(rnameexec_time value([^]), resp.text).group(1) # 2. 模拟前端对密码的加密处理这里以最简单的 MD5 为例 password_raw your_password password_md5 hashlib.md5(password_raw.encode(utf-8)).hexdigest() # 3. 构造登录请求 login_data { username: your_student_id, password: password_md5, csrf_token: csrf_token, exec_time: exec_time, submit: 登录 } login_url http://jw.example.edu.cn/login/submit login_resp session.post(login_url, datalogin_data, headersheaders) # 4. 验证是否登录成功 if 个人中心 in login_resp.text or 退出 in login_resp.text: print([] 登录成功) else: print([-] 登录失败请检查参数)这段代码最关键的点是第 8 行的 Session 复用和第 19 行的密码加密。实际的加密方式可能是 AES 或 RSA你需要在浏览器里打断点看前端脚本的处理逻辑加密算法不一样代码就要换。另一个值得注意的地方是登录页有时候会在 URL 里带一个redirect参数这个参数在后续请求中也需要原样携带。2.3 登录态保持与失效重连应对超时的两种策略登录态是抢课脚本的生命线。教务系统的会话超时时间通常在 20 到 40 分钟之间而一个抢课脚本可能要挂在后台跑一整天。我常用的策略是两种结合一是增加一个“心跳”请求每隔几分钟访问一次个人主页让服务端认为你还在活跃二是在每次请求的返回里检查登录态是否失效如果发现响应里出现“请重新登录”之类的提示或者 URL 跳转到了登录页就自动重新执行一次完整的登录。不过心跳请求也要控制频率有的系统会统计接口访问次数你每分钟刷一次页面也会被当成异常行为。我一般把心跳间隔设在 5 到 8 分钟并且请求一个体积小、不会产生副作用的页面比如个人信息概览。如果发现系统对登录态失效没有明显的页面提示而是所有接口都返回 302 跳转那就在请求函数里统一判断最终响应的 URL 是不是登录页是的话就重连。**宁可多写一个重连函数也不要赌系统不会踢你下线。**这点在三四轮持续捡漏时特别重要因为晚上才是退课高峰你不可能守着电脑脚本一旦断线就等于白挂。3. 选课检测核心轮询逻辑与课程状态判断3.1 课程列表页背后的接口与数据流登录成功之后脚本要解决的第二个问题是如何拿到课程列表以及判断一门课当前是“可选”还是“已满”。大多数教务系统在选课页面里是通过 Ajax 请求加载课程数据的你可能在页面上看到的是表格实际上浏览器在背后请求了一个 JSON 接口。用开发者工具抓一下“查询课程”的请求你会在 XHR 列表里看到一个返回课程信息的接口请求参数通常是学期编号、课程性质、课程名称或课程号。为什么这么强调接口而不是直接从 HTML 里解析原因很简单JSON 接口返回的数据结构化程度高课程编号、课程名称、余量、容量、状态这些字段都是独立的解析起来稳定得多。而 HTML 页面随时可能因为系统改版、课表模板调整而改变结构你写好的正则表达式第二天就失效了。我处理过的某高校系统课程状态字段是一个数字0表示不可选1表示可选2表示容量已满但可以排队候补。你必须在动手写脚本之前把字段含义确认清楚否则轮询条件写错了看到 status 等于 1 就提交结果那是“课程已关闭”的意思那就闹笑话了。3.2 轮询脚本定时检测心仪课程的最小实现轮询是抢课脚本的发动机。你要做的其实就是每隔一段时间去请求一次课程接口把返回的数据里你关心的那门课的余量提取出来如果余量大于 0 或者状态变成了可选立刻触发选课提交。下面是不含提交逻辑的纯检测轮询代码你完全可以先跑起来看日志是否符合预期。import time import datetime import requests session requests.Session() # 假设已经登录成功 course_api http://jw.example.edu.cn/api/course/quota # 查询参数包含学期号和课程编号具体字段以你抓包得到的为准 query_params { semester: 2024-2025-1, course_code: COMP1012, stu_type: undergraduate } def check_course_quota(session, params): 查询单门课程的实时余量返回余数和状态码 resp session.get(course_api, paramsparams, timeout5) data resp.json() # 假设返回结构里 status 是课程状态quota 是剩余名额 status data.get(status) quota data.get(quota, 0) return status, quota def poll_loop(session, params, interval3): 持续轮询课程余量检测到可选名额就退出循环 while True: try: status, quota check_course_quota(session, params) now datetime.datetime.now().strftime(%H:%M:%S) if status 1 and quota 0: print(f[{now}] 课程可抢余量: {quota}) return True else: print(f[{now}] 暂无余量状态码: {status}, 余量: {quota}) except requests.exceptions.RequestException as e: # 网络抖动是常态别让一个超时打断整个轮询 print(f[!] 请求异常: {e}) # 动态间隔如果请求耗时接近超时阈值就把间隔拉大 time.sleep(interval) # 启动轮询interval 单位是秒3 秒一次是相对温和的频率 poll_loop(session, query_params, interval3)这段代码里最容易被忽略的是第 22 行的 try-except。教务系统在选课高峰期的响应速度很不稳定偶尔一次超时会直接抛异常导致脚本崩溃你挂在后台跑一晚上睡醒发现脚本在第一次异常时就停了那是一整夜的损失。另一个点是interval参数不要设得太小1 秒一次连续请求很容易被系统限流3 到 5 秒是比较合理的区间原因我在避坑章节里会专门展开。3.3 抢课时间窗口与捡漏信号识别三四轮选课的名额释放规律其实有迹可循。我观察下来的常见情况是退课高峰集中在每天的午休时段和晚上 11 点前后因为学生改选课通常发生在这些时间另外一门课的容量可能在系统后台被教务处手动调整调整的瞬间会释放一批名额。所以轮询策略要配合时间窗口不必全天候高频刷新在退课高峰时段加密轮询间隔其余时间可以把间隔放宽到 10 秒左右既省资源又降低被封风险。捡漏信号的识别有两种一种是余量从 0 变成正数另一种是课程状态从“不可选”变成“可选”比如某些课程有选课门槛过了门槛开放时间点状态会瞬间翻转。你的脚本需要同时监控这两个信号不能只看余量因为有些系统余量显示为 0 但候补名单还没满提交依然可能被接收。4. 提交选课从 POST 请求到结果确认4.1 选课提交的请求构造轮询检测到了名额接下来就是提交选课请求。这是整个脚本里最不能出错的一步——你要提交的字段必须跟手动选课时浏览器发送的完全一致。手动点一次“选课”按钮抓下来的请求通常会带这些字段课程编号、课程班号、学分、选课轮次标识偶尔还有上一次请求返回的 transactionId。这些字段不齐服务端会直接拒绝。import json import requests def submit_course(session, course_code, class_code, semester): 提交选课请求返回服务端的响应文本 submit_url http://jw.example.edu.cn/api/course/select payload { course_code: course_code, # 课程编号 class_code: class_code, # 教学班编号 semester: semester, # 学期编码 select_round: 3, # 选课轮次第三轮 } # 有的系统要求在 Header 里带上 X-Requested-With 表示这是 AJAX 请求 headers { X-Requested-With: XMLHttpRequest, Content-Type: application/json } resp session.post(submit_url, jsonpayload, headersheaders, timeout10) return resp.text # 调用示例提交一门课程编号为 COMP1012、教学班为 01 的课 result submit_course(session, COMP1012, 01, 2024-2025-1) print(result)这里有一个关键区别有些教务系统的提交接口要求Content-Type是application/x-www-form-urlencoded也就是用data传参而不是json两种传参方式后台接收到的格式完全不同。判断标准很简单打开浏览器开发者工具看提交请求的 Payload 显示的是form data还是request payload前者用 data后者用 json。4.2 结果确认与失败重试提交之后脚本必须确认选课是否真的成功。服务端的返回文本可能是 JSON也可能是一段 HTML 提示比如“选课成功”或“该课程已选”。你需要把这些成功标志收集成一组关键词然后写进判断逻辑里。确认成功之后脚本要做的事情是立刻停止该课程的轮询然后向你已经选中的课程列表里发起一次查询从结果里验证这门课确实出现在你的课表里。如果提交失败常见错误是“课程余量不足”或“选课时间未开放”。这时候脚本应该继续回到轮询循环里因为别人也可能在抢同一门课名额可能在几秒内被释放出来。**失败重试的唯一原则是不能无限重试到把账号锁死。**一般我会设定一个单日重试上限比如 50 次超过这个次数就暂停半小时再继续避免触发系统的风控机制。4.3 关于并发提交的边界一个账号能做的事情有限有人会想抢课的时候我用多个线程同时提交同一个选课请求是不是成功率更高我的经验是**在大多数教务系统里这不是提效而是自杀。**服务端的选课逻辑通常有幂等处理同一个学生在同一时刻提交多次请求系统会按请求到达顺序处理多余的请求只会增加你账号被风控的概率。而且并发请求处理不当会让你的 Session 在某个瞬间发生竞态条件导致后面的请求带着失效的会话标识。真正有效率的做法是单线程轮询 单次提交提交失败后快速回到轮询状态。把并发浪费在对单个账号没有意义的事情上不如把精力花在轮询间隔的合理性上。记住抢课脚本的瓶颈在网络延迟和服务端处理能力不在客户端并发数。5. 抢课脚本的避坑清单从登录失败到被系统踢下线5.1 现象会话一开始能用轮询几次后突然失效这个坑我踩过不止一次。脚本跑了一个小时突然所有请求都返回“请先登录”。原因是服务端的会话超时机制不是简单地 30 分钟无操作就失效而是每次请求都会刷新会话时间窗口但如果你在短时间内请求太密集触发了频控服务端会直接把你的会话标记为异常并要求重新登录。解决方法是把轮询间隔拉到 5 秒以上并且在请求里带一个随机 User-Agent 头避免因为请求特征太单一被识别为脚本。另一个做法是监听登录态我发现通用的方案是当一个请求返回的响应里出现登录跳转时立即停止轮询并重新执行登录流程登录成功后再继续原来的循环而不是等到下一次请求才发现失效。5.2 现象接口返回明明成功课表里却没有新课程有一次我在模拟项目里调试代码提交接口返回“选课成功”我高兴地去查课表结果里面没有这门课。查了半天才发现这个接口只完成了“预选”步骤还需要在页面上点击一次“确认”才会正式写入课表。很多教务系统的选课是两步走的第一步提交预选第二步在待选列表里确认。你看上去是提交成功实际上漏了第二步。解决方法是看提交请求返回的响应里有没有跳转链接或新的操作按钮如果里面有“确认选课”字样就用脚本模拟点击一次。你在抓包的时候一定要把整个手动选课过程的所有请求都记录下来一个都不能漏。5.3 现象请求触发验证码后续所有接口被拉黑这是频率太高被风控了。你 1 秒刷一次课程接口系统判定你是脚本要求你输入验证码。这时候你的 Session 不仅访问不了课程接口连正常的上课信息查询都会受影响。解决方法是给轮询请求加上随机延迟比如 3 到 6 秒之间浮动而不是固定 3 秒一次。一旦发现请求返回里出现验证码相关字段立即停止轮询等 10 到 15 分钟再重新登录。经验法则是一天之内单个账号的选课相关请求总次数不要超过 2000 次超出这个量风险陡增。5.4 现象抢到了课但没学分或者被踢出课程三四轮选课的规则比前两轮复杂。有些课程在选课系统里显示可选但你的培养方案里并没有这个课组要求提交之后可能显示成功最后却因为课程与培养方案不匹配被管理员踢出。还有一种情况是课程容量超了系统按“先到先得”原则处理你提交时有余量但提交请求到达服务端时名额已经被别人占了你排在候补位后续没有转正就被自动取消。解决方法是在脚本每天开始运行前先手动登录教务系统确认目标课程确实在当前轮次的选课范围里并且确认提交后的“在选课程”列表中有记录。不要只看提交接口的返回值系统的最终状态才是一切。现象核心原因处理手段会话失效频率过高触发风控拉大轮询间隔增加随机延迟提交成功但无课漏掉确认步骤抓包完整流程补上确认请求触发验证码请求特征单一随机 User-Agent动态延时被踢出课程规则与课程要求不符运行前人工核对课程简况6. 让捡漏从“碰运气”变成“确定性收益”两个进阶技巧6.1 重试策略从固定间隔到指数退避前面提到失败重试不能无限循环但重试也不是每次都等同样的时间。我习惯用指数退避的策略第一次失败后等 2 秒第二次等 4 秒第三次等 8 秒最多等到 30 秒就封顶。这样做的好处是在名额释放的瞬间你依然保持着相对高频的请求而不会因为持续高频被封同时服务端系统繁忙的时间段往往非常短退避可以帮你避开最拥挤的那几十秒。import time import random def retry_with_backoff(func, max_retries20): 带指数退避的重试函数适用于可选课提交失败后的重新检测 base_delay 2 for attempt in range(max_retries): result func() if result is not None: return result delay min(base_delay * (2 ** attempt), 30) random.uniform(0, 1) print(f[*] 第 {attempt 1} 次失败{delay:.1f} 秒后重试) time.sleep(delay) return None6.2 验证你的脚本用一个冷门课程做冒烟测试脚本写完之后不要直接拿心仪课程试跑。我的习惯是先用一门几乎没有竞争、绝对冷门的课程做冒烟测试跑通“登录 → 轮询 → 提交 → 验证课表”的完整链路。如果冷门课程能成功选上说明脚本的请求格式和会话逻辑没问题再切换到心仪课程时就只需要关注轮询条件和时间窗口了。等确认脚本稳定后把提交函数替换成“只检测、不提交”的演练模式跑一个晚上看日志里的轮询是否稳定再打开真正的自动提交。抢课脚本本质上是个半自动工具它替你省掉的只是“盯屏幕刷新”这个体力活而选哪门课、什么时候退课、这门课值不值得我们花学分去抢这些判断还是得你自己来。我的习惯是宁可费点功夫在目标课程的选择上也不要把脚本跑得过于激进——你要的是拿到一门真正有用的课而不是在一个不值得的课程上浪费选课次数。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑