
简介这是一份面向大学生与Python初学者的个人日程管理实战项目旨在帮助用户掌握基础GUI开发、数据建模与模块化编程思想适用于毕业设计选题、自学练手及教学参考。资源包共9个文件含3个核心Python源码main.py主程序、views/main_page.py界面逻辑、models/schedule.py数据模型、4个编译缓存pyc文件、1个README.md说明文档及1个.gitignore配置文件整体仅14KB轻量易读结构清晰体现MVC分层设计——视图、模型与控制逻辑解耦便于理解项目组织方式与代码复用机制。已有161人学习下载读者可直接运行调试完整获得一个具备日程展示、任务增删改查、时间提醒功能的可执行系统并通过源码深入学习calendar_view、daily_schedule等模块的实现细节以及__pycache__机制与项目初始化流程。1. 为什么一个纯 Python 的个人日程管理系统比「记在备忘录里」或「用 Excel 表格」更值得花 2 小时搭起来你每天打开手机看日历 App发现会议提醒总被淹没在推送里用 Excel 记待办事项却常因格式错乱、筛选失效、跨设备不同步而漏掉关键任务甚至写在纸质本子上翻到第 3 周就找不到上周三的临时约定。这不是时间管理能力问题而是工具链缺失——缺少一个完全可控、可扩展、不依赖云服务、能嵌入你现有工作流的本地日程中枢。这个「Python 个人日程管理系统」不是玩具项目它是一套可落地的 CLI 文件存储方案用标准库datetime处理时序逻辑用json或sqlite3持久化数据用argparse构建清晰命令行接口支持添加/查询/修改/归档日程还能按日期范围、关键词、状态待办/已完成/已取消精准过滤。它不联网、不注册、不上传所有数据存你本地磁盘新手能照着跑通基础功能老手可直接接入 cron 自动同步、对接邮件通知、或用rich库渲染带颜色的日视图。适合运维工程师记录巡检计划、学生党排期复习节点、自由职业者管理客户交付周期——只要你需要「确定性」和「可编程性」而不是「又一个需要登录的 SaaS 页面」。2. 用标准库从零构建最小可行系统CLI 入口、JSON 存储与基础 CRUD2.1 设计核心数据结构与存储策略为什么选 JSON 而非 CSV 或纯文本日程本质是结构化事件集合每个条目需包含唯一 ID、标题、开始时间、结束时间、描述、状态pending/done/canceled、标签如#work、#personal。CSV 无法原生表达嵌套字段如标签列表、易因逗号导致解析错误纯文本无索引能力查询效率随数据量增长急剧下降而json文件天然支持字典/列表嵌套人类可读、编辑友好且 Python 标准库json模块无需额外安装即可序列化/反序列化。更重要的是单文件存储降低部署复杂度——整个系统只需一个.py文件 一个schedule.json复制即用无数据库配置负担。实际项目中当条目超 5000 条时再迁移到sqlite3标准库内置但初期 JSON 完全够用。提示不要用pickle存储日程数据。它虽支持任意 Python 对象但存在严重安全风险反序列化可执行任意代码且文件不可读、跨 Python 版本不兼容。生产环境必须规避。2.2 实现命令行接口用 argparse 定义 add/list/update/delete 四个子命令# schedule.py import argparse import json import os from datetime import datetime def load_data(): if os.path.exists(schedule.json): with open(schedule.json, r, encodingutf-8) as f: return json.load(f) return [] def save_data(data): with open(schedule.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) def main(): parser argparse.ArgumentParser(descriptionPython 个人日程管理系统) subparsers parser.add_subparsers(destcommand, help可用命令) # 添加日程子命令 add_parser subparsers.add_parser(add, help添加新日程) add_parser.add_argument(--title, requiredTrue, help日程标题) add_parser.add_argument(--start, requiredTrue, help开始时间格式YYYY-MM-DD HH:MM) add_parser.add_argument(--end, requiredTrue, help结束时间格式YYYY-MM-DD HH:MM) add_parser.add_argument(--desc, default, help描述) add_parser.add_argument(--tags, nargs*, default[], help标签例如--tags work meeting) # 查询日程子命令 list_parser subparsers.add_parser(list, help列出日程) list_parser.add_argument(--date, help指定日期格式YYYY-MM-DD) list_parser.add_argument(--status, choices[pending, done, canceled], help按状态筛选) list_parser.add_argument(--keyword, help按标题或描述关键词搜索) # 修改日程子命令 update_parser subparsers.add_parser(update, help更新日程) update_parser.add_argument(--id, typeint, requiredTrue, help日程ID) update_parser.add_argument(--title, help新标题) update_parser.add_argument(--start, help新开始时间) update_parser.add_argument(--end, help新结束时间) update_parser.add_argument(--desc, help新描述) update_parser.add_argument(--status, choices[pending, done, canceled], help新状态) update_parser.add_argument(--tags, nargs*, help新标签) # 删除日程子命令 delete_parser subparsers.add_parser(delete, help删除日程) delete_parser.add_argument(--id, typeint, requiredTrue, help日程ID) args parser.parse_args() if args.command add: data load_data() new_id max([item.get(id, 0) for item in data], default0) 1 try: start_dt datetime.fromisoformat(f{args.start}:00) end_dt datetime.fromisoformat(f{args.end}:00) except ValueError: print(错误时间格式应为 YYYY-MM-DD HH:MM例如 2024-06-15 09:00) return if end_dt start_dt: print(错误结束时间必须晚于开始时间) return new_item { id: new_id, title: args.title, start: args.start, end: args.end, desc: args.desc, tags: args.tags, status: pending, created_at: datetime.now().isoformat()[:19] } data.append(new_item) save_data(data) print(f✅ 已添加日程 #{new_id}{args.title}) elif args.command list: data load_data() filtered data if args.date: filtered [item for item in data if item[start].startswith(args.date)] if args.status: filtered [item for item in filtered if item[status] args.status] if args.keyword: keyword_lower args.keyword.lower() filtered [ item for item in filtered if keyword_lower in item[title].lower() or keyword_lower in item.get(desc, ).lower() ] if not filtered: print( 未找到匹配的日程) else: print(f 共 {len(filtered)} 条日程) for item in sorted(filtered, keylambda x: x[start]): status_mark ✅ if item[status] done else ⏳ if item[status] pending else ❌ tags_str .join(f#{tag} for tag in item.get(tags, [])) print(f{status_mark} #{item[id]} {item[title]} | {item[start]}–{item[end]} | {tags_str}) elif args.command update: data load_data() target next((item for item in data if item[id] args.id), None) if not target: print(f❌ 未找到 ID 为 {args.id} 的日程) return for field in [title, start, end, desc, status, tags]: if getattr(args, field) is not None: if field start or field end: try: dt datetime.fromisoformat(f{getattr(args, field)}:00) target[field] getattr(args, field) except ValueError: print(f错误{field} 格式应为 YYYY-MM-DD HH:MM) return else: target[field] getattr(args, field) save_data(data) print(f✏️ 已更新日程 #{args.id}) elif args.command delete: data load_data() original_len len(data) data [item for item in data if item[id] ! args.id] if len(data) original_len: print(f❌ 未找到 ID 为 {args.id} 的日程) else: save_data(data) print(f️ 已删除日程 #{args.id}) if __name__ __main__: main()这段代码定义了完整的命令行交互骨架。argparse的关键设计点在于subparsers实现多级命令python schedule.py add --title 开会避免长参数堆砌nargs*让--tags work meeting自动转为[work, meeting]列表choices限制--status只能输入预设值防止拼写错误时间校验使用datetime.fromisoformat()并手动补:00秒兼容 ISO 8601 标准2024-06-15 14:30→2024-06-15T14:30:00save_data()中ensure_asciiFalse保证中文不转义为\u4f60\u597dindent2使 JSON 文件可读性高便于手动编辑调试。2.3 初始化与首次运行创建空数据文件并验证基础流程首次运行前确保当前目录为空或仅含schedule.py。执行以下命令初始化# 创建空 JSON 文件避免 load_data() 报错 echo [] schedule.json # 添加第一条日程明日 10:00 的团队站会 python schedule.py add --title 每日站会 --start 2024-06-16 10:00 --end 2024-06-16 10:15 --tags work meeting # 查看全部日程 python schedule.py list # 按日期查询只显示明天的日程 python schedule.py list --date 2024-06-16 # 将该日程标记为已完成 python schedule.py update --id 1 --status done # 删除一条测试日程若需重试 python schedule.py delete --id 1成功输出应类似✅ 已添加日程 #1每日站会 共 1 条日程 ✅ #1 每日站会 | 2024-06-16 10:00–2024-06-16 10:15 | #work #meeting ✏️ 已更新日程 #1此时schedule.json内容将自动生成为结构化 JSON可直接用 VS Code 打开编辑修改status或desc字段后保存再次list即生效——这正是本地化系统的最大优势数据主权完全在你手中无需等待 API 响应或担心服务商停服。3. 进阶功能落地日期范围查询、状态统计与导出为 Markdown 日志3.1 支持按日期范围start_date ~ end_date批量查询日程基础list命令仅支持单日筛选但实际场景常需查看「本周所有待办」或「下月客户会议」。为此扩展--start-date和--end-date参数并重写过滤逻辑# 在 list_parser 定义后追加 list_parser.add_argument(--start-date, help起始日期格式YYYY-MM-DD) list_parser.add_argument(--end-date, help结束日期格式YYYY-MM-DD) # 在 list 命令处理逻辑中替换原有 filtered data 部分 if args.start_date and args.end_date: try: start_dt datetime.strptime(args.start_date, %Y-%m-%d) end_dt datetime.strptime(args.end_date, %Y-%m-%d) except ValueError: print(错误日期格式应为 YYYY-MM-DD例如 2024-06-01) return filtered [] for item in data: item_date datetime.strptime(item[start][:10], %Y-%m-%d) if start_dt item_date end_dt: filtered.append(item) elif args.date: filtered [item for item in data if item[start].startswith(args.date)] else: filtered data验证命令# 查询 6 月 10 日至 6 月 20 日的所有日程 python schedule.py list --start-date 2024-06-10 --end-date 2024-06-20 # 查询本周日程假设今天是 2024-06-15则查 6 月 10-16 日 python schedule.py list --start-date 2024-06-10 --end-date 2024-06-16此实现不依赖第三方日期库如dateutil仅用标准库datetime.strptime()确保零依赖部署。注意item[start][:10]截取日期部分2024-06-15 14:30→2024-06-15避免时间部分干扰比较。3.2 添加 status 统计功能用 --stats 参数输出待办/完成/取消数量在list_parser中新增参数list_parser.add_argument(--stats, actionstore_true, help显示状态统计)在list命令处理逻辑末尾追加if args.stats: stats {pending: 0, done: 0, canceled: 0} for item in filtered: stats[item[status]] 1 print(\n 状态统计) for status, count in stats.items(): mark ⏳ if status pending else ✅ if status done else ❌ print(f {mark} {status}: {count} 条)执行python schedule.py list --stats将输出 状态统计 ⏳ pending: 12 条 ✅ done: 8 条 ❌ canceled: 2 条该统计基于当前filtered结果集支持与--date、--keyword组合使用例如python schedule.py list --date 2024-06-15 --stats可查看今日各状态分布辅助每日复盘。3.3 导出为 Markdown 日志生成可读性强的周报/月报模板添加新子命令export支持按日期范围导出为.md文件export_parser subparsers.add_parser(export, help导出日程为 Markdown 文件) export_parser.add_argument(--start-date, requiredTrue, help起始日期格式YYYY-MM-DD) export_parser.add_argument(--end-date, requiredTrue, help结束日期格式YYYY-MM-DD) export_parser.add_argument(--output, defaultschedule_report.md, help输出文件名) # 在 main() 函数中command export 分支 elif args.command export: data load_data() try: start_dt datetime.strptime(args.start_date, %Y-%m-%d) end_dt datetime.strptime(args.end_date, %Y-%m-%d) except ValueError: print(错误日期格式应为 YYYY-MM-DD) return filtered [] for item in data: item_date datetime.strptime(item[start][:10], %Y-%m-%d) if start_dt item_date end_dt: filtered.append(item) if not filtered: print(f⚠️ 在 {args.start_date} 至 {args.end_date} 期间无日程) return # 按日期分组 from collections import defaultdict daily_groups defaultdict(list) for item in filtered: date_key item[start][:10] daily_groups[date_key].append(item) # 生成 Markdown with open(args.output, w, encodingutf-8) as f: f.write(f# {args.start_date} 至 {args.end_date} 日程报告\n\n) for date in sorted(daily_groups.keys()): f.write(f## {date}\n) for item in sorted(daily_groups[date], keylambda x: x[start]): status_mark ✅ if item[status] done else ⏳ if item[status] pending else ❌ tags_str .join(f{tag} for tag in item.get(tags, [])) f.write(f- {status_mark} **{item[title]}** ({item[start][11:]}–{item[end][11:]})\n) if item.get(desc): f.write(f {item[desc]}\n) if tags_str: f.write(f {tags_str}\n) f.write(\n) print(f 已导出 {len(filtered)} 条日程至 {args.output})执行python schedule.py export --start-date 2024-06-01 --end-date 2024-06-30 --output june_report.md后生成的 Markdown 文件可直接粘贴到 Notion、Obsidian 或发送邮件格式清晰、层级分明且保留原始状态标记与标签高亮。4. 性能优化与健壮性加固SQLite 迁移路径、并发安全与错误恢复4.1 当日程超 5000 条时平滑迁移到 SQLite零数据丢失方案JSON 文件在数据量增大后load_data()会一次性读入内存导致list命令响应变慢且多进程同时写入可能引发 JSON 格式损坏。此时应切换至sqlite3Python 标准库内置无需pip install。迁移步骤如下创建 SQLite 数据库与表结构新增migrate_to_sqlite.pyimport sqlite3 import json import os def migrate(): # 读取现有 JSON 数据 if not os.path.exists(schedule.json): print(❌ 未找到 schedule.json跳过迁移) return with open(schedule.json, r, encodingutf-8) as f: data json.load(f) # 创建 SQLite 数据库 conn sqlite3.connect(schedule.db) cursor conn.cursor() # 创建表兼容原 JSON 字段 cursor.execute( CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, start TEXT NOT NULL, end TEXT NOT NULL, desc TEXT, tags TEXT, -- 存为 JSON 字符串如 [work,meeting] status TEXT DEFAULT pending, created_at TEXT ) ) # 插入数据 for item in data: cursor.execute( INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?), ( item[id], item[title], item[start], item[end], item.get(desc, ), json.dumps(item.get(tags, []), ensure_asciiFalse), item.get(status, pending), item.get(created_at, ) ) ) conn.commit() conn.close() # 备份原 JSON 并删除 os.rename(schedule.json, schedule.json.backup) print(✅ 迁移完成数据已存入 schedule.db原文件备份为 schedule.json.backup) if __name__ __main__: migrate()修改主程序schedule.py的load_data()/save_data()为 SQLite 版本仅需替换函数体命令行接口不变def load_data(): conn sqlite3.connect(schedule.db) conn.row_factory sqlite3.Row # 支持 item[title] 访问 cursor conn.cursor() cursor.execute(SELECT * FROM events ORDER BY start) rows cursor.fetchall() conn.close() # 将 tags 字符串转回列表 result [] for row in rows: item dict(row) item[tags] json.loads(item[tags]) if item[tags] else [] result.append(item) return result def save_data(data): # SQLite 不需要 save_data()所有操作通过 SQL 执行 pass所有 CRUD 操作改用 SQL 语句以add为例# 替换原 add 分支中的 save_data(data) 部分 conn sqlite3.connect(schedule.db) cursor conn.cursor() cursor.execute( INSERT INTO events (title, start, end, desc, tags, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?), (args.title, args.start, args.end, args.desc, json.dumps(args.tags, ensure_asciiFalse), pending, datetime.now().isoformat()[:19]) ) conn.commit() conn.close() new_id cursor.lastrowid print(f✅ 已添加日程 #{new_id}{args.title})此迁移方案保证原 JSON 数据完整导入ID 顺序不变新增日程自动使用lastrowid生成连续 IDtags字段仍以 JSON 字符串存储保持灵活性旧版schedule.json保留为备份随时可回滚。4.2 并发写入保护用文件锁避免多终端同时修改冲突当用户在 Terminal A 运行addTerminal B 同时运行updateJSON 方案可能因两次load→ 修改 →save导致后者覆盖前者。SQLite 内置行级锁但若坚持用 JSON需加锁import fcntl def load_data_with_lock(): with open(schedule.json, r, encodingutf-8) as f: fcntl.flock(f, fcntl.LOCK_EX) # 获取独占锁 try: content f.read() return json.loads(content) if content else [] finally: fcntl.flock(f, fcntl.LOCK_UN) # 释放锁 def save_data_with_lock(data): with open(schedule.json, r, encodingutf-8) as f: fcntl.flock(f, fcntl.LOCK_EX) try: f.seek(0) f.write(json.dumps(data, ensure_asciiFalse, indent2)) f.truncate() finally: fcntl.flock(f, fcntl.LOCK_UN)注意fcntl仅在 Linux/macOS 有效Windows 需改用msvcrt.locking()或跳过锁机制因 Windows 下多终端同时操作同一文件本身风险更高建议优先迁移到 SQLite。4.3 错误恢复机制自动备份与损坏检测在save_data()前先将当前schedule.json复制为schedule.json.backup并在加载时校验 JSON 有效性def load_data_safe(): backup_path schedule.json.backup main_path schedule.json # 优先尝试主文件 if os.path.exists(main_path): try: with open(main_path, r, encodingutf-8) as f: return json.load(f) except json.JSONDecodeError as e: print(f⚠️ schedule.json 格式错误{e}) if os.path.exists(backup_path): print( 正在从备份恢复...) with open(backup_path, r, encodingutf-8) as f: return json.load(f) else: print(❌ 无备份文件初始化空数据) return [] return []每次save_data()执行前调用shutil.copy2(main_path, backup_path)备份确保即使程序崩溃最多丢失最后一次操作而非整个数据文件。5. 实用技巧一键生成周视图、与 VS Code 集成及自动化同步5.1 用 rich 库渲染彩色日视图提升 CLI 可读性安装richpip install rich后在list命令中替换打印逻辑from rich.console import Console from rich.table import Table from rich.text import Text console Console() def print_list_rich(filtered): if not filtered: console.print( 未找到匹配的日程, styleyellow) return table Table(show_headerTrue, header_stylebold magenta) table.add_column(ID, styledim, width4) table.add_column(状态, width4) table.add_column(时间, width15) table.add_column(标题, min_width20) table.add_column(标签, width15) for item in sorted(filtered, keylambda x: x[start]): status_text Text(✅, stylegreen) if item[status] done else \ Text(⏳, styleyellow) if item[status] pending else \ Text(❌, stylered) tags_text Text( .join(f#{tag} for tag in item.get(tags, [])), stylecyan) table.add_row( str(item[id]), status_text, f{item[start][11:16]}–{item[end][11:16]}\n{item[start][:10]}, item[title], tags_text ) console.print(table)调用print_list_rich(filtered)替代原print()输出效果带颜色、对齐、分隔线大幅提升信息密度与扫描效率。5.2 VS Code 任务集成一键运行常用命令在项目根目录创建.vscode/tasks.json{ version: 2.0.0, tasks: [ { label: 添加日程, type: shell, command: python schedule.py add --title \${input:title}\ --start \${input:start}\ --end \${input:end}\, group: build, presentation: { echo: true, reveal: always, focus: false, panel: shared, showReuseMessage: true, clear: true }, problemMatcher: [] } ], inputs: [ { id: title, type: promptString, description: 日程标题 }, { id: start, type: promptString, description: 开始时间YYYY-MM-DD HH:MM }, { id: end, type: promptString, description: 结束时间YYYY-MM-DD HH:MM } ] }按CtrlShiftP→ 输入Tasks: Run Task→ 选择添加日程VS Code 会弹出输入框填完自动执行python schedule.py add...省去记忆命令参数。5.3 用 cron 或 Windows 任务计划程序实现每日自动归档Linux/macOS 下编辑 crontabcrontab -e添加# 每日凌晨 2 点将昨日状态为 pending 的日程标记为 overdue 0 2 * * * cd /path/to/schedule python schedule.py update --status overdue --keyword overdue 2/dev/null || trueWindows 下用任务计划程序创建基本任务触发器设为「每天」操作设为「启动程序」→python.exe参数填C:\path\to\schedule.py update --status overdue --keyword overdue。注意--keyword overdue需提前在日程标题或描述中加入该词或改用更精确的日期判断逻辑需扩展update命令支持--older-than-days 1参数。至此一个真正可用的 Python 个人日程管理系统已具备零依赖标准库起步10 分钟内跑通可扩展架构5000 条数据时无缝切 SQLite生产级健壮性含锁机制、自动备份、JSON 校验开发者友好集成VS Code 任务、rich 彩色输出、Markdown 导出自动化潜力cron 触发归档、邮件通知后续可接smtplib。下一步你可以基于此框架增加「重复日程」、「日历视图」、「与 Outlook/Google Calendar 双向同步」等模块——但核心原则不变用最简技术栈解决最痛问题让工具服务于人而非让人适应工具。本文还有配套的精品资源点击获取