1
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal 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
|
||||
408
Debian服务器部署指南.md
Normal file
408
Debian服务器部署指南.md
Normal 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
|
||||
|
||||
# 方法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`
|
||||
35
config(模板).ini
Normal file
35
config(模板).ini
Normal 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
16
grade-monitor.service
Normal 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
739
monitor.py
Normal 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
157
readme.md
Normal 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
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests>=2.31.0
|
||||
beautifulsoup4>=4.12.0
|
||||
91
setup_python.sh
Normal file
91
setup_python.sh
Normal 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
144
常见问题解决.md
Normal 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
69
打包.sh
Normal 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
516
部署.md
Normal 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
|
||||
- 防火墙拦截
|
||||
|
||||
**解决方法:**
|
||||
- 使用校内服务器
|
||||
- 配置代理
|
||||
- 联系网络管理员
|
||||
|
||||
### 问题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` 了解更多功能说明
|
||||
|
||||
---
|
||||
|
||||
部署完成后,系统将自动监控成绩变化,有新课程成绩时会立即发送邮件通知!
|
||||
19
重启服务.md
Normal file
19
重启服务.md
Normal 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
|
||||
Reference in New Issue
Block a user