commit 609b2334e81ec324534a8046878984553eb435b1 Author: ChuXun <70203584+ChuXunYu@users.noreply.github.com> Date: Sun Jan 18 18:48:20 2026 +0800 1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe4df5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# 敏感配置文件 +config.ini + +# Python虚拟环境 +venv/ +*.pyc +__pycache__/ + +# 运行时生成的文件 +.last_grade_*.txt +.last_grade_*.html +.last_courses.txt +.debug_response.html +monitor.log + +# 备份文件 +*.backup +*_old.* + +# 系统文件 +.DS_Store +Thumbs.db diff --git a/Debian服务器部署指南.md b/Debian服务器部署指南.md new file mode 100644 index 0000000..731e416 --- /dev/null +++ b/Debian服务器部署指南.md @@ -0,0 +1,408 @@ +# 成绩监控系统 - Debian服务器部署指南 + +## 📦 项目文件说明 + +### 必需文件(需要上传到服务器) + +| 文件名 | 用途 | 何时使用 | +|--------|------|----------| +| `monitor.py` | **主程序** | 运行监控的核心文件 | +| `config.ini` | **配置文件** | 包含账号密码、邮箱配置(⚠️ 敏感文件) | +| `requirements.txt` | **Python依赖列表** | 安装Python包时使用 | +| `setup_python.sh` | **环境安装脚本** | 首次部署时执行,安装所有依赖 | +| `grade-monitor.service` | **systemd服务配置** | 设置开机自启和后台运行 | + +### 可选文件(参考文档) + +| 文件名 | 用途 | +|--------|------| +| `config(模板).ini` | 配置文件模板,新用户参考 | +| `readme.md` | 项目说明文档 | +| `部署.md` | 详细部署步骤 | +| `常见问题解决.md` | 常见问题和解决方案 | + +### 不需要的文件(不要上传) + +- `venv/` - 虚拟环境(服务器上重新创建) +- `.git/` - Git仓库(可选) +- `.last_*` - 运行时生成的缓存文件 +- `monitor.log` - 运行时生成的日志文件 + +--- + +## 🚀 Debian服务器部署步骤 + +### 第一步:准备文件 + +```bash +# 在本地打包必需文件 +cd /mnt/e/50425/Documents/Github/GPA_Monitoring + +# 创建压缩包(只包含必需文件) +tar -czf gpa_monitor.tar.gz \ + monitor.py \ + config.ini \ + requirements.txt \ + setup_python.sh \ + grade-monitor.service \ + readme.md + +# 查看压缩包内容 +tar -tzf gpa_monitor.tar.gz +``` + +### 第二步:上传到服务器 + +```bash +# 方法1:使用 scp +scp gpa_monitor.tar.gz 用户名@服务器IP:/home/用户名/ + +# 方法2:使用 rsync(推荐) +rsync -avz gpa_monitor.tar.gz 用户名@服务器IP:/home/用户名/ + +# 方法3:使用 sftp +sftp 用户名@服务器IP +put gpa_monitor.tar.gz +``` + +### 第三步:在服务器上解压并安装 + +```bash +# 登录到Debian服务器 +ssh 用户名@服务器IP + +# 解压文件 +cd ~ +tar -xzf gpa_monitor.tar.gz +cd gpa_monitor # 或者你解压到的目录 + +# 给脚本添加执行权限 +chmod +x setup_python.sh + +# 运行安装脚本(会自动安装Python、创建虚拟环境、安装依赖) +./setup_python.sh +``` + +### 第四步:检查配置文件 + +```bash +# 编辑配置文件(如果需要修改) +nano config.ini + +# 确认配置正确: +# - USERNAME 和 PASSWORD(学号和密码) +# - 邮箱配置(SENDER_EMAIL、SENDER_PASSWORD、RECEIVER_EMAIL) +# - CHECK_INTERVAL(建议120秒以上) +``` + +### 第五步:测试运行 + +```bash +# 激活虚拟环境 +source venv/bin/activate + +# 测试运行(获取一次成绩,不进行监控) +python3 monitor.py --test + +# 如果测试成功,运行正式监控 +python3 monitor.py +# 按 Ctrl+C 停止 +``` + +--- + +## 🔧 设置后台运行(三选一) + +### 方案A:使用 tmux(推荐,简单易用) + +```bash +# 1. 安装 tmux(如果没有) +sudo apt update +sudo apt install tmux + +# 2. 创建会话并运行 +tmux new -s grade_monitor +source venv/bin/activate +python3 monitor.py + +# 3. 离开会话(程序继续运行) +# 按 Ctrl+B,然后按 D + +# 4. 重新连接查看 +tmux attach -t grade_monitor + +# 5. 查看所有会话 +tmux ls +``` + +**tmux 使用时机:** +- ✅ 测试阶段使用 +- ✅ 需要随时查看程序输出 +- ✅ 临时运行,不需要开机自启 + +--- + +### 方案B:使用 systemd 服务(推荐,生产环境) + +**`grade-monitor.service` 文件用途:** +这是 systemd 服务配置文件,告诉系统如何启动、管理和自动重启你的程序。 + +```bash +# 1. 编辑服务文件,修改用户名和路径 +nano grade-monitor.service + +# 确保这些路径正确: +# User=你的用户名 +# WorkingDirectory=/home/你的用户名/gpa_monitor +# ExecStart=/home/你的用户名/gpa_monitor/venv/bin/python3 /home/你的用户名/gpa_monitor/monitor.py + +# 2. 复制服务文件到系统目录 +sudo cp grade-monitor.service /etc/systemd/system/ + +# 3. 重新加载 systemd +sudo systemctl daemon-reload + +# 4. 启动服务 +sudo systemctl start grade-monitor + +# 5. 查看状态 +sudo systemctl status grade-monitor + +# 6. 设置开机自启 +sudo systemctl enable grade-monitor +``` + +**systemd 常用命令:** +```bash +# 启动 +sudo systemctl start grade-monitor + +# 停止 +sudo systemctl stop grade-monitor + +# 重启 +sudo systemctl restart grade-monitor + +# 查看状态 +sudo systemctl status grade-monitor + +# 查看日志 +journalctl -u grade-monitor -f + +# 开机自启 +sudo systemctl enable grade-monitor + +# 禁用自启 +sudo systemctl disable grade-monitor +``` + +**systemd 使用时机:** +- ✅ 生产环境长期运行 +- ✅ 需要开机自启动 +- ✅ 程序崩溃后自动重启 +- ✅ 系统化管理 + +--- + +### 方案C:使用 nohup(最简单,但不推荐) + +```bash +# 后台运行 +source venv/bin/activate +nohup python3 monitor.py > output.log 2>&1 & + +# 查看进程 +ps aux | grep monitor.py + +# 停止程序 +pkill -f monitor.py +``` + +**nohup 使用时机:** +- ✅ 临时快速运行 +- ❌ 不适合长期运行 +- ❌ 程序崩溃不会自动重启 + +--- + +## 📊 监控和维护 + +### 查看日志 + +```bash +# 查看监控日志(程序自己的日志) +tail -f ~/gpa_monitor/monitor.log + +# 查看最后100行 +tail -n 100 ~/gpa_monitor/monitor.log + +# 搜索关键词 +grep "新增课程" ~/gpa_monitor/monitor.log + +# 查看系统日志(如果用systemd) +sudo journalctl -u grade-monitor -f +sudo journalctl -u grade-monitor --since "1 hour ago" +``` + +### 检查运行状态 + +```bash +# 方法1:查看进程 +ps aux | grep monitor.py + +# 方法2:查看日志时间戳 +ls -lh ~/gpa_monitor/monitor.log + +# 方法3:systemd状态(如果用systemd) +sudo systemctl status grade-monitor +``` + +### 更新程序 + +```bash +# 1. 停止程序 +# tmux: Ctrl+C 或 tmux kill-session -t grade_monitor +# systemd: sudo systemctl stop grade-monitor + +# 2. 备份配置 +cp config.ini config.ini.backup + +# 3. 上传新版本文件并解压 + +# 4. 恢复配置 +cp config.ini.backup config.ini + +# 5. 重启程序 +# tmux: 重新运行 +# systemd: sudo systemctl restart grade-monitor +``` + +--- + +## 🔒 安全建议 + +### 1. 保护配置文件 + +```bash +# 设置文件权限(只有所有者可读写) +chmod 600 config.ini + +# 查看权限 +ls -l config.ini +# 应该显示:-rw------- 1 用户名 用户名 +``` + +### 2. 不要上传敏感文件到 GitHub + +在 `.gitignore` 中添加: +``` +config.ini +*.log +.last_* +venv/ +``` + +### 3. 定期检查日志 + +```bash +# 检查是否有异常 +grep -i "error\|fail\|warning" monitor.log + +# 检查登录情况 +grep "登录" monitor.log | tail -20 +``` + +--- + +## ❓ 常见问题 + +### Q1: 如何确认程序在运行? + +```bash +# 方法1:查看进程 +ps aux | grep monitor.py + +# 方法2:查看日志最后几行 +tail monitor.log + +# 方法3:查看文件修改时间 +ls -lh monitor.log +``` + +### Q2: 程序报错"请不要过快点击"怎么办? + +```bash +# 编辑配置文件,增加检查间隔 +nano config.ini + +# 修改 CHECK_INTERVAL 为更大的值(如300秒) +CHECK_INTERVAL = 300 + +# 重启程序 +``` + +### Q3: 如何在多台服务器部署? + +```bash +# 每台服务器重复部署步骤,注意: +# 1. 每台服务器使用不同的监控账号(如果可能) +# 2. 适当增加 CHECK_INTERVAL 避免同时访问 +# 3. 可以设置不同的邮件接收地址 +``` + +### Q4: 忘记 tmux 会话名怎么办? + +```bash +# 列出所有会话 +tmux ls + +# 连接到第一个会话 +tmux attach +``` + +--- + +## 📝 快速命令参考 + +```bash +# === 部署 === +tar -xzf gpa_monitor.tar.gz +cd gpa_monitor +chmod +x setup_python.sh +./setup_python.sh + +# === 运行 === +# 测试 +source venv/bin/activate && python3 monitor.py --test + +# tmux运行 +tmux new -s grade_monitor +source venv/bin/activate && python3 monitor.py + +# systemd运行 +sudo cp grade-monitor.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl start grade-monitor +sudo systemctl enable grade-monitor + +# === 监控 === +# 查看日志 +tail -f monitor.log + +# 查看状态 +sudo systemctl status grade-monitor + +# === 停止 === +# tmux: Ctrl+C +# systemd: sudo systemctl stop grade-monitor +# nohup: pkill -f monitor.py +``` + +--- + +## 📞 获取帮助 + +- 查看项目 README: `cat readme.md` +- 查看常见问题: `cat 常见问题解决.md` +- 查看详细部署: `cat 部署.md` +- 查看程序帮助: `python3 monitor.py --help` diff --git a/config(模板).ini b/config(模板).ini new file mode 100644 index 0000000..7902794 --- /dev/null +++ b/config(模板).ini @@ -0,0 +1,35 @@ +# 成绩监控配置文件 +# 请根据实际情况修改以下配置 + +[login] +# 统一身份认证账号 +USERNAME = +# 统一身份认证密码 +PASSWORD = +# 登录页面URL +LOGIN_URL = https://webvpn.neu.edu.cn/http/62304135386136393339346365373340e2b0fd71d8941093ab4e2527/eams/homeExt.action +# 成绩查询URL +GRADE_URL = https://webvpn.neu.edu.cn/http/62304135386136393339346365373340e2b0fd71d8941093ab4e2527/eams/teach/grade/course/person!search.action?semesterId=113&projectType= + +[email] +# 发件人邮箱 +SENDER_EMAIL = +# 163邮箱SMTP授权码(不是邮箱密码!) +# 需要在163邮箱设置中开启SMTP服务并获取授权码 +SENDER_PASSWORD = +# 收件人邮箱 +RECEIVER_EMAIL = +# SMTP服务器 +SMTP_SERVER = +# SMTP端口 +SMTP_PORT = + +[monitor] +# 检查间隔(秒),建议60秒以上 +CHECK_INTERVAL = 60 +# 请求间隔(秒),建议5秒以上 +REQUEST_DELAY = 5 +# 重试次数 +MAX_RETRIES = 3 +# 重试间隔(秒) +RETRY_DELAY = 10 diff --git a/grade-monitor.service b/grade-monitor.service new file mode 100644 index 0000000..b3933f2 --- /dev/null +++ b/grade-monitor.service @@ -0,0 +1,16 @@ +[Unit] +Description=GPA Grade Monitor Service +After=network.target + +[Service] +Type=simple +User=chuxun +WorkingDirectory=/mnt/e/50425/Documents/Github/GPA_Monitoring +ExecStart=/mnt/e/50425/Documents/Github/GPA_Monitoring/venv/bin/python3 /mnt/e/50425/Documents/Github/GPA_Monitoring/monitor.py +Restart=on-failure +RestartSec=30 +StandardOutput=append:/mnt/e/50425/Documents/Github/GPA_Monitoring/monitor.log +StandardError=append:/mnt/e/50425/Documents/Github/GPA_Monitoring/monitor.log + +[Install] +WantedBy=multi-user.target diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..636aa76 --- /dev/null +++ b/monitor.py @@ -0,0 +1,739 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +成绩监控系统 +功能:自动登录并监控成绩页面变化 +作者:GitHub Copilot +日期:2026-01-17 +""" + +import os +import sys +import time +import logging +import hashlib +import smtplib +import ssl +import requests +import argparse +import random +from pathlib import Path +from datetime import datetime +from configparser import ConfigParser +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +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__) + + +class GradeMonitor: + """成绩监控类""" + + def __init__(self, config_file: str = 'config.ini', test_mode: bool = False): + """初始化监控器""" + self.config_file = config_file + self.config = self._load_config() + self.test_mode = test_mode + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }) + + # 文件路径 + self.script_dir = Path(__file__).parent.absolute() + self.last_grade_file = self.script_dir / '.last_grade_hash.txt' + self.last_grade_html_file = self.script_dir / '.last_grade_page.html' + self.last_grade_content_file = self.script_dir / '.last_grade_content.txt' + + # 运行标志 + self.running = True + + # 设置信号处理 + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + + def _signal_handler(self, signum, frame): + """处理终止信号""" + logger.info("收到停止信号,正在退出...") + self.running = False + sys.exit(0) + + def _load_config(self) -> Dict: + """加载配置文件""" + if not os.path.exists(self.config_file): + logger.error(f"配置文件不存在: {self.config_file}") + sys.exit(1) + + parser = ConfigParser() + parser.read(self.config_file, encoding='utf-8') + + try: + config = { + # 登录配置 + 'username': parser.get('login', 'USERNAME', fallback=''), + 'password': parser.get('login', 'PASSWORD', fallback=''), + 'login_url': parser.get('login', 'LOGIN_URL', fallback=''), + 'grade_url': parser.get('login', 'GRADE_URL', fallback=''), + + # 邮件配置 + 'sender_email': parser.get('email', 'SENDER_EMAIL', fallback=''), + 'sender_password': parser.get('email', 'SENDER_PASSWORD', fallback=''), + 'receiver_email': parser.get('email', 'RECEIVER_EMAIL', fallback=''), + 'smtp_server': parser.get('email', 'SMTP_SERVER', fallback='smtp.163.com'), + 'smtp_port': parser.getint('email', 'SMTP_PORT', fallback=465), + + # 监控配置 + 'check_interval': parser.getint('monitor', 'CHECK_INTERVAL', fallback=60), + 'request_delay': parser.getint('monitor', 'REQUEST_DELAY', fallback=5), + 'max_retries': parser.getint('monitor', 'MAX_RETRIES', fallback=3), + 'retry_delay': parser.getint('monitor', 'RETRY_DELAY', fallback=10), + } + + # 验证必要配置 + if not config['username'] or not config['password']: + logger.error("请在配置文件中设置 USERNAME 和 PASSWORD") + sys.exit(1) + + if not config['sender_email'] or not config['sender_password'] or not config['receiver_email']: + logger.error("请在配置文件中设置邮件相关配置") + sys.exit(1) + + logger.info("配置文件加载成功") + return config + + except Exception as e: + logger.error(f"配置文件格式错误: {e}") + sys.exit(1) + + def login(self) -> bool: + """登录统一身份认证""" + logger.info("开始登录统一身份认证...") + + for attempt in range(self.config['max_retries']): + try: + logger.info(f"登录尝试 {attempt + 1}/{self.config['max_retries']}") + + # 第一步:访问成绩页面,会重定向到登录页 + logger.info("步骤1:访问成绩页面,获取登录页面...") + response = self.session.get( + self.config['grade_url'], + timeout=30, + allow_redirects=True + ) + + # 随机等待5-10秒,模拟真实用户 + wait_time = random.uniform(5, 10) + logger.info(f"第一次访问完成,等待 {wait_time:.1f} 秒(模拟用户行为)...") + time.sleep(wait_time) + + # 检查是否需要登录 + if '统一身份认证' in response.text or 'tpass/login' in response.url: + logger.info("需要登录,提取登录表单信息...") + + # 提取lt值 + import re + lt_match = re.search(r'name="lt"\s+value="([^"]+)"', response.text) + execution_match = re.search(r'name="execution"\s+value="([^"]+)"', response.text) + + lt = lt_match.group(1) if lt_match else '' + execution = execution_match.group(1) if execution_match else 'e1s1' + + if not lt: + logger.warning("无法提取lt值,使用默认值") + lt = '00000000' + + logger.info(f"提取到 lt={lt}, execution={execution}") + + # 获取登录提交URL + login_submit_url = response.url.split('?')[0] + + # 准备登录数据(按照login_neu.js的逻辑) + username = self.config['username'] + password = self.config['password'] + + login_data = { + 'rsa': username + password + lt, + 'ul': str(len(username)), + 'pl': str(len(password)), + 'sl': '0', + 'lt': lt, + 'execution': execution, + '_eventId': 'submit' + } + + logger.info(f"步骤2:提交登录表单到 {login_submit_url}") + # 随机等待5-10秒后提交,模拟用户填写表单 + wait_time = random.uniform(5, 10) + logger.info(f"等待 {wait_time:.1f} 秒后提交登录(模拟填写表单)...") + time.sleep(wait_time) + + response = self.session.post( + login_submit_url, + data=login_data, + timeout=30, + allow_redirects=True + ) + + # 登录后等待5-10秒,让服务器处理 + wait_time = random.uniform(5, 10) + logger.info(f"登录表单已提交,等待 {wait_time:.1f} 秒处理...") + time.sleep(wait_time) + + # 第三步:再次访问成绩页面验证登录 + logger.info("步骤3:验证登录状态,访问成绩页面...") + # 随机等待5-10秒,模拟用户操作 + wait_time = random.uniform(5, 10) + logger.info(f"等待 {wait_time:.1f} 秒后访问成绩页面(模拟用户操作)...") + time.sleep(wait_time) + + response = self.session.get( + self.config['grade_url'], + timeout=30, + allow_redirects=True + ) + + # 检查是否成功(不再是登录页面,且不是"请不要过快点击") + if '统一身份认证' not in response.text and 'tpass/login' not in response.url and '请不要过快点击' not in response.text: + # 进一步验证:检查是否包含成绩相关内容 + if '学年学期' in response.text or '课程名称' in response.text or '成绩' in response.text: + logger.info("登录成功!成功访问成绩页面") + return True + else: + logger.warning(f"页面内容异常,可能不是成绩页面") + logger.info(f"当前URL: {response.url}") + else: + if '请不要过快点击' in response.text: + wait_time = 30 + (attempt * 15) # 大幅递增等待时间:30秒、45秒、60秒... + logger.warning(f"⚠️ 请求过快被拦截!等待 {wait_time} 秒后重试...") + logger.info("建议:1) 检查 config.ini 中的 CHECK_INTERVAL 是否 >= 120 秒") + logger.info(" 2) 避免频繁手动测试,建议间隔至少5分钟") + time.sleep(wait_time) + continue # 直接进入下一次循环,不再额外sleep + else: + logger.warning(f"登录失败,尝试重试 {attempt + 1}/{self.config['max_retries']}") + logger.info(f"当前URL: {response.url}") + + except requests.RequestException as e: + logger.warning(f"登录请求异常: {e},尝试重试 {attempt + 1}/{self.config['max_retries']}") + + if '请不要过快点击' not in response.text: + logger.info(f"等待 {self.config['retry_delay']} 秒后重试...") + time.sleep(self.config['retry_delay']) + + logger.error("登录失败,已达到最大重试次数") + logger.info("建议:1) 检查用户名密码是否正确") + logger.info(" 2) 手动在浏览器中登录一次,确认账号正常") + logger.info(" 3) 查看 .debug_response.html 了解实际响应内容") + return False + + def fetch_grade_page(self) -> Optional[str]: + """获取成绩页面""" + logger.info("获取成绩页面...") + + for attempt in range(self.config['max_retries']): + try: + # 在每次请求前增加延迟,避免触发"请不要过快点击" + if attempt > 0: + wait_time = random.uniform(5, 8) + logger.info(f"等待 {wait_time:.1f} 秒后发送请求...") + time.sleep(wait_time) + + response = self.session.get( + self.config['grade_url'], + timeout=30 + ) + + # 检查是否触发"请不要过快点击" + if '请不要过快点击' in response.text: + wait_time = 30 + (attempt * 15) + logger.warning(f"⚠️ 请求过快被拦截!等待 {wait_time} 秒后重试...") + time.sleep(wait_time) + continue + + if response.status_code == 200: + return response.text + elif response.status_code in [302, 401]: + logger.warning("会话已过期,重新登录...") + if self.login(): + continue + else: + return None + else: + logger.warning(f"获取成绩页面失败 (HTTP {response.status_code}),尝试重试 {attempt + 1}/{self.config['max_retries']}") + + except requests.RequestException as e: + logger.warning(f"获取成绩页面异常: {e},尝试重试 {attempt + 1}/{self.config['max_retries']}") + + time.sleep(self.config['retry_delay']) + + logger.error("获取成绩页面失败,已达到最大重试次数") + return None + + def extract_grade_info(self, html: str) -> str: + """提取成绩信息的关键内容""" + try: + from bs4 import BeautifulSoup + soup = BeautifulSoup(html, 'html.parser') + + result = [] + + # 提取总平均绩点 + gpa_div = soup.find('div', string=lambda x: x and '总平均绩点' in x) + if gpa_div: + gpa_text = gpa_div.get_text(strip=True) + result.append("=" * 60) + result.append(gpa_text) + result.append("=" * 60) + result.append("") + + # 提取成绩表格 + table = soup.find('table', {'class': 'gridtable'}) + if table: + # 提取表头 + 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) + + # 提取每一行成绩 + tbody = table.find('tbody') + if tbody: + for row in tbody.find_all('tr'): + cells = row.find_all('td') + if cells: + row_data = [] + for cell in cells: + # 提取文本,包括sup标签的内容 + text = cell.get_text(strip=True, separator=' ') + row_data.append(text) + result.append(' | '.join(row_data)) + + if not result: + logger.warning("未能提取到成绩信息,返回原始文本") + return self._fallback_extract(html) + + return '\n'.join(result) + + except ImportError: + logger.warning("未安装 beautifulsoup4,使用简单文本提取") + return self._fallback_extract(html) + except Exception as e: + logger.error(f"提取成绩信息时出错: {e}") + return self._fallback_extract(html) + + def _fallback_extract(self, html: str) -> str: + """备用的简单文本提取方法""" + import re + # 移除script和style标签 + html = re.sub(r']*>.*?', '', html, flags=re.DOTALL) + html = re.sub(r']*>.*?', '', html, flags=re.DOTALL) + return html + + def parse_courses(self, grade_text: str) -> list: + """解析成绩文本,提取课程列表""" + courses = [] + lines = grade_text.split('\n') + + for line in lines: + # 跳过标题行、分隔行和空行 + stripped = line.strip() + # 跳过空行、纯分隔符行、标题行 + if not stripped or stripped.startswith('=') or stripped.startswith('-') or '学年学期' in line or '总平均绩点' in line: + continue + + # 解析课程行 + parts = [p.strip() for p in line.split('|')] + if len(parts) >= 4: # 至少有学期、代码、序号、名称 + # 提取关键信息:学期 + 课程代码 + 课程名称 + semester = parts[0] if len(parts) > 0 else '' + course_code = parts[1] if len(parts) > 1 else '' + course_name = parts[3] if len(parts) > 3 else '' + + if course_code and course_name: + course_key = f"{semester}|{course_code}|{course_name}" + courses.append(course_key) + + return courses + + def calculate_hash(self, content: str) -> str: + """计算内容的哈希值""" + return hashlib.md5(content.encode('utf-8')).hexdigest() + + def check_grade_changes(self, current_content: str, current_html: str = None) -> bool: + """检查成绩是否有变化""" + # 保存当前内容(供用户查看) + self.last_grade_content_file.write_text(current_content, encoding='utf-8') + if current_html: + self.last_grade_html_file.write_text(current_html, encoding='utf-8') + + # 解析当前课程列表 + current_courses = self.parse_courses(current_content) + + if not self.last_grade_file.exists(): + logger.info("=" * 60) + logger.info("首次运行,保存当前成绩状态") + logger.info(f"当前共有 {len(current_courses)} 门课程") + logger.info(f"原始HTML已保存到: {self.last_grade_html_file}") + logger.info(f"提取的成绩内容已保存到: {self.last_grade_content_file}") + logger.info("请检查这些文件确认内容是否正确!") + logger.info("=" * 60) + + # 保存课程列表 + courses_file = self.script_dir / '.last_courses.txt' + courses_file.write_text('\n'.join(current_courses), encoding='utf-8') + + # 计算哈希 + current_hash = self.calculate_hash(current_content) + self.last_grade_file.write_text(current_hash, encoding='utf-8') + return False + + # 读取上次的课程列表 + courses_file = self.script_dir / '.last_courses.txt' + if courses_file.exists(): + last_courses = courses_file.read_text(encoding='utf-8').strip().split('\n') + else: + last_courses = [] + + # 比较课程数量和内容,检测新增课程 + new_courses = [c for c in current_courses if c not in last_courses] + # 检测课程成绩变化(课程存在但内容不同) + current_hash = self.calculate_hash(current_content) + last_hash = self.last_grade_file.read_text(encoding='utf-8').strip() if self.last_grade_file.exists() else '' + content_changed = current_hash != last_hash + + if new_courses: + logger.info("=" * 60) + logger.info(f"✓ 检测到新增课程成绩!共 {len(new_courses)} 门") + logger.info("-" * 60) + for idx, course in enumerate(new_courses, 1): + parts = course.split('|') + if len(parts) >= 3: + semester = parts[0] if parts[0] else '未知学期' + course_name = parts[2] if len(parts) > 2 else '未知课程' + course_code = parts[1] if len(parts) > 1 else '' + logger.info(f" {idx}. [{semester}] {course_name} ({course_code})") + logger.info("=" * 60) + logger.info(f"详细内容已保存到: {self.last_grade_content_file}") + + # 更新保存的课程列表和哈希 + courses_file.write_text('\n'.join(current_courses), encoding='utf-8') + self.last_grade_file.write_text(current_hash, encoding='utf-8') + + return True + elif content_changed and current_courses: + # 课程数量没变,但内容变了(可能是成绩更新) + logger.info("=" * 60) + logger.info("✓ 检测到成绩内容发生变化(可能是成绩更新)") + logger.info(f"当前共有 {len(current_courses)} 门课程") + logger.info("=" * 60) + logger.info(f"详细内容已保存到: {self.last_grade_content_file}") + + # 更新哈希值 + self.last_grade_file.write_text(current_hash, encoding='utf-8') + + return True + else: + logger.info(f"成绩无变化(共 {len(current_courses)} 门课程)") + return False + + def send_email_notification(self, new_courses: list = None) -> bool: + """发送邮件通知""" + logger.info("准备发送邮件通知...") + + if new_courses: + subject = f"【成绩更新通知】新增 {len(new_courses)} 门课程成绩" + courses_text = "\n".join([f" - {course.split('|')[0]} {course.split('|')[2]}" for course in new_courses if len(course.split('|')) >= 3]) + body = f"""尊敬的用户: + +您好!系统检测到成绩页面已更新。 + +新增课程(共{len(new_courses)}门): +{courses_text} + +请及时查看详细成绩。 + +查看地址:{self.config['grade_url']} + +检测时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +此邮件由系统自动发送,请勿回复。""" + else: + subject = "【成绩更新通知】成绩页面已更新" + body = f"""尊敬的用户: + +您好!系统检测到成绩页面已更新。 + +请及时查看详细成绩。 + +查看地址:{self.config['grade_url']} + +检测时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +此邮件由系统自动发送,请勿回复。""" + + try: + message = MIMEMultipart("alternative") + message["Subject"] = subject + message["From"] = self.config['sender_email'] + message["To"] = self.config['receiver_email'] + + part = MIMEText(body, "plain", "utf-8") + message.attach(part) + + context = ssl.create_default_context() + + with smtplib.SMTP_SSL( + self.config['smtp_server'], + self.config['smtp_port'], + context=context + ) as server: + server.login( + self.config['sender_email'], + self.config['sender_password'] + ) + server.sendmail( + self.config['sender_email'], + self.config['receiver_email'], + message.as_string() + ) + + logger.info("邮件发送成功") + return True + + except Exception as e: + logger.error(f"邮件发送失败: {e}") + return False + + def send_error_notification(self, error_msg: str) -> bool: + """发送错误通知邮件""" + logger.info("准备发送错误通知邮件...") + + subject = "【成绩监控系统】程序运行出错" + body = f"""尊敬的用户: + +成绩监控系统在运行过程中遇到错误: + +错误信息: +{error_msg} + +发生时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +请检查系统状态或查看日志文件 monitor.log 了解详情。 + +此邮件由系统自动发送,请勿回复。""" + + try: + message = MIMEMultipart("alternative") + message["Subject"] = subject + message["From"] = self.config['sender_email'] + message["To"] = self.config['receiver_email'] + + part = MIMEText(body, "plain", "utf-8") + message.attach(part) + + context = ssl.create_default_context() + + with smtplib.SMTP_SSL( + self.config['smtp_server'], + self.config['smtp_port'], + context=context + ) as server: + server.login( + self.config['sender_email'], + self.config['sender_password'] + ) + server.sendmail( + self.config['sender_email'], + self.config['receiver_email'], + message.as_string() + ) + + logger.info("错误通知邮件发送成功") + return True + + except Exception as e: + logger.error(f"错误通知邮件发送失败: {e}") + return False + + def test_fetch(self): + """测试模式:获取一次成绩并显示内容""" + logger.info("=" * 60) + logger.info("测试模式:获取成绩页面") + logger.info("=" * 60) + + # 登录 + if not self.login(): + logger.error("登录失败,无法获取成绩页面") + return + + # 获取成绩页面 + logger.info("正在获取成绩页面...") + grade_html = self.fetch_grade_page() + if not grade_html: + logger.error("获取成绩页面失败") + return + + # 保存原始HTML + self.last_grade_html_file.write_text(grade_html, encoding='utf-8') + logger.info(f"✓ 原始HTML已保存到: {self.last_grade_html_file}") + + # 提取成绩信息 + grade_info = self.extract_grade_info(grade_html) + self.last_grade_content_file.write_text(grade_info, encoding='utf-8') + logger.info(f"✓ 提取的成绩内容已保存到: {self.last_grade_content_file}") + + # 显示前500个字符 + logger.info("=" * 60) + logger.info("提取的成绩内容预览(前500字符):") + logger.info("-" * 60) + print(grade_info[:500]) + if len(grade_info) > 500: + logger.info(f"\n... (还有 {len(grade_info) - 500} 个字符)") + logger.info("-" * 60) + logger.info(f"完整内容请查看文件: {self.last_grade_content_file}") + logger.info("=" * 60) + + def monitor_loop(self): + """主监控循环""" + logger.info("=" * 50) + logger.info("成绩监控程序启动") + logger.info(f"检查间隔: {self.config['check_interval']} 秒") + logger.info("按 Ctrl+C 停止监控") + logger.info("=" * 50) + + # 首次登录 + if not self.login(): + logger.error("初始登录失败,程序退出") + self.send_error_notification("初始登录失败,无法访问成绩页面") + return + + # 登录成功后等待一段时间再进行首次检查,避免触发"请不要过快点击" + wait_time = random.uniform(5, 10) + logger.info(f"登录成功,等待 {wait_time:.1f} 秒后开始首次检查(避免请求过快)...") + time.sleep(wait_time) + + consecutive_errors = 0 + max_consecutive_errors = 5 + + while self.running: + try: + logger.info("-" * 50) + logger.info("开始新一轮检查") + + # 获取成绩页面 + grade_html = self.fetch_grade_page() + if not grade_html: + consecutive_errors += 1 + logger.error(f"获取成绩页面失败,等待下次检查 (连续失败: {consecutive_errors}/{max_consecutive_errors})") + + 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 + + # 提取成绩信息 + grade_info = self.extract_grade_info(grade_html) + + # 检查变化 + if self.check_grade_changes(grade_info, grade_html): + # 获取新增课程列表 + 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门 + else: + self.send_email_notification() + + # 成功执行,重置错误计数 + consecutive_errors = 0 + + logger.info(f"等待 {self.config['check_interval']} 秒后进行下次检查...") + time.sleep(self.config['check_interval']) + + except Exception as e: + consecutive_errors += 1 + error_msg = f"监控循环发生异常: {e}" + logger.error(error_msg) + + if consecutive_errors >= max_consecutive_errors: + logger.error(f"连续 {consecutive_errors} 次出现异常") + self.send_error_notification(f"{error_msg}\n\n连续失败次数: {consecutive_errors}") + consecutive_errors = 0 + + time.sleep(self.config['retry_delay']) + + def run(self): + """运行监控""" + try: + if self.test_mode: + self.test_fetch() + else: + self.monitor_loop() + except KeyboardInterrupt: + logger.info("收到键盘中断信号,程序退出") + except Exception as e: + logger.error(f"程序异常退出: {e}") + sys.exit(1) + + +def main(): + """主函数""" + parser = argparse.ArgumentParser( + description='成绩监控系统 - 自动监控成绩变化并发送邮件通知', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +使用示例: + # 正常监控模式 + python3 monitor.py + + # 测试模式(获取一次成绩并显示内容) + python3 monitor.py --test + + # 查看保存的成绩内容 + cat .last_grade_content.txt + + # 查看原始HTML + cat .last_grade_page.html + """ + ) + + parser.add_argument( + '--test', + action='store_true', + help='测试模式:仅获取一次成绩页面并显示内容,不进行监控' + ) + + parser.add_argument( + '--config', + default='config.ini', + help='配置文件路径(默认:config.ini)' + ) + + args = parser.parse_args() + + monitor = GradeMonitor(config_file=args.config, test_mode=args.test) + monitor.run() + + +if __name__ == '__main__': + main() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..550a8e8 --- /dev/null +++ b/readme.md @@ -0,0 +1,157 @@ +# 成绩监控系统 + +自动监控东北大学成绩系统,当有新课程成绩发布时自动发送邮件通知。 + +## 快速开始 + +### 1. 安装依赖(WSL环境) + +```bash +cd /mnt/e/50425/Documents/Github/GPA_Monitoring +chmod +x setup_python.sh +./setup_python.sh +``` + +### 2. 配置 + +编辑 `config.ini` 文件,填入你的信息: + +```ini +[login] +USERNAME = 你的学号 +PASSWORD = 你的密码 + +[email] +SENDER_EMAIL = your_email@163.com +SENDER_PASSWORD = SMTP授权码(不是邮箱密码!) +RECEIVER_EMAIL = 接收通知的邮箱 +``` + +**获取163邮箱SMTP授权码:** +1. 登录163邮箱网页版 +2. 设置 → POP3/SMTP/IMAP +3. 开启"IMAP/SMTP服务" +4. 发送短信获取授权码 + +### 3. 测试运行 + +```bash +source venv/bin/activate +python3 monitor.py --test +``` + +检查生成的文件确认内容正确: +- `.last_grade_content.txt` - 提取的成绩内容(整齐格式) +- `.last_grade_page.html` - 原始HTML页面 + +### 4. 正式运行 + +```bash +# 使用tmux后台运行(推荐) +tmux new -s grade_monitor +source venv/bin/activate +python3 monitor.py +# 按 Ctrl+B 然后按 D 离开会话 +``` + +## 功能特点 + +✅ **智能检测** - 检测新增课程(而非简单的页面变化) +✅ **详细通知** - 邮件包含新增课程名称 +✅ **格式化输出** - 成绩保存为整齐的文本格式,包含总绩点 +✅ **错误通知** - 连续失败5次自动发送错误通知邮件 +✅ **稳定可靠** - 自动重试、会话管理、异常处理 +✅ **防封禁** - 请求间隔控制,避免过快访问 + +## 文件说明 + +**主要文件:** +- `monitor.py` - 主程序 +- `config.ini` - 配置文件 +- `requirements.txt` - Python依赖 +- `setup_python.sh` - 安装脚本 + +**自动生成的文件:** +- `.last_grade_content.txt` - 当前成绩内容(格式化) +- `.last_grade_page.html` - 原始HTML页面 +- `.last_courses.txt` - 课程列表(用于检测新增) +- `.last_grade_hash.txt` - 内容哈希(用于检测变化) +- `monitor.log` - 运行日志 + +## 命令说明 + +```bash +# 查看帮助 +python3 monitor.py --help + +# 测试模式(获取一次成绩) +python3 monitor.py --test + +# 正常监控模式 +python3 monitor.py + +# 查看日志 +tail -f monitor.log + +# 查看成绩 +cat .last_grade_content.txt +``` + +## 监控原理 + +1. **定时访问** - 每60秒(可配置)访问一次成绩页面 +2. **解析成绩** - 提取总绩点和每门课程信息 +3. **智能对比** - 对比课程列表,检测新增课程 +4. **邮件通知** - 发现新课程立即发送邮件 + +## 故障排查 + +### 问题:登录失败 + +**解决:** +1. 检查 `config.ini` 中的用户名密码 +2. 手动登录网页版确认账号正常 +3. 查看 `.debug_response.html` 了解实际响应 + +### 问题:邮件发送失败 + +**解决:** +1. 确认使用SMTP授权码(不是邮箱密码) +2. 检查163邮箱是否开启SMTP服务 +3. 查看 `monitor.log` 了解错误详情 + +### 问题:提示"请不要过快点击" + +**解决:** +- 程序会自动增加等待时间 +- 可在 `config.ini` 中增大 `CHECK_INTERVAL` + +## 配置说明 + +```ini +[monitor] +CHECK_INTERVAL = 60 # 检查间隔(秒) +REQUEST_DELAY = 5 # 请求延迟(秒) +MAX_RETRIES = 3 # 重试次数 +RETRY_DELAY = 10 # 重试间隔(秒) +``` + +## 停止监控 + +```bash +# 前台运行时按 Ctrl+C + +# tmux中 +tmux attach -t grade_monitor +# 然后按 Ctrl+C +``` + +## 注意事项 + +⚠️ **请勿过于频繁地检查** - 建议检查间隔不小于60秒 +⚠️ **保护好配置文件** - 包含账号密码,不要上传到公共仓库 +⚠️ **授权码不是密码** - 163邮箱需要单独获取SMTP授权码 + +## 许可证 + +MIT License \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f5e5eea --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.31.0 +beautifulsoup4>=4.12.0 diff --git a/setup_python.sh b/setup_python.sh new file mode 100644 index 0000000..28d0502 --- /dev/null +++ b/setup_python.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +############################################# +# Python版本依赖安装脚本 +# 适用于 Debian 12 (WSL) +############################################# + +set -euo pipefail + +echo "========== 成绩监控系统 (Python版本) - 依赖安装 ==========" +echo "" + + +# 更新软件包列表 +echo "1. 更新软件包列表..." +sudo apt-get update + +# 安装Python3和pip +echo "" +echo "2. 安装Python3和pip..." +sudo apt-get install -y python3 python3-pip python3-venv + +# 创建虚拟环境(可选但推荐) +echo "" +echo "3. 创建Python虚拟环境..." +if [[ ! -d "venv" ]]; then + python3 -m venv venv + echo "虚拟环境创建成功" +else + echo "虚拟环境已存在,跳过创建" +fi + +# 激活虚拟环境并安装依赖 +echo "" +echo "4. 安装Python依赖包..." +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +echo "" +echo "5. 重命名配置文件..." +if [[ -f "config_new.ini" ]] && [[ ! -f "config.ini" ]]; then + mv config_new.ini config.ini + echo "配置文件已重命名为 config.ini" +elif [[ -f "config_new.ini" ]]; then + echo "config.ini 已存在,保留原配置" + echo "新配置保存在 config_new.ini,请手动合并" +fi + +# 给脚本添加执行权限 +echo "" +echo "6. 添加执行权限..." +chmod +x monitor.py + +# 验证安装 +echo "" +echo "========== 验证安装 ==========" +echo "" + +python3 --version +pip --version + +echo "" +echo "已安装的Python包:" +pip list | grep -E "requests|beautifulsoup4" + +echo "" +echo "========== 所有依赖安装成功! ==========" +echo "" +echo "下一步:" +echo "1. 确认 config.ini 文件中的配置正确" +echo "2. 运行监控程序:" +echo " 方式1(使用虚拟环境 - 推荐):" +echo " source venv/bin/activate" +echo " python3 monitor.py" +echo "" +echo " 方式2(直接运行):" +echo " ./monitor.py" +echo "" +echo " 方式3(后台运行):" +echo " nohup python3 monitor.py > /dev/null 2>&1 &" +echo "" +echo " 方式4(使用tmux - 推荐):" +echo " tmux new -s grade_monitor" +echo " source venv/bin/activate" +echo " python3 monitor.py" +echo " # 按 Ctrl+B 然后按 D 离开会话" +echo "" +echo "提示:" +echo "- 163邮箱需要在邮箱设置中开启SMTP服务并获取授权码" +echo "- 查看日志: tail -f monitor.log" diff --git a/常见问题解决.md b/常见问题解决.md new file mode 100644 index 0000000..d50663f --- /dev/null +++ b/常见问题解决.md @@ -0,0 +1,144 @@ +# 常见问题解决方案 + +## ⚠️ "请不要过快点击" 错误 + +### 问题原因 +学校WebVPN系统有严格的访问频率限制,短时间内多次访问会被拦截。 + +### 解决方案 + +#### 1. 停止手动测试至少5分钟 +```bash +# 如果在服务器上运行,请先停止程序 +ps aux | grep monitor.py +kill <进程ID> + +# 等待至少5分钟后再重新测试 +``` + +#### 2. 修改配置文件增加间隔 +编辑 `config.ini`: +```ini +[monitor] +# 将检查间隔改为5分钟(300秒)或更长 +CHECK_INTERVAL = 300 + +# 增加请求延迟 +REQUEST_DELAY = 15 + +# 增加重试间隔 +RETRY_DELAY = 60 +``` + +#### 3. 使用正确的测试方式 +```bash +# 测试前确保距离上次测试至少5分钟 +python3 monitor.py --test + +# 如果还是被拦截,等10分钟后再试 +``` + +#### 4. 部署后不要频繁测试 +```bash +# 部署systemd服务后,让它自动运行 +sudo systemctl start grade-monitor + +# 只通过日志查看运行情况 +tail -f monitor.log + +# 不要反复启停服务或手动测试 +``` + +## 🔍 如何判断系统正常工作 + +### 方法1:查看日志 +```bash +tail -f ~/grade_monitor/monitor.log +``` + +正常日志应该类似: +``` +2026-01-17 10:00:00 - INFO - 开始检查成绩变化... +2026-01-17 10:00:05 - INFO - 登录成功! +2026-01-17 10:00:10 - INFO - 成功获取成绩页面 +2026-01-17 10:00:12 - INFO - 未发现新课程 +2026-01-17 10:02:00 - INFO - 等待下次检查... +``` + +### 方法2:查看保存的成绩文件 +```bash +cat ~/.last_grade_content.txt +``` + +如果文件包含完整的课程列表和GPA,说明系统工作正常。 + +### 方法3:查看进程状态 +```bash +ps aux | grep monitor.py +``` + +如果有进程在运行,说明程序正在监控中。 + +## 📝 最佳实践 + +### 测试阶段 +1. **首次测试**:运行 `python3 monitor.py --test` +2. **等待5分钟** +3. **再次测试**:确认能正常获取成绩 +4. **等待10分钟** +5. **部署服务**:配置systemd或使用tmux + +### 运行阶段 +1. **设置CHECK_INTERVAL为120秒或更长** +2. **让程序自动运行,不要手动干预** +3. **每天查看一次日志即可** +4. **不要频繁重启服务** + +### 如果被持续拦截 +1. **停止所有监控程序** +2. **等待至少30分钟** +3. **将CHECK_INTERVAL改为600秒(10分钟)** +4. **重新启动,让它慢慢运行** +5. **确认稳定后,再逐步减少间隔** + +## 🛠️ 调试技巧 + +### 查看实际响应内容 +```bash +cat ~/grade_monitor/.debug_response.html +``` + +如果文件包含"请不要过快点击",说明请求被拦截。 + +### 检查登录状态 +```bash +# 运行测试模式 +python3 monitor.py --test + +# 查看是否成功登录 +grep "登录成功" monitor.log +``` + +### 测试网络连接 +```bash +# 测试是否能访问学校网站 +curl -I https://webvpn.neu.edu.cn + +# 测试SMTP邮件服务器 +nc -zv smtp.163.com 465 +``` + +## 💡 温馨提示 + +1. **成绩更新频率不高**:学校不会每分钟更新成绩,建议CHECK_INTERVAL设为120-300秒 +2. **避免过度监控**:频繁访问可能被学校系统封禁IP +3. **合理设置间隔**:既能及时发现新成绩,又不会触发限流 +4. **信任自动化**:部署后让程序自己运行,不需要频繁检查 + +## 📞 仍然无法解决? + +1. 检查配置文件中的账号密码是否正确 +2. 确认能在浏览器中正常登录学校系统 +3. 查看 `.debug_response.html` 了解实际响应 +4. 将CHECK_INTERVAL增加到600秒(10分钟) +5. 考虑只在特定时间段运行(如每天上午10点和下午3点) diff --git a/打包.sh b/打包.sh new file mode 100644 index 0000000..53f3f4d --- /dev/null +++ b/打包.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# 成绩监控系统 - 打包脚本 +# 用途:打包项目文件准备上传到服务器 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ARCHIVE_NAME="gpa_monitor.tar.gz" + +echo "=========================================" +echo " 成绩监控系统 - 打包工具" +echo "=========================================" +echo "" + +cd "$SCRIPT_DIR" + +# 检查config.ini是否存在 +if [ ! -f "config.ini" ]; then + echo "⚠️ 警告: 未找到 config.ini" + echo " 请先配置 config.ini 文件" + echo "" + read -p "是否继续打包?(y/n): " continue_pack + if [ "$continue_pack" != "y" ]; then + echo "已取消" + exit 0 + fi +fi + +echo "📦 正在打包必需文件..." +echo "" + +# 打包必需文件 +tar -czf "$ARCHIVE_NAME" \ + monitor.py \ + config.ini \ + requirements.txt \ + setup_python.sh \ + grade-monitor.service \ + readme.md \ + "Debian服务器部署指南.md" \ + 2>/dev/null + +if [ $? -eq 0 ]; then + echo "✓ 打包成功!" + echo "" + echo "压缩包信息:" + ls -lh "$ARCHIVE_NAME" + echo "" + echo "包含文件:" + tar -tzf "$ARCHIVE_NAME" + echo "" + echo "=========================================" + echo "下一步:" + echo "1. 上传到服务器:" + echo " scp $ARCHIVE_NAME 用户名@服务器IP:~/" + echo "" + echo "2. 在服务器上解压:" + echo " tar -xzf $ARCHIVE_NAME" + echo " cd gpa_monitor" + echo "" + echo "3. 运行安装脚本:" + echo " chmod +x setup_python.sh" + echo " ./setup_python.sh" + echo "" + echo "详细说明请查看: Debian服务器部署指南.md" + echo "=========================================" +else + echo "✗ 打包失败" + exit 1 +fi diff --git a/部署.md b/部署.md new file mode 100644 index 0000000..48376be --- /dev/null +++ b/部署.md @@ -0,0 +1,516 @@ +# 服务器部署指南 + +本文档详细说明如何将成绩监控系统部署到Linux服务器上。 + +## 📋 前置要求 + +- Linux服务器(Debian/Ubuntu/CentOS等) +- Python 3.7+ +- 服务器能访问外网 +- SSH访问权限 + +## 📦 准备文件 + +### 需要上传的文件 + +``` +grade_monitor/ +├── monitor.py # 主程序(必需) +├── config.ini # 配置文件(必需) +├── requirements.txt # Python依赖(必需) +├── setup_python.sh # 安装脚本(必需) +└── readme.md # 说明文档(可选) +``` + +### 打包文件 + +在Windows本地执行: + +```powershell +# 方法1:使用tar(需要WSL或Git Bash) +tar -czf grade_monitor.tar.gz monitor.py config.ini requirements.txt setup_python.sh readme.md + +# 方法2:使用7-Zip或WinRAR手动打包 +``` + +## 🚀 部署步骤 + +### 步骤1:修改配置文件 + +**在上传前**,确保 `config.ini` 中填入了正确的信息: + +```ini +[login] +USERNAME = 你的学号 +PASSWORD = 你的密码 +LOGIN_URL = ... +GRADE_URL = ... + +[email] +SENDER_EMAIL = your_email@163.com +SENDER_PASSWORD = SMTP授权码 +RECEIVER_EMAIL = 接收通知的邮箱 + +[monitor] +CHECK_INTERVAL = 60 +REQUEST_DELAY = 5 +MAX_RETRIES = 3 +RETRY_DELAY = 10 +``` + +### 步骤2:上传到服务器 + +#### 方法A:使用SCP上传 + +```bash +# 上传打包文件 +scp grade_monitor.tar.gz username@server-ip:/home/username/ + +# 或直接上传文件 +scp monitor.py config.ini requirements.txt setup_python.sh username@server-ip:/home/username/grade_monitor/ +``` + +#### 方法B:使用SFTP上传 + +```bash +sftp username@server-ip +put grade_monitor.tar.gz +exit +``` + +#### 方法C:使用FTP客户端 + +使用FileZilla、WinSCP等工具上传文件。 + +### 步骤3:连接到服务器 + +```bash +ssh username@server-ip +``` + +### 步骤4:解压和安装 + +```bash +# 创建工作目录 +mkdir -p ~/grade_monitor +cd ~/grade_monitor + +# 添加执行权限 +chmod +x setup_python.sh + +# 运行安装脚本 +./setup_python.sh +``` + +安装脚本会自动: +- 更新apt包列表 +- 安装Python3和pip +- 创建虚拟环境 +- 安装所需的Python包(requests、beautifulsoup4) + +### 步骤5:测试运行 + +```bash +# 激活虚拟环境 +source venv/bin/activate + +# 测试模式运行 +python3 monitor.py --test +``` + +**检查输出:** +- 是否成功登录 +- 是否获取到成绩页面 +- 查看提取的成绩内容 + +```bash +# 查看提取的成绩 +cat .last_grade_content.txt + +# 应该看到类似这样的内容: +# ============================================================ +# 总平均绩点:4.3471 +# ============================================================ +# +# 学年学期 | 课程代码 | 课程序号 | 课程名称 | ... +# -------------------------------------------------------- +# 2025-2026 秋季 | A0801051020 | A095478 | C++程序设计 | ... +``` + +## 🔄 后台运行方案 + +### 方案A:使用systemd(推荐,适合长期运行) + +#### 1. 创建服务文件 + +```bash +sudo nano /etc/systemd/system/grade-monitor.service +``` + +#### 2. 填入以下内容 + +```ini +[Unit] +Description=Grade Monitoring Service +After=network.target + +[Service] +Type=simple +User=你的用户名 +WorkingDirectory=/home/你的用户名/grade_monitor +ExecStart=/home/你的用户名/grade_monitor/venv/bin/python3 /home/你的用户名/grade_monitor/monitor.py +Restart=always +RestartSec=30 +StandardOutput=append:/home/你的用户名/grade_monitor/monitor.log +StandardError=append:/home/你的用户名/grade_monitor/monitor.log + +[Install] +WantedBy=multi-user.target +``` + +**注意:** 将上面的"你的用户名"替换为实际的Linux用户名 + +#### 3. 启动服务 + +```bash +# 重载systemd配置 +sudo systemctl daemon-reload + +# 启用开机自启 +sudo systemctl enable grade-monitor + +# 启动服务 +sudo systemctl start grade-monitor + +# 查看服务状态 +sudo systemctl status grade-monitor +``` + +#### 4. 管理服务 + +```bash +# 查看日志 +sudo journalctl -u grade-monitor -f + +# 停止服务 +sudo systemctl stop grade-monitor + +# 重启服务 +sudo systemctl restart grade-monitor + +# 禁用开机自启 +sudo systemctl disable grade-monitor +``` + +### 方案B:使用tmux(简单,适合临时运行) + +#### 1. 安装tmux + +```bash +# Debian/Ubuntu +sudo apt install tmux + +# CentOS/RHEL +sudo yum install tmux +``` + +#### 2. 创建会话并运行 + +```bash +# 创建新的tmux会话 +tmux new -s grade_monitor + +# 激活虚拟环境 +source venv/bin/activate + +# 运行程序 +python3 monitor.py +``` + +#### 3. 离开和重连会话 + +```bash +# 离开会话(程序继续运行) +# 按键:Ctrl+B,然后按 D + +# 重新连接到会话 +tmux attach -t grade_monitor + +# 查看所有会话 +tmux ls + +# 关闭会话 +tmux kill-session -t grade_monitor +``` + +### 方案C:使用screen + +```bash +# 安装screen +sudo apt install screen + +# 创建新会话 +screen -S grade_monitor + +# 激活环境并运行 +source venv/bin/activate +python3 monitor.py + +# 离开会话:Ctrl+A,然后按 D + +# 重新连接 +screen -r grade_monitor + +# 查看所有会话 +screen -ls +``` + +### 方案D:使用nohup(最简单) + +```bash +# 激活虚拟环境 +source venv/bin/activate + +# 后台运行 +nohup python3 monitor.py > monitor.log 2>&1 & + +# 查看进程 +ps aux | grep monitor.py + +# 停止进程 +kill <进程ID> +``` + +## 📊 监控和维护 + +### 查看运行状态 + +```bash +# 方法1:查看日志文件 +tail -f ~/grade_monitor/monitor.log + +# 方法2:查看systemd日志(如果使用systemd) +sudo journalctl -u grade-monitor -n 50 -f + +# 方法3:查看进程 +ps aux | grep monitor.py +``` + +### 查看当前成绩 + +```bash +cd ~/grade_monitor +cat .last_grade_content.txt +``` + +### 手动触发测试 + +```bash +cd ~/grade_monitor +source venv/bin/activate +python3 monitor.py --test +``` + +## 🔧 常见问题 + +### 问题1:连接服务器失败 + +**可能原因:** +- SSH端口被防火墙拦截 +- 服务器IP或用户名错误 +- SSH密钥配置问题 + +**解决方法:** +```bash +# 指定端口 +ssh -p 端口号 username@server-ip + +# 使用密钥 +ssh -i /path/to/key.pem username@server-ip +``` + +### 问题2:无法访问学校网站 + +**可能原因:** +- 服务器在校外,需要VPN +- 防火墙拦截 + +**解决方法:** +- 使用校内服务器 +- 配置代理 +- 联系网络管理员 + +### 问题3:pip安装失败 + +**解决方法:** +```bash +# 更新pip +pip install --upgrade pip + +# 使用国内镜像 +pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 或使用清华源 +pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ +``` + +### 问题4:权限不足 + +**解决方法:** +```bash +# 修改文件所有者 +sudo chown -R username:username ~/grade_monitor + +# 修改执行权限 +chmod +x ~/grade_monitor/*.sh +``` + +### 问题5:邮件发送失败 + +**解决方法:** +- 确认SMTP授权码正确(不是邮箱密码) +- 检查服务器能否访问smtp.163.com(端口465) +- 查看详细错误信息:`tail -f monitor.log` + +```bash +# 测试网络连接 +telnet smtp.163.com 465 +# 或 +nc -zv smtp.163.com 465 +``` + +## 🔐 安全建议 + +### 1. 保护配置文件 + +```bash +# 限制config.ini权限 +chmod 600 ~/grade_monitor/config.ini + +# 确保只有自己能访问 +ls -la ~/grade_monitor/config.ini +# 应显示:-rw------- 1 username username +``` + +### 2. 使用环境变量(可选) + +不在配置文件中存储明文密码,而是使用环境变量: + +```bash +# 设置环境变量 +export GRADE_USERNAME="你的学号" +export GRADE_PASSWORD="你的密码" +export EMAIL_PASSWORD="SMTP授权码" + +# 添加到.bashrc使其永久生效 +echo 'export GRADE_USERNAME="你的学号"' >> ~/.bashrc +echo 'export GRADE_PASSWORD="你的密码"' >> ~/.bashrc +echo 'export EMAIL_PASSWORD="SMTP授权码"' >> ~/.bashrc +``` + +### 3. 定期更新 + +```bash +# 更新系统 +sudo apt update && sudo apt upgrade + +# 更新Python包 +cd ~/grade_monitor +source venv/bin/activate +pip install --upgrade requests beautifulsoup4 +``` + +## 📱 监控建议 + +### 设置监控脚本 + +创建一个检查脚本 `check_status.sh`: + +```bash +#!/bin/bash + +# 检查进程是否运行 +if pgrep -f "monitor.py" > /dev/null; then + echo "✓ 监控程序正在运行" +else + echo "✗ 监控程序未运行!" + # 可以在这里添加重启逻辑 + # systemctl start grade-monitor +fi + +# 检查最近的日志 +echo "" +echo "最近的日志:" +tail -n 5 ~/grade_monitor/monitor.log +``` + +### 定期检查(使用cron) + +```bash +# 编辑crontab +crontab -e + +# 添加以下行(每天检查一次) +0 12 * * * /home/username/grade_monitor/check_status.sh +``` + +## 🎯 完整部署示例 + +```bash +# === 本地操作(Windows/WSL) === +cd E:\50425\Documents\Github\GPA_Monitoring +tar -czf grade_monitor.tar.gz monitor.py config.ini requirements.txt setup_python.sh readme.md +scp grade_monitor.tar.gz user@server.com:~/ + +# === 服务器操作 === +ssh user@server.com + +# 解压和安装 +mkdir -p ~/grade_monitor +cd ~/grade_monitor +tar -xzf ../grade_monitor.tar.gz +chmod +x setup_python.sh +./setup_python.sh + +# 测试 +source venv/bin/activate +python3 monitor.py --test +cat .last_grade_content.txt + +# 配置systemd服务 +sudo nano /etc/systemd/system/grade-monitor.service +# (填入服务配置) + +sudo systemctl daemon-reload +sudo systemctl enable grade-monitor +sudo systemctl start grade-monitor +sudo systemctl status grade-monitor + +# 查看日志 +sudo journalctl -u grade-monitor -f +``` + +## ✅ 部署检查清单 + +- [ ] 文件已上传到服务器 +- [ ] config.ini配置正确(账号、密码、邮箱) +- [ ] 运行setup_python.sh安装依赖 +- [ ] 测试模式运行成功 +- [ ] 成绩提取格式正确 +- [ ] 配置后台运行(systemd/tmux/screen) +- [ ] 服务正常启动 +- [ ] 日志输出正常 +- [ ] 收到测试邮件 +- [ ] 设置文件权限(chmod 600 config.ini) + +## 📞 获取帮助 + +如果遇到问题: +1. 查看 `monitor.log` 了解详细错误 +2. 运行 `python3 monitor.py --test` 测试 +3. 检查 `.debug_response.html` 了解实际响应 +4. 查看 `readme.md` 了解更多功能说明 + +--- + +部署完成后,系统将自动监控成绩变化,有新课程成绩时会立即发送邮件通知! diff --git a/重启服务.md b/重启服务.md new file mode 100644 index 0000000..f1efa31 --- /dev/null +++ b/重启服务.md @@ -0,0 +1,19 @@ +# 停止服务 +systemctl stop grade-monitor + +# 备份旧文件 +cp monitor.py monitor.py.backup + +# 解压新文件(会覆盖) +tar -xzf ~/gpa_monitor.tar.gz -C ~/grade_monitor --strip-components=0 + +# 或者只替换 monitor.py +# 你可以直接编辑: +nano monitor.py +# 找到 parse_courses 函数,修改那一行判断条件 + +# 重启服务 +systemctl restart grade-monitor + +# 查看日志 +tail -f monitor.log \ No newline at end of file