用 Python 内置 sqlite3 写一个命令行待办清单:建表、增删改查与按截止日期排序
很多待办工具一打开就要注册、同步、开会员,其实对学生和个人开发者来说,一个本地小工具往往更够用。本文用 Python 内置的 sqlite3 模块写一个命令行待办清单,不需要安装第三方库,也不需要启动数据库服务,一个文件就是一个数据库,复制走就能用。
这个小工具要解决什么问题
日常学习里任务很容易散落在聊天记录、备忘录和纸上,最后不是忘了交作业,就是把紧急的事拖到最后。待办清单至少要做到四件事:能随时记下来,能改,能删,能按截止日期排个先后。把数据存在 SQLite 里还有一个好处:关机重启后任务还在,可以按条件查,而不是每次都从头翻一遍。
设计思路:先定表结构,再写命令
一张任务表就够了,不必一开始就设计得很复杂。字段可以这样定:编号是自增主键,标题不能为空,截止日期用文本保存,状态默认是待办,创建时间自动生成。截止日期统一写成 2026-10-28 这样的年月日格式,字符串按字典序排序时就和日期顺序一致,这是后面按截止日期排序的关键。如果格式不统一,有人写 10 月 28 日,有人写 2026/10/28,排序结果就会乱掉。
建表语句里还有一个细节:标题用 NOT NULL 约束,避免存进一条空任务;状态用默认值,新增时不用每次都写。程序启动时先判断表是否存在,不存在就创建,这样第一次运行和之后运行用的是同一份代码,不用手动建库。
完整可运行代码
把下面代码保存为待办脚本文件,直接用 Python 运行即可:
import argparse
import sqlite3
import sys
from datetime import date
DB_PATH = "todo.db"
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
due_date TEXT,
status TEXT NOT NULL DEFAULT 'todo',
created_at TEXT NOT NULL DEFAULT (date('now'))
)
""")
return conn
def add(title, due_date):
with get_conn() as conn:
conn.execute(
"INSERT INTO todos (title, due_date) VALUES (?, ?)",
(title, due_date),
)
print("已添加:" + title)
def list_todos(only_todo=False):
sql = "SELECT id, title, due_date, status FROM todos"
if only_todo:
sql += " WHERE status = 'todo'"
sql += " ORDER BY due_date IS NULL, due_date ASC, id ASC"
with get_conn() as conn:
rows = conn.execute(sql).fetchall()
if not rows:
print("暂无任务")
return
for task_id, title, due_date, status in rows:
mark = "已完成" if status == "done" else "待办"
print(f"{task_id}. [{mark}] {title} 截止:{due_date or '未设置'}")
def done(task_id):
with get_conn() as conn:
cur = conn.execute(
"UPDATE todos SET status = 'done' WHERE id = ?", (task_id,)
)
print("已完成" if cur.rowcount else "没有这个编号")
def edit(task_id, title=None, due_date=None):
if title is None and due_date is None:
print("请至少给出新的标题或截止日期")
return
with get_conn() as conn:
if title is not None:
conn.execute(
"UPDATE todos SET title = ? WHERE id = ?", (title, task_id)
)
if due_date is not None:
conn.execute(
"UPDATE todos SET due_date = ? WHERE id = ?", (due_date, task_id)
)
print("已更新")
def delete(task_id):
with get_conn() as conn:
cur = conn.execute("DELETE FROM todos WHERE id = ?", (task_id,))
print("已删除" if cur.rowcount else "没有这个编号")
def main():
parser = argparse.ArgumentParser(description="命令行待办清单")
sub = parser.add_subparsers(dest="cmd", required=True)
p_add = sub.add_parser("add")
p_add.add_argument("title")
p_add.add_argument("--due")
sub.add_parser("list")
p_done = sub.add_parser("done")
p_done.add_argument("id", type=int)
p_edit = sub.add_parser("edit")
p_edit.add_argument("id", type=int)
p_edit.add_argument("--title")
p_edit.add_argument("--due")
p_del = sub.add_parser("delete")
p_del.add_argument("id", type=int)
args = parser.parse_args()
if args.cmd == "add":
add(args.title, args.due)
elif args.cmd == "list":
list_todos()
elif args.cmd == "done":
done(args.id)
elif args.cmd == "edit":
edit(args.id, args.title, args.due)
elif args.cmd == "delete":
delete(args.id)
if __name__ == "__main__":
main()
使用步骤
第一步,保存代码并在代码所在目录打开终端。第二步,添加任务,例如运行添加命令并传入标题与截止日期。第三步,查看清单,运行列表命令,这时任务会按截止日期从早到晚排列,没有截止日期的排在最后。第四步,完成一项后运行完成命令并传入编号,其中的数字是清单前面显示的编号。第五步,需要改期时运行编辑命令并传入新截止日期;不再需要的任务用删除命令并传入编号删除。上面每一步的命令名依次是 add、list、done、edit、delete,脚本名就是保存代码时用的文件名。
为什么排序要这样写
如果只写 ORDER BY due_date ASC,空值在 SQLite 里会排在最前面,结果就是没写日期的任务看起来最紧急,这不符合直觉。加上 due_date IS NULL 这一项后,表达式对有日期的任务是 0,对空值是 1,排序时有日期的先排,空值的后排。同一天截止的任务再按编号排序,保证每次列出的顺序稳定,不会今天一个样、明天又一个样。
几个容易踩的坑
第一,SQL 里一定要用问号占位符传参数,不要把标题直接拼进字符串,否则标题里出现引号就可能报错,也容易写出不安全的代码。第二,写操作后要提交,本文用 with 语句管理连接,正常退出时会自动提交,省去到处写提交的麻烦。第三,删除是不可逆的,误删后不能从这个小工具里找回,重要任务先完成再删,或者先备份数据库文件。第四,截止日期只是文本,程序没有校验它是不是真实日期,输入时自己保持年月日格式即可。
可以继续扩展的方向
这个版本已经能覆盖日常使用,如果还想练手,可以按顺序加功能:先加优先级字段,再按优先级加截止日期双重排序;然后加按关键字搜索;最后加一个导出功能,把待办导出成文本方便打印。每次只加一个功能,跑通再加下一个,比一开始就写一个大而全的程序更容易坚持。本地小工具的价值不在于功能多,而在于随手可用、数据在自己手里、出了问题能自己修。
- 点赞
- 收藏
- 关注作者
评论(0)