diff --git a/weather_monitor/.env.example b/weather_monitor/.env.example new file mode 100644 index 0000000..48f5ade --- /dev/null +++ b/weather_monitor/.env.example @@ -0,0 +1,34 @@ +# 天气监控与邮件告警系统 - 环境变量配置示例 +# 复制此文件为 .env 并填写您的实际配置 + +# ======================================== +# 和风天气 API 配置 +# ======================================== +# 获取API密钥: https://dev.qweather.com/ +WEATHER_API_KEY=your_api_key_here + +# 城市名称或城市ID(如:Beijing 或 101010100) +WEATHER_CITY=Beijing + +# 温度阈值(摄氏度),低于此值将触发告警 +TEMP_THRESHOLD=5 + +# ======================================== +# SMTP 邮件服务器配置 +# ======================================== +# 常用SMTP服务器: +# QQ邮箱: smtp.qq.com, 端口 587 +# 163邮箱: smtp.163.com, 端口 465 +# Gmail: smtp.gmail.com, 端口 587 +SMTP_SERVER=smtp.qq.com +SMTP_PORT=587 + +# 发件人邮箱地址 +SMTP_USER=your_email@qq.com + +# 邮箱授权码(不是登录密码!) +# QQ邮箱获取: 设置 -> 账户 -> POP3/SMTP服务 -> 生成授权码 +SMTP_PASSWORD=your_authorization_code_here + +# 告警邮件接收地址 +ALERT_RECIPIENT=recipient@example.com diff --git a/weather_monitor/README.md b/weather_monitor/README.md new file mode 100644 index 0000000..43988db --- /dev/null +++ b/weather_monitor/README.md @@ -0,0 +1,230 @@ +# 天气监控与邮件告警系统 + +生产级Python自动化脚本,定时获取天气数据并在温度低于阈值时自动发送邮件提醒。 + +## 项目结构 + +``` +weather_monitor/ +├── weather_monitor.py # 主程序脚本 +├── requirements.txt # Python依赖 +├── .env.example # 环境变量配置示例 +├── README.md # 说明文档 +└── weather_monitor.log # 运行日志(运行后自动生成) +``` + +--- + +## 第一部分:安装依赖 + +### 1. 创建虚拟环境(推荐) + +```bash +# Windows +python -m venv venv +venv\Scripts\activate + +# Linux/Mac +python3 -m venv venv +source venv/bin/activate +``` + +### 2. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 3. 配置环境变量 + +**Windows PowerShell:** +```powershell +$env:WEATHER_API_KEY="your_api_key" +$env:WEATHER_CITY="Beijing" +$env:TEMP_THRESHOLD="5" +$env:SMTP_SERVER="smtp.qq.com" +$env:SMTP_PORT="587" +$env:SMTP_USER="your_email@qq.com" +$env:SMTP_PASSWORD="your_authorization_code" +$env:ALERT_RECIPIENT="recipient@example.com" +``` + +**Linux/Mac:** +```bash +export WEATHER_API_KEY="your_api_key" +export WEATHER_CITY="Beijing" +export TEMP_THRESHOLD="5" +export SMTP_SERVER="smtp.qq.com" +export SMTP_PORT="587" +export SMTP_USER="your_email@qq.com" +export SMTP_PASSWORD="your_authorization_code" +export ALERT_RECIPIENT="recipient@example.com" +``` + +### 4. 运行程序 + +```bash +python weather_monitor.py +``` + +--- + +## 第二部分:后台运行建议 + +### Windows 系统 + +#### 方案一:使用 nohup(需安装Git Bash或Cygwin) + +```bash +nohup python weather_monitor.py > output.log 2>&1 & +``` + +#### 方案二:使用 Windows 任务计划程序 + +1. 打开 **任务计划程序**(搜索 "Task Scheduler") +2. 点击 **创建基本任务** +3. 配置如下: + - **名称**: WeatherMonitor + - **触发器**: 计算机启动时 + - **操作**: 启动程序 + - **程序**: `python.exe` 完整路径 + - **参数**: `weather_monitor.py` 完整路径 + - **起始于**: 脚本所在目录 + +4. 完成后,右键任务 → **属性** → 勾选 **不管用户是否登录都要运行** + +#### 方案三:使用 NSSM 安装为Windows服务 + +```powershell +# 下载 NSSM: https://nssm.cc/download +nssm install WeatherMonitor "C:\Python\python.exe" "C:\path\to\weather_monitor.py" +nssm start WeatherMonitor +``` + +--- + +### Linux 系统 + +#### 方案一:使用 nohup + +```bash +nohup python3 weather_monitor.py > output.log 2>&1 & + +# 查看进程 +ps aux | grep weather_monitor + +# 停止进程 +kill +``` + +#### 方案二:使用 systemd 服务(推荐) + +1. 创建服务文件: + +```bash +sudo nano /etc/systemd/system/weather-monitor.service +``` + +2. 写入以下内容: + +```ini +[Unit] +Description=Weather Monitor Service +After=network.target + +[Service] +Type=simple +User=your_username +WorkingDirectory=/path/to/weather_monitor +Environment="WEATHER_API_KEY=your_api_key" +Environment="WEATHER_CITY=Beijing" +Environment="TEMP_THRESHOLD=5" +Environment="SMTP_SERVER=smtp.qq.com" +Environment="SMTP_PORT=587" +Environment="SMTP_USER=your_email@qq.com" +Environment="SMTP_PASSWORD=your_authorization_code" +Environment="ALERT_RECIPIENT=recipient@example.com" +ExecStart=/usr/bin/python3 /path/to/weather_monitor/weather_monitor.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +3. 启用并启动服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable weather-monitor +sudo systemctl start weather-monitor + +# 查看状态 +sudo systemctl status weather-monitor + +# 查看日志 +sudo journalctl -u weather-monitor -f +``` + +#### 方案三:使用 crontab + +```bash +# 编辑定时任务 +crontab -e + +# 添加以下行(每小时执行一次) +0 * * * * cd /path/to/weather_monitor && /usr/bin/python3 weather_monitor.py >> /var/log/weather_monitor.log 2>&1 +``` + +--- + +## API密钥获取 + +### 和风天气(推荐,国内访问稳定) + +1. 访问 https://dev.qweather.com/ +2. 注册账号并登录 +3. 创建应用,选择 **Web API** +4. 复制 **KEY** 即为 `WEATHER_API_KEY` + +### OpenWeatherMap(备选) + +1. 访问 https://openweathermap.org/api +2. 注册账号 +3. 在 API Keys 页面获取密钥 + +--- + +## 邮箱授权码获取(QQ邮箱示例) + +1. 登录 QQ邮箱 网页版 +2. 点击 **设置** → **账户** +3. 找到 **POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务** +4. 开启 **POP3/SMTP服务** +5. 点击 **生成授权码**,按提示发送短信 +6. 获得的16位授权码即为 `SMTP_PASSWORD` + +--- + +## 日志查看 + +程序运行后会生成 `weather_monitor.log` 日志文件: + +```bash +# 实时查看日志 +tail -f weather_monitor.log + +# Windows PowerShell +Get-Content weather_monitor.log -Wait +``` + +--- + +## 故障排查 + +| 问题 | 可能原因 | 解决方案 | +|------|----------|----------| +| 配置加载失败 | 环境变量未设置 | 检查所有必需变量是否正确设置 | +| API返回错误 | API密钥无效或过期 | 重新获取API密钥 | +| SMTP认证失败 | 授权码错误 | 确认使用授权码而非登录密码 | +| 网络连接失败 | 网络问题 | 检查网络连接和防火墙设置 | diff --git a/weather_monitor/requirements.txt b/weather_monitor/requirements.txt new file mode 100644 index 0000000..d26d780 --- /dev/null +++ b/weather_monitor/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.31.0 +schedule>=1.2.0 diff --git a/weather_monitor/weather_monitor.py b/weather_monitor/weather_monitor.py new file mode 100644 index 0000000..a9c334c --- /dev/null +++ b/weather_monitor/weather_monitor.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- +""" +天气监控与邮件告警系统 (Weather Monitor & Email Alert System) +功能: 定时获取天气数据,当温度低于阈值时自动发送邮件提醒 +作者: 智能脚本架构师 +版本: 1.0 +""" + +import os +import time +import logging +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from typing import Optional, Dict, Any +from datetime import datetime + +import requests +import schedule + +# ============================================================================ +# 日志配置 +# ============================================================================ +LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' +LOG_FILE = 'weather_monitor.log' + +logging.basicConfig( + level=logging.INFO, + format=LOG_FORMAT, + datefmt=LOG_DATE_FORMAT, + handlers=[ + logging.FileHandler(LOG_FILE, encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ============================================================================ +# 配置管理 - 从环境变量读取敏感信息 +# ============================================================================ +""" +【重要】使用前请设置以下环境变量: + +Windows PowerShell: + $env:WEATHER_API_KEY="your_api_key" + $env:WEATHER_CITY="Beijing" + $env:TEMP_THRESHOLD="5" + $env:SMTP_SERVER="smtp.qq.com" + $env:SMTP_PORT="587" + $env:SMTP_USER="your_email@qq.com" + $env:SMTP_PASSWORD="your_authorization_code" + $env:ALERT_RECIPIENT="recipient@example.com" + +Linux/Mac: + export WEATHER_API_KEY="your_api_key" + export WEATHER_CITY="Beijing" + export TEMP_THRESHOLD="5" + export SMTP_SERVER="smtp.qq.com" + export SMTP_PORT="587" + export SMTP_USER="your_email@qq.com" + export SMTP_PASSWORD="your_authorization_code" + export ALERT_RECIPIENT="recipient@example.com" + +API密钥获取: + - 和风天气: https://dev.qweather.com/ + - OpenWeatherMap: https://openweathermap.org/api +""" + + +def get_config() -> Dict[str, Any]: + """ + 从环境变量获取配置信息 + + 返回: + Dict[str, Any]: 包含所有配置项的字典 + + 异常: + ValueError: 当必需的环境变量未设置时抛出 + """ + required_vars = [ + 'WEATHER_API_KEY', + 'WEATHER_CITY', + 'TEMP_THRESHOLD', + 'SMTP_SERVER', + 'SMTP_PORT', + 'SMTP_USER', + 'SMTP_PASSWORD', + 'ALERT_RECIPIENT' + ] + + missing_vars = [var for var in required_vars if not os.environ.get(var)] + if missing_vars: + error_msg = f"缺少必需的环境变量: {', '.join(missing_vars)}" + logger.error(error_msg) + raise ValueError(error_msg) + + config = { + 'api_key': os.environ.get('WEATHER_API_KEY'), + 'city': os.environ.get('WEATHER_CITY'), + 'temp_threshold': float(os.environ.get('TEMP_THRESHOLD', '5')), + 'smtp_server': os.environ.get('SMTP_SERVER'), + 'smtp_port': int(os.environ.get('SMTP_PORT', '587')), + 'smtp_user': os.environ.get('SMTP_USER'), + 'smtp_password': os.environ.get('SMTP_PASSWORD'), + 'alert_recipient': os.environ.get('ALERT_RECIPIENT') + } + + logger.info("配置加载成功") + return config + + +# ============================================================================ +# 天气数据获取 +# ============================================================================ +def fetch_weather(api_key: str, city: str) -> Optional[Dict[str, Any]]: + """ + 从和风天气API获取天气数据 + + 参数: + api_key: 和风天气API密钥 + city: 城市名称或城市ID + + 返回: + Optional[Dict[str, Any]]: 天气数据字典,包含温度、天气状况等; + 请求失败时返回None + """ + url = "https://devapi.qweather.com/v7/weather/now" + params = { + 'location': city, + 'key': api_key + } + + try: + logger.info(f"正在获取 {city} 的天气数据...") + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + + if data.get('code') != '200': + logger.error(f"API返回错误: {data.get('message', '未知错误')}") + return None + + now = data.get('now', {}) + weather_info = { + 'temp': float(now.get('temp', 0)), + 'feels_like': float(now.get('feelsLike', 0)), + 'text': now.get('text', '未知'), + 'humidity': now.get('humidity', '未知'), + 'wind_dir': now.get('windDir', '未知'), + 'wind_scale': now.get('windScale', '未知'), + 'update_time': data.get('updateTime', datetime.now().isoformat()) + } + + logger.info(f"天气数据获取成功: 温度 {weather_info['temp']}°C, {weather_info['text']}") + return weather_info + + except requests.exceptions.Timeout: + logger.error("请求超时,请检查网络连接") + return None + except requests.exceptions.ConnectionError: + logger.error("网络连接失败,请检查网络设置") + return None + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP错误: {e}") + return None + except (KeyError, ValueError, TypeError) as e: + logger.error(f"数据解析错误: {e}") + return None + except Exception as e: + logger.error(f"获取天气数据时发生未知错误: {e}") + return None + + +# ============================================================================ +# 邮件发送功能 +# ============================================================================ +def send_email_alert(weather_info: Dict[str, Any], config: Dict[str, Any]) -> bool: + """ + 发送低温预警邮件 + + 参数: + weather_info: 天气信息字典 + config: 配置字典 + + 返回: + bool: 发送成功返回True,失败返回False + """ + try: + msg = MIMEMultipart('alternative') + msg['From'] = config['smtp_user'] + msg['To'] = config['alert_recipient'] + msg['Subject'] = f"【低温预警】{config['city']} 当前温度 {weather_info['temp']}°C" + + text_content = f""" +您好! + +【低温预警通知】 + +城市: {config['city']} +当前温度: {weather_info['temp']}°C +体感温度: {weather_info['feels_like']}°C +天气状况: {weather_info['text']} +相对湿度: {weather_info['humidity']}% +风向风力: {weather_info['wind_dir']} {weather_info['wind_scale']}级 +数据更新时间: {weather_info['update_time']} + +当前温度已低于预设阈值 {config['temp_threshold']}°C,请注意保暖! + +--- +此邮件由天气监控系统自动发送 +时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +""" + + html_content = f""" + + + + + +
+
+

