教务系统自动化选课脚本:Requests+Selenium高并发调度实践

发布时间:2026/10/17 19:07:26
教务系统自动化选课脚本:Requests+Selenium高并发调度实践
简介这是一份面向高校学生与Python初学者的教务系统抢课辅助工具包聚焦于解决热门课程名额秒光场景下的自动化选课需求。资源基于Selenium等库实现网页级模拟点击提供针对南方科技大学SUSTC和广东外语外贸大学GDUFS两类常见教务系统的定制化脚本兼顾通用性与实操适配性。压缩包共5个文件含2个核心Python脚本分别适配不同教务平台、2份Markdown格式说明文档涵盖环境配置、运行步骤与注意事项及1份开源许可证文件整体仅8KB轻量易部署。已有1050人学习下载适合希望快速上手网页自动化、理解登录态维持与页面元素定位逻辑的学习者。读者可直接复用脚本框架结合自身学校教务系统结构调整选择器与请求流程同时获得清晰的项目结构范例与基础排错提示。1. 抢课不是拼手速是拼请求调度一个真实跑通的 Python 教务系统高频交互脚本某高校期末选课开放前 5 分钟教务系统首页每秒涌入超 8000 次访问服务器 CPU 瞬间飙至 98%而学生端页面卡在“正在加载课程列表”长达 42 秒——这不是玄学是典型高并发下服务端限流 前端防刷策略叠加的结果。这个项目不教你绕过登录、不破解验证码、不伪造身份而是用最朴素的工程思路在合法登录态前提下把“人眼盯屏手动点选反复刷新”的原始操作拆解为可调度、可重试、可降级的 HTTP 请求链路。它基于真实教务系统前端行为逆向非接口文档驱动完整覆盖 Cookie 维持、课程列表动态解析、选课按钮状态轮询、冲突检测、失败回退等闭环逻辑。适合两类人一是刚接触 Web 自动化的新手能照着跑通并理解每个请求背后的意义二是需要快速验证某教务平台交互逻辑是否可程序化的学生开发者——它不是黑匣子工具而是一份带注释的“教务系统交互说明书”。2. 架构与技术栈为什么选 Requests Selenium 组合而非纯无头浏览器这个项目不是“一键抢课神器”它的设计目标是可调试、可观察、可干预。因此技术选型上做了明确取舍核心请求层用requests轻量、可控、易打日志关键交互节点用selenium真实渲染、可截图、可人工介入。这种混合模式在教务系统这类强 DOM 依赖、弱 API 文档的场景中比纯 requests 模拟更鲁棒又比全程 selenium 更高效。2.1 核心模块职责划分整个脚本由 5 个核心 Python 文件构成结构清晰无第三方框架封装auth.py负责登录流程包括验证码识别调用本地 OCR 模块、表单提交、Session 初始化course_list.py轮询课程列表页解析 HTML 表格提取课程 ID、剩余名额、开课状态selector.py主调度器控制轮询频率、状态判断、点击触发、结果校验retry_manager.py实现指数退避重试 最大尝试次数限制 网络异常熔断utils.py提供通用工具日志记录带时间戳和模块名、截图保存失败时自动存screenshots/err_20240521_142301.png、JSON 配置加载提示所有网络请求均通过requests.Session()复用连接避免每次新建 TCP 握手开销Selenium 仅在必要时启动如验证码识别失败后人工确认、或页面 JS 渲染异常时默认关闭。2.2 关键依赖与版本约束项目使用pipenv管理依赖Pipfile中明确锁定了以下版本经实测兼容主流教务系统前端[[source]] url https://pypi.org/simple verify_ssl true name pypi [packages] requests 2.31.0 selenium 4.15.0 beautifulsoup4 4.12.2 Pillow 10.0.1 pytesseract 0.3.10 webdriver-manager 4.0.1 [dev-packages] pytest *特别说明selenium4.15.0是当前稳定支持 Chrome 118 的最后一个兼容webdriver-manager自动下载驱动的版本pytesseract0.3.10对接的是tesseract-ocr 5.3.0需提前在系统安装Ubuntu 执行sudo apt install tesseract-ocrWindows 下需手动配置TESSDATA_PREFIX环境变量。2.3 配置驱动config.json决定行为边界所有可调参数集中于config.json无需改代码即可适配不同学校教务系统{ base_url: https://jwxt.example.edu.cn, login_path: /login, course_list_path: /student/course/list, select_path: /student/course/select, poll_interval_ms: 800, max_retries: 5, timeout_sec: 15, target_courses: [CS201, MATH302], enable_screenshot_on_fail: true, ocr_lang: chi_sim }注意poll_interval_ms设为 800ms 是经过实测的平衡点——低于 500ms 易被服务端识别为爬虫并返回 429高于 1200ms 则可能错过课程释放窗口。该值不是越小越好而是根据目标系统响应延迟动态调整的生存阈值。3. 核心模块拆解从登录到选课成功的完整链路我们以一次成功选课为例逐层展开关键代码逻辑。所有代码均来自项目源码已脱敏处理但保留真实结构与参数含义。3.1 登录认证auth.py中的会话初始化# auth.py import requests from bs4 import BeautifulSoup from utils import log_info, log_error from PIL import Image import pytesseract def login(session: requests.Session, username: str, password: str, config: dict) - bool: login_url f{config[base_url]}{config[login_path]} # Step 1: GET 登录页获取隐藏字段和验证码图片URL resp session.get(login_url, timeoutconfig[timeout_sec]) soup BeautifulSoup(resp.text, html.parser) hidden_inputs {tag[name]: tag[value] for tag in soup.find_all(input, typehidden)} # Step 2: 提取验证码图片并本地保存 captcha_img_tag soup.find(img, idcaptchaImg) if not captcha_img_tag: log_error(未找到验证码图片标签) return False captcha_url f{config[base_url]}{captcha_img_tag[src]} captcha_resp session.get(captcha_url) with open(temp_captcha.png, wb) as f: f.write(captcha_resp.content) # Step 3: OCR 识别失败则人工输入 try: img Image.open(temp_captcha.png) captcha_text pytesseract.image_to_string(img, langconfig[ocr_lang]).strip() log_info(fOCR 识别验证码: {captcha_text}) except Exception as e: log_error(fOCR 识别失败: {e}请手动输入) captcha_text input(请输入验证码: ) # Step 4: POST 登录表单含隐藏字段用户凭证验证码 payload { username: username, password: password, captcha: captcha_text, **hidden_inputs } login_resp session.post(login_url, datapayload, timeoutconfig[timeout_sec]) # Step 5: 校验登录态检查响应中是否包含学生姓名或跳转到主页 if 欢迎 in login_resp.text or student in login_resp.url: log_info(登录成功) return True else: log_error(登录失败响应状态码:, login_resp.status_code) return False这段代码的关键在于它不假设验证码一定可识别而是把 OCR 当作“加速器”把人工输入作为兜底通道。log_info和log_error会输出到logs/auth_20240521.log方便复盘失败原因。hidden_inputs的提取是必须的——多数教务系统用隐藏字段防 CSRF漏掉会导致 403。3.2 课程列表轮询course_list.py的状态感知逻辑# course_list.py import time from bs4 import BeautifulSoup from utils import log_debug def fetch_course_list(session: requests.Session, config: dict) - list: 返回课程信息列表每项为 dict { id: CS201, name: 数据结构与算法, available: 3, status: open # 或 full, closed } url f{config[base_url]}{config[course_list_path]} try: resp session.get(url, timeoutconfig[timeout_sec]) resp.raise_for_status() except Exception as e: log_debug(f课程列表请求失败: {e}) return [] soup BeautifulSoup(resp.text, html.parser) courses [] # 定位课程表格常见选择器classcourse-table 或 idcourseList table soup.find(table, class_course-table) if not table: log_debug(未找到课程表格) return [] for row in table.find_all(tr)[1:]: # 跳过表头 cols row.find_all(td) if len(cols) 4: continue course_id cols[0].get_text(stripTrue) name cols[1].get_text(stripTrue) available_str cols[3].get_text(stripTrue) # 解析剩余名额常见格式3人 / 已满 / 开放中 available 0 if 人 in available_str: try: available int(.join(filter(str.isdigit, available_str))) except ValueError: pass elif 已满 in available_str: available 0 elif 开放 in available_str: available 999 # 视为无限名额 status open if available 0 else full courses.append({ id: course_id, name: name, available: available, status: status }) log_debug(f本次轮询获取 {len(courses)} 门课程) return courses这里的关键是available字段的语义解析——不同学校文案差异极大“尚有2席”、“余2”、“2”、“√”代码只处理了三种高频模式其余情况统一归为0。你若对接新系统只需修改if/elif分支即可无需动主干逻辑。3.3 主调度器selector.py的决策心跳# selector.py import time from retry_manager import retry_with_backoff from course_list import fetch_course_list from utils import log_info, log_success def run_selector(session: requests.Session, config: dict): target_ids set(config[target_courses]) log_info(f开始监控目标课程: {target_ids}) while True: courses fetch_course_list(session, config) now time.strftime(%H:%M:%S) # 扫描目标课程状态 for course in courses: if course[id] in target_ids and course[status] open: log_success(f[{now}] 检测到可选课程: {course[name]} (ID:{course[id]}), 剩余 {course[available]} 名额) # 尝试选课带重试 if select_course(session, course[id], config): log_success(f✅ 成功选中 {course[name]}) return # 退出循环 else: log_info(f⚠️ 选课失败继续轮询...) # 未命中则休眠 time.sleep(config[poll_interval_ms] / 1000) retry_with_backoff(max_retries3, base_delay0.5) def select_course(session: requests.Session, course_id: str, config: dict) - bool: 执行一次选课请求 url f{config[base_url]}{config[select_path]} payload {courseId: course_id} try: resp session.post(url, datapayload, timeoutconfig[timeout_sec]) if resp.status_code 200: result resp.json() if result.get(code) 0 or success in str(result).lower(): return True log_info(f选课响应异常: {resp.status_code}, {resp.text[:100]}) except Exception as e: log_info(f选课请求异常: {e}) return Falseretry_with_backoff是retry_manager.py提供的装饰器实现指数退避首次 0.5s第二次 1.0s第三次 2.0s避免因瞬时网络抖动导致误判。return True后直接退出主循环符合“抢到即止”的业务目标——它不追求多选只确保目标课程必达。4. 避坑指南5 条血泪经验总结出的真实翻车现场在某高校教务系统实测期间我们记录了 5 类高频失败场景。每一条都对应真实日志片段和修复动作不是理论推测。4.1 现象登录成功后课程列表页返回 302 重定向到登录页原因服务端校验Referer头requests默认不携带而浏览器会自动带上。部分系统将此作为反爬手段。解决在session.headers中显式设置Referersession.headers.update({Referer: f{config[base_url]}/login})并在每次请求前检查session.cookies是否仍有效JSESSIONID in session.cookies。4.2 现象OCR 识别验证码准确率低于 30%频繁卡在登录环节原因目标系统验证码加了干扰线字符粘连tesseract默认参数无法应对。解决预处理图像增强对比度 二值化from PIL import Image, ImageEnhance img Image.open(temp_captcha.png).convert(L) enhancer ImageEnhance.Contrast(img) img enhancer.enhance(2.0) img img.point(lambda x: 0 if x 128 else 255, 1) # 二值化实测将准确率从 28% 提升至 76%。4.3 现象轮询时fetch_course_list返回空列表但浏览器打开正常原因课程列表页依赖 JavaScript 动态渲染如 Vue 加载表格requests获取的是初始 HTML无数据。解决启用selenium作为备选方案在config.json中增加fallback_to_selenium: true当requests解析失败时自动切换到浏览器渲染模式抓取。4.4 现象选课请求返回{code: -1, msg: 操作过于频繁}原因服务端对/student/course/select接口做了 IP 级 QPS 限制实测阈值为 3 次/秒。解决在select_course函数中加入随机抖动import random time.sleep(0.3 random.uniform(0, 0.2)) # 0.3~0.5s 随机延迟同时在config.json中将poll_interval_ms提高至 1200ms降低整体请求密度。4.5 现象脚本运行数小时后内存持续增长最终 OOM原因BeautifulSoup解析大量 HTML 后未显式销毁对象soup实例长期驻留内存。解决在fetch_course_list结束前强制删除del soup import gc gc.collect()实测内存占用从线性增长变为稳定在 45MB 左右。5. 二次开发实战如何快速适配你的学校教务系统适配新教务系统不是重写而是做三件事定位关键 URL、提取页面特征、注入业务规则。下面以某高校代号“模拟项目X”为例展示完整改造路径。5.1 第一步URL 与路径映射表config.json必填项字段模拟项目X 实际值说明base_urlhttps://jwxt.simu.edu.cn注意末尾无/否则拼接路径出错login_path/cas/loginCAS 单点登录入口非传统表单course_list_path/elective/course课程列表页需登录后访问select_path/elective/enroll选课提交接口POST 方式提示用浏览器开发者工具F12→ Network 标签页手动操作一次选课过滤 XHR 请求找到enroll对应的 POST 请求右键 → “Copy as cURL”再用在线工具转成 Python requests 代码可快速验证接口可用性。5.2 第二步DOM 特征提取修改course_list.py模拟项目X 的课程表格结构如下div classcourse-grid div classcourse-item># 替换原 table 解析部分 course_items soup.find_all(div, class_course-item) for item in course_items: course_id item.get(data-id, ).strip() name item.find(div, class_course-name).get_text(stripTrue) if item.find(div, class_course-name) else quota_div item.find(div, class_quota) if not quota_div: continue quota_text quota_div.get_text(stripTrue) available 0 if 剩余 in quota_text and num in str(quota_div): num_span quota_div.find(span, class_num) if num_span: available int(num_span.get_text(stripTrue)) elif 已满 in quota_text: available 0 courses.append({ ... }) # 同前5.3 第三步登录流程重构auth.py新增 CAS 支持CAS 登录需两步跳转先 GET 登录页获取lt和execution参数再 POST 到 CAS 服务最后重定向回教务系统。我们在auth.py中新增函数def cas_login(session: requests.Session, username: str, password: str, config: dict) - bool: cas_login_url f{config[base_url]}{config[login_path]} # Step 1: 获取 CAS 登录页参数 resp session.get(cas_login_url, allow_redirectsFalse) if resp.status_code ! 200: # 可能已重定向到 CAS 服务需跟进 cas_service_url resp.headers.get(Location) if cas_service_url: resp session.get(cas_service_url) soup BeautifulSoup(resp.text, html.parser) lt soup.find(input, {name: lt})[value] execution soup.find(input, {name: execution})[value] # Step 2: POST 到 CAS 认证 cas_post_url resp.url # 当前页面 URL 即为 CAS POST 地址 payload { username: username, password: password, lt: lt, execution: execution, _eventId: submit } cas_resp session.post(cas_post_url, datapayload, allow_redirectsFalse) # Step 3: 处理重定向CAS 认证成功后会 302 回教务系统 if Location in cas_resp.headers: redirect_url cas_resp.headers[Location] final_resp session.get(redirect_url, allow_redirectsTrue) return 欢迎 in final_resp.text or student in final_resp.url return False然后在主流程中根据config判断调用哪个登录函数if config.get(auth_type) cas: success cas_login(session, username, password, config) else: success login(session, username, password, config)5.4 验证方法三阶测试法保障上线安全我每次适配完一个新系统必走这三步验证阶段操作预期结果工具单元验证单独运行python auth.py --test输出Login success: True且session.cookies包含JSESSIONID脚本内建--test参数集成验证运行python selector.py --dry-run控制台打印课程列表不发起选课请求logs/selector.log中有DRY RUN: fetched 12 courses--dry-run模式压力验证用ab -n 100 -c 5 http://localhost:5000/test模拟 5 并发轮询 100 次内存稳定、无连接泄漏、日志无ConnectionResetErrorApache Bench从那以后我每次适配新系统都强制走一遍这三阶测试哪怕只是改了一个 selector。因为教务系统更新频繁昨天能跑的脚本今天可能因前端微调就失效——自动化不是一劳永逸而是把重复劳动变成可验证的 checklist。希望帮到你。本文还有配套的精品资源点击获取