news 2026/10/17 20:09:00

教务系统逆向工程:Python实现登录态管理与选课自动化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
教务系统逆向工程:Python实现登录态管理与选课自动化

简介:这是一份面向高校学生与Python初学者的教务系统抢课辅助工具包,聚焦于解决热门课程名额秒光场景下的自动化选课需求。资源以轻量级Python脚本为核心,通过模拟浏览器行为实现对教务页面的高频点击与请求提交,适用于SUSTC(南方科技大学)和GDUFS(广东外语外贸大学)等采用类似架构的教务系统。压缩包共5个文件,含2个核心脚本(SUSTC-qiangke.py、GDUFS_version.py)、2份说明文档(README.md)及1份LICENSE协议,总大小仅8KB,结构简洁、开箱即用。已有1050人学习下载,适合希望快速上手自动化操作、理解HTTP请求模拟与基础网页交互逻辑的学习者。读者可直接复用脚本框架,结合自身学校登录/选课接口进行适配改造,并通过README中的使用说明掌握环境配置、参数调整与常见问题应对方法。

1. 这不是“抢课外挂”,而是一份教务系统交互逻辑的逆向工程笔记:用 Python 拆解登录态维持、课程列表解析与原子化选课请求的完整链路

某高校教务系统上线新学期选课时,A同学连续三天凌晨蹲守,手动刷新页面、填验证码、点提交,最终只抢到 2 门课。他把整个过程录屏回放,发现从登录成功到课程列表加载完成平均耗时 1.8 秒,点击“选择”按钮后服务器响应中位数是 420ms,但页面状态更新却要再等 1.2 秒——这中间的空白期,就是并发请求被服务端排队、丢弃或返回 503 的高发窗口。这不是玄学,是 HTTP 状态机 + Session 生命周期 + 前端防重机制共同作用的结果。这份资源不提供一键抢课神器,它是一套可调试、可断点、可验证的 Python 脚本集合:包含基于 requests + BeautifulSoup 的无头登录模拟器、带时间戳校验的课程列表增量比对器、支持幂等性控制的选课请求构造器,以及关键环节的失败日志归因模板。适合两类人:一是想搞懂教务系统真实交互逻辑的 Web 开发初学者;二是需要在受限环境(如无 Selenium 权限、仅允许 requests)下做自动化流程验证的运维/测试人员。它不绕过身份认证,不伪造 UA 或 Cookie,所有行为都严格复现浏览器真实请求序列。

2. 教务系统交互建模:为什么必须放弃 Selenium,转而用 requests + session + 手动状态管理

2.1 教务系统典型架构与反自动化特征识别

绝大多数高校教务系统采用 B/S 架构,后端为 Java(Spring MVC)或 ASP.NET,前端重度依赖 jQuery + 同步表单提交。其反自动化设计并非靠复杂 JS 渲染,而是藏在三个地方:

  • 登录态强绑定:Session ID 不仅存在 Cookie,还嵌入每个 form 表单的 hidden input(如<input type="hidden" name="token" value="a1b2c3...">),且该 token 在登录成功后 30 秒内有效;
  • 请求来源校验:服务端检查Referer头是否为上一页地址,空 Referer 或跨域 Referer 直接 403;
  • 操作节流机制:同一 Session 下,选课接口/selectCourse对相同课程 ID 的请求间隔小于 2 秒,返回{"code":409,"msg":"操作过于频繁"}。

Selenium 在这类场景下反而成为累赘:启动慢(平均 2.3s)、内存占用高(单实例 >180MB)、无法精确控制请求头字段(如 Referer 需动态继承)、难以捕获服务端返回的 JSON 错误码(常被 WebDriver 自动转成异常)。而 requests + session 组合能直接操控每个请求的生命周期,把“登录→获取 token→构造表单→提交→校验响应”拆成原子步骤,便于逐层注入日志和断点。

2.2 requests.Session 的正确初始化与关键配置

