20254226 2025-2026-2《Python程序设计》实验四报告
20254226 2025-2026-2 《Python程序设计》实验4报告
课程:《Python程序设计》
班级:2542
学号:20254226
实验教师:王志强
实验时间:2026年5月30日
必修/选修:专选课
一、实验内容
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等
注:在Windows/Linux系统上使用VIM、PDB、IDLE、Pycharm等工具编程实现。
评分标准:
(1)程序能运行,功能丰富(至少5个功能)。(需求提交源代码,并建议录制程序运行的视频)
(2)综合实践报告,要体现实验分析、设计、实现过程、结果等信息,格式规范,逻辑清晰,结构合理。
(3)在实践报告中,需要对全课进行总结,并写课程感想体会、意见和建议等。
二、实验目的
由于我们学校每天早上六点要跑操,大家总是期待接收到不跑操的通知,但是每个人手机显示的天气不大一样,总是跟着用户本人的手机型号来变,打算通过爬取官方天气数据来获取第二天早上六点的天气信息,再通过天气判断明天是否跑操,并将第二天的跑操结果发送至邮箱,让大家得知一个准确的信息,不必多次揣测多变的天气。
三、实验过程
步骤 1:搭配实验环境
打开 PyCharm 的终端,输入(显示已安装):
pip install requests matplotlib schedule

步骤 2:创建项目文件夹
由于步骤较多,所以新建文件夹,这里统一放:
D:\Python_Program\WeatherReport

在这个文件夹里,创建四个子文件夹:
| 文件夹 | 用途 |
|---|---|
data |
存放 CSV 表格 |
charts |
存温度折线图 |
logs |
存程序运行日志 |
diagnoses |
存调试诊断脚本 |

步骤 3:配置清单 weather_config.py
为了方便改动,将可能要改的东西都放置一处。
在 PyCharm 的项目目录上右键 → New → Python File,命名为 weather_config.py。
第 1 步:获取和风天气的API Key和 API Host
- 打开和风天气开发者官网:https://dev.qweather.com/

- 注册账号并建立应用,获取免费版
API Key

-
获取免费版
API Host
![image]()
-
填写三个接口地址(用 f-string 把 API_HOST 拼进去)
-
填写所属城市:北京
-
代码如下:
# 和风天气 API 配置
QWEATHER_API_KEY = "a4557fb819e4471e9b03f41adfbf26b6"
QWEATHER_API_HOST = "m9564w9rmc.re.qweatherapi.com"
# 三个接口的完整网址
QWEATHER_CITY_LOOKUP_URL = f"https://{QWEATHER_API_HOST}/geo/v2/city/lookup"
QWEATHER_HOURLY_URL = f"https://{QWEATHER_API_HOST}/v7/weather/24h"
QWEATHER_AIR_URL = f"https://{QWEATHER_API_HOST}/v7/air/now"
# 目标城市
TARGET_CITY = "北京"
TARGET_CITY_ID = "101010100" # 北京的城市编号,可从和风天气官网查询
第 2 步:写邮箱信息。
-
打开并登陆qq邮箱:https://mail.qq.com/
-
在安全设置
POP3/IMAP/SMTP/Exchange/CardDAV 服务中生成授权码。


- 代码如下:
# 邮件 SMTP 配置
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 465 # SSL 加密端口
SMTP_USER = "3862799071@qq.com"
SMTP_PASSWORD = "bkstwtcekkksccbj"
# 接收方邮箱
RECEIVER_EMAILS = "3528753906@qq.com"
# 邮件主题模板
EMAIL_SUBJECT_TEMPLATE = "【跑操提醒】{date} 早晨跑操通知"
第 3 步:写文件存放地址。
-
指定程序的工作目录和各类文件的存放位置
-
代码如下:
import os
# 项目根目录
BASE_DIR = r"D:\Python_Program\WeatherReport"
# 三个子文件夹的路径
DATA_DIR = os.path.join(BASE_DIR, "data") # 存 CSV 表格
CHART_DIR = os.path.join(BASE_DIR, "charts") # 存折线图
LOG_DIR = os.path.join(BASE_DIR, "logs") # 存日志
# 自动创建缺失的文件夹
for d in (DATA_DIR, CHART_DIR, LOG_DIR):
os.makedirs(d, exist_ok=True)
# 各文件的具体路径
HISTORY_CSV_PATH = os.path.join(DATA_DIR, "weather_history.csv")
CHART_PATH_TEMPLATE = os.path.join(CHART_DIR, "temperature_{date}.png")
LOG_PATH = os.path.join(LOG_DIR, "runner.log")
第 4 步:写出操判断的标准。
-
列出阈值用于自动判断早上六点是否出操,相关值有空气质量,风力,极端天气等。只要满足任一条件,即判定为不出操。
-
代码如下:
# 极端温度
TEMPERATURE_MIN = 0 # 低于 0°C 不出操
TEMPERATURE_MAX = 35 # 高于 35°C 不出操
# 空气质量阈值
AQI_MAX = 100 # AQI 大于 100 不出操
# 大风阈值
WIND_LEVEL_MAX = 6 # 风力大于 6 级不出操
WIND_SPEED_MAX = 30 # 风速大于 30 km/h 不出操
# 恶劣天气关键词(天气描述里包含任一即不出操)
RAIN_SNOW_KEYWORDS = ["雨", "雪", "雾", "霾", "沙尘", "冰雹", "冻雨", "霜"]
# 早晨跑操时间(看早上 6 点的天气)
RUN_TIME_HOUR = 6
第 5 步:写界面字体和定时时间。
为了提高用户的可读性和页面的美观性,通知页面设置成固定的字体和字号,并进行定时任务配置,每天下午五点自动发放。
-
设置窗口字体(
微软雅黑)及定时任务配置(每天 17:00 自动检查)。 -
代码如下:
# UI 字体配置
UI_FONT_FAMILY = "微软雅黑"
UI_FONT_TITLE = (UI_FONT_FAMILY, 14, "bold")
UI_FONT_LABEL = (UI_FONT_FAMILY, 11)
UI_FONT_BUTTON = (UI_FONT_FAMILY, 10)
UI_FONT_RESULT = (UI_FONT_FAMILY, 12, "bold")
# 定时任务配置
SCHEDULED_TIME = "17:00"
SCHEDULE_INTERVAL = 30
# 辅助函数:打印配置摘要(调试用)
def print_config_summary():
print("=" * 50)
print(f" 目标城市: {TARGET_CITY}")
print(f" 发件邮箱: {SMTP_USER}")
print(f" 收件邮箱: {RECEIVER_EMAILS}")
print(f" 定时时间: 每天 {SCHEDULED_TIME}")
print("=" * 50)
步骤 4: 历史数据模块weather_history.py
新建 weather_history.py,负责:
初始化 CSV 历史记录文件。
将每次获取的天气数据追加写入 CSV。
读取历史数据。
提供日志记录功能。
第 1 步:检查文件夹的函数。
确保数据目录和日志目录存在。程序启动时调用,防止后续写入操作因目录不存在而报错。
-
配置文件路径,并自动创建所需目录。
-
代码如下:
import os, csv, datetime
from weather_config import HISTORY_CSV_PATH, LOG_PATH, DATA_DIR, LOG_DIR
def ensure_data_dirs():
"""确保数据目录和日志目录存在,不存在就自动创建。"""
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(LOG_DIR, exist_ok=True)
第 2 步:新建 CSV 表格的函数。
- 函数新建一个 CSV 文件。如果文件不存在,则创建并写入表头;如果已存在,不覆盖。

