This commit is contained in:
ChuXun
2026-01-18 18:48:20 +08:00
commit 609b2334e8
13 changed files with 2220 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

22
.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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
# 方法3systemd状态如果用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`

35
config(模板).ini Normal file
View File

@@ -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

16
grade-monitor.service Normal file
View File

@@ -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

739
monitor.py Normal file
View File

@@ -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'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL)
html = re.sub(r'<style[^>]*>.*?</style>', '', 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()

157
readme.md Normal file
View File

@@ -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

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
requests>=2.31.0
beautifulsoup4>=4.12.0

91
setup_python.sh Normal file
View File

@@ -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"

144
常见问题解决.md Normal file
View File

@@ -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点

69
打包.sh Normal file
View File

@@ -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

516
部署.md Normal file
View File

@@ -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
- 防火墙拦截
**解决方法:**
- 使用校内服务器
- 配置代理
- 联系网络管理员
### 问题3pip安装失败
**解决方法:**
```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` 了解更多功能说明
---
部署完成后,系统将自动监控成绩变化,有新课程成绩时会立即发送邮件通知!

19
重启服务.md Normal file
View File

@@ -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