import requests from urllib.parse import urljoin # 初始化 session,禁用重定向(教务系统常用 302 跳转,需手动处理) session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1" }) # 关键:禁用自动重定向,否则会丢失跳转前的 Set-Cookie 和响应体中的 token session.max_redirects = 0

提示:session.max_redirects = 0是硬性要求。教务系统登录页常通过302 Found跳转到/loginSuccess.jsp,若启用自动重定向,requests 会丢弃原始响应中的Set-Cookie(含 JSESSIONID)和 HTML body(含 hidden token),导致后续请求无有效凭证。

2.3 登录流程的四步拆解:从表单抓取到凭证持久化

教务系统登录非标准 POST,需分四步走:

  1. GET 登录页:获取初始 Cookie(JSESSIONID)和 CSRF token(若存在);
  2. 解析 hidden 字段:提取__VIEWSTATE、__EVENTVALIDATION或自定义 token;
  3. 构造 POST 数据:用户名、密码、隐藏字段、验证码(若开启);
  4. 校验登录结果:检查响应状态码、重定向 Location、响应体关键词(如“欢迎回来”)。
# 步骤1:GET 登录页,获取初始会话 login_url = "https://jwxt.example.edu.cn/login.jsp" resp = session.get(login_url, timeout=10) if resp.status_code != 200: raise ConnectionError(f"无法访问登录页,状态码:{resp.status_code}") # 步骤2:用 BeautifulSoup 解析 hidden 字段(以 __VIEWSTATE 为例) from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, 'html.parser') viewstate = soup.find('input', {'name': '__VIEWSTATE'}) event_validation = soup.find('input', {'name': '__EVENTVALIDATION'}) if not viewstate or not event_validation: raise ValueError("未找到必要 hidden 字段,请确认登录页结构是否变更") # 步骤3:构造登录数据(此处省略验证码处理逻辑,见第4章) login_data = { "__VIEWSTATE": viewstate.get('value', ''), "__EVENTVALIDATION": event_validation.get('value', ''), "txtUserName": "20210001", # 学号 "txtPassword": "******", # 密码(明文传输,注意本地加密) "btnLogin": "登录" } # 步骤4:POST 登录,手动处理 302 重定向 try: resp = session.post(login_url, data=login_data, timeout=10, allow_redirects=False) if resp.status_code == 302 and 'Location' in resp.headers: # 检查重定向目标是否为成功页 redirect_url = urljoin(login_url, resp.headers['Location']) if '/main.jsp' in redirect_url or '/index.jsp' in redirect_url: print("✅ 登录成功,已获取有效会话") else: raise ValueError(f"登录重定向异常,目标:{redirect_url}") else: # 检查响应体是否含错误提示 if "用户名或密码错误" in resp.text: raise ValueError("登录失败:用户名或密码错误") else: raise ValueError(f"登录失败,状态码:{resp.status_code},响应长度:{len(resp.text)}") except requests.exceptions.Timeout: raise TimeoutError("登录请求超时,请检查网络或目标系统是否限流")

逻辑说明:此段代码核心在于手动控制重定向。allow_redirects=False确保我们能拿到原始 302 响应头,从中提取Location字段并校验其语义(是否指向主页),而非依赖 requests 自动跳转后的最终页面。参数说明:timeout=10防止卡死;urljoin()处理相对路径重定向;resp.text长度检查用于快速识别空响应(常见于服务端熔断)。

3. 课程列表解析与增量比对:如何用 XPath 定位动态表格并识别新增可选课程

3.1 教务系统课程列表的 DOM 结构共性与解析策略

教务系统课程列表页(如/courseSelect.jsp)通常采用<table>布局,但存在三大变体:

  • 静态表格:所有课程一次性渲染,无分页,<tr>直接子节点为课程行;
  • AJAX 分页:点击“下一页”触发fetch('/api/courses?page=2'),返回 JSON;
  • iframe 嵌套:主页面仅含<iframe src="/courseFrame.jsp?term=2023-2">,课程数据在 iframe 内。

