ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python定时提醒工具开发:从配置管理到邮件推送的自动化实践

Python定时提醒工具开发:从配置管理到邮件推送的自动化实践 如果你刚开始学习 Python想找一个不大不小、能练手又能真正用在日常的项目可以试试从“提醒类小工具”入手。“天上掉下一只小日和”是我为一个桌面助手类小项目起的名字核心功能并不复杂定时读取指定数据源把提醒内容推送到本地控制台或邮箱。这类项目很适合把 Python 的基础语法、配置文件解析、异常处理、日志、网络请求和定时调度串起来学完以后你再看其他自动化脚本也会更有底气。本文会从一个完整的项目视角来拆解先讲需求和功能边界再设计项目结构然后逐段编写代码最后讨论运行验证、常见坑点和工程化建议。无论你是 Python 初学者还是想做一个内部工具的后端工程师都可以按这篇文章完整复现。1. 项目背景与需求分析1.1 为什么会有“天上掉下一只小日和”很多人在写脚本时都是从“一个小需求”开始的。比如你每天上午容易忘记查看日程下午容易忘记喝水晚上回家前又容易忘记处理待办事项。如果每次都靠手机闹钟倒也能解决但闹钟不能把“提醒内容”一并带上也不能在电脑上留下一条可追溯的记录。于是就有了这样的想法写一个常驻后台的小程序每隔一段时间读取一份提醒数据把满足条件的提醒内容打印出来或者通过邮件推送到自己的邮箱。“天上掉下一只小日和”这个项目本质上就是一个带调度能力的轻量提醒工具只不过我给程序里的数据源和推送对象起了个拟人化的名字。它并不是一个需要高并发、分布式、微服务的复杂项目恰恰因为不复杂才适合拿来练习工程化思维。一个很小的脚本如果从一开始就注意配置分离、模块拆分、异常处理和日志记录后面扩展起来会非常顺畅反过来如果一上来就把所有逻辑塞进一个main.py初期可能很快写完但维护成本会迅速上升。1.2 项目要解决什么问题这个项目要解决三个核心问题。第一数据从哪来。提醒内容不能写死在代码里因为写死在代码里意味着每次修改提醒内容都要改代码、重启程序。更合理的做法是把提醒内容放在独立的 JSON 文件或外部 API 中程序启动后动态读取。第二什么时候推送。一个提醒工具必须支持两种常见的调度方式一种是间隔调度比如每 30 秒检查一次另一种是固定时间调度比如每天 09:00 执行一次。两种模式需要能在配置文件里灵活切换而不是修改代码。第三推送到哪里。对于个人小工具最低成本的推送方式是控制台输出如果需要跨设备查看可以接入邮件如果以后接入企业微信、钉钉或飞书机器人也应该在不影响现有代码结构的前提下扩展。一个合格的项目不是把功能做出来就结束而是要把“数据读取”“定时调度”“通知推送”这三个职责分开。这样后期替换任意一环都不会影响另外两环。1.3 核心功能与边界为了方便新手理解我们把项目边界先划清楚。当前版本支持以下能力从本地 JSON 文件读取提醒数据。支持interval和fixed两种调度模式。每次调度只发送“当前时间已到”的提醒并且避免重复发送。支持控制台打印和邮件推送两种通知方式。统一通过config.ini管理配置。使用标准库实现不依赖第三方包。当前版本不包含的功能我们也不回避不做 Web 管理界面。不做任务持久化程序重启后会重新开始去重。不支持分布式部署适合单机运行。邮件服务需要你自己准备可用的 SMTP 配置。明确边界很重要。如果你打算把这个项目扩展到生产环境可以直接在现有模块上增加数据库存储、任务状态管理和多通道通知后面的章节会给出具体扩展方向。2. 环境准备与技术选型2.1 运行环境本文示例以 Python 3.8 以上的常见环境为例操作系统可以是 Windows、macOS 或 Linux。项目只使用 Python 标准库理论上不需要安装任何第三方模块因此对环境的要求非常低。你可以使用任意一款顺手的 IDE比如 PyCharm、VS Code甚至直接用系统自带的文本编辑器。运行前只需要确认 Python 已正确加入系统环境变量在命令行中输入下面命令能正常显示版本号即可python --version如果终端提示找不到python在 Linux 或 macOS 下可以试试python3 --version不同操作系统对python和python3的命令区分不一样。Windows 安装 Python 时一般会生成python命令Linux 发行版可能同时存在两个命令。只要确定一个能运行后面统一替换即可。2.2 项目目录结构一个好的项目从目录结构就能看出模块边界。你可以在本地新建一个文件夹名字随意比如tian_rihe然后把项目文件按下面的结构放置tian_rihe/ ├── config.ini ├── data.json ├── reminder.py ├── notifier.py ├── scheduler.py ├── main.py └── requirements.txt各文件的职责如下文件职责config.ini存放程序运行所需配置data.json提醒数据的默认数据源reminder.py负责加载提醒数据notifier.py负责把提醒内容推送到外部scheduler.py负责定时调度核心逻辑main.py程序入口负责组装各个模块requirements.txt依赖声明文件当前标准库可留空说明2.3 依赖说明因为本项目使用的是configparser、json、logging、threading、smtplib、urllib.request等标准库模块所以不存在版本冲突问题。这是刻意为之目的是让新手可以先聚焦在项目结构本身而不是花大量时间解决依赖兼容问题。如果你以后想增加额外的数据源比如订阅某个天气接口、新闻接口或者接入更丰富的推送渠道再根据实际需要引入requests、apscheduler等第三方库。版本号请以官方文档和你的 Python 环境为准不要在不确定时盲目固定一个版本。3. 核心模块设计3.1 为什么要把配置放在config.ini程序中的时间间隔、推送方式、SMTP 账号等信息属于“环境相关配置”不适合和代码写在一起。把配置放到独立文件后你可以只修改配置文件就改变程序行为不需要重新发布代码。config.ini使用 INI 格式通过configparser解析。这种格式在 Windows 和 Linux 下都有广泛支持阅读起来也比较直观。下面是配置文件的设计思路; 文件路径config.ini [app] log_level INFO [schedule] mode interval interval_seconds 30 fixed_time 09:00 [reminder] source mock mock_file data.json api_url api_key [notify] type console smtp_host smtp.example.com smtp_port 465 sender your_accountexample.com password your_password receiver targetexample.com这里有几个需要注意的地方[schedule]中的mode支持interval和fixed两个值。interval_seconds表示间隔调度的秒数如果mode interval程序会每隔这么多秒执行一次。fixed_time表示固定时间调度如果mode fixed程序会在每天该时刻执行。[reminder]中的source表示数据来源当前支持mock和api两种。[notify]中的type表示通知方式当前支持console和email两种。写配置文件时#或;开头表示注释。不要把密码写死在代码中这也是工程化里很重要的一条基本原则。3.2 数据加载模块的职责reminder.py的职责是从不同数据源加载提醒内容并返回统一的 list 结构。每一项是一个 dict包含title、time、content三个字段。例如{ reminders: [ { title: 上午站会, time: 10:00, content: 同步项目进度 } ] }设计这个模块时重点是把“数据从哪来”和“数据怎么用”解耦。调度器不需要关心数据是来自本地文件还是远程 API它只需要调用loader.load()拿到结果。如果以后想接数据库只需重写reminder.py内部的实现对外接口保持不变。3.3 通知模块的职责notifier.py的职责是把一条提醒内容推送到指定渠道。当前版本提供两种实现console直接打印到控制台适合调试和本地使用。email通过 SMTP 发送邮件适合需要跨设备查看的场景。发送邮件时需要注意大部分邮箱服务商并不要求使用登录密码而是要求使用“授权码”或“客户端专用密码”。不同服务商的差异比较大这一点不能一概而论你需要以自己邮箱服务商提供的说明为准。另一个容易出错的点是端口号465一般对应SMTP_SSL587一般对应SMTP配合starttls。本文示例使用465如果你的服务商只支持587需要调整代码中的连接方式。3.4 调度器的职责调度器是整个程序的发动机。它需要完成三件事读取配置确认调度模式。在合适的时机调用数据加载模块。发送满足条件的提醒并记录已经发送过的提醒避免重复。最简单的调度实现是while True time.sleep。但这种实现有局限性如果程序在睡眠期间要停止会有延迟如果调度逻辑出现异常可能导致整个线程退出。更好的做法是使用threading.Event()来配合睡眠既能在stop()时快速唤醒又能在循环中保留异常处理。在实际生产中你还可以用操作系统的cron或systemd timer来触发脚本不一定需要让 Python 进程常驻。不过作为一个教学项目理解 Python 内部的调度实现依然很有价值因为很多自动化框架的底层思路也是类似的。4. 完整代码实现4.1 配置文件和示例数据首先创建config.ini文件; 文件路径config.ini [app] log_level INFO [schedule] mode interval interval_seconds 30 fixed_time 09:00 [reminder] source mock mock_file data.json api_url api_key [notify] type console smtp_host smtp.example.com smtp_port 465 sender your_accountexample.com password your_password receiver targetexample.com接着创建data.json作为本地提醒数据源{ reminders: [ { title: 上午站会, time: 10:00, content: 同步项目进度确认风险项 }, { title: 喝水提醒, time: 15:00, content: 起来活动一下补充水分 } ] }这里的时间字段time使用 24 小时制的HH:MM格式方便程序用字符串比较。真实项目中你可能还需要日期字段比如2025-01-01 09:00因为字符串直接比较在不同日期下会误判。本文把功能拆到最简只处理每天循环发生的提醒所以使用HH:MM是够用的。4.2 数据加载模块reminder.py# 文件路径reminder.py import json import logging import urllib.request logger logging.getLogger(__name__) class ReminderLoader: 负责加载提醒内容。 默认从本地 JSON 文件读取也可以改成从 API 获取。 将数据源封装在这一层是为了让调度模块和通知模块不关心数据从哪来。 def __init__(self, config): self.config config self.source config.get(reminder, source, fallbackmock) def load(self): if self.source mock: return self._load_from_file() if self.source api: return self._load_from_api() logger.warning(未知的 reminder.source: %s, self.source) return [] def _load_from_file(self): mock_file self.config.get(reminder, mock_file, fallbackdata.json) with open(mock_file, r, encodingutf-8) as f: data json.load(f) return data.get(reminders, []) def _load_from_api(self): api_url self.config.get(reminder, api_url, fallback) if not api_url: logger.warning(api_url 为空请先在 config.ini 中配置) return [] req urllib.request.Request(api_url, headers{User-Agent: Mozilla/5.0}) with urllib.request.urlopen(req, timeout10) as resp: data json.loads(resp.read().decode(utf-8)) # 不同的 API 返回结构不同这里只做最保守的处理 if isinstance(data, list): return data if isinstance(data, dict): return data.get(reminders, data.get(data, [])) return []代码中有几个关键点值得说明。config.get(reminder, source, fallbackmock)是configparser提供的安全读取方式即使配置项缺失也不会抛异常而是返回默认值。这个写法在处理多环境配置时非常实用。_load_from_api中使用了urllib.request.Request并设置了User-Agent这是因为很多接口会拒绝没有用户代理标识的请求。具体的字段格式取决于你接入的接口文档所以在解析响应时只做了最保守的处理没有对某个特定字段做依赖。4.3 通知模块notifier.py# 文件路径notifier.py import logging import smtplib from email.mime.text import MIMEText from email.header import Header logger logging.getLogger(__name__) class Notifier: 负责把提醒内容推送到外部。 目前支持 console 和 email 两种方式。 如果日后需要接入企业微信、钉钉或飞书机器人可以继续扩展。 def __init__(self, config): self.config config self.type config.get(notify, type, fallbackconsole) def send(self, reminder): if self.type console: self._send_console(reminder) elif self.type email: self._send_email(reminder) else: logger.warning(未知的通知类型%s, self.type) def _send_console(self, reminder): print( * 40) print(提醒, reminder.get(title)) print(时间, reminder.get(time)) print(内容, reminder.get(content)) print( * 40) def _send_email(self, reminder): smtp_host self.config.get(notify, smtp_host, fallback) smtp_port self.config.getint(notify, smtp_port, fallback465) sender self.config.get(notify, sender, fallback) password self.config.get(notify, password, fallback) receiver self.config.get(notify, receiver, fallback) if not sender or not receiver or not smtp_host: logger.error(邮件配置不完整请检查 config.ini 的 [notify] 节点) return subject f提醒{reminder.get(title)} body f{reminder.get(time)}\n\n{reminder.get(content)} msg MIMEText(body, plain, utf-8) msg[Subject] Header(subject, utf-8) msg[From] sender msg[To] receiver try: with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout15) as server: server.login(sender, password) server.sendmail(sender, [receiver], msg.as_string()) logger.info(邮件已发送到 %s, receiver) except smtplib.SMTPException: logger.exception(邮件发送失败%s, 请检查 SMTP 配置和网络连接)这段代码体现了通知模块的两个特点。第一对外只暴露send(reminder)上层不关心具体发送方式。以后新增钉钉机器人只需要在send方法中增加一个分支再补一个_send_dingtalk私有方法即可。第二邮件发送相关的异常被捕获并用logger.exception记录。注意logger.exception只能在except块中使用它会自动把堆栈信息追加到日志中方便排查问题。另外SMTP 密码是敏感信息最好不要直接放在config.ini中明文保存。开发环境可以临时使用生产环境更推荐通过环境变量或密钥管理服务注入。4.4 调度模块scheduler.py# 文件路径scheduler.py import logging import threading import time from datetime import datetime logger logging.getLogger(__name__) class Scheduler: 一个轻量定时调度器。 不依赖外部库只使用标准库的线程和 sleep。 每次循环会读取一次数据并把符合条件的提醒推送出去。 def __init__(self, loader, notifier, config): self.loader loader self.notifier notifier self.config config self._stop_event threading.Event() self._sent_keys set() def _read_schedule_config(self): mode self.config.get(schedule, mode, fallbackinterval) interval self.config.getint(schedule, interval_seconds, fallback30) fixed_time self.config.get(schedule, fixed_time, fallback09:00) return mode, interval, fixed_time def _need_send(self, reminder): rtime reminder.get(time, ) if not rtime: return False current datetime.now().strftime(%H:%M) return current rtime def _send_key(self, reminder): today datetime.now().strftime(%Y-%m-%d) return f{today}:{reminder.get(time)}:{reminder.get(title)} def run_once(self): reminders self.loader.load() logger.info(本轮共读取到 %d 条提醒, len(reminders)) for item in reminders: key self._send_key(item) if key in self._sent_keys: continue if self._need_send(item): self.notifier.send(item) self._sent_keys.add(key) def _run_fixed_cycle(self, fixed_time): now datetime.now().strftime(%H:%M) today datetime.now().strftime(%Y-%m-%d) key f{today}:{fixed_time} if now fixed_time and key not in self._sent_keys: self.run_once() self._sent_keys.add(key) self._stop_event.wait(1) def start(self): mode, interval, fixed_time self._read_schedule_config() logger.info(调度器已启动模式%s, mode) while not self._stop_event.is_set(): try: if mode fixed: self._run_fixed_cycle(fixed_time) else: self.run_once() self._stop_event.wait(interval) except KeyboardInterrupt: raise except Exception: logger.exception(调度循环执行出错) self._stop_event.wait(5) def stop(self): self._stop_event.set()调度器中最核心的设计是_sent_keys去重集合。在没有去重的情况下interval模式每 30 秒执行一次如果某条提醒时间设置为10:00并且当前时间是10:01那么从10:01开始每次循环都会发送一次直到当前时间变成11:00。这显然不是我们想要的。加上_sent_keys后当天某个时间点的提醒只发送一次。为什么要加上日期前缀因为程序可能不是一次性退出的它可能连续运行好几天。如果不加日期昨天10:00发送过的 key 会一直留在集合里今天10:00的提醒就不会再次发送。加上日期前缀后不同日期同一个时间点会形成不同的 key逻辑上更严谨。再来看_need_send方法。它使用字符串直接比较当前时间和提醒时间这种写法仅限于HH:MM这种固定长度且按数值顺序排列的格式。如果时间字段变成9:00字符串比较就会出错因为字符串9:00大于10:00。所以配置文件和数据文件中必须统一使用补零后的两位小时格式。4.5 程序入口main.py# 文件路径main.py import configparser import logging import os import sys from reminder import ReminderLoader from notifier import Notifier from scheduler import Scheduler LOG_FORMAT %(asctime)s - %(name)s - %(levelname)s - %(message)s def load_config(pathconfig.ini): config configparser.ConfigParser() if not os.path.exists(path): logging.error(配置文件不存在%s, path) sys.exit(1) config.read(path, encodingutf-8) return config def setup_logging(level_nameINFO): level getattr(logging, level_name.upper(), logging.INFO) logging.basicConfig(levellevel, formatLOG_FORMAT) def main(): config load_config() log_level config.get(app, log_level, fallbackINFO) setup_logging(log_level) loader ReminderLoader(config) notifier Notifier(config) scheduler Scheduler(loader, notifier, config) try: scheduler.start() except KeyboardInterrupt: logging.info(收到中断信号调度器即将退出) scheduler.stop() sys.exit(0) if __name__ __main__: main()入口文件做的事情很纯粹加载配置、初始化日志、组装三个核心模块、启动调度器。logging.basicConfig只能在程序启动时调用一次如果重复调用后续的配置可能不会生效甚至会产生重复日志。因此我把日志初始化单独封装在setup_logging里并且只在main()中调用一次。if __name__ __main__:这个写法保证模块被导入时不会执行启动逻辑只有作为脚本运行时才会进入main()。这样后面写单元测试时可以直接 import 这些模块而不会意外启动调度器。4.6 依赖声明requirements.txt当前项目不需要第三方依赖但保留requirements.txt可以养成好的工程习惯。内容可以简单写一行说明# 当前版本仅使用 Python 标准库 # 后续如引入第三方依赖请按实际版本填写如果你在运行环境里执行pip install -r requirements.txt因为文件里只有注释所以不会安装任何东西。这也是给未来维护者的一种提示。5. 运行与验证5.1 首次运行在项目目录下打开终端执行python main.py如果一切正常你会看到类似下面的输出INFO - scheduler - 调度器已启动模式interval INFO - reminder - 本轮共读取到 2 条提醒因为示例数据中提醒时间是10:00和15:00如果你的当前时间还没到这两个时间点那么本轮不会触发推送。这是符合预期的说明调度器已经在等待。你也可以临时把data.json中的时间改成当前时间的前一分钟再重新运行程序这样很快就能看到控制台输出提醒内容 提醒 上午站会 时间 10:00 内容 同步项目进度确认风险项 5.2 验证去重逻辑去重是调度器最容易出问题的地方。你可以这样验证把interval_seconds改成 5让程序每 5 秒跑一次观察控制台是否只打印一次相同提醒。如果配置了mode fixed例如fixed_time 09:00那么程序会每秒判断一次当前时间。当系统时间正好是09:00且当天没发送过时会执行一轮。这里有一个需要注意的边界now fixed_time是精确相等判断如果run_once()执行得很快下一秒now就变成09:01不会重复发送但如果程序启动时时间已经过了09:00当天就不会再触发。这是当前实现的简化逻辑生产环境建议把逻辑改成“大于等于”并配合日期状态管理。5.3 修改为邮件推送把config.ini中的[notify]改成[notify] type email smtp_host smtp.example.com smtp_port 465 sender your_accountexample.com password your_password receiver targetexample.com重新运行程序满足条件的提醒会被封装成邮件发送到receiver。如果邮箱是 QQ 邮箱、163 邮箱或 Gmail 等SMTP 服务器地址和端口都可能不同需要以邮件服务商官方文档为准。如果发送失败日志中会记录异常可以先检查端口是否被防火墙拦截以及账号是否使用了授权码。6. 常见问题与排查思路6.1 常见错误对照表问题现象常见原因解决思路启动时报configparser.NoSectionError配置文件缺少对应节点检查config.ini中是否有[reminder]、[notify]等节点启动后没有任何输出log_level设置过高或mode配置错误检查log_level是否为INFO确认mode是interval还是fixed提醒一直重复发送_sent_keys去重逻辑没有生效确认当前时间是否在提醒时间之后检查_send_key是否包含日期前缀邮件发送失败SMTP 服务商参数错误或没有使用授权码核对服务商提供的 SMTP 地址、端口和账号密码类型读取 JSON 文件失败文件编码不是 UTF-8或数据结构不匹配用 UTF-8 保存data.json确认reminders是数组当前时间到了但没触发提醒时间格式不一致统一使用HH:MM避免9:00这种缺零格式6.2 排查步骤如果你遇到问题不要先急着改代码可以按下面的顺序排查。第一步看控制台日志。程序启动后会在日志中输出调度模式和每轮读取到的提醒条数。如果日志显示“本轮共读取到 0 条提醒”说明问题出在数据加载层先检查data.json。第二步看配置文件。用文本编辑器打开config.ini确认mode、interval_seconds、notify.type这些值是否和你预期一致。注意 INI 文件中的注释符;或#后面不能有空格开头否则解析器可能报错。第三步临时把调度间隔调短。比如把interval_seconds从 30 改成 5并把data.json中的时间改成当前时间的前一分钟这样能快速确认整条链路是否连通。第四步检查日志文件。如果程序是在后台运行控制台输出可能不容易看到最好把日志同时输出到文件。这一部分在最佳实践章节会更详细说明。6.3 几个容易踩的细节第一个细节是datetime.now().strftime(%H:%M)的输出格式。自动补零是HH的特征所以你必须保证数据文件和系统时间都使用两位小时格式。如果你在数据中用time: 9:00当系统时间是10:00时10:00 9:00在字符串比较中结果是 False导致提醒被跳过。第二个细节是smtplib.SMTP_SSL的端口问题。很多邮箱服务商对465和587支持情况不一样有些服务商只支持587并要求先启动starttls。如果你使用465报错可以尝试把端口改成587并用smtplib.SMTP配合server.starttls()。第三个细节是配置密码中的特殊字符。如果你在config.ini中直接写密码而密码包含;或#解析时可能被误认为是注释导致登录失败。更安全的做法是不要明文保存密码而是通过环境变量读取。7. 最佳实践与工程建议7.1 配置管理与敏感信息config.ini是开发环境最快上手的配置方式但生产环境不建议把密码、Token 等敏感信息明文写在仓库里。推荐的做法是使用环境变量注入敏感配置例如import os password os.getenv(SMTP_PASSWORD, )如果密码为空则不尝试登录邮箱直接记录错误。这样既避免密钥泄露也方便在不同环境间切换配置。另外一个常见的工程实践是准备多套配置文件比如config.dev.ini、config.prod.ini通过启动参数指定加载哪一套。对于当前项目可以在main.py中增加一个参数解析逻辑但不要为了演示而引入过多复杂度。7.2 异常处理与重试策略当前代码在调度循环中捕获了所有Exception避免单个异常导致常驻进程退出。但这种粗粒度捕获并不适合所有场景比如网络请求失败时更好的做法是区分“可重试异常”和“不可重试异常”。对于读取外部 API 的场景建议在_load_from_api中增加简单的重试逻辑第一次失败后等待几秒重试连续三次失败才放弃并记录一条错误日志。对于邮件发送如果 SMTP 服务商临时不可用也可以类似处理。不过重试间隔不要太短否则可能加重邮件服务商压力反而容易被封禁。7.3 日志与可观测性开发环境打印到控制台足够部署到服务器后最好同时保留文件日志。可以通过logging.handlers.RotatingFileHandler实现按大小轮转的日志文件避免单个日志文件无限增长。示例思路如下import logging from logging.handlers import RotatingFileHandler handler RotatingFileHandler(app.log, maxBytes5 * 1024 * 1024, backupCount5) formatter logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) handler.setFormatter(formatter) root_logger logging.getLogger() root_logger.addHandler(handler)这段代码要放在setup_logging中执行。注意basicConfig和手动添加handler不要同时混用否则会出现重复日志。7.4 部署方式选择当前调度器依赖 Python 进程常驻。如果你的机器运行不稳定进程会退出建议结合操作系统的守护进程机制。Linux 系统上可以写一个systemd service文件Windows 上可以计划任务macOS 上可以使用launchd。更简单的方案是让脚本只执行一次然后用系统的cron定时触发。比如把main.py改造成“启动后立即执行一次然后退出”的批处理模式再用 cron 在每天09:00调用一次。这样做的好处是进程生命周期短资源占用低也不会有常驻内存导致的内存泄漏问题。缺点是调度能力依赖操作系统不适合需要精确到秒的场景。7.5 测试策略虽然项目不大但可以根据模块编写简单的单元测试。重点测试以下逻辑ReminderLoader.load()能正确读取 JSON 文件。Scheduler._need_send()在不同时间下返回正确结果。Scheduler._sent_keys在一天内不会重复发送。配置文件缺失时main.load_config()能正常提示。单元测试的代码量可能比项目本身还大但对长期维护很有帮助。至少要把_need_send这类纯逻辑抽出来单独测试而不是只测试整个调度循环因为调度循环依赖时间和线程测试起来不稳定。8. 学习路线与扩展思路8.1 下一步可以继续学什么如果你已经把这个项目完整跑通说明你已经掌握了 Python 脚本项目的基本抽象思路。下一步可以从三个方向继续深入。第一个方向是数据源扩展。可以接入真实可用的天气 API实现“天气变化时提醒我带伞”的效果。你需要了解 HTTP 请求、JSON 解析、接口鉴权等知识。接入真实 API 前先查阅官方文档申请合法授权并且不要把密钥提交到公开仓库。第二个方向是调度引擎升级。当前调度器是简单自研实现如果你需要更复杂的 cron 表达式支持可以学习apscheduler这种第三方定时任务库。它可以精确描述“每周一上午九点”“每月最后一天”这类复杂规则。第三个方向是推送渠道扩展。可以尝试将提醒发送到企业微信、钉钉或飞书群机器人。这类机器人通常只需要向一个 webhook 地址发送带签名的 JSON 请求对 Python 来说并不复杂而且能很快在手机上收到通知。8.2 生产环境优先关注哪些风险如果这个小工具要真正用在工作环境中我建议你先关注三个风险。第一是时间准确性。定时任务依赖系统时间如果服务器时区设置错误所有提醒都会偏移。项目配置中已经预留了timezone字段虽然代码暂时没有使用但你在部署时要确认服务器date命令输出的时间是否正确。第二是提醒丢失。如果程序在提醒时间附近重启可能会因为_sent_keys是内存集合而丢失判断状态。解决思路是持久化已发送记录最简单的做法是把 key 写入一个 log 文件启动时加载已有 key。第三是消息风暴。如果数据源返回了异常多的提醒或者调度逻辑出现 bug邮件可能被连续发送上百次。工程上可以增加发送频率限制例如每个 key 最多发送一次、每小时最多发送 N 条一旦超过阈值就停止并告警。8.3 把项目当成一块练习场“天上掉下一只小日和”这个项目听起来好像只是一个随手写的小玩具但它覆盖了真实业务开发中大量关键点配置、调度、IO、异常、日志、去重、扩展性。你可以把这个项目作为练习场逐步加入数据库、Web 界面、消息队列、容器化部署等能力。如果你在本地跑通第一版建议改一改data.json中的内容把提醒改成你自己的学习和工作安排让这个程序真正为你服务。技术只有落到自己的实际场景里才能真正形成手感。如果本文对你有帮助可以收藏备用。接下来打开终端创建你的tian_rihe目录写一个属于你自己的“小日和”吧。
返回列表