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

image

步骤 2:创建项目文件夹

由于步骤较多,所以新建文件夹,这里统一放:

D:\Python_Program\WeatherReport

image

在这个文件夹里,创建四个子文件夹:

文件夹 用途
data 存放 CSV 表格
charts 存温度折线图
logs 存程序运行日志
diagnoses 存调试诊断脚本

image

步骤 3:配置清单 weather_config.py

为了方便改动,将可能要改的东西都放置一处。

在 PyCharm 的项目目录上右键 → New → Python File,命名为 weather_config.py

第 1 步:获取和风天气的API KeyAPI Host

image

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

image

  • 获取免费版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 服务中生成授权码。

image

image

  • 代码如下:
# 邮件 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 文件。如果文件不存在,则创建并写入表头;如果已存在,不覆盖。

image

  • 在第一行写表头,每一列存信息:日期、小时、温度、天气、风力、风速、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 步:日志的函数。

记录程序运行日志
image

  • 代码如下:
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

  • 创建主窗口,放置屏幕正中间,再搭界面,启动后台定时线程。

image

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.pyRun,程序会弹出一个窗口。

验证项 操作 预期结果
天气获取 点击【立即检查明天天气】 结果区显示:日期、天气、温度、风力、AQI、判定结果、理由
出操判断 城市为北京 根据当地天气正确判定"出操"或"不出操"
历史数据 多次点击【立即检查】 data/weather_history.csv 中有多条记录,每次追加不覆盖
折线图 点击【查看温度折线图】 (由于现在只测了两天,所以暂时连不成线) 弹出 PNG 图片,显示近 7 天温度折线,带数据标注
邮件发送 确认邮箱配置正确后点击【立即检查】 收件箱收到 HTML 邮件,含天气表格和折线图附件
日志留痕 打开 logs/runner.log 日志中有"程序启动"、"定时任务已注册"、"邮件已发送"等记录
定时任务 等待到 17:00 到点后自动执行检查,状态栏显示"定时检查完成"

四、实验运行结果

  • 点击运行weather_runner.py,出现弹窗(这个有点奇怪 要点击quit)

image

  • 点击quit
    image
  • charts里面查看温度折线图

image

image

  • 查看历史数据

image

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

image

image

源代码链接

源代码

视频链接

哔哩哔哩

五、实验功能分析

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),希望可以换成课后有知识文档,大家可以自行进行复习;或者课上先在大屏幕出现五分钟的知识点记忆,然后再询问。嘿嘿()不过老师已经讲的很好了!!感恩相遇(#.#)

参考资料

《Python程序设计》

posted @ 2026-06-16 22:35  20254226黄婉婷  阅读(7)  评论(0)    收藏  举报