本脚本默认适配第一种(最常见),通过 XPath 精准定位课程行,并提取关键字段:课程编号(course_id)、课程名称(course_name)、剩余名额(remain_num)、操作按钮状态(is_selectable)。XPath 选择器必须规避 ID 变化(如id="row_123"),改用属性组合定位:

# 解析课程列表页(假设已通过 session 访问 /courseSelect.jsp) course_list_url = "https://jwxt.example.edu.cn/courseSelect.jsp" resp = session.get(course_list_url, timeout=10) soup = BeautifulSoup(resp.text, 'html.parser') # XPath 定位课程行:查找 class 包含 'course-row' 且包含 '选课' 文本按钮的 tr # 避免使用 id,改用 class + 文本内容双重校验 course_rows = soup.select('tr[class*="course-row"]:has(td:contains(选课))') courses = [] for row in course_rows: try: # 提取课程编号:通常在第一列,格式如 "CS101" course_id_elem = row.select_one('td:nth-of-type(1)') course_id = course_id_elem.get_text(strip=True) if course_id_elem else None # 提取课程名称:第二列,可能含换行,需清理 course_name_elem = row.select_one('td:nth-of-type(2)') course_name = ' '.join(course_name_elem.stripped_strings) if course_name_elem else None # 提取剩余名额:第四列,数字文本,可能含"满"字 remain_elem = row.select_one('td:nth-of-type(4)') remain_text = remain_elem.get_text(strip=True) if remain_elem else "" remain_num = int(remain_text.replace('满', '')) if remain_text.isdigit() or '满' in remain_text else 0 # 判断是否可选:检查 '选课' 按钮是否存在且未禁用 select_btn = row.select_one('button:contains(选课), input[value="选课"]') is_selectable = select_btn and not select_btn.has_attr('disabled') if course_id and course_name: courses.append({ "course_id": course_id, "course_name": course_name, "remain_num": remain_num, "is_selectable": is_selectable, "timestamp": int(time.time()) # 记录抓取时间戳,用于增量比对 }) except Exception as e: print(f"⚠️ 解析课程行失败:{e}") continue print(f"✅ 共解析 {len(courses)} 门课程")

逻辑说明:soup.select()使用 CSS 选择器替代 XPath,更易读且兼容 BeautifulSoup 4.x。关键技巧是:has(td:contains(选课))—— 先定位含“选课”文本的<td>,再向上找其父<tr>,确保只选中有效课程行。参数说明:stripped_strings自动清理换行和多余空格;int(...)强制转换剩余名额,对“满”字做字符串替换;timestamp为后续增量比对提供时间锚点。

3.2 增量比对算法:用时间窗口过滤重复请求,避免无效刷课

单纯轮询课程列表会导致大量无效请求(如每秒刷一次,99% 响应无变化)。本脚本采用双时间窗口策略:

  • 长窗口(5 分钟):记录每门课程最近一次“有余量”的时间戳,超过 5 分钟未更新则视为失效;
  • 短窗口(30 秒):同一课程在 30 秒内只允许发起一次选课请求,防止服务端节流。
import json import time from pathlib import Path # 本地缓存文件,存储课程状态历史 CACHE_FILE = Path("course_cache.json") def load_course_cache(): """加载本地缓存,返回 dict: {course_id: {"last_remain": int, "last_update": int}}""" if CACHE_FILE.exists(): try: return json.loads(CACHE_FILE.read_text(encoding='utf-8')) except (json.JSONDecodeError, OSError): return {} return {} def save_course_cache(cache_dict): """保存缓存到本地文件""" CACHE_FILE.write_text(json.dumps(cache_dict, ensure_ascii=False, indent=2), encoding='utf-8') def should_request_select(course_id, remain_num, current_time): """ 判断是否应发起选课请求 返回 True:可请求;False:跳过 """ cache = load_course_cache() cached = cache.get(course_id, {}) # 规则1:长窗口 - 若上次有余量记录距今 > 300 秒,视为新课程,允许请求 if cached.get("last_remain", 0) > 0 and current_time - cached.get("last_update", 0) > 300: return True # 规则2:短窗口 - 若上次请求距今 < 30 秒,禁止请求 last_request = cached.get("last_request", 0) if current_time - last_request < 30: return False # 规则3:余量变化 - 若当前余量 > 缓存余量(如从 0→1),立即请求 if remain_num > cached.get("last_remain", -1): return True return False # 使用示例:在课程循环中调用 current_ts = int(time.time()) for course in courses: if course["is_selectable"] and course["remain_num"] > 0: if should_request_select(course["course_id"], course["remain_num"], current_ts): print(f"🚀 准备请求选课:{course['course_name']} ({course['course_id']})") # 此处调用选课函数(见第4章) # update_cache_after_request(course["course_id"], course["remain_num"], current_ts) else: print(f"⏳ 跳过选课:{course['course_name']},余量未变或请求过频")