🌡️ 低温预警通知

+
+
+
城市: {config['city']}
+
当前温度: {weather_info['temp']}°C
+
体感温度: {weather_info['feels_like']}°C
+
天气状况: {weather_info['text']}
+
相对湿度: {weather_info['humidity']}%
+
风向风力: {weather_info['wind_dir']} {weather_info['wind_scale']}级
+
数据更新时间: {weather_info['update_time']}
+
+

+ ⚠️ 当前温度已低于预设阈值 {config['temp_threshold']}°C,请注意保暖! +

+
+ +
+ + +""" + + part1 = MIMEText(text_content, 'plain', 'utf-8') + part2 = MIMEText(html_content, 'html', 'utf-8') + + msg.attach(part1) + msg.attach(part2) + + logger.info(f"正在连接邮件服务器 {config['smtp_server']}...") + + with smtplib.SMTP(config['smtp_server'], config['smtp_port']) as server: + server.starttls() + server.login(config['smtp_user'], config['smtp_password']) + server.sendmail( + config['smtp_user'], + config['alert_recipient'], + msg.as_string() + ) + + logger.info(f"预警邮件发送成功 -> {config['alert_recipient']}") + return True + + except smtplib.SMTPAuthenticationError: + logger.error("SMTP认证失败,请检查邮箱地址和授权码") + return False + except smtplib.SMTPException as e: + logger.error(f"SMTP错误: {e}") + return False + except Exception as e: + logger.error(f"发送邮件时发生未知错误: {e}") + return False + + +# ============================================================================ +# 主逻辑函数 +# ============================================================================ +def check_weather_and_alert() -> None: + """ + 主检查函数:获取天气数据、判断阈值、触发告警 + """ + logger.info("=" * 50) + logger.info("开始执行天气检查任务") + + try: + config = get_config() + except ValueError: + logger.error("配置加载失败,跳过本次检查") + return + + weather_info = fetch_weather(config['api_key'], config['city']) + + if weather_info is None: + logger.warning("无法获取天气数据,跳过本次检查") + return + + current_temp = weather_info['temp'] + threshold = config['temp_threshold'] + + logger.info(f"当前温度: {current_temp}°C, 阈值: {threshold}°C") + + if current_temp < threshold: + logger.warning(f"温度 {current_temp}°C 低于阈值 {threshold}°C,触发告警!") + send_email_alert(weather_info, config) + else: + logger.info(f"温度正常,无需告警") + + logger.info("天气检查任务完成") + logger.info("=" * 50) + + +# ============================================================================ +# 程序入口 +# ============================================================================ +def main() -> None: + """ + 主函数:初始化配置并启动定时任务调度 + """ + logger.info("=" * 60) + logger.info("天气监控与邮件告警系统启动") + logger.info("=" * 60) + + try: + config = get_config() + logger.info(f"监控城市: {config['city']}") + logger.info(f"温度阈值: {config['temp_threshold']}°C") + logger.info(f"告警接收邮箱: {config['alert_recipient']}") + except ValueError: + logger.error("配置初始化失败,程序退出") + return + + schedule.every().hour.do(check_weather_and_alert) + + logger.info("定时任务已设置: 每小时检查一次天气") + logger.info("按 Ctrl+C 停止程序") + + check_weather_and_alert() + + while True: + try: + schedule.run_pending() + time.sleep(60) + except KeyboardInterrupt: + logger.info("收到停止信号,程序退出") + break + except Exception as e: + logger.error(f"主循环发生错误: {e}") + time.sleep(60) + + +if __name__ == "__main__": + main()