- 在第一行写表头,每一列存信息:日期、小时、温度、天气、风力、风速、AQI、判定结果、理由、写入时间。
| 字段名 | 含义 | 说明 |
|---|---|---|
date |
日期 | YYYY-MM-DD |
hour |
小时 | 目标跑操时间(如 6) |
temperature |
温度 | 摄氏度 |
weather_text |
天气描述 | 如"晴"、"小雨" |
wind_level |
风力等级 | 1-17 级 |
wind_speed |
风速 | 公里/小时 |
aqi |
空气质量指数 | 数值 |
aqi_category |
AQI 等级 | 优/良/轻度污染等 |
decision |
出操判定 | "出操" 或 "不出操" |
reason |
判定理由 | 多条用分号分隔 |
timestamp |
记录写入时间 | 精确到秒 |
- 代码如下:
_CSV_HEADER = [
"date", "hour", "temperature", "weather_text", "wind_level",
"wind_speed", "aqi", "aqi_category", "decision", "reason", "timestamp",
]
def init_csv():
"""初始化 CSV 文件。如果不存在,创建并写入表头;如果已存在,不覆盖。"""
ensure_data_dirs()
if not os.path.exists(HISTORY_CSV_PATH):
with open(HISTORY_CSV_PATH, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(_CSV_HEADER)
write_log(f"已创建历史数据文件: {HISTORY_CSV_PATH}")
第 3 步:追加一条记录的函数。
每次查完天气、判完出操,主程序就会调用这个函数,把结果追加到 CSV 表格的最后一行。其中使用的是追加(
"a")模式,不会覆盖以前的数据。
- 代码如下:
def append_record(date_str, hour, temperature, weather_text,
wind_level, wind_speed, aqi, aqi_category,
decision, reason):
"""将一条天气记录追加到 CSV 历史文件中。"""
init_csv()
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
row = [date_str, hour, temperature, weather_text, wind_level,
wind_speed, aqi, aqi_category, decision, reason, timestamp]
with open(HISTORY_CSV_PATH, "a", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(row)
write_log(f"已追加历史记录: {date_str} {decision}")
第 4 步:写"读取最近几天记录"的函数。
画温度折线图时,需要知道最近几天的温度。从 CSV 里把所有记录读出来,按日期倒序排,取最近N天,再按日期正序排回去,为温度折线图提供数据。
- 代码如下:
def read_all_history():
"""读取全部历史数据,返回字典列表。"""
init_csv()
records = []
with open(HISTORY_CSV_PATH, "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row in reader:
records.append(row)
return records
def read_recent_history(days=7):
"""读取最近 N 天的历史记录,用于画折线图。"""
all_records = read_all_history()
all_records_sorted = sorted(
all_records, key=lambda x: x.get("date", ""), reverse=True
)
recent = all_records_sorted[:days]
recent = sorted(recent, key=lambda x: x.get("date", ""))
return recent
第 5 步:日志的函数。
记录程序运行日志
- 代码如下:
def write_log(message):
"""写入日志文件。每条日志带时间戳。"""
ensure_data_dirs()
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{timestamp}] {message}\n"
with open(LOG_PATH, "a", encoding="utf-8") as f:
f.write(line)
步骤 5:运行程序weather_runner.py
新建 weather_runner.py,负责运行所有的程序。
第 1 步:引入所有工具。
- 把 Python 标准库、第三方库和前面写好的两个文件引进来。
- 代码如下:
import os, sys, json, time, csv, datetime, smtplib, socket, threading
import email.mime.multipart, email.mime.text, email.mime.base
from email import encoders
import requests
import matplotlib
matplotlib.use("Agg") # 使用非交互后端,避免后台线程弹窗
import matplotlib.pyplot as plt
import schedule
import tkinter as tk
import weather_config as cfg
from weather_history import (
append_record, read_recent_history, write_log, init_csv, ensure_data_dirs
)
# 全局字体设置:确保图表中文正常显示
plt.rcParams["font.family"] = ["Microsoft YaHei", "SimHei", "sans-serif"]
plt.rcParams["axes.unicode_minus"] = False
第 2 步:爬取和风天气网站 WeatherFetcher
内部含有发网络请求、处理异常、查城市 ID、 24 小时预报、空气质量、"明天早上 6 点"等数据。
- 设置网络请求
class WeatherFetcher:
def __init__(self, api_key, city_id=None, city_name=None):
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 ... Chrome/126.0.0.0 Safari/537.36"
})
def _get(self, url, params):
response = requests.get(url, params=params, headers=headers, timeout=15)
response.raise_for_status()
data = response.json()
- 将和风天气返回的未来 24 小时逐小时数据翻译成 Python 能看懂的字典列表。
def fetch_hourly_forecast(self):
data = self._get(cfg.QWEATHER_HOURLY_URL, params)
hourly_list = data["hourly"]
records = []
for item in hourly_list:
fx_time = item.get("fxTime", "")
dt = datetime.datetime.fromisoformat(fx_time.replace("Z", "+00:00"))
records.append({
"datetime": dt,
"date": dt.strftime("%Y-%m-%d"),
"hour": dt.hour,
"temperature": float(item.get("temp", 0)),
...
})
点击展开全部代码:
class WeatherFetcher:
def __init__(self, api_key, city_id=None, city_name=None):
self.api_key = api_key
self.city_id = city_id
self.city_name = city_name
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/126.0.0.0 Safari/537.36"
})
def _get(self, url, params):
"""发送 GET 请求,处理异常,返回 JSON 字典。"""
try:
headers = {
"User-Agent": "Mozilla/5.0 ...",
"X-QW-Api-Key": self.api_key,
}
response = requests.get(url, params=params, headers=headers, timeout=15)
response.raise_for_status()
data = response.json()
if "error" in data or data.get("code") != "200":
write_log(f"API 返回错误: {data}")
return None
return data
except requests.exceptions.RequestException as e:
write_log(f"网络请求异常: {e}")
return None
def fetch_city_id(self):
"""根据城市名查询 Location ID。"""
params = {"location": self.city_name, "key": self.api_key}
data = self._get(cfg.QWEATHER_CITY_LOOKUP_URL, params)
if data and data.get("location"):
self.city_id = data["location"][0]["id"]
write_log(f"已获取城市 ID: {self.city_id}")
return self.city_id
return None
def fetch_hourly_forecast(self):
"""获取未来 24 小时逐小时预报。"""
if not self.city_id:
self.fetch_city_id()
params = {"location": self.city_id, "key": self.api_key}
data = self._get(cfg.QWEATHER_HOURLY_URL, params)
if not data or "hourly" not in data:
return []
records = []
for item in data["hourly"]:
fx_time = item.get("fxTime", "")
dt = datetime.datetime.fromisoformat(fx_time.replace("Z", "+00:00"))
records.append({
"datetime": dt, "date": dt.strftime("%Y-%m-%d"), "hour": dt.hour,
"temperature": float(item.get("temp", 0)),
"weather_text": item.get("text", ""),
"wind_level": item.get("windScale", "0"),
"wind_speed": float(item.get("windSpeed", 0)),
"humidity": item.get("humidity", ""),
"precip": item.get("precip", ""),
})
return records
def fetch_air_quality(self):
"""获取实时空气质量。免费版可能返回 403,做降级处理。"""
if not self.city_id:
self.fetch_city_id()
params = {"location": self.city_id, "key": self.api_key}
data = self._get(cfg.QWEATHER_AIR_URL, params)
if not data or "now" not in data:
return {"aqi": -1, "category": "该订阅不包含空气质量数据", "pm10": -1, "pm2p5": -1}
now = data["now"]
return {
"aqi": int(now.get("aqi", -1)),
"category": now.get("category", "未知"),
"pm10": int(now.get("pm10", -1)),
"pm2p5": int(now.get("pm2p5", -1)),
}
def get_target_hour_weather(self, target_hour=6, target_date_offset=1):
"""从 24 小时数据中筛选出明天 target_hour 点的那一条。"""
today = datetime.date.today()
target_date = today + datetime.timedelta(days=target_date_offset)
target_date_str = target_date.strftime("%Y-%m-%d")
hourly = self.fetch_hourly_forecast()
for rec in hourly:
if rec["date"] == target_date_str and rec["hour"] == target_hour:
return rec
return None
第 3 步:判断是否出操 WeatherDecision
拿到天气数据后,对照条件,进行判断,并且把原因记下来。
- 使用if语句检查极端温度
if temp < cfg.TEMPERATURE_MIN:
self.reasons.append(f"温度过低({temp}°C < {cfg.TEMPERATURE_MIN}°C),易引发感冒或冻伤")
if temp > cfg.TEMPERATURE_MAX:
self.reasons.append(f"温度过高({temp}°C > {cfg.TEMPERATURE_MAX}°C),存在中暑风险")
- 使用for循环检查有没有不跑操的条件
for keyword in cfg.RAIN_SNOW_KEYWORDS:
if keyword in text:
self.reasons.append(f"天气状况不佳({text}),不适合户外运动")
break
点击展开全部代码:
class WeatherDecision:
def __init__(self, weather_rec, air_rec):
self.weather = weather_rec
self.air = air_rec
self.reasons = [] # 收集不出操的理由
def judge(self):
"""执行判断,返回 (decision, reason)。"""
if not self.weather:
return "不出操", "无法获取天气数据,请检查网络或 API 配置。"
temp = self.weather["temperature"]
text = self.weather["weather_text"]
wind_level = str(self.weather.get("wind_level", "0"))
wind_speed = self.weather.get("wind_speed", 0)
aqi = self.air.get("aqi", -1)
# 1. 检查极端低温
if temp < cfg.TEMPERATURE_MIN:
self.reasons.append(f"温度过低({temp}°C < {cfg.TEMPERATURE_MIN}°C),易引发感冒或冻伤")
# 2. 检查极端高温
if temp > cfg.TEMPERATURE_MAX:
self.reasons.append(f"温度过高({temp}°C > {cfg.TEMPERATURE_MAX}°C),存在中暑风险")
# 3. 检查雨雪雾霾等恶劣天气
for keyword in cfg.RAIN_SNOW_KEYWORDS:
if keyword in text:
self.reasons.append(f"天气状况不佳({text}),不适合户外运动")
break
# 4. 检查风力等级
try:
wl = int(wind_level.replace("级", "").split("-")[0])
except ValueError:
wl = 0
if wl > cfg.WIND_LEVEL_MAX:
self.reasons.append(f"风力过大({wind_level} > {cfg.WIND_LEVEL_MAX}级),易影响跑步安全")
# 5. 检查风速
if wind_speed > cfg.WIND_SPEED_MAX:
self.reasons.append(f"风速过快({wind_speed}km/h > {cfg.WIND_SPEED_MAX}km/h),体感不适")
# 6. 检查 AQI(免费版可能不可用,aqi == -1 时跳过)
if aqi != -1 and aqi > cfg.AQI_MAX:
self.reasons.append(f"空气质量差(AQI {aqi} > {cfg.AQI_MAX}),不利于呼吸健康")
# 综合判定
if self.reasons:
decision = "不出操"
reason = ";".join(self.reasons)
else:
aqi_display = aqi if aqi != -1 else "暂无数据"
decision = "出操"
reason = f"天气良好({text},{temp}°C,AQI {aqi_display}),请同学们及时到指定地点集合!"
return decision, reason
第 4 步:温度折线图TemperatureChart
- 读取 CSV 里最近几天的温度,分别提取日期列表和温度列表。
class TemperatureChart:
@staticmethod
def draw(days=7, save_path=None):
records = read_recent_history(days=days)
dates = [r["date"] for r in records]
temps = [float(r["temperature"]) for r in records]
- 创建画布,画折线,标记颜色,在每个数据点画一个小圆点,在折线下方填充半透明红色
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=150)
ax.plot(dates, temps, color="#E74C3C", linewidth=2.5, marker="o", markersize=6, zorder=3)
ax.fill_between(dates, temps, alpha=0.15, color="#E74C3C")
- 使用for 循环给每个数据点上方标注温度数值,同时标记遍历索引,日期和温度。
for i, (d, t) in enumerate(zip(dates, temps)):
ax.annotate(f"{t:.1f}°C", (i, t), textcoords="offset points", xytext=(0, 10), ...)
- 把横坐标的日期标签旋转 30 度,自动调整边距,保存为PNG格式。
class TemperatureChart:
@staticmethod
def draw(days=7, save_path=None):
"""绘制最近 days 天的温度变化折线图,保存为 PNG。"""
records = read_recent_history(days=days)
if not records:
write_log("历史数据不足,无法绘制折线图")
return None
dates = [r["date"] for r in records]
temps = [float(r["temperature"]) for r in records]
点击展开全部代码:
class TemperatureChart:
@staticmethod
def draw(days=7, save_path=None):
"""绘制最近 days 天的温度变化折线图,保存为 PNG。"""
records = read_recent_history(days=days)
if not records:
write_log("历史数据不足,无法绘制折线图")
return None
dates = [r["date"] for r in records]
temps = [float(r["temperature"]) for r in records]
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=150)
# 画红色折线 + 数据点
ax.plot(dates, temps, color="#E74C3C", linewidth=2.5, marker="o", markersize=6, zorder=3)
# 填充下方区域(半透明)
ax.fill_between(dates, temps, alpha=0.15, color="#E74C3C")
# 在每个数据点上方标注温度数值
for i, (d, t) in enumerate(zip(dates, temps)):
ax.annotate(f"{t:.1f}°C", (i, t), textcoords="offset points",
xytext=(0, 10), ha="center", fontsize=9, color="#333333")
# 标题与标签
ax.set_title(f"近 {len(dates)} 天早晨 {cfg.RUN_TIME_HOUR}:00 温度变化趋势",
fontsize=14, fontweight="bold", color="#2C3E50")
ax.set_xlabel("日期", fontsize=11, color="#555555")
ax.set_ylabel("温度 (°C)", fontsize=11, color="#555555")
# 网格与美化
ax.grid(True, linestyle="--", alpha=0.4, color="#999999")
ax.set_axisbelow(True)
plt.xticks(rotation=30, ha="right")
ax.set_facecolor("#FAFAFA")
fig.patch.set_facecolor("#FFFFFF")
for spine in ax.spines.values():
spine.set_color("#CCCCCC")
plt.tight_layout()
if not save_path:
today_str = datetime.date.today().strftime("%Y%m%d")
save_path = cfg.CHART_PATH_TEMPLATE.format(date=today_str)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
plt.savefig(save_path, dpi=150, bbox_inches="tight")
plt.close(fig)
write_log(f"温度折线图已保存: {save_path}")
return save_path
第 6 步:用户界面优化WeatherApp
- 创建主窗口,放置屏幕正中间,再搭界面,启动后台定时线程。