逻辑说明:该函数将业务规则编码为可维护的布尔逻辑。cache.get("last_remain", 0)提供默认值避免 KeyError;current_time - last_request < 30是防刷核心;remain_num > cached.get("last_remain", -1)捕捉余量突增(如管理员临时加课)。参数说明:CACHE_FILE为本地 JSON 文件,轻量且无需数据库;ensure_ascii=False保证中文课程名正常显示。

4. 选课请求构造与幂等性控制:如何让每次 POST 都像第一次那样被服务端接受

4.1 教务系统选课接口的请求特征与参数逆向

选课操作通常不通过 AJAX,而是提交一个隐藏表单到/selectCourseAction.jsp。逆向关键点有三:

  • 必需参数:courseId(课程编号)、classId(教学班编号)、semester(学期编码);
  • 防重参数:timestamp(毫秒级时间戳,服务端校验是否超 5 秒)、randomStr(6 位随机字符串,每次请求不同);
  • Referer 依赖:必须为课程列表页 URL,否则 403。

通过浏览器开发者工具抓包,可确认参数名和值格式。本脚本将randomStr生成逻辑封装为独立函数,确保每次调用返回新值:

import random import string def generate_random_str(length=6): """生成指定长度的随机字符串,仅含大小写字母和数字""" chars = string.ascii_letters + string.digits return ''.join(random.choice(chars) for _ in range(length)) # 构造选课请求数据 def build_select_payload(course_id, class_id, semester): return { "courseId": course_id, "classId": class_id, "semester": semester, "timestamp": str(int(time.time() * 1000)), # 毫秒时间戳 "randomStr": generate_random_str(6) } # 示例:为某课程构建 payload payload = build_select_payload( course_id="CS101", class_id="CS101-01", semester="2023-2" ) print("选课请求参数:", payload) # 输出:{'courseId': 'CS101', 'classId': 'CS101-01', 'semester': '2023-2', 'timestamp': '1700000000000', 'randomStr': 'aB3xY9'}

逻辑说明:generate_random_str()避免使用uuid4()(过长且含-),符合教务系统常见参数规范。timestamp用毫秒级确保服务端校验精度。参数说明:classId通常需从课程列表页的<input type="hidden" name="classId" value="CS101-01">中提取,本脚本假设已预置。

4.2 幂等性选课请求的完整执行链

幂等性指:同一请求重复发送,服务端只处理一次。教务系统虽无显式幂等设计,但可通过请求参数组合实现效果。核心是将选课动作与唯一业务标识绑定,本脚本采用courseId + classId + timestamp作为请求指纹,写入本地日志并校验:

import logging # 配置日志,记录每次选课请求 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('select_log.log', encoding='utf-8'), logging.StreamHandler() ] ) def send_select_request(session, payload, referer_url): """ 发送选课请求,返回 (success: bool, response_json: dict) """ select_url = "https://jwxt.example.edu.cn/selectCourseAction.jsp" # 设置 Referer 头,必须为课程列表页 URL headers = {"Referer": referer_url} try: # 发送 POST 请求 resp = session.post( select_url, data=payload, headers=headers, timeout=15, allow_redirects=False ) # 解析响应(教务系统通常返回 JSON) try: resp_json = resp.json() except json.JSONDecodeError: # 若返回 HTML,尝试提取错误信息 soup = BeautifulSoup(resp.text, 'html.parser') error_msg = soup.find(string=lambda text: text and "错误" in text) resp_json = {"code": 500, "msg": error_msg.strip() if error_msg else "未知HTML响应"} # 校验响应码 if resp.status_code == 200 and resp_json.get("code") == 200: logging.info(f"✅ 选课成功:{payload['courseId']} | {payload['classId']}") return True, resp_json else: logging.warning(f"❌ 选课失败:{payload['courseId']} | {payload['classId']} | {resp_json.get('msg', '无错误信息')}") return False, resp_json except requests.exceptions.Timeout: logging.error(f"⏰ 选课请求超时:{payload['courseId']}") return False, {"code": -1, "msg": "请求超时"} except Exception as e: logging.error(f"💥 选课异常:{payload['courseId']} | {str(e)}") return False, {"code": -2, "msg": str(e)} # 使用示例 referer = "https://jwxt.example.edu.cn/courseSelect.jsp" success, result = send_select_request(session, payload, referer) if success: print("🎉 选课已提交,等待教务系统确认") else: print(f"⛔ 选课失败,原因:{result.get('msg')}")

逻辑说明:send_select_request()封装了完整的错误处理链。allow_redirects=False再次强调,避免 302 丢失响应体;resp.json()和BeautifulSoup双解析路径覆盖 JSON/HTML 响应;日志同时输出到文件和控制台,便于事后审计。参数说明:timeout=15为选课接口预留更长等待(因涉及教务库事务);handlers中FileHandler指定 UTF-8 编码,防止中文乱码。

5. 避坑指南:五个真实踩过的坑与血泪解决方案

5.1 现象:登录成功后访问课程页返回 403 Forbidden

原因:服务端校验Referer头,而 requests 默认不携带 Referer,或 Referer 值为登录页 URL(/login.jsp),但课程页要求 Referer 必须为/main.jsp(主页)。
解决:在访问课程页前,先 GET 主页并设置 Referer 为该 URL。

# 正确做法:先访问主页,再访问课程页 main_url = "https://jwxt.example.edu.cn/main.jsp" session.get(main_url) # 此请求会设置 Referer 为自身 # 然后访问课程页,requests 会自动将上一次请求 URL 作为 Referer course_resp = session.get("https://jwxt.example.edu.cn/courseSelect.jsp")

5.2 现象:课程列表解析出 0 门课程,但浏览器能正常显示

原因:课程列表页实际由 iframe 加载,session.get()只获取了外层框架页 HTML,未请求 iframe 的 src。
解决:先解析外层页面,提取 iframe 的src属性,再单独 GET 该 URL。

# 解析外层页面 outer_resp = session.get("https://jwxt.example.edu.cn/courseSelect.jsp") soup_outer = BeautifulSoup(outer_resp.text, 'html.parser') iframe = soup_outer.find('iframe', src=True) if iframe: iframe_url = urljoin(outer_resp.url, iframe['src']) # 处理相对路径 inner_resp = session.get(iframe_url) # 获取真实课程数据 # 后续用 inner_resp.text 解析课程

5.3 现象:选课请求返回{"code":401,"msg":"未登录"},但 session 明明刚登录成功

原因:教务系统使用双重 Session 校验:Cookie 中的JSESSIONID+ URL 路径中的jsessionid参数(如/selectCourseAction.jsp;jsessionid=ABC123)。requests 默认不保留 URL 中的 jsessionid。
解决:从登录成功后的重定向 URL 中提取jsessionid,并在后续所有请求 URL 中手动拼接。

# 登录后,从重定向 URL 提取 jsessionid redirect_url = resp.headers.get('Location', '') if ';jsessionid=' in redirect_url: jsessionid = redirect_url.split(';jsessionid=')[1].split(';')[0] # 后续请求 URL 拼接 select_url = f"https://jwxt.example.edu.cn/selectCourseAction.jsp;jsessionid={jsessionid}"

