From d1dc08a16de4c876192b5ee4822c96c1c688c001 Mon Sep 17 00:00:00 2001 From: ChuXun <70203584+ChuXunYu@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:39:01 +0800 Subject: [PATCH] 1 --- BUG修复_邮件课程列表不正确.md | 213 +++++++++++++++++ diagnose.sh | 154 +++++++++++++ monitor.py | 242 +++++++++++++++++--- readme.md | 39 ++++ 修复说明_2026-01-21.md | 202 ++++++++++++++++ 修复说明_2026-01-22_重复日志和自动重登录.md | 94 ++++++++ 常见问题解决.md | 95 ++++++++ 快速更新指南.md | 179 +++++++++++++++ 打包.sh | 48 +++- 故障排查指南.md | 143 ++++++++++++ 重启服务.md | 43 +++- 间歇性解析失败说明.md | 194 ++++++++++++++++ 12 files changed, 1607 insertions(+), 39 deletions(-) create mode 100644 BUG修复_邮件课程列表不正确.md create mode 100644 diagnose.sh create mode 100644 修复说明_2026-01-21.md create mode 100644 修复说明_2026-01-22_重复日志和自动重登录.md create mode 100644 快速更新指南.md create mode 100644 故障排查指南.md create mode 100644 间歇性解析失败说明.md diff --git a/BUG修复_邮件课程列表不正确.md b/BUG修复_邮件课程列表不正确.md new file mode 100644 index 0000000..7b3d224 --- /dev/null +++ b/BUG修复_邮件课程列表不正确.md @@ -0,0 +1,213 @@ +# BUG修复:邮件通知课程列表不正确 + +## 问题描述 + +**症状:** +- 日志显示检测到2门新增课程: + - 马克思主义基本原理 (A3505000018) + - 软件需求分析与设计 (A0801050220) + +- 但邮件通知显示的是3门**完全不同**的课程: + - C++程序设计 + - 体育(三) 乒乓球 + - 概率论与数理统计 + +## 根本原因 + +代码逻辑错误: + +```python +# 错误的实现 +if self.check_grade_changes(grade_info, grade_html): + courses_file = self.script_dir / '.last_courses.txt' + if courses_file.exists(): + # ❌ 发送的是当前所有课程的前3门,而不是新增课程 + self.send_email_notification(current_courses[:3]) +``` + +### 问题分析 + +1. `check_grade_changes` 函数内部计算了新增课程列表 `new_courses` +2. 但函数只返回 `True/False`,**没有返回新增课程列表** +3. 主循环中使用了 `current_courses[:3]`(当前所有课程的前3门) +4. 导致邮件中显示的课程与实际新增的课程不一致 + +### 举例说明 + +**场景:** +- 之前有6门课程:A、B、C、D、E、F +- 新增2门课程:G、H +- 当前总共8门课程:A、B、C、D、E、F、G、H + +**错误行为:** +- 日志显示:新增2门(G、H)✓ 正确 +- 邮件显示:A、B、C(前3门)❌ 错误 + +**期望行为:** +- 邮件显示:G、H ✓ 正确 + +## 修复方案 + +### 1. 修改 `check_grade_changes` 函数签名 + +```python +# 修改前 +def check_grade_changes(self, current_content: str, current_html: str = None) -> bool: + +# 修改后 +def check_grade_changes(self, current_content: str, current_html: str = None) -> tuple: + """检查成绩是否有变化 + + Returns: + tuple: (是否有变化: bool, 新增课程列表: list) + """ +``` + +### 2. 返回新增课程列表 + +```python +# 修改前 +if new_courses: + # ... 记录日志 ... + return True + +# 修改后 +if new_courses: + # ... 记录日志 ... + return (True, new_courses) # 返回元组 + +# 其他情况 +return (False, []) # 无变化 +return (True, []) # 有变化但无新增课程(成绩更新) +``` + +### 3. 更新调用代码 + +```python +# 修改前 +if self.check_grade_changes(grade_info, grade_html): + self.send_email_notification(current_courses[:3]) # ❌ 错误 + +# 修改后 +has_changes, new_courses = self.check_grade_changes(grade_info, grade_html) + +if has_changes: + if new_courses: + # 有新增课程,发送真正的新增课程列表 + self.send_email_notification(new_courses) # ✅ 正确 + else: + # 没有新增课程但内容变化(成绩更新),发送通用通知 + self.send_email_notification() +``` + +## 修复效果 + +### 修复前 +``` +日志: +[INFO] ✓ 检测到新增课程成绩!共 2 门 +[INFO] 1. [2025-2026 秋季] 马克思主义基本原理 (A3505000018) +[INFO] 2. [2025-2026 秋季] 软件需求分析与设计 (A0801050220) + +邮件: +新增课程(共3门): + - 2025-2026 秋季 C++程序设计 ❌ 错误! + - 2025-2026 秋季 体育(三) 乒乓球 ❌ 错误! + - 2025-2026 秋季 概率论与数理统计 ❌ 错误! +``` + +### 修复后 +``` +日志: +[INFO] ✓ 检测到新增课程成绩!共 2 门 +[INFO] 1. [2025-2026 秋季] 马克思主义基本原理 (A3505000018) +[INFO] 2. [2025-2026 秋季] 软件需求分析与设计 (A0801050220) + +邮件: +新增课程(共2门): + - 2025-2026 秋季 马克思主义基本原理 ✅ 正确! + - 2025-2026 秋季 软件需求分析与设计 ✅ 正确! +``` + +## 测试建议 + +### 1. 单元测试场景 + +**场景1:新增课程** +- 之前:3门课程 +- 现在:5门课程(新增2门) +- 期望:邮件显示新增的2门 + +**场景2:成绩更新** +- 之前:5门课程 +- 现在:5门课程(某门成绩变化) +- 期望:邮件显示通用通知 + +**场景3:无变化** +- 之前:5门课程 +- 现在:5门课程(完全相同) +- 期望:不发送邮件 + +### 2. 集成测试 + +```bash +# 1. 删除历史记录(模拟首次运行) +cd ~/grade_monitor +rm .last_* + +# 2. 运行测试 +python3 monitor.py --test + +# 3. 再次运行(模拟检测到变化) +# 手动编辑 .last_courses.txt,删除几行 +python3 monitor.py --test + +# 4. 检查邮件内容是否正确 +``` + +## 影响评估 + +### 向后兼容性 +- ✅ 完全兼容:函数返回元组,Python 会自动解包 +- ✅ 不影响测试模式:test_fetch 不调用此函数 +- ✅ 不影响旧版数据:历史文件格式不变 + +### 风险等级 +- 🟢 **低风险**:仅修改返回值类型 +- 🟢 所有调用点已更新 +- 🟢 已通过语法检查 + +## 部署步骤 + +```bash +# 1. 本地打包 +cd /mnt/e/50425/Documents/Github/GPA_Monitoring +./打包.sh + +# 2. 上传到服务器 +scp gpa_monitor.tar.gz user@server:~/ + +# 3. 服务器上更新 +systemctl stop grade-monitor +cd ~/grade_monitor +cp monitor.py monitor.py.backup.$(date +%Y%m%d_%H%M%S) +tar -xzf ~/gpa_monitor.tar.gz +systemctl restart grade-monitor + +# 4. 验证 +tail -f monitor.log +``` + +## 后续优化建议 + +1. **添加单元测试** - 测试 `check_grade_changes` 函数 +2. **记录邮件内容** - 在日志中记录发送的课程列表 +3. **邮件确认机制** - 发送成功后记录一份副本 + +--- + +**BUG编号:** #001 +**严重程度:** 高(数据不一致) +**修复日期:** 2026-01-21 +**修复人员:** GitHub Copilot +**测试状态:** 待用户验证 diff --git a/diagnose.sh b/diagnose.sh new file mode 100644 index 0000000..095422d --- /dev/null +++ b/diagnose.sh @@ -0,0 +1,154 @@ +#!/bin/bash +# 快速修复成绩提取失败问题 + +echo "=========================================" +echo "成绩监控系统 - 故障诊断与修复工具" +echo "=========================================" +echo "" + +cd ~/grade_monitor || cd "$(dirname "$0")" + +# 1. 检查 BeautifulSoup 安装 +echo "1. 检查依赖库..." +if python3 -c "import bs4" 2>/dev/null; then + echo " ✓ BeautifulSoup4 已安装" +else + echo " ✗ BeautifulSoup4 未安装,正在安装..." + pip3 install beautifulsoup4 +fi + +if python3 -c "import requests" 2>/dev/null; then + echo " ✓ requests 已安装" +else + echo " ✗ requests 未安装,正在安装..." + pip3 install requests +fi + +echo "" + +# 2. 运行测试模式 +echo "2. 运行诊断测试..." +echo " 执行: python3 monitor.py --test --debug" +echo "=========================================" +python3 monitor.py --test --debug 2>&1 | tee test_output.log +echo "=========================================" +echo "" + +# 3. 检查输出 +echo "3. 分析测试结果..." + +if grep -q "找到成绩表格" test_output.log; then + echo " ✓ 成功找到成绩表格" +else + echo " ✗ 未找到成绩表格" +fi + +if grep -q "共解析到.*门课程" test_output.log; then + COURSE_COUNT=$(grep "共解析到.*门课程" test_output.log | tail -1 | grep -oP '\d+') + if [ "$COURSE_COUNT" -gt 0 ]; then + echo " ✓ 成功解析 $COURSE_COUNT 门课程" + else + echo " ✗ 解析到 0 门课程" + fi +else + echo " ✗ 无法解析课程信息" +fi + +if grep -q "检测到登录页面\|LOGIN_REQUIRED" test_output.log; then + echo " ⚠ 检测到登录失败,可能需要检查账号密码" +fi + +echo "" + +# 4. 检查调试文件 +echo "4. 检查生成的调试文件..." +if [ -f "debug_page.html" ]; then + echo " ✓ 发现 debug_page.html" + FILE_SIZE=$(stat -f%z "debug_page.html" 2>/dev/null || stat -c%s "debug_page.html" 2>/dev/null) + echo " 文件大小: $FILE_SIZE 字节" + + if grep -qi "gridtable\|dataList" debug_page.html; then + echo " ✓ HTML 中包含成绩表格标签" + else + echo " ✗ HTML 中未找到预期的表格标签" + echo " 建议: 手动检查 debug_page.html 文件" + fi +fi + +if [ -f ".last_grade_content.txt" ]; then + echo " ✓ 发现 .last_grade_content.txt" + LINES=$(wc -l < .last_grade_content.txt) + echo " 内容行数: $LINES" +fi + +echo "" + +# 5. 提供建议 +echo "5. 修复建议:" +echo "=========================================" + +if ! python3 -c "import bs4" 2>/dev/null; then + echo "❌ 缺少 beautifulsoup4 库" + echo " 修复: pip3 install beautifulsoup4" + echo "" +fi + +if grep -q "共解析到 0 门课程" test_output.log; then + echo "❌ 无法解析课程信息" + echo " 可能原因:" + echo " 1. 登录失效 - 检查 config.ini 中的账号密码" + echo " 2. 网页结构变化 - 查看 debug_page.html" + echo " 3. 网络问题 - 检查是否能访问学校网站" + echo " 4. 临时性问题 - 程序会自动重试" + echo "" + echo " 下一步:" + echo " - 查看详细日志: cat test_output.log" + echo " - 查看HTML: cat debug_page.html | head -50" + echo " - 阅读排查指南: cat 故障排查指南.md" + echo "" +fi + +# 检查间歇性错误 +if [ -f "monitor.log" ]; then + FAILED_COUNT=$(grep -c "未找到成绩表格\|共解析到 0 门课程" monitor.log 2>/dev/null || echo "0") + SUCCESS_COUNT=$(grep -c "共解析到 [1-9]" monitor.log 2>/dev/null || echo "0") + + if [ "$FAILED_COUNT" -gt 0 ] && [ "$SUCCESS_COUNT" -gt 0 ]; then + FAILURE_RATE=$((FAILED_COUNT * 100 / (FAILED_COUNT + SUCCESS_COUNT))) + echo "⚠️ 检测到间歇性解析失败" + echo " 成功次数: $SUCCESS_COUNT" + echo " 失败次数: $FAILED_COUNT" + echo " 失败率: ${FAILURE_RATE}%" + echo "" + if [ "$FAILURE_RATE" -lt 20 ]; then + echo " ✓ 失败率较低(<20%),属于正常范围" + echo " 原因: 网络波动或服务器临时问题" + echo " 处理: 程序已自动重试,无需干预" + else + echo " ✗ 失败率偏高(>20%),需要检查" + echo " 建议: 检查网络连接和登录状态" + fi + echo "" + fi +fi + +if grep -q "找到.*行成绩数据" test_output.log && grep -q "共解析到.*门课程" test_output.log; then + COURSE_COUNT=$(grep "共解析到.*门课程" test_output.log | tail -1 | grep -oP '\d+') + if [ "$COURSE_COUNT" -gt 0 ]; then + echo "✅ 系统工作正常!" + echo " 成功解析了 $COURSE_COUNT 门课程" + echo "" + echo " 如果服务未运行,启动服务:" + echo " systemctl start grade-monitor" + echo "" + echo " 查看服务状态:" + echo " systemctl status grade-monitor" + echo "" + fi +fi + +echo "=========================================" +echo "" +echo "测试日志已保存到: test_output.log" +echo "如需帮助,请查看: 故障排查指南.md" +echo "" diff --git a/monitor.py b/monitor.py index 636aa76..2b5089c 100644 --- a/monitor.py +++ b/monitor.py @@ -27,18 +27,31 @@ from typing import Optional, Dict import signal # 配置日志 -logging.basicConfig( - level=logging.INFO, - format='[%(levelname)s] [%(asctime)s] %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - handlers=[ - logging.FileHandler('monitor.log', encoding='utf-8'), - logging.StreamHandler() - ] -) - logger = logging.getLogger(__name__) +# 避免重复添加处理器 +if not logger.handlers: + logger.setLevel(logging.INFO) + + # 设置日志格式 + formatter = logging.Formatter( + '[%(levelname)s] [%(asctime)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # 文件处理器 + file_handler = logging.FileHandler('monitor.log', encoding='utf-8') + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # 防止日志传播到根日志记录器 + logger.propagate = False + class GradeMonitor: """成绩监控类""" @@ -291,6 +304,11 @@ class GradeMonitor: result = [] + # 检查是否是登录页面 + if soup.find('input', {'name': 'username'}) or soup.find('input', {'name': 'password'}): + logger.error("检测到登录页面,可能需要重新登录") + return "LOGIN_REQUIRED" + # 提取总平均绩点 gpa_div = soup.find('div', string=lambda x: x and '总平均绩点' in x) if gpa_div: @@ -299,21 +317,37 @@ class GradeMonitor: result.append(gpa_text) result.append("=" * 60) result.append("") + logger.debug(f"找到总平均绩点: {gpa_text}") - # 提取成绩表格 + # 尝试多种方式查找成绩表格 table = soup.find('table', {'class': 'gridtable'}) + if not table: + table = soup.find('table', {'id': 'dataList'}) + if not table: + # 尝试查找任何包含成绩相关标题的表格 + for tbl in soup.find_all('table'): + headers = tbl.find_all('th') + if headers and any('课程名称' in th.get_text() or '成绩' in th.get_text() for th in headers): + table = tbl + logger.info("通过表头关键词找到成绩表格") + break + if table: + logger.debug("找到成绩表格") # 提取表头 thead = table.find('thead') if thead: headers = [th.get_text(strip=True) for th in thead.find_all('th')] result.append(' | '.join(headers)) result.append("-" * 120) + logger.debug(f"表头: {headers}") # 提取每一行成绩 tbody = table.find('tbody') if tbody: - for row in tbody.find_all('tr'): + rows = tbody.find_all('tr') + logger.debug(f"找到 {len(rows)} 行成绩数据") + for row in rows: cells = row.find_all('td') if cells: row_data = [] @@ -322,11 +356,28 @@ class GradeMonitor: text = cell.get_text(strip=True, separator=' ') row_data.append(text) result.append(' | '.join(row_data)) + else: + logger.warning("未找到成绩表格") if not result: - logger.warning("未能提取到成绩信息,返回原始文本") + logger.warning("未能提取到成绩信息,页面可能结构异常") + # 保存HTML以便调试 + debug_file = self.script_dir / 'debug_page.html' + debug_file.write_text(html, encoding='utf-8') + logger.info(f"已保存HTML到 {debug_file} 供调试") + + # 记录HTML摘要用于诊断 + html_preview = html[:500].replace('\n', ' ') + logger.debug(f"HTML前500字符: {html_preview}") + + # 检查是否是网络错误或空页面 + if len(html) < 100: + logger.warning(f"HTML内容过短({len(html)}字节),可能是网络问题") + return "NETWORK_ERROR" + return self._fallback_extract(html) + logger.info(f"成功提取成绩信息,共 {len(result)} 行") return '\n'.join(result) except ImportError: @@ -346,9 +397,27 @@ class GradeMonitor: def parse_courses(self, grade_text: str) -> list: """解析成绩文本,提取课程列表""" + # 检查是否需要重新登录 + if grade_text == "LOGIN_REQUIRED": + logger.warning("检测到登录失效,需要重新登录") + return [] + + # 检查是否是网络错误 + if grade_text == "NETWORK_ERROR": + logger.warning("检测到网络错误,返回空列表") + return [] + courses = [] lines = grade_text.split('\n') + # 检查是否是原始HTML(未成功解析的标记) + if ' str: """计算内容的哈希值""" return hashlib.md5(content.encode('utf-8')).hexdigest() - def check_grade_changes(self, current_content: str, current_html: str = None) -> bool: - """检查成绩是否有变化""" + def check_grade_changes(self, current_content: str, current_html: str = None) -> tuple: + """检查成绩是否有变化 + + Returns: + tuple: (是否有变化: bool, 新增课程列表: list) + """ # 保存当前内容(供用户查看) self.last_grade_content_file.write_text(current_content, encoding='utf-8') if current_html: @@ -400,7 +475,7 @@ class GradeMonitor: # 计算哈希 current_hash = self.calculate_hash(current_content) self.last_grade_file.write_text(current_hash, encoding='utf-8') - return False + return (False, []) # 读取上次的课程列表 courses_file = self.script_dir / '.last_courses.txt' @@ -434,7 +509,7 @@ class GradeMonitor: courses_file.write_text('\n'.join(current_courses), encoding='utf-8') self.last_grade_file.write_text(current_hash, encoding='utf-8') - return True + return (True, new_courses) elif content_changed and current_courses: # 课程数量没变,但内容变了(可能是成绩更新) logger.info("=" * 60) @@ -446,10 +521,10 @@ class GradeMonitor: # 更新哈希值 self.last_grade_file.write_text(current_hash, encoding='utf-8') - return True + return (True, []) # 内容变化但没有新增课程 else: logger.info(f"成绩无变化(共 {len(current_courses)} 门课程)") - return False + return (False, []) def send_email_notification(self, new_courses: list = None) -> bool: """发送邮件通知""" @@ -641,10 +716,21 @@ class GradeMonitor: consecutive_errors += 1 logger.error(f"获取成绩页面失败,等待下次检查 (连续失败: {consecutive_errors}/{max_consecutive_errors})") + # 连续失败3次尝试重新登录 + if consecutive_errors >= 3: + logger.warning("⚠️ 尝试重新登录以解决页面获取问题...") + if self.login(): + logger.info("✓ 重新登录成功,重置错误计数") + consecutive_errors = 0 # 重置错误计数 + time.sleep(5) + continue + else: + logger.error("✗ 重新登录失败") + if consecutive_errors >= max_consecutive_errors: error_msg = f"连续 {consecutive_errors} 次获取成绩页面失败" logger.error(error_msg) - self.send_error_notification(error_msg) + self.send_error_notification(f"{error_msg}\n\n可能需要检查网络或账号状态") consecutive_errors = 0 # 重置计数,避免重复发送 time.sleep(self.config['check_interval']) @@ -653,15 +739,104 @@ class GradeMonitor: # 提取成绩信息 grade_info = self.extract_grade_info(grade_html) - # 检查变化 - if self.check_grade_changes(grade_info, grade_html): - # 获取新增课程列表 + # 检查是否需要重新登录 + if grade_info == "LOGIN_REQUIRED": + consecutive_errors += 1 + logger.warning(f"检测到会话过期,尝试重新登录... (连续失败: {consecutive_errors}/{max_consecutive_errors})") + + if self.login(): + logger.info("重新登录成功,继续监控") + consecutive_errors = 0 # 重置错误计数 + else: + logger.error("重新登录失败") + + if consecutive_errors >= max_consecutive_errors: + error_msg = f"连续 {consecutive_errors} 次登录失败,会话可能已过期" + logger.error(error_msg) + self.send_error_notification(error_msg) + consecutive_errors = 0 + + time.sleep(self.config['check_interval']) + continue + + # 检查是否是网络错误(临时性问题) + if grade_info == "NETWORK_ERROR": + consecutive_errors += 1 + logger.warning(f"检测到网络错误或页面过短,可能是临时问题 (连续失败: {consecutive_errors}/{max_consecutive_errors})") + + # 连续失败3次尝试重新登录 + if consecutive_errors >= 3: + logger.warning("⚠️ 尝试重新登录以解决网络问题...") + if self.login(): + logger.info("✓ 重新登录成功,重置错误计数") + consecutive_errors = 0 # 重置错误计数 + time.sleep(5) + continue + else: + logger.error("✗ 重新登录失败") + + if consecutive_errors >= max_consecutive_errors: + error_msg = f"连续 {consecutive_errors} 次网络错误" + logger.error(error_msg) + self.send_error_notification(f"{error_msg}\n\n可能需要检查网络连接或服务器状态") + consecutive_errors = 0 + else: + # 临时错误,短暂等待后重试 + logger.info(f"等待 30 秒后重试...") + time.sleep(30) + continue + + # 解析课程列表 + current_courses = self.parse_courses(grade_info) + + # 如果解析失败(0门课程)且之前有课程,可能是临时问题 + if len(current_courses) == 0: + # 检查是否之前有课程记录 courses_file = self.script_dir / '.last_courses.txt' if courses_file.exists(): - current_courses = self.parse_courses(grade_info) - # 简单通知有新课程 - self.send_email_notification(current_courses[:3]) # 只显示前3门 + try: + previous_courses = courses_file.read_text(encoding='utf-8').strip().split('\n') + previous_courses = [c for c in previous_courses if c] + + if len(previous_courses) > 0: + consecutive_errors += 1 + logger.warning(f"⚠️ 解析到0门课程,但之前有{len(previous_courses)}门课程,可能是临时问题") + logger.warning(f"连续异常: {consecutive_errors}/{max_consecutive_errors}") + + if consecutive_errors >= 3: # 连续3次解析失败尝试重新登录 + error_msg = f"连续 {consecutive_errors} 次无法解析课程(之前有{len(previous_courses)}门课程)" + logger.error(error_msg) + + # 尝试重新登录 + logger.warning("⚠️ 尝试重新登录以解决解析问题...") + if self.login(): + logger.info("✓ 重新登录成功,重置错误计数") + consecutive_errors = 0 # 重置错误计数 + time.sleep(5) # 等待一下再继续 + continue + else: + logger.error("✗ 重新登录失败") + if consecutive_errors >= max_consecutive_errors: + self.send_error_notification(f"{error_msg}\n\n重新登录失败,可能需要人工介入") + consecutive_errors = 0 + else: + # 短暂等待后重试 + logger.info(f"等待 30 秒后重试...") + time.sleep(30) + continue + except Exception as e: + logger.debug(f"检查历史课程时出错: {e}") + + # 检查变化 + has_changes, new_courses = self.check_grade_changes(grade_info, grade_html) + + if has_changes: + if new_courses: + # 有新增课程,发送包含课程列表的通知 + logger.info(f"共解析到 {len(current_courses)} 门课程") + self.send_email_notification(new_courses) else: + # 没有新增课程但内容变化(成绩更新),发送通用通知 self.send_email_notification() # 成功执行,重置错误计数 @@ -709,6 +884,9 @@ def main(): # 测试模式(获取一次成绩并显示内容) python3 monitor.py --test + # 调试模式(显示详细日志) + python3 monitor.py --debug + # 查看保存的成绩内容 cat .last_grade_content.txt @@ -723,6 +901,12 @@ def main(): help='测试模式:仅获取一次成绩页面并显示内容,不进行监控' ) + parser.add_argument( + '--debug', + action='store_true', + help='调试模式:显示详细的调试信息' + ) + parser.add_argument( '--config', default='config.ini', @@ -731,6 +915,12 @@ def main(): args = parser.parse_args() + # 如果启用调试模式,调整日志级别 + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + logger.info("已启用调试模式") + monitor = GradeMonitor(config_file=args.config, test_mode=args.test) monitor.run() diff --git a/readme.md b/readme.md index 550a8e8..d2a1c98 100644 --- a/readme.md +++ b/readme.md @@ -106,6 +106,44 @@ cat .last_grade_content.txt ## 故障排查 +### 快速诊断工具 + +如果遇到问题,使用诊断脚本快速检查: +```bash +chmod +x diagnose.sh +./diagnose.sh +``` + +该脚本会自动检查: +- 依赖库是否安装 +- 能否成功提取成绩 +- 调试文件内容 +- 并提供修复建议 + +### 问题:未能提取到成绩信息(共 0 门课程) + +**症状:** 日志显示 `[WARNING] 未能提取到成绩信息,返回原始文本` + +**快速修复:** +```bash +# 1. 检查并安装依赖 +pip3 install beautifulsoup4 requests + +# 2. 运行调试模式 +python3 monitor.py --test --debug + +# 3. 查看详细信息 +cat .last_grade_content.txt +cat debug_page.html | head -50 +``` + +**可能原因:** +1. BeautifulSoup 库未安装 +2. 登录会话失效 +3. 网页结构变化 + +**详细解决方案:** 查看 [故障排查指南.md](故障排查指南.md) + ### 问题:登录失败 **解决:** @@ -125,6 +163,7 @@ cat .last_grade_content.txt **解决:** - 程序会自动增加等待时间 - 可在 `config.ini` 中增大 `CHECK_INTERVAL` +- 详见 [常见问题解决.md](常见问题解决.md) ## 配置说明 diff --git a/修复说明_2026-01-21.md b/修复说明_2026-01-21.md new file mode 100644 index 0000000..00fd6c0 --- /dev/null +++ b/修复说明_2026-01-21.md @@ -0,0 +1,202 @@ +# 问题修复说明 + +## 问题描述 + +服务器运行时出现以下错误: +``` +[WARNING] 未能提取到成绩信息,返回原始文本 +[INFO] 成绩无变化(共 0 门课程) +``` + +## 根本原因 + +成绩提取函数 `extract_grade_info` 在某些情况下无法正确解析HTML页面,返回原始HTML文本,导致 `parse_courses` 函数无法从原始HTML中提取课程信息。 + +可能的具体原因包括: +1. BeautifulSoup 库未安装 +2. 登录会话失效,返回的是登录页面 +3. 学校网页结构发生变化 +4. 网络问题导致获取的HTML不完整 + +## 修复内容 + +### 1. 改进 `extract_grade_info` 函数 (monitor.py 第286-370行) + +**新增功能:** +- ✅ 检测登录页面,返回特殊标记 `LOGIN_REQUIRED` +- ✅ 支持多种表格选择器(`gridtable`, `dataList`, 关键词搜索) +- ✅ 添加详细的调试日志(表头、行数等) +- ✅ 自动保存问题HTML到 `debug_page.html` 便于排查 +- ✅ 提供成功提取的反馈信息 + +**改进代码片段:** +```python +# 检查是否是登录页面 +if soup.find('input', {'name': 'username'}) or soup.find('input', {'name': 'password'}): + logger.error("检测到登录页面,可能需要重新登录") + return "LOGIN_REQUIRED" + +# 尝试多种方式查找成绩表格 +table = soup.find('table', {'class': 'gridtable'}) +if not table: + table = soup.find('table', {'id': 'dataList'}) +if not table: + # 尝试查找任何包含成绩相关标题的表格 + for tbl in soup.find_all('table'): + headers = tbl.find_all('th') + if headers and any('课程名称' in th.get_text() or '成绩' in th.get_text() for th in headers): + table = tbl + logger.info("通过表头关键词找到成绩表格") + break +``` + +### 2. 改进 `parse_courses` 函数 (monitor.py 第347-378行) + +**新增功能:** +- ✅ 检测 `LOGIN_REQUIRED` 标记 +- ✅ 检测未解析的原始HTML +- ✅ 添加详细的解析日志 +- ✅ 统计解析的课程数量 + +**改进代码片段:** +```python +# 检查是否需要重新登录 +if grade_text == "LOGIN_REQUIRED": + logger.warning("检测到登录失效,需要重新登录") + return [] + +# 检查是否是原始HTML(未成功解析的标记) +if '
... +[INFO] 共解析到 0 门课程 +``` + +## 优势 + +1. **更好的错误提示** - 明确告知问题原因 +2. **自动保存调试信息** - debug_page.html 便于排查 +3. **灵活的表格查找** - 支持多种网页结构 +4. **详细的日志** - 方便追踪每一步 +5. **完善的文档** - 提供多种解决方案 + +## 注意事项 + +1. **不要过度使用调试模式** - 生产环境可能不需要 +2. **定期检查 debug_page.html** - 了解实际获取的内容 +3. **及时更新表格选择器** - 如果学校网页结构变化 +4. **保护敏感信息** - debug_page.html 可能包含个人信息 + +## 下一步 + +如果问题仍未解决: +1. 查看 `debug_page.html` 的实际内容 +2. 检查HTML中的表格结构 +3. 可能需要调整 `extract_grade_info` 中的选择器 +4. 咨询开发者或提交 Issue + +--- + +**版本:** 2026-01-21 +**修复人员:** GitHub Copilot +**测试状态:** 待用户测试确认 diff --git a/修复说明_2026-01-22_重复日志和自动重登录.md b/修复说明_2026-01-22_重复日志和自动重登录.md new file mode 100644 index 0000000..cb6d313 --- /dev/null +++ b/修复说明_2026-01-22_重复日志和自动重登录.md @@ -0,0 +1,94 @@ +# 修复说明 - 2026年1月22日 + +## 修复的问题 + +### 1. 日志重复输出问题 +**现象**:每条日志都会输出两次 + +**原因**:`logging.basicConfig()` 配置会在模块每次被导入时重复添加处理器,导致日志重复输出。 + +**解决方案**: +- 使用 `if not logger.handlers:` 检查,确保处理器只被添加一次 +- 设置 `logger.propagate = False` 防止日志传播到根记录器 +- 使用显式的logger配置替代 `basicConfig` + +### 2. 连续异常时自动重新登录 +**现象**:当连续多次出现解析失败、获取页面失败或网络错误时,程序不会尝试重新登录,导致持续失败。 + +**解决方案**: +在以下场景达到3次连续失败时,自动触发重新登录: + +1. **获取成绩页面失败** + - 连续3次失败 → 尝试重新登录 + - 登录成功 → 重置错误计数,继续监控 + - 登录失败 → 继续累积错误计数 + +2. **网络错误(页面过短)** + - 连续3次网络错误 → 尝试重新登录 + - 登录成功 → 重置错误计数 + - 登录失败 → 继续累积错误计数 + +3. **解析课程失败(0门课程)** + - 连续3次解析失败 → 尝试重新登录 + - 登录成功 → 重置错误计数 + - 登录失败 → 发送通知,可能需要人工介入 + +## 技术细节 + +### 日志配置改进 +```python +# 旧代码 +logging.basicConfig(...) +logger = logging.getLogger(__name__) + +# 新代码 +logger = logging.getLogger(__name__) +if not logger.handlers: + # 配置处理器 + logger.propagate = False +``` + +### 重新登录逻辑 +```python +if consecutive_errors >= 3: + logger.warning("⚠️ 尝试重新登录以解决问题...") + if self.login(): + logger.info("✓ 重新登录成功,重置错误计数") + consecutive_errors = 0 + continue + else: + logger.error("✗ 重新登录失败") +``` + +## 使用建议 + +1. **重启服务**:修改后需要重启服务使改动生效 + ```bash + sudo systemctl restart grade-monitor + ``` + +2. **查看日志**:确认日志不再重复 + ```bash + sudo journalctl -u grade-monitor -f + # 或 + tail -f monitor.log + ``` + +3. **监控效果**: + - 日志应该只输出一次 + - 连续3次异常会自动尝试重新登录 + - 重新登录成功后会显示 "✓ 重新登录成功" + +## 预期效果 + +- ✅ 日志不再重复输出 +- ✅ 连续3次异常自动重新登录 +- ✅ 重新登录成功后重置错误计数 +- ✅ 减少因登录会话过期导致的持续失败 +- ✅ 更好的容错性和自愈能力 + +## 注意事项 + +- 重新登录会记录在日志中,便于追踪 +- 如果重新登录失败,仍会继续累积错误计数 +- 达到5次最大连续错误后会发送邮件通知 diff --git a/常见问题解决.md b/常见问题解决.md index d50663f..8139bf3 100644 --- a/常见问题解决.md +++ b/常见问题解决.md @@ -1,5 +1,100 @@ # 常见问题解决方案 +## 🚨 "未能提取到成绩信息,共 0 门课程" 错误 + +### 问题症状 +日志中反复出现: +``` +[WARNING] 未能提取到成绩信息,返回原始文本 +[INFO] 成绩无变化(共 0 门课程) +``` + +### 快速诊断 + +#### 1. 检查 BeautifulSoup 是否安装 +```bash +python3 -c "import bs4; print('BeautifulSoup4 已安装')" +``` + +如果报错,安装依赖: +```bash +pip3 install beautifulsoup4 +# 或完整安装所有依赖 +pip3 install -r requirements.txt +``` + +#### 2. 使用调试模式查看详细信息 +```bash +cd ~/grade_monitor +python3 monitor.py --test --debug +``` + +查看输出中是否有: +- ✅ "找到成绩表格" +- ✅ "找到 X 行成绩数据" +- ✅ "共解析到 X 门课程" + +如果都没有,继续下一步。 + +#### 3. 检查是否登录失效 +查看日志是否有 "检测到登录页面" 错误: +```bash +grep "登录页面\|LOGIN_REQUIRED" ~/grade_monitor/monitor.log +``` + +如果有,重启服务重新登录: +```bash +systemctl restart grade-monitor +``` + +#### 4. 查看调试HTML文件 +程序会自动保存问题页面到 `debug_page.html`: +```bash +cd ~/grade_monitor + +# 检查文件是否存在 +ls -lh debug_page.html + +# 查看是否包含成绩表格 +grep -i "gridtable\|课程名称\|成绩" debug_page.html | head -5 +``` + +### 解决方案 + +**方案1:重新安装依赖并重启** +```bash +cd ~/grade_monitor +pip3 install -r requirements.txt +systemctl restart grade-monitor +tail -f monitor.log +``` + +**方案2:启用调试模式监控** +编辑服务配置: +```bash +sudo nano /etc/systemd/system/grade-monitor.service +``` + +修改 ExecStart 行: +```ini +ExecStart=/usr/bin/python3 /home/yourusername/grade_monitor/monitor.py --debug +``` + +重新加载并重启: +```bash +sudo systemctl daemon-reload +sudo systemctl restart grade-monitor +tail -f ~/grade_monitor/monitor.log +``` + +**方案3:如果是网页结构变化** +查看详细排查指南: +```bash +cat ~/grade_monitor/故障排查指南.md +``` + +--- + ## ⚠️ "请不要过快点击" 错误 ### 问题原因 diff --git a/快速更新指南.md b/快速更新指南.md new file mode 100644 index 0000000..177cb6e --- /dev/null +++ b/快速更新指南.md @@ -0,0 +1,179 @@ +# 快速更新指南 - 修复会话过期问题 + +## 🎯 本次更新解决的问题 + +**症状:** 程序运行几小时后出现: +``` +[WARNING] 未能提取到成绩信息,返回原始文本 +[INFO] 成绩无变化(共 0 门课程) +``` + +**原因:** 登录会话过期,需要重新登录 + +**解决:** ✅ 自动检测并重新登录 + +--- + +## 📦 方式1:快速更新(推荐) + +### 在本地打包 +```bash +cd /mnt/e/50425/Documents/Github/GPA_Monitoring +chmod +x 打包.sh +./打包.sh +``` + +### 上传到服务器 +```bash +scp gpa_monitor.tar.gz 用户名@服务器IP:~/ +``` + +### 在服务器上更新 +```bash +# 停止服务 +systemctl stop grade-monitor + +# 备份 +cd ~/grade_monitor +cp monitor.py monitor.py.backup.$(date +%Y%m%d) + +# 解压更新 +tar -xzf ~/gpa_monitor.tar.gz -C ~/grade_monitor + +# 赋予权限 +chmod +x diagnose.sh + +# 重启服务 +systemctl restart grade-monitor + +# 查看日志(确认自动重新登录功能) +tail -f monitor.log +``` + +--- + +## 🔧 方式2:仅更新 monitor.py + +如果只想更新主程序文件: + +### 在本地 +```bash +# WSL中 +cd /mnt/e/50425/Documents/Github/GPA_Monitoring +scp monitor.py 用户名@服务器IP:~/grade_monitor/ +``` + +### 在服务器上 +```bash +systemctl stop grade-monitor +systemctl start grade-monitor +tail -f ~/grade_monitor/monitor.log +``` + +--- + +## ✅ 验证更新成功 + +查看日志,应该能看到: + +### 正常运行 +``` +[INFO] 开始新一轮检查 +[INFO] 获取成绩页面... +[DEBUG] 找到成绩表格 +[INFO] 成功提取成绩信息,共 17 行 +[INFO] 共解析到 15 门课程 +[INFO] 成绩无变化(共 15 门课程) +``` + +### 会话过期时自动处理 +``` +[WARNING] 检测到会话过期,尝试重新登录... +[INFO] 开始登录统一身份认证... +[INFO] 登录成功!成功访问成绩页面 +[INFO] 重新登录成功,继续监控 +``` + +--- + +## 🔍 故障诊断 + +如果更新后仍有问题: + +```bash +cd ~/grade_monitor + +# 运行诊断工具 +./diagnose.sh + +# 或手动测试 +python3 monitor.py --test --debug + +# 查看生成的调试文件 +ls -lh debug_page.html +cat .last_grade_content.txt +``` + +--- + +## 📋 更新内容详细列表 + +1. **会话过期自动重新登录** (monitor.py 第697-718行) + - 检测 `LOGIN_REQUIRED` 状态 + - 自动调用 `login()` 重新登录 + - 记录详细日志 + +2. **改进成绩提取** (monitor.py 第286-370行) + - 检测登录页面 + - 支持多种表格选择器 + - 自动保存 debug_page.html + - 添加详细调试日志 + +3. **改进课程解析** (monitor.py 第375-405行) + - 检测登录失效 + - 识别未解析的HTML + - 统计课程数量 + +4. **新增调试模式** + - `python3 monitor.py --debug` + - 显示详细运行信息 + +5. **新增诊断工具** + - `diagnose.sh` - 自动检测并给出建议 + - `故障排查指南.md` - 详细排查步骤 + +--- + +## 💡 最佳实践 + +1. **定期检查日志**(每天1-2次即可) + ```bash + tail -n 50 ~/grade_monitor/monitor.log + ``` + +2. **不要频繁重启服务** + - 新版本会自动处理会话过期 + - 只在真正出问题时才重启 + +3. **保留备份** + ```bash + # 每次更新前都会自动备份 + ls -lh ~/grade_monitor/monitor.py.backup.* + ``` + +4. **合理设置检查间隔** + - 建议 120-300 秒 + - 避免触发学校系统限流 + +--- + +## 🆘 需要帮助? + +1. 先运行诊断工具:`./diagnose.sh` +2. 查看故障排查指南:`cat 故障排查指南.md` +3. 查看详细修复说明:`cat 修复说明_2026-01-21.md` + +--- + +**更新日期:** 2026-01-21 +**版本:** v2.0 - 自动会话恢复版 diff --git a/打包.sh b/打包.sh index 53f3f4d..7eac550 100644 --- a/打包.sh +++ b/打包.sh @@ -34,9 +34,14 @@ tar -czf "$ARCHIVE_NAME" \ config.ini \ requirements.txt \ setup_python.sh \ + diagnose.sh \ grade-monitor.service \ readme.md \ + "常见问题解决.md" \ + "故障排查指南.md" \ + "重启服务.md" \ "Debian服务器部署指南.md" \ + "修复说明_2026-01-21.md" \ 2>/dev/null if [ $? -eq 0 ]; then @@ -45,23 +50,46 @@ if [ $? -eq 0 ]; then echo "压缩包信息:" ls -lh "$ARCHIVE_NAME" echo "" - echo "包含文件:" + echo "📄 包含文件:" tar -tzf "$ARCHIVE_NAME" echo "" echo "=========================================" - echo "下一步:" - echo "1. 上传到服务器:" + echo "✨ 本次更新内容(2026-01-21):" + echo " 1. ✅ 修复会话过期问题 - 自动重新登录" + echo " 2. ✅ 改进成绩提取 - 多种表格选择器" + echo " 3. ✅ 新增调试模式 - --debug 参数" + echo " 4. ✅ 新增诊断工具 - diagnose.sh" + echo " 5. ✅ 自动保存 debug_page.html 供排查" + echo "" + echo "=========================================" + echo "📤 上传到服务器:" echo " scp $ARCHIVE_NAME 用户名@服务器IP:~/" echo "" - echo "2. 在服务器上解压:" - echo " tar -xzf $ARCHIVE_NAME" - echo " cd gpa_monitor" + echo "🔧 服务器上部署更新:" + echo " # 停止服务" + echo " systemctl stop grade-monitor" echo "" - echo "3. 运行安装脚本:" - echo " chmod +x setup_python.sh" - echo " ./setup_python.sh" + echo " # 备份旧文件" + echo " cd ~/grade_monitor" + echo " cp monitor.py monitor.py.backup" echo "" - echo "详细说明请查看: Debian服务器部署指南.md" + echo " # 解压新文件" + echo " tar -xzf ~/gpa_monitor.tar.gz -C ~/grade_monitor" + echo "" + echo " # 赋予执行权限" + echo " chmod +x diagnose.sh" + echo "" + echo " # 重启服务" + echo " systemctl restart grade-monitor" + echo "" + echo " # 查看日志" + echo " tail -f monitor.log" + echo "" + echo "🔍 如果遇到问题:" + echo " cd ~/grade_monitor" + echo " ./diagnose.sh" + echo "" + echo "详细说明: 修复说明_2026-01-21.md" echo "=========================================" else echo "✗ 打包失败" diff --git a/故障排查指南.md b/故障排查指南.md new file mode 100644 index 0000000..ac31452 --- /dev/null +++ b/故障排查指南.md @@ -0,0 +1,143 @@ +# 故障排查指南 + +## 问题:未能提取到成绩信息,共 0 门课程 + +### 症状 +日志中出现以下警告信息: +``` +[WARNING] 未能提取到成绩信息,返回原始文本 +[INFO] 成绩无变化(共 0 门课程) +``` + +### 可能的原因和解决方案 + +#### 1. BeautifulSoup 未安装 +**检查方法:** +```bash +python3 -c "import bs4; print('BeautifulSoup4 已安装')" +``` + +**解决方案:** +```bash +pip3 install beautifulsoup4 +# 或 +pip3 install -r requirements.txt +``` + +#### 2. 登录会话失效 +**症状:** 日志中可能出现 "检测到登录页面" 的错误 + +**解决方案:** +```bash +# 停止服务 +systemctl stop grade-monitor + +# 重新启动服务(会重新登录) +systemctl start grade-monitor +``` + +#### 3. 网页结构变化 +**检查方法:** +程序会自动保存 HTML 到 `debug_page.html`,查看这个文件: +```bash +# 查看 debug_page.html 是否存在 +ls -la ~/grade_monitor/debug_page.html + +# 查看 HTML 内容,确认是否有成绩表格 +grep -i "gridtable\|课程名称\|成绩" ~/grade_monitor/debug_page.html +``` + +**解决方案:** +如果网页结构变化,可能需要修改 `monitor.py` 中的 `extract_grade_info` 函数。 +当前支持的表格选择器: +- `