first commit
This commit is contained in:
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 配置文件不打包,由 volumes 挂载
|
||||||
|
VOLUME ["/app/config.yaml", "/app/last_notifications.json"]
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 教务处通知监控工具
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- 定时监控教务处网站通知
|
||||||
|
- 有新通知时自动发送邮件
|
||||||
|
- 可配置监控频率(分钟)
|
||||||
|
- 状态持久化,记录已发送通知避免重复
|
||||||
|
|
||||||
|
## 部署步骤
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置
|
||||||
|
|
||||||
|
编辑 `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
monitor:
|
||||||
|
url: "https://jwc.your.edu.cn/tzgg.htm" # 改为你的教务处地址
|
||||||
|
frequency_minutes: 30 # 监控频率
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
notification:
|
||||||
|
smtp_host: "smtp.gmail.com" # 邮件服务器
|
||||||
|
smtp_port: 587
|
||||||
|
smtp_user: "your_email@gmail.com"
|
||||||
|
smtp_password: "your_app_password" # 推荐使用应用专用密码
|
||||||
|
to_email: "notify@example.com"
|
||||||
|
use_tls: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 后台运行(Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nohup python main.py > output.log 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用 systemd 服务:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=JWC Monitor
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=your_user
|
||||||
|
WorkingDirectory=/path/to/project
|
||||||
|
ExecStart=/usr/bin/python3 main.py
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 常见邮件服务商 SMTP 设置
|
||||||
|
|
||||||
|
| 服务商 | SMTP Host | 端口 |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| Gmail | smtp.gmail.com | 587 |
|
||||||
|
| QQ | smtp.qq.com | 587 |
|
||||||
|
| 163 | smtp.163.com | 465/994 |
|
||||||
|
|
||||||
|
**Gmail 提示**:需要开启"应用专用密码"或降低账户安全级别。
|
||||||
|
|
||||||
|
## 适配不同网站
|
||||||
|
|
||||||
|
如果抓取不到通知,可能需要调整选择器。参考 `main.py` 中的 `_parse_page` 方法,根据实际网页结构修改选择器。
|
||||||
|
|
||||||
|
常见问题:
|
||||||
|
1. 通知列表在哪个标签里?(ul/div/table)
|
||||||
|
2. 每条通知的标题和链接在哪?
|
||||||
|
3. 网页编码是什么?
|
||||||
BIN
__pycache__/notifier.cpython-311.pyc
Normal file
BIN
__pycache__/notifier.cpython-311.pyc
Normal file
Binary file not shown.
20
config.yaml
Normal file
20
config.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
monitor:
|
||||||
|
frequency_minutes: 30
|
||||||
|
|
||||||
|
sites:
|
||||||
|
- name: "物理与电子科学学院"
|
||||||
|
url: "https://www.csust.edu.cn/wdxy/xytz.htm"
|
||||||
|
encoding: "utf-8"
|
||||||
|
- name: "教务处"
|
||||||
|
url: "https://www.csust.edu.cn/jwc/index/tzgg.htm"
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
notification:
|
||||||
|
smtp_host: "smtp.163.com"
|
||||||
|
smtp_port: 25
|
||||||
|
smtp_user: "chenyouyuan0505@163.com"
|
||||||
|
smtp_password: "AEszrLBtZZK5fJSv"
|
||||||
|
to_email: "3289288508@qq.com"
|
||||||
|
use_tls: true
|
||||||
|
|
||||||
|
state_file: "last_notifications.json"
|
||||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
monitor:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
- ./last_notifications.json:/app/last_notifications.json
|
||||||
|
- ./monitor.log:/app/monitor.log
|
||||||
|
# 环境变量方式配置邮件(可选,优先级高于 config.yaml)
|
||||||
|
# environment:
|
||||||
|
# - SMTP_HOST=smtp.gmail.com
|
||||||
|
# - SMTP_PORT=587
|
||||||
|
# - SMTP_USER=your_email@gmail.com
|
||||||
|
# - SMTP_PASSWORD=your_app_password
|
||||||
12
environment.yml
Normal file
12
environment.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: jwc-monitor
|
||||||
|
channels:
|
||||||
|
- defaults
|
||||||
|
- conda-forge
|
||||||
|
dependencies:
|
||||||
|
- python=3.11
|
||||||
|
- pip
|
||||||
|
- pip:
|
||||||
|
- requests>=2.28.0
|
||||||
|
- beautifulsoup4>=4.11.0
|
||||||
|
- pyyaml>=6.0
|
||||||
|
- schedule>=1.1.0
|
||||||
245
main.py
Normal file
245
main.py
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
教务处通知监控系统
|
||||||
|
支持多网站监控,有新通知时发送邮件提醒
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import schedule
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from notifier import EmailNotifier
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('monitor.log'),
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JWCMonitor:
|
||||||
|
def __init__(self, config_path='config.yaml'):
|
||||||
|
self.config = self._load_config(config_path)
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
})
|
||||||
|
self.notifier = EmailNotifier(self.config['notification'])
|
||||||
|
self.state_file = Path(self.config.get('state_file', 'last_notifications.json'))
|
||||||
|
self.state = self._load_state()
|
||||||
|
|
||||||
|
def _load_config(self, path):
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
def _load_state(self):
|
||||||
|
if self.state_file.exists():
|
||||||
|
with open(self.state_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_state(self):
|
||||||
|
with open(self.state_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.state, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
def fetch_notifications(self, site):
|
||||||
|
"""抓取单个网站的通知列表"""
|
||||||
|
url = site['url']
|
||||||
|
encoding = site.get('encoding', 'utf-8')
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"正在抓取 [{site['name']}]: {url}")
|
||||||
|
response = self.session.get(url, timeout=30)
|
||||||
|
response.encoding = encoding
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
notifications = self._parse_page(soup, url)
|
||||||
|
logger.info(f"[{site['name']}] 获取到 {len(notifications)} 条通知")
|
||||||
|
return notifications
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[{site['name']}] 抓取失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _parse_page(self, soup, base_url):
|
||||||
|
"""解析通知列表页面"""
|
||||||
|
notifications = []
|
||||||
|
|
||||||
|
selectors = [
|
||||||
|
('ul', 'list'),
|
||||||
|
('ul', 'news-list'),
|
||||||
|
('ul', 'tzgg'),
|
||||||
|
('div', 'list'),
|
||||||
|
('div', 'news'),
|
||||||
|
('div', 'article-list'),
|
||||||
|
('table', 'list'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for tag, class_name in selectors:
|
||||||
|
items = soup.find_all(tag, class_=class_name)
|
||||||
|
for container in items:
|
||||||
|
links = container.find_all('a', href=True)
|
||||||
|
for a_tag in links:
|
||||||
|
href = a_tag['href']
|
||||||
|
# 过滤 mailto 和 javascript
|
||||||
|
if not href or 'mailto:' in href or 'javascript' in href.lower():
|
||||||
|
continue
|
||||||
|
title = a_tag.get_text(strip=True)
|
||||||
|
if len(title) > 8 and not title.startswith('#') and not title.startswith('http'):
|
||||||
|
link = self._abs_url(href, base_url)
|
||||||
|
date = self._extract_date_from_element(a_tag)
|
||||||
|
notifications.append({
|
||||||
|
'title': title,
|
||||||
|
'link': link,
|
||||||
|
'date': date,
|
||||||
|
'id': href
|
||||||
|
})
|
||||||
|
if notifications:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not notifications:
|
||||||
|
for a_tag in soup.find_all('a', href=True):
|
||||||
|
href = a_tag['href']
|
||||||
|
if not href or 'mailto:' in href or 'javascript' in href.lower():
|
||||||
|
continue
|
||||||
|
title = a_tag.get_text(strip=True)
|
||||||
|
if len(title) > 8 and not title.startswith('http'):
|
||||||
|
notifications.append({
|
||||||
|
'title': title,
|
||||||
|
'link': self._abs_url(href, base_url),
|
||||||
|
'date': '',
|
||||||
|
'id': href
|
||||||
|
})
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
unique = []
|
||||||
|
for n in notifications:
|
||||||
|
if n['title'] not in seen and n['link']:
|
||||||
|
seen.add(n['title'])
|
||||||
|
unique.append(n)
|
||||||
|
|
||||||
|
return unique[:20]
|
||||||
|
|
||||||
|
def _extract_date_from_element(self, element):
|
||||||
|
parent = element.parent
|
||||||
|
if parent:
|
||||||
|
text = parent.get_text(strip=True)
|
||||||
|
match = re.search(r'\d{4}-\d{2}-\d{2}', text)
|
||||||
|
if match:
|
||||||
|
return match.group(0)
|
||||||
|
for sibling in element.find_next_siblings():
|
||||||
|
text = sibling.get_text(strip=True)
|
||||||
|
match = re.search(r'\d{4}-\d{2}-\d{2}', text)
|
||||||
|
if match:
|
||||||
|
return match.group(0)
|
||||||
|
title = element.get_text(strip=True)
|
||||||
|
match = re.search(r'(\d{4}-\d{2}-\d{2})', title)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _abs_url(self, href, base_url):
|
||||||
|
if not href or href.startswith('#') or 'javascript' in href.lower():
|
||||||
|
return ''
|
||||||
|
if href.startswith('http'):
|
||||||
|
return href
|
||||||
|
return urljoin(base_url, href)
|
||||||
|
|
||||||
|
def _is_valid_date(self, notification):
|
||||||
|
"""过滤2026年之前的通知"""
|
||||||
|
date_str = notification.get('date', '')
|
||||||
|
if not date_str:
|
||||||
|
match = re.search(r'(\d{4})-\d{2}-\d{2}', notification.get('title', ''))
|
||||||
|
if match:
|
||||||
|
date_str = match.group(1)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
year = int(date_str)
|
||||||
|
return year >= 2026
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_updates(self):
|
||||||
|
"""检查所有网站的更新"""
|
||||||
|
sites = self.config.get('sites', [])
|
||||||
|
if not sites:
|
||||||
|
# 兼容单网站配置
|
||||||
|
if 'monitor' in self.config and 'url' in self.config['monitor']:
|
||||||
|
sites = [{
|
||||||
|
'name': '默认网站',
|
||||||
|
'url': self.config['monitor']['url'],
|
||||||
|
'encoding': self.config['monitor'].get('encoding', 'utf-8')
|
||||||
|
}]
|
||||||
|
|
||||||
|
all_new = []
|
||||||
|
|
||||||
|
for site in sites:
|
||||||
|
current = self.fetch_notifications(site)
|
||||||
|
if not current:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 过滤日期
|
||||||
|
current = [n for n in current if self._is_valid_date(n)]
|
||||||
|
|
||||||
|
# 获取该网站的历史状态
|
||||||
|
site_url = site['url']
|
||||||
|
existing_ids = set(self.state.get(site_url, []))
|
||||||
|
|
||||||
|
new_notifications = []
|
||||||
|
for n in current:
|
||||||
|
if n['id'] not in existing_ids:
|
||||||
|
new_notifications.append(n)
|
||||||
|
existing_ids.add(n['id'])
|
||||||
|
|
||||||
|
if new_notifications:
|
||||||
|
for n in new_notifications:
|
||||||
|
n['source'] = site['name']
|
||||||
|
all_new.extend(new_notifications)
|
||||||
|
logger.info(f"[{site['name']}] 发现 {len(new_notifications)} 条新通知")
|
||||||
|
|
||||||
|
# 更新该网站状态
|
||||||
|
self.state[site_url] = list(existing_ids)
|
||||||
|
|
||||||
|
if all_new:
|
||||||
|
logger.info(f"共发现 {len(all_new)} 条新通知")
|
||||||
|
self.notifier.send(all_new)
|
||||||
|
self._save_state()
|
||||||
|
else:
|
||||||
|
logger.info("没有新通知")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""启动监控"""
|
||||||
|
frequency = self.config.get('monitor', {}).get('frequency_minutes', 30)
|
||||||
|
sites = self.config.get('sites', [])
|
||||||
|
site_names = [s['name'] for s in sites] if sites else ['默认网站']
|
||||||
|
logger.info(f"启动监控,每 {frequency} 分钟检查一次")
|
||||||
|
logger.info(f"监控网站: {', '.join(site_names)}")
|
||||||
|
|
||||||
|
self.check_updates()
|
||||||
|
|
||||||
|
schedule.every(frequency).minutes.do(self.check_updates)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
monitor = JWCMonitor()
|
||||||
|
monitor.run()
|
||||||
102
monitor.log
Normal file
102
monitor.log
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
2026-03-24 21:07:26,073 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:07:26,073 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:07:26,073 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:08:06,509 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:08:06,509 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:08:10,437 - ERROR - 邮件发送失败: (535, b'5.7.8 Username and Password not accepted. For more information, go to\n5.7.8 https://support.google.com/mail/?p=BadCredentials d2e1a72fcca58-82b03bbebbfsm15412218b3a.18 - gsmtp')
|
||||||
|
2026-03-24 21:31:12,546 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:31:12,546 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:31:12,546 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:31:42,963 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:31:42,964 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:32:05,402 - ERROR - 邮件发送失败: Connection unexpectedly closed
|
||||||
|
2026-03-24 21:36:52,466 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:36:52,466 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:36:52,466 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:37:22,930 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:37:22,930 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:37:43,247 - ERROR - 邮件发送失败: Connection unexpectedly closed
|
||||||
|
2026-03-24 21:38:59,146 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:38:59,146 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:38:59,146 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:39:29,536 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:39:29,536 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:39:37,605 - INFO - 邮件发送成功: 【教务通知】发现 15 条新通知
|
||||||
|
2026-03-24 21:46:33,382 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:46:33,382 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:46:33,382 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:47:03,777 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:47:03,777 - INFO - 过滤后剩余 15 条通知
|
||||||
|
2026-03-24 21:47:03,778 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:47:08,020 - INFO - 邮件发送成功: 【教务通知】发现 15 条新通知
|
||||||
|
2026-03-24 21:52:11,622 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:52:11,622 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:52:11,622 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:53:11,704 - ERROR - 抓取失败: HTTPSConnectionPool(host='www.csust.edu.cn', port=443): Read timed out. (read timeout=30)
|
||||||
|
2026-03-24 21:53:11,705 - WARNING - 未获取到通知
|
||||||
|
2026-03-24 21:53:35,395 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:53:35,395 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:53:35,396 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:54:05,791 - INFO - 通知: 物理与电子科学学院2025年度人才引进公告2024-03-27 -> info/1011/6500.htm
|
||||||
|
2026-03-24 21:54:05,792 - INFO - 通知: 关于举办2026年全国大学生光电设计竞赛校内选拔赛的通知2026-03-19 -> info/1011/11927.htm
|
||||||
|
2026-03-24 21:54:05,792 - INFO - 通知: 物理与电子科学学院2025级本科生转专业拟接收名单公示2026-03-11 -> info/1011/11911.htm
|
||||||
|
2026-03-24 21:54:05,792 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:54:05,792 - INFO - 过滤后剩余 15 条通知
|
||||||
|
2026-03-24 21:54:05,792 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:54:11,803 - INFO - 邮件发送成功: 【教务通知】发现 15 条新通知
|
||||||
|
2026-03-24 21:55:47,362 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:55:47,362 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:55:47,362 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:56:17,816 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 21:56:17,816 - INFO - 过滤后剩余 15 条通知
|
||||||
|
2026-03-24 21:56:17,816 - INFO - 发现 15 条新通知
|
||||||
|
2026-03-24 21:56:20,188 - INFO - 邮件发送成功: 【教务通知】发现 15 条新通知
|
||||||
|
2026-03-24 21:59:02,678 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 21:59:02,678 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 21:59:02,678 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:00:03,068 - ERROR - 抓取失败: HTTPSConnectionPool(host='www.csust.edu.cn', port=443): Read timed out. (read timeout=30)
|
||||||
|
2026-03-24 22:00:03,070 - WARNING - 未获取到通知
|
||||||
|
2026-03-24 22:00:20,261 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 22:00:20,262 - INFO - 监控地址: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:00:20,262 - INFO - 正在抓取: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:00:50,819 - INFO - 获取到 15 条通知
|
||||||
|
2026-03-24 22:00:50,820 - INFO - 过滤后剩余 8 条通知
|
||||||
|
2026-03-24 22:00:50,820 - INFO - 发现 8 条新通知
|
||||||
|
2026-03-24 22:00:52,165 - INFO - 邮件发送成功: 【教务通知】发现 8 条新通知
|
||||||
|
2026-03-24 22:15:44,640 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 22:15:44,640 - INFO - 监控网站: 物理与电子科学学院, 教务处
|
||||||
|
2026-03-24 22:15:44,640 - INFO - 正在抓取 [物理与电子科学学院]: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:16:15,123 - INFO - [物理与电子科学学院] 获取到 15 条通知
|
||||||
|
2026-03-24 22:16:15,124 - INFO - [物理与电子科学学院] 发现 8 条新通知
|
||||||
|
2026-03-24 22:16:15,124 - INFO - 正在抓取 [教务处]: https://www.csust.edu.cn/jwc/index/tzgg.htm
|
||||||
|
2026-03-24 22:16:15,191 - INFO - [教务处] 获取到 13 条通知
|
||||||
|
2026-03-24 22:16:15,191 - INFO - [教务处] 发现 13 条新通知
|
||||||
|
2026-03-24 22:16:15,191 - INFO - 共发现 21 条新通知
|
||||||
|
2026-03-24 22:16:19,954 - INFO - 邮件发送成功: 【教务通知】发现 21 条新通知
|
||||||
|
2026-03-24 22:22:02,051 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 22:22:02,051 - INFO - 监控网站: 物理与电子科学学院, 教务处
|
||||||
|
2026-03-24 22:22:02,051 - INFO - 正在抓取 [物理与电子科学学院]: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:22:32,383 - INFO - [物理与电子科学学院] 获取到 15 条通知
|
||||||
|
2026-03-24 22:22:32,384 - INFO - [物理与电子科学学院] 发现 8 条新通知
|
||||||
|
2026-03-24 22:22:32,385 - INFO - 正在抓取 [教务处]: https://www.csust.edu.cn/jwc/index/tzgg.htm
|
||||||
|
2026-03-24 22:22:32,460 - INFO - [教务处] 获取到 8 条通知
|
||||||
|
2026-03-24 22:22:32,460 - INFO - [教务处] 发现 8 条新通知
|
||||||
|
2026-03-24 22:22:32,460 - INFO - 共发现 16 条新通知
|
||||||
|
2026-03-24 22:22:33,731 - INFO - 邮件发送成功: 【教务通知】发现 16 条新通知
|
||||||
|
2026-03-24 22:25:16,791 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 22:25:16,791 - INFO - 监控网站: 物理与电子科学学院, 教务处
|
||||||
|
2026-03-24 22:25:16,792 - INFO - 正在抓取 [物理与电子科学学院]: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:25:47,063 - INFO - [物理与电子科学学院] 获取到 15 条通知
|
||||||
|
2026-03-24 22:25:47,065 - INFO - 正在抓取 [教务处]: https://www.csust.edu.cn/jwc/index/tzgg.htm
|
||||||
|
2026-03-24 22:25:47,139 - INFO - [教务处] 获取到 8 条通知
|
||||||
|
2026-03-24 22:25:47,139 - INFO - 没有新通知
|
||||||
|
2026-03-24 22:26:28,009 - INFO - 启动监控,每 30 分钟检查一次
|
||||||
|
2026-03-24 22:26:28,009 - INFO - 监控网站: 物理与电子科学学院, 教务处
|
||||||
|
2026-03-24 22:26:28,009 - INFO - 正在抓取 [物理与电子科学学院]: https://www.csust.edu.cn/wdxy/xytz.htm
|
||||||
|
2026-03-24 22:26:58,282 - INFO - [物理与电子科学学院] 获取到 15 条通知
|
||||||
|
2026-03-24 22:26:58,283 - INFO - [物理与电子科学学院] 发现 8 条新通知
|
||||||
|
2026-03-24 22:26:58,283 - INFO - 正在抓取 [教务处]: https://www.csust.edu.cn/jwc/index/tzgg.htm
|
||||||
|
2026-03-24 22:26:58,364 - INFO - [教务处] 获取到 8 条通知
|
||||||
|
2026-03-24 22:26:58,364 - INFO - [教务处] 发现 8 条新通知
|
||||||
|
2026-03-24 22:26:58,364 - INFO - 共发现 16 条新通知
|
||||||
|
2026-03-24 22:26:59,775 - INFO - 邮件发送成功: 【通知监控】发现 16 条新通知
|
||||||
97
notifier.py
Normal file
97
notifier.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
邮件通知模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailNotifier:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.smtp_host = config['smtp_host']
|
||||||
|
self.smtp_port = config['smtp_port']
|
||||||
|
self.smtp_user = config['smtp_user']
|
||||||
|
self.smtp_password = config['smtp_password']
|
||||||
|
self.to_email = config['to_email']
|
||||||
|
self.use_tls = config.get('use_tls', True)
|
||||||
|
|
||||||
|
def send(self, notifications):
|
||||||
|
"""发送邮件通知"""
|
||||||
|
if not notifications:
|
||||||
|
return
|
||||||
|
|
||||||
|
subject = f"【通知监控】发现 {len(notifications)} 条新通知"
|
||||||
|
|
||||||
|
html_body = self._build_html(notifications)
|
||||||
|
text_body = self._build_text(notifications)
|
||||||
|
|
||||||
|
msg = MIMEMultipart('alternative')
|
||||||
|
msg['Subject'] = subject
|
||||||
|
msg['From'] = self.smtp_user
|
||||||
|
msg['To'] = self.to_email
|
||||||
|
|
||||||
|
msg.attach(MIMEText(text_body, 'plain', 'utf-8'))
|
||||||
|
msg.attach(MIMEText(html_body, 'html', 'utf-8'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
||||||
|
if self.use_tls:
|
||||||
|
server.starttls()
|
||||||
|
server.login(self.smtp_user, self.smtp_password)
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
logger.info(f"邮件发送成功: {subject}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"邮件发送失败: {e}")
|
||||||
|
|
||||||
|
def _build_html(self, notifications):
|
||||||
|
# 按来源网站分组
|
||||||
|
from collections import defaultdict
|
||||||
|
by_site = defaultdict(list)
|
||||||
|
for n in notifications:
|
||||||
|
source = n.get('source', '未知来源')
|
||||||
|
by_site[source].append(n)
|
||||||
|
|
||||||
|
html_parts = []
|
||||||
|
for site_name, items in by_site.items():
|
||||||
|
rows = []
|
||||||
|
for n in items:
|
||||||
|
date = n.get('date', '')
|
||||||
|
rows.append(f'<tr><td><a href="{n["link"]}">{n["title"]}</a></td><td>{date}</td></tr>')
|
||||||
|
html_parts.append(f'''
|
||||||
|
<h3>{site_name}</h3>
|
||||||
|
<table border="1" cellpadding="5" cellspacing="0">
|
||||||
|
<thead><tr><th>标题</th><th>日期</th></tr></thead>
|
||||||
|
<tbody>{''.join(rows)}</tbody>
|
||||||
|
</table>
|
||||||
|
''')
|
||||||
|
|
||||||
|
return f'''
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>新通知 (共 {len(notifications)} 条)</h2>
|
||||||
|
{''.join(html_parts)}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
def _build_text(self, notifications):
|
||||||
|
from collections import defaultdict
|
||||||
|
by_site = defaultdict(list)
|
||||||
|
for n in notifications:
|
||||||
|
source = n.get('source', '未知来源')
|
||||||
|
by_site[source].append(n)
|
||||||
|
|
||||||
|
lines = [f'新通知 (共 {len(notifications)} 条)\n']
|
||||||
|
for site_name, items in by_site.items():
|
||||||
|
lines.append(f'\n=== {site_name} ===')
|
||||||
|
for n in items:
|
||||||
|
date = n.get('date', '')
|
||||||
|
lines.append(f"- {n['title']} {date}")
|
||||||
|
lines.append(f" {n['link']}")
|
||||||
|
return '\n'.join(lines)
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
requests>=2.28.0
|
||||||
|
beautifulsoup4>=4.11.0
|
||||||
|
pyyaml>=6.0
|
||||||
|
schedule>=1.1.0
|
||||||
Reference in New Issue
Block a user