From 609b2334e81ec324534a8046878984553eb435b1 Mon Sep 17 00:00:00 2001
From: ChuXun <70203584+ChuXunYu@users.noreply.github.com>
Date: Sun, 18 Jan 2026 18:48:20 +0800
Subject: [PATCH] 1
---
.gitattributes | 2 +
.gitignore | 22 ++
Debian服务器部署指南.md | 408 ++++++++++++++++++++++
config(模板).ini | 35 ++
grade-monitor.service | 16 +
monitor.py | 739 ++++++++++++++++++++++++++++++++++++++++
readme.md | 157 +++++++++
requirements.txt | 2 +
setup_python.sh | 91 +++++
常见问题解决.md | 144 ++++++++
打包.sh | 69 ++++
部署.md | 516 ++++++++++++++++++++++++++++
重启服务.md | 19 ++
13 files changed, 2220 insertions(+)
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 Debian服务器部署指南.md
create mode 100644 config(模板).ini
create mode 100644 grade-monitor.service
create mode 100644 monitor.py
create mode 100644 readme.md
create mode 100644 requirements.txt
create mode 100644 setup_python.sh
create mode 100644 常见问题解决.md
create mode 100644 打包.sh
create mode 100644 部署.md
create mode 100644 重启服务.md
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