class WeatherApp:
def __init__(self, root):
self.root = root
self.root.title("天气跑操提醒助手")
self.root.geometry("720x580")
self.root.configure(bg="#F5F6FA")
self.root.resizable(False, False)
self.center_window()
self.fetcher = WeatherFetcher(...)
self.mailer = MailSender(...)
self._build_ui()
self._start_scheduler_thread()
def _start_scheduler_thread(self):
def scheduler_loop():
schedule.every().day.at(cfg.SCHEDULED_TIME).do(self._scheduled_task)
while True:
schedule.run_pending()
time.sleep(cfg.SCHEDULE_INTERVAL)
t = threading.Thread(target=scheduler_loop, daemon=True)
t.start()
- 设置按钮处于可点击状态
def on_run_now(self):
self.btn_run.config(state=tk.DISABLED, text="检查中...")
self.root.update()
try:
self._execute_check(send_email=True)
except Exception as e:
self._update_result(f"运行出错:{e}", error=True)
finally:
self.btn_run.config(state=tk.NORMAL, text="立即检查明天天气")
- 核心执行路程
def _execute_check(self, send_email=False):
# 1. 更新配置
city = self.entry_city.get().strip() or cfg.TARGET_CITY
# 2. 获取天气数据
weather_rec = self.fetcher.get_target_hour_weather(...)
air_rec = self.fetcher.fetch_air_quality()
# 3. 出操判断
decider = WeatherDecision(weather_rec, air_rec)
decision, reason = decider.judge()
# 4. 保存历史数据
append_record(...)
# 5. 绘制温度折线图
chart_path = TemperatureChart.draw()
# 6. 发送邮件
if send_email and weather_rec and receiver:
self.mailer.send(...)
- 优化界面字体颜色,“不出操”是红色,“出操”是绿色
def _build_email_html(self, weather_rec, air_rec, decision, reason):
color = "#27AE60" if decision == "出操" else "#E74C3C"
card = f"""
<div style="...">
<h2>🌤 明日跑操提醒</h2>
<table style="...">...</table>
<div style="background-color:{color};..."> {decision} </div>
</div>
"""
return card
点击展开全部代码:
class WeatherApp:
def __init__(self, root):
self.root = root
self.root.title("天气跑操提醒助手")
self.root.geometry("720x580")
self.root.configure(bg="#F5F6FA")
self.root.resizable(False, False)
self.center_window()
# 初始化各部门
self.fetcher = WeatherFetcher(cfg.QWEATHER_API_KEY, cfg.TARGET_CITY_ID, cfg.TARGET_CITY)
self.mailer = MailSender(cfg.SMTP_SERVER, cfg.SMTP_PORT, cfg.SMTP_USER, cfg.SMTP_PASSWORD)
self._build_ui()
self._start_scheduler_thread()
def center_window(self):
"""将窗口居中显示于屏幕。"""
self.root.update_idletasks()
width, height = 720, 580
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.root.geometry(f"{width}x{height}+{x}+{y}")
def _build_ui(self):
"""构建 tkinter 界面:标题栏、输入框、按钮、结果文本框。"""
font_title = cfg.UI_FONT_TITLE
font_label = cfg.UI_FONT_LABEL
font_btn = cfg.UI_FONT_BUTTON
font_result = cfg.UI_FONT_RESULT
# 顶部蓝色标题栏
title_frame = tk.Frame(self.root, bg="#2C3E50", height=50)
title_frame.pack(fill=tk.X)
title_frame.pack_propagate(False)
tk.Label(title_frame, text="☀ 天气跑操提醒助手", font=font_title,
bg="#2C3E50", fg="#FFFFFF").pack(side=tk.LEFT, padx=15, pady=8)
# 主内容区域
main_frame = tk.Frame(self.root, bg="#F5F6FA")
main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
# --- 配置信息区 ---
cfg_frame = tk.LabelFrame(main_frame, text="配置信息", font=font_label,
bg="#FFFFFF", fg="#333333", padx=10, pady=8)
cfg_frame.pack(fill=tk.X, pady=5)
tk.Label(cfg_frame, text="目标城市:", font=font_label, bg="#FFFFFF").grid(row=0, column=0, sticky=tk.W, pady=3)
self.entry_city = tk.Entry(cfg_frame, width=20, font=font_label)
self.entry_city.grid(row=0, column=1, sticky=tk.W, pady=3, padx=5)
self.entry_city.insert(0, cfg.TARGET_CITY)
tk.Label(cfg_frame, text="API Key:", font=font_label, bg="#FFFFFF").grid(row=0, column=2, sticky=tk.W, pady=3, padx=10)
self.entry_key = tk.Entry(cfg_frame, width=30, font=font_label, show="*")
self.entry_key.grid(row=0, column=3, sticky=tk.W, pady=3, padx=5)
self.entry_key.insert(0, cfg.QWEATHER_API_KEY)
tk.Label(cfg_frame, text="发件邮箱:", font=font_label, bg="#FFFFFF").grid(row=1, column=0, sticky=tk.W, pady=3)
self.entry_sender = tk.Entry(cfg_frame, width=20, font=font_label)
self.entry_sender.grid(row=1, column=1, sticky=tk.W, pady=3, padx=5)
self.entry_sender.insert(0, cfg.SMTP_USER)
tk.Label(cfg_frame, text="收件邮箱:", font=font_label, bg="#FFFFFF").grid(row=1, column=2, sticky=tk.W, pady=3, padx=10)
self.entry_receiver = tk.Entry(cfg_frame, width=30, font=font_label)
self.entry_receiver.grid(row=1, column=3, sticky=tk.W, pady=3, padx=5)
self.entry_receiver.insert(0, cfg.RECEIVER_EMAILS)
# --- 操作按钮区 ---
btn_frame = tk.Frame(main_frame, bg="#F5F6FA")
btn_frame.pack(fill=tk.X, pady=8)
self.btn_run = tk.Button(btn_frame, text="立即检查明天天气", font=font_btn,
bg="#3498DB", fg="#FFFFFF", width=18, height=2,
cursor="hand2", command=self.on_run_now)
self.btn_run.pack(side=tk.LEFT, padx=5)
self.btn_chart = tk.Button(btn_frame, text="查看温度折线图", font=font_btn,
bg="#2ECC71", fg="#FFFFFF", width=18, height=2,
cursor="hand2", command=self.on_show_chart)
self.btn_chart.pack(side=tk.LEFT, padx=5)
self.btn_history = tk.Button(btn_frame, text="查看历史数据", font=font_btn,
bg="#9B59B6", fg="#FFFFFF", width=18, height=2,
cursor="hand2", command=self.on_show_history)
self.btn_history.pack(side=tk.LEFT, padx=5)
# --- 状态提示区 ---
self.status_var = tk.StringVar(value="定时任务已启动:每天 17:00 自动检查")
tk.Label(main_frame, textvariable=self.status_var,
font=(cfg.UI_FONT_FAMILY, 9), bg="#F5F6FA", fg="#666666").pack(anchor=tk.W, pady=5)
# --- 结果显示区 ---
result_frame = tk.LabelFrame(main_frame, text="检查结果", font=font_label,
bg="#FFFFFF", fg="#333333", padx=10, pady=8)
result_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.result_text = tk.Text(result_frame, font=font_result, bg="#FFFFFF",
fg="#333333", wrap=tk.WORD, height=14, state=tk.DISABLED)
self.result_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
scrollbar = tk.Scrollbar(result_frame, command=self.result_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.result_text.config(yscrollcommand=scrollbar.set)
def _start_scheduler_thread(self):
"""启动后台线程,每天 17:00 自动检查。"""
def scheduler_loop():
schedule.every().day.at(cfg.SCHEDULED_TIME).do(self._scheduled_task)
write_log(f"定时任务已注册:每天 {cfg.SCHEDULED_TIME} 执行")
while True:
schedule.run_pending()
time.sleep(cfg.SCHEDULE_INTERVAL)
t = threading.Thread(target=scheduler_loop, daemon=True)
t.start()
def _scheduled_task(self):
"""定时任务回调:每天 17:00 自动执行。"""
write_log("定时任务触发:开始自动检查天气...")
self.status_var.set(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 正在执行定时检查...")
try:
self._execute_check(send_email=True)
self.status_var.set(f"定时检查完成:{datetime.datetime.now().strftime('%H:%M:%S')}")
except Exception as e:
write_log(f"定时任务异常: {e}")
self.status_var.set(f"定时检查失败: {e}")
def on_run_now(self):
"""点击【立即检查】按钮。"""
self.btn_run.config(state=tk.DISABLED, text="检查中...")
self.root.update()
try:
self._execute_check(send_email=True)
except Exception as e:
self._update_result(f"运行出错:{e}", error=True)
write_log(f"手动运行异常: {e}")
finally:
self.btn_run.config(state=tk.NORMAL, text="立即检查明天天气")
def _execute_check(self, send_email=False):
"""核心流程:查天气 → 判出操 → 存历史 → 画图 → 发邮件。"""
# 1. 更新配置(从 UI 输入框读取最新值)
city = self.entry_city.get().strip() or cfg.TARGET_CITY
key = self.entry_key.get().strip() or cfg.QWEATHER_API_KEY
sender = self.entry_sender.get().strip() or cfg.SMTP_USER
receiver = self.entry_receiver.get().strip() or cfg.RECEIVER_EMAILS
self.fetcher.city_name = city
self.fetcher.api_key = key
self.mailer.user = sender
# 2. 获取天气数据
target_hour = cfg.RUN_TIME_HOUR
weather_rec = self.fetcher.get_target_hour_weather(target_hour=target_hour, target_date_offset=1)
air_rec = self.fetcher.fetch_air_quality()
# 3. 出操判断
decider = WeatherDecision(weather_rec, air_rec)
decision, reason = decider.judge()
# 4. 保存历史数据
if weather_rec:
append_record(
date_str=weather_rec["date"], hour=weather_rec["hour"],
temperature=weather_rec["temperature"], weather_text=weather_rec["weather_text"],
wind_level=weather_rec.get("wind_level", "0"), wind_speed=weather_rec.get("wind_speed", 0),
aqi=air_rec.get("aqi", -1), aqi_category=air_rec.get("category", "未知"),
decision=decision, reason=reason,
)
# 5. 绘制温度折线图
chart_path = TemperatureChart.draw()
# 6. 显示结果到 UI
if weather_rec:
aqi_val = air_rec.get('aqi', '未知')
aqi_cat = air_rec.get('category', '未知')
if aqi_val == -1:
aqi_display = "暂无数据(免费版订阅不包含空气质量接口)"
aqi_cat_display = "—"
else:
aqi_display = f"{aqi_val}"
aqi_cat_display = aqi_cat
result_lines = [
f"【查询日期】{weather_rec['date']} {target_hour}:00",
f"【天气状况】{weather_rec['weather_text']}",
f"【温度】{weather_rec['temperature']} °C",
f"【风力】{weather_rec.get('wind_level', '0')} 级 / 风速 {weather_rec.get('wind_speed', 0)} km/h",
f"【AQI】{aqi_display} ({aqi_cat_display})",
"",
f"【判定结果】{decision}",
f"【详细理由】{reason}",
]
else:
result_lines = [
"【查询失败】无法获取天气数据。",
"请检查以下事项:",
" 1. 网络连接是否正常;",
" 2. API Key 是否正确;",
" 3. 目标城市名称是否填写正确。",
]
result_text = "\n".join(result_lines)
self._update_result(result_text, error=(decision == "不出操" or not weather_rec))
# 7. 发送邮件(若需要)
if send_email and weather_rec and receiver:
subject = cfg.EMAIL_SUBJECT_TEMPLATE.format(date=weather_rec["date"])
html_body = self._build_email_html(weather_rec, air_rec, decision, reason)
attachments = [chart_path] if chart_path else None
self.mailer.send(receiver, subject, html_body, attachments)
def _build_email_html(self, weather_rec, air_rec, decision, reason):
"""构建 HTML 格式的邮件正文。"""
aqi_val = air_rec.get('aqi', '未知')
aqi_cat = air_rec.get('category', '未知')
if aqi_val == -1:
aqi_display = "暂无数据"
aqi_cat_display = "免费版不包含空气质量接口"
else:
aqi_display = f"{aqi_val}"
aqi_cat_display = aqi_cat
color = "#27AE60" if decision == "出操" else "#E74C3C"
return f"""
<div style="max-width:600px;margin:20px auto;padding:20px;border:1px solid #ddd;
border-radius:8px;font-family:Microsoft YaHei,sans-serif;">
<h2 style="color:#2C3E50;text-align:center;">🌤 明日跑操提醒</h2>
<table style="width:100%;border-collapse:collapse;margin:15px 0;">
<tr><td style="padding:8px;border-bottom:1px solid #eee;font-weight:bold;">日期</td>
<td style="padding:8px;border-bottom:1px solid #eee;">{weather_rec['date']} {cfg.RUN_TIME_HOUR}:00</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;font-weight:bold;">天气</td>
<td style="padding:8px;border-bottom:1px solid #eee;">{weather_rec['weather_text']}</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;font-weight:bold;">温度</td>
<td style="padding:8px;border-bottom:1px solid #eee;">{weather_rec['temperature']} °C</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;font-weight:bold;">风力</td>
<td style="padding:8px;border-bottom:1px solid #eee;">{weather_rec.get('wind_level', '0')} 级</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid #eee;font-weight:bold;">AQI</td>
<td style="padding:8px;border-bottom:1px solid #eee;">{aqi_display} ({aqi_cat_display})</td></tr>
</table>
<div style="text-align:center;padding:15px;background-color:{color};color:#fff;
border-radius:6px;font-size:18px;font-weight:bold;">
{decision}
</div>
<p style="margin-top:15px;color:#555;font-size:14px;line-height:1.6;">
<strong>判定理由:</strong>{reason}
</p>
<hr style="border:none;border-top:1px solid #eee;margin:15px 0;">
<p style="font-size:12px;color:#999;text-align:center;">
本邮件由 天气跑操提醒助手 自动发送<br>
发送时间:{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
</p>
</div>
"""
def _update_result(self, text, error=False):
"""更新结果文本框内容,并根据是否出错改变文字颜色。"""
self.result_text.config(state=tk.NORMAL)
self.result_text.delete("1.0", tk.END)
color = "#E74C3C" if error else "#2C3E50"
self.result_text.config(fg=color)
self.result_text.insert(tk.END, text)
self.result_text.config(state=tk.DISABLED)
def on_show_chart(self):
"""点击【查看温度折线图】按钮。"""
chart_path = TemperatureChart.draw()
if chart_path:
self._update_result(f"温度折线图已生成,保存路径:\n{chart_path}\n\n正在打开图片...")
os.startfile(chart_path)
else:
self._update_result("暂无足够历史数据,请先运行一次天气检查。", error=True)
def on_show_history(self):
"""点击【查看历史数据】按钮。"""
records = read_recent_history(days=30)
if not records:
self._update_result("暂无历史数据。", error=True)
return
lines = ["【最近历史记录】\n"]
for r in records[-10:]:
aqi_hist = r['aqi']
aqi_hist_display = "暂无" if aqi_hist == "-1" or aqi_hist == -1 else f"AQI {aqi_hist}"
lines.append(f"{r['date']} {r['hour']}:00 | {r['weather_text']} {r['temperature']}°C | "
f"{aqi_hist_display} | 判定:{r['decision']}")
self._update_result("\n".join(lines))
第 7 步:写程序入口
- 设置python的主程序入口
if __name__ == "__main__":
ensure_data_dirs()
init_csv()
write_log("=" * 40)
write_log("程序启动")
cfg.print_config_summary()
root = tk.Tk()
app = WeatherApp(root)
root.mainloop()
点击展开全部代码:程序入口
if __name__ == "__main__":
# 确保目录和 CSV 文件存在
ensure_data_dirs()
init_csv()
write_log("=" * 40)
write_log("程序启动")
cfg.print_config_summary()
# 检查 tkinter 可用性
try:
tk._test()
except AttributeError:
pass
except Exception as e:
print(f"错误:tkinter 环境异常: {e}")
sys.exit(1)
# 启动主窗口
root = tk.Tk()
app = WeatherApp(root)
root.mainloop()
步骤 6:运行程序并验证功能
三个文件都写好后,在 PyCharm 中右键 weather_runner.py → Run,程序会弹出一个窗口。
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 天气获取 | 点击【立即检查明天天气】 | 结果区显示:日期、天气、温度、风力、AQI、判定结果、理由 |
| 出操判断 | 城市为北京 | 根据当地天气正确判定"出操"或"不出操" |
| 历史数据 | 多次点击【立即检查】 | data/weather_history.csv 中有多条记录,每次追加不覆盖 |
| 折线图 | 点击【查看温度折线图】 (由于现在只测了两天,所以暂时连不成线) | 弹出 PNG 图片,显示近 7 天温度折线,带数据标注 |
| 邮件发送 | 确认邮箱配置正确后点击【立即检查】 | 收件箱收到 HTML 邮件,含天气表格和折线图附件 |
| 日志留痕 | 打开 logs/runner.log |
日志中有"程序启动"、"定时任务已注册"、"邮件已发送"等记录 |
| 定时任务 | 等待到 17:00 | 到点后自动执行检查,状态栏显示"定时检查完成" |
四、实验运行结果
- 点击运行
weather_runner.py,出现弹窗(这个有点奇怪 要点击quit)

- 点击quit
![image]()
- 在
charts里面查看温度折线图


- 查看历史数据

- 查看发送邮箱的内容,包含正文和附件


源代码链接
视频链接
五、实验功能分析
1. 天气数据获取
- 调用和风天气API,获取未来24小时逐小时预报与实时空气质量,定位次日早晨6点的数据。
2. 出操智能判断
- 按温度上下限、雨雪关键词、风力等级、AQI阈值等条件,自动判定次日是否适合出操,并给出具体理由。
3. 邮件自动发送
- 通过SMTP发送HTML格式邮件,内含判定结果与温度图表
4. 温度折线图绘制
- 基于历史CSV数据,用matplotlib生成近7天温度变化趋势折线图,保存为PNG图片。
5. 日志留痕与历史存档
- 每次查询结果追加写入CSV文件,保存日期、温度、天气、风力、AQI等数据,便于长期追踪与回溯,同时历史数据不会被覆盖。
六、实验存在的不足与改进之处
1. 免费版 API 数据受限,缺少空气质量指数
- 由于使用的是和风天气免费版订阅,无法获取空气质量(AQI),程序中只能以“暂无数据”占位。虽然不影响出操判断,但邮件和图表中缺少空气质量这一判断是否出操的重要信息。
改进方向:后续使用付费版 API 或切换至其他支持免费 AQI 的网址。(但有些网址反爬强,需要仔细斟酌,把握好尺度)
2.折线图没有绘制成功
- 由于样本数量不总,仅有两天的数字,构造不成7天的趋势。
改进方向:持续实验,获取更多的样本。
3. 未考虑凌晨降水对六点跑操时跑道湿滑的持续影响
- 当前仅依据六点时的实时天气判断是否适合出操,但忽略了凌晨时段的降水及其持续时间。如果凌晨下雨且持续时间的长,跑道则可能积水。
改进方向:开启自动分析功能,结合降水起止时间与持续时长,推算六点时跑道的湿滑风险等级,从而决定是否出操。
4.邮件定时发送功能失败
- 时间并不固定,一般来说是依据我所发送的时间来定。
改进方向:可以继续优化代码从而实现这个功能,或者尝试不关闭主程序,让其进行运转。
七、实验的感悟
这次实验是由每次痛苦的跑操催生出来的,其中的过程也如同跑操一样令人痛苦,不过更是在精神层面上。一开始,以为实验已经成功了,但是发现数据不准确,如显示部分降水,但是降水量为0,加上一直爬取不到和风天气的网址,近乎让我想要放弃这个实验,但是在同学的帮助下!!(感恩),发现爬取不到和风天气的网址是因为我们一直没有获取host的授权码!!之后就成功了!!嘿嘿(▽)但是还有有点小遗憾,就是关于空气质量只能通过付费来进行,希望我下次可以持续优化它,尝试使用微信公众号来实现(我看到这样的例子了,但还没有尝试),还有就是markdown很好玩_
八、课程总结
1、Python 基础语法
-
单行用井号,多行用三个单引号或三个双引号包裹,注释内容不执行。
-
缩进:Python 用缩进表示代码块,同一层级必须对齐。
-
标识符与保留字:变量名由字母、数字、下划线组成,不能以数字开头,不能使用 print、if 等保留字,区分大小写。
-
输入与输出:input 函数获取用户输入,print 函数输出内容。
2、基本数据类型
-
数字类型:整数、浮点数、复数,支持常规数学运算。
-
字符串:用单引号或双引号表示单行,三引号表示多行,支持转义字符,可用加号拼接,乘号重复。
-
布尔类型:只有 True 和 False 两个值,用于逻辑判断。
-
类型转换:用 int、float 等函数强制转换,input 函数得到的字符串需转换后才能做数值运算。
3、程序流程控制
-
顺序结构:代码默认从上到下逐行执行。
-
条件判断:if、elif、else 根据条件真假执行不同分支。
-
循环结构:while 循环在条件为真时重复执行,for 循环可设定循环次数。
-
位运算:左移相当于乘2,右移相当于除2。
4、序列类型
-
列表用中括号包裹,可变,支持增删改。
-
元组用圆括号包裹,不可变。
-
字典用大括号包裹,以键值对形式存储。
-
集合用大括号包裹,元素无序且不重复。
-
通用操作:索引从零开始,切片格式为开始、结束、步长,遵循左闭右开规则。
-
加号可拼接序列,乘号可重复序列,in 可判断元素是否存在,len 计算长度,max 和 min 获取最值。
5、字符串常用方法
-
count 方法统计子串出现次数。
-
find 方法返回子串首次出现的位置。
-
upper 和 lower 方法转换大小写。
-
split 方法按分隔符拆分为列表。
-
join 方法用指定字符合并多个字符串。
-
百分号可实现字符串格式化,动态填充内容。
6、正则表达式
-
正则表达式用于检查字符串是否包含某种模式,也可替换或提取内容。
-
尖角号表示开头,美元符号表示结尾。
-
方括号加横杠表示范围,可匹配字母或数字。
-
竖线表示或关系。
-
加号表示前面字符出现一次或多次,星号表示零次或多次,问号表示零次或一次。
7、面向对象
-
对象是具体的事物,类是同类事物的抽象概括。
-
封装将数据和操作打包在一起。
-
继承允许子类获得父类的属性和方法。
-
多态允许不同子类对同一方法有不同实现。
8、文件与数据库
-
文件操作有三种模式:读取、写入、追加,读取模式要求文件必须存在。
-
seek 方法移动文件指针,tell 方法获取当前位置。
-
二进制模式用于处理图片和视频等文件。
-
数据库采用结构化存储,基本 SQL 语句包括创建表、插入数据、查询数据。
-
通过连接对象和游标对象操作数据库。
9、异常处理
-
使用 try 和 except 捕获错误,避免程序因异常而中断。
-
常用于文件操作和网络请求等容易出错的场景。
10、网络爬虫
-
网络爬虫可自动获取其他平台的数据,需注意法律风险。
-
通过发送网络请求获取信息,解析 HTML 提取所需内容。
-
遇到反爬虫机制时,可设置请求头信息进行绕过。
九、课程体会
- 结束一门课,总会让人不免放松许多,但老师这门课本身所带来的压力也不多,更多裹挟而来的是课程结束的复杂心绪。高中毕业时的暑假,我畅想着学习python,而学到循环就被我的懒惰所吞没,不了了之。大一上,便随着人群涌动,加入各种活动,在匆匆忙忙中偶尔会闪过python的影子,随之便被繁杂纷乱的信息所覆盖。我总爱幻想着所谓的寒假,暑假,未知的下学期,去进行薛定谔的努力,下次我一定好好学习......下次又是何时呢?下次便是这一次!这是每一天的英语打卡告诉我的,是每一份认真书写的实验报告叙说给我的,是每一次失败后再尝试的站立鼓动我的,虽然通过这些课,我还不能完全手敲代码,但我会去爬网,去进行服务端和客户端,去调试,会借助大模型,慢慢磨出自己所想要的世界,也算一种新奇的体验;课上的欢颜笑语,老师的耐心指导,同学的互帮互助,构成我记忆中绚烂的回忆。这些的这些,让我更加有勇气去面对未知与迷惘,去尝试,去努力。
- 小小意见:课上敲代码的环节有点快,我的手速有点跟不上,尤其是切换大小写字母的时候(不过我也要再接着练打字!!!)
- 建议:提问环节感觉大家有一些忘记了(因为已经过了一周了o(╥﹏╥)o),希望可以换成课后有知识文档,大家可以自行进行复习;或者课上先在大屏幕出现五分钟的知识点记忆,然后再询问。嘿嘿(▽)不过老师已经讲的很好了!!感恩相遇(#.#)




浙公网安备 33010602011771号