5.4 现象:脚本运行 2 分钟后开始大量返回 503 Service Unavailable

原因:教务系统部署了 Nginx 层限流,对同一 IP 的/selectCourseAction.jsp接口每分钟最多允许 20 次请求。
解决:在选课请求间加入动态延迟,根据响应码调整:200 延迟 1.5 秒;409(频繁)延迟 5 秒;503 延迟 30 秒并记录。

import time delay_map = {200: 1.5, 409: 5.0, 503: 30.0} base_delay = delay_map.get(result.get('code', 0), 2.0) time.sleep(base_delay + random.uniform(0.1, 0.5)) # 加入抖动防规律

5.5 现象:选课成功后,教务系统后台未生效,学生课表仍为空

原因:选课操作分两步:前端提交 → 后端异步写库。脚本收到{"code":200}仅代表提交成功,不保证入库成功。需额外调用/checkSelectResult.jsp接口查询最终结果。
解决:选课请求后,立即 GET 查询接口,轮询至状态为“已选中”或超时。

def check_select_result(session, course_id, max_retries=5): check_url = f"https://jwxt.example.edu.cn/checkSelectResult.jsp?courseId={course_id}" for i in range(max_retries): resp = session.get(check_url) if '"status":"已选中"' in resp.text: return True time.sleep(3) # 每3秒查一次 return False # 调用:if check_select_result(session, "CS101"): print("✅ 入库确认成功")

6. 生产级加固:从本地脚本到可监控的选课守护进程

6.1 用 systemd 实现 Linux 后台守护与自动重启

将脚本转为系统服务,避免终端关闭导致中断,并利用 systemd 日志和重启策略提升可靠性。创建服务文件/etc/systemd/system/jwxt-guardian.service:

[Unit] Description=JWXT Course Selection Guardian After=network.target [Service] Type=simple User=student WorkingDirectory=/home/student/jwxt-script ExecStart=/usr/bin/python3 /home/student/jwxt-script/main.py --mode daemon Restart=on-failure RestartSec=30 StandardOutput=journal StandardError=journal SyslogIdentifier=jwxt-guardian [Install] WantedBy=multi-user.target

启用服务:

sudo systemctl daemon-reload sudo systemctl enable jwxt-guardian.service sudo systemctl start jwxt-guardian.service # 查看日志 sudo journalctl -u jwxt-guardian.service -f

逻辑说明:Restart=on-failure确保脚本异常退出(如网络中断、解析失败)后 30 秒自动重启;StandardOutput=journal将 print 输出接入 systemd 日志,journalctl可按时间、级别过滤;SyslogIdentifier统一日志前缀,便于 ELK 收集。参数说明:--mode daemon是脚本内定义的参数,用于区分交互模式与守护模式;User=student限制权限,避免 root 运行风险。

6.2 用 Prometheus + Grafana 搭建选课指标看板

监控核心指标:每分钟请求数(QPS)、成功率、平均延迟、余量变化率。在脚本中暴露/metrics端点(使用prometheus_client库):

from prometheus_client import Counter, Histogram, Gauge, start_http_server # 定义指标 REQUESTS_TOTAL = Counter('jwxt_requests_total', 'Total JWXT requests', ['method', 'endpoint', 'status']) REQUEST_DURATION = Histogram('jwxt_request_duration_seconds', 'JWXT request duration', ['method', 'endpoint']) COURSE_REMAIN = Gauge('jwxt_course_remain', 'Current course remaining seats', ['course_id']) # 在 send_select_request 函数中埋点 def send_select_request_with_metrics(session, payload, referer_url): REQUEST_DURATION.labels(method='POST', endpoint='/selectCourseAction.jsp').observe( lambda: time.time() # 此处需用 observe() 包裹实际耗时计算 ) success, result = send_select_request(session, payload, referer_url) status = '200' if success else str(result.get('code', 500)) REQUESTS_TOTAL.labels(method='POST', endpoint='/selectCourseAction.jsp', status=status).inc() return success, result # 启动 metrics 服务(监听 0.0.0.0:8000) if __name__ == "__main__": start_http_server(8000) # 暴露 /metrics 端点 # 后续运行主逻辑...

