纯Python本地日程管理系统:CLI+JSON+SQLite实战指南
2026/9/12 13:56:23 网站建设 项目流程

简介:这是一份面向大学生与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处理时序逻辑,用jsonsqlite3持久化数据,用argparse构建清晰命令行接口,支持添加/查询/修改/归档日程,还能按日期范围、关键词、状态(待办/已完成/已取消)精准过滤。它不联网、不注册、不上传,所有数据存你本地磁盘;新手能照着跑通基础功能,老手可直接接入 cron 自动同步、对接邮件通知、或用rich库渲染带颜色的日视图。适合运维工程师记录巡检计划、学生党排期复习节点、自由职业者管理客户交付周期——只要你需要「确定性」和「可编程性」,而不是「又一个需要登录的 SaaS 页面」。

2. 用标准库从零构建最小可行系统:CLI 入口、JSON 存储与基础 CRUD

2.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", encoding="utf-8") as f: return json.load(f) return [] def save_data(data): with open("schedule.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def main(): parser = argparse.ArgumentParser(description="Python 个人日程管理系统") subparsers = parser.add_subparsers(dest="command", help="可用命令") # 添加日程子命令 add_parser = subparsers.add_parser("add", help="添加新日程") add_parser.add_argument("--title", required=True, help="日程标题") add_parser.add_argument("--start", required=True, help="开始时间,格式:YYYY-MM-DD HH:MM") add_parser.add_argument("--end", required=True, 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", type=int, required=True, 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", type=int, required=True, help="日程ID") args = parser.parse_args() if args.command == "add": data = load_data() new_id = max([item.get("id", 0) for item in data], default=0) + 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, key=lambda 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:302024-06-15T14:30:00);
  • save_data()ensure_ascii=False保证中文不转义为\u4f60\u597dindent=2使 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 打开编辑,修改statusdesc字段后保存,再次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:302024-06-15),避免时间部分干扰比较。

3.2 添加 status 统计功能:用 --stats 参数输出待办/完成/取消数量

list_parser中新增参数:

list_parser.add_argument("--stats", action="store_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", required=True, help="起始日期,格式:YYYY-MM-DD") export_parser.add_argument("--end-date", required=True, help="结束日期,格式:YYYY-MM-DD") export_parser.add_argument("--output", default="schedule_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", encoding="utf-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], key=lambda 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 格式损坏。此时应切换至sqlite3(Python 标准库内置,无需pip install)。迁移步骤如下:

  1. 创建 SQLite 数据库与表结构(新增migrate_to_sqlite.py):
import sqlite3 import json import os def migrate(): # 读取现有 JSON 数据 if not os.path.exists("schedule.json"): print("❌ 未找到 schedule.json,跳过迁移") return with open("schedule.json", "r", encoding="utf-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_ascii=False), 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()
  1. 修改主程序schedule.pyload_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
  1. 所有 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_ascii=False), "pending", datetime.now().isoformat()[:19]) ) conn.commit() conn.close() new_id = cursor.lastrowid print(f"✅ 已添加日程 #{new_id}:{args.title}")

此迁移方案保证:

  • 原 JSON 数据完整导入,ID 顺序不变;
  • 新增日程自动使用lastrowid生成连续 ID;
  • tags字段仍以 JSON 字符串存储,保持灵活性;
  • 旧版schedule.json保留为备份,随时可回滚。

4.2 并发写入保护:用文件锁避免多终端同时修改冲突

当用户在 Terminal A 运行add,Terminal B 同时运行update,JSON 方案可能因两次load→ 修改 →save导致后者覆盖前者。SQLite 内置行级锁,但若坚持用 JSON,需加锁:

import fcntl def load_data_with_lock(): with open("schedule.json", "r+", encoding="utf-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+", encoding="utf-8") as f: fcntl.flock(f, fcntl.LOCK_EX) try: f.seek(0) f.write(json.dumps(data, ensure_ascii=False, indent=2)) 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", encoding="utf-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", encoding="utf-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("🔍 未找到匹配的日程", style="yellow") return table = Table(show_header=True, header_style="bold magenta") table.add_column("ID", style="dim", width=4) table.add_column("状态", width=4) table.add_column("时间", width=15) table.add_column("标题", min_width=20) table.add_column("标签", width=15) for item in sorted(filtered, key=lambda x: x["start"]): status_text = Text("✅", style="green") if item["status"] == "done" else \ Text("⏳", style="yellow") if item["status"] == "pending" else \ Text("❌", style="red") tags_text = Text(" ".join(f"#{tag}" for tag in item.get("tags", [])), style="cyan") 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)" } ] }

Ctrl+Shift+P→ 输入Tasks: Run Task→ 选择添加日程,VS Code 会弹出输入框,填完自动执行python schedule.py add...,省去记忆命令参数。

5.3 用 cron 或 Windows 任务计划程序实现每日自动归档

Linux/macOS 下,编辑 crontab(crontab -e)添加:

# 每日凌晨 2 点,将昨日状态为 pending 的日程标记为 overdue 0 2 * * * cd /path/to/schedule && python schedule.py update --status overdue --keyword "overdue" 2>/dev/null || true

Windows 下,用任务计划程序创建基本任务,触发器设为「每天」,操作设为「启动程序」→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 双向同步」等模块——但核心原则不变:用最简技术栈解决最痛问题,让工具服务于人,而非让人适应工具。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询