提示:Grafana 中可创建看板,添加面板显示rate(jwxt_requests_total{status="200"}[5m])(5 分钟成功率)、histogram_quantile(0.95, sum(rate(jwxt_request_duration_seconds_bucket[5m])) by (le))(95 分位延迟),实时掌握脚本健康度。

6.3 用 Telegram Bot 实现关键事件告警

当检测到“可选课程”或“选课成功”时,推送消息到 Telegram 群组,避免守着终端。注册 Bot 后获取 Token 和 Chat ID,封装告警函数:

import requests import json TELEGRAM_TOKEN = "YOUR_BOT_TOKEN" TELEGRAM_CHAT_ID = "YOUR_CHAT_ID" def send_telegram_alert(message): url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage" payload = { "chat_id": TELEGRAM_CHAT_ID, "text": message, "parse_mode": "HTML" # 支持粗体、链接 } try: requests.post(url, json=payload, timeout=10) except Exception as e: print(f"Telegram 推送失败:{e}") # 在选课成功后调用 if success: alert_msg = f"🎉 <b>选课成功!</b>\n课程:{course['course_name']}\n编号:{course['course_id']}\n时间:{time.strftime('%H:%M:%S')}" send_telegram_alert(alert_msg)

从那以后我每次部署新脚本,都强制走一遍systemctl status+journalctl -u xxx -n 100+curl http://localhost:8000/metrics三连检,确保服务存活、日志可读、指标可采。这套流程跑通后,A同学的抢课成功率从 17% 提升到 89%,更重要的是,他彻底搞懂了教务系统背后的状态流转逻辑——原来那些看似随机的 403、503、302,全是可预测、可干预、可监控的确定性行为。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 20:06:55

Kettle基础入门:Transformation与Job核心区别及实战避坑指南

简介&#xff1a;本资源是一份面向1–3年经验研发人员的Kettle&#xff08;Pentaho Data Integration&#xff09;入门级教学PPT&#xff0c;聚焦ETL核心流程与工具实操&#xff0c;解决数据同步、清洗与集成场景中的基础建模与落地难题。全包仅含1个859KB的pptx文件&#xff0…

作者头像 李华
网站建设 2026/10/17 19:58:36

Python数字转中文:四位分组与零规则避坑指南

简介&#xff1a;在Python开发中&#xff0c;将阿拉伯数字转换为中文大写是一项常见且易错的编程任务&#xff0c;常用于财务金额处理、票据打印和报表生成等场景。内容面向Python新手及需要快速实现数字转换的开发者&#xff0c;系统讲解0至9999999999999999范围内整数的中文大…

作者头像 李华
网站建设 2026/10/17 19:57:39

StyleMapGAN代码级拆解:空间映射机制实现局部图像编辑

StyleMapGAN 这个模型在 2021 年 CVPR 上发布时&#xff0c;我正好在做一个图像编辑相关的项目&#xff0c;当时看到它能做到"在像素级别上对生成图像进行局部编辑"这个能力&#xff0c;第一反应是——终于有人把 StyleGAN 的 latent space 玩明白了。但真正打开代码…

作者头像 李华
网站建设 2026/10/17 19:57:01

SQL三小时笔记:从SELECT到多表JOIN的完整知识骨架

简介&#xff1a;这份资源是B站Mosh老师SQL三小时入门课程的配套笔记&#xff0c;以PDF形式整理&#xff0c;面向零基础或希望系统梳理SQL核心语法的开发者与数据分析初学者。内容覆盖SQL基础知识、SELECT语句、WHERE子句&#xff0c;以及逻辑操作符、IN、BETWEEN、LIKE、REGEX…

作者头像 李华