ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【第43期】Python 命令行待办:不用数据库,把列表、文件和异常做成可恢复的 CLI

2026/9/24 17:32:19 拓冰建站 浏览量
【第43期】Python 命令行待办:不用数据库,把列表、文件和异常做成可恢复的 CLI 【第43期】Python 命令行待办不用数据库把列表、文件和异常做成可恢复的 CLICSDN 完整教程系列《从小白到 AI 大模型开发工程师的进阶之路》技术点AI-0131 命令行待办事项主人公小蓝伞前置AI-0123 文件读写AI-0130 调试与测试本期产出可持久化 CLI 工具第 42 期把测试习惯立住了。小蓝伞接下来要一个真正能每天用的小工具待办事项。他第一版把列表只放在内存里关终端就丢第二版写入todos.json从项目根运行和从脚本目录运行时却出现两份内容。问题不是 JSON 偶尔失忆而是相对路径跟随当前工作目录。本期交付一个支持add/list/done的完整 CLI路径固定、写入可恢复、坏文件明确失败并用六组输入验证数据不会在错误分支中被清空。这些习惯会直接影响下一期抓取结果如何可靠落盘。一、小蓝伞遇到的问题需求看起来简单添加、完成、列出、退出后还能看见。真正的坑是空输入、重复标题、完成一个不存在的编号JSON 写到一半断电读回来是半截文件相对路径依赖当前工作目录不是脚本所在目录建议插图 1内存列表、错误 cwd 下的 json、脚本旁 json 三种结果对照。二、先给结论能关终端再打开仍在的数据才叫持久化写到哪里必须由脚本位置决定而不是你碰巧站在哪个目录。用列表保存待办用 JSON 落盘。文件路径用Path(__file__).resolve().parent。先写临时文件再替换避免写到一半损坏。所有命令先校验输入再改数据。用第 42 期的测试方式覆盖空输入、缺文件、坏 JSON。三、本文要解决什么项目内容目标可持久化 CLI输入add/list/done/quit 文本命令输出todos.json重启后可还原成功判据正常添加完成编号越界失败且不丢数据坏 JSON 可报错不崩溃不在范围多用户、数据库、Web UI四、前置准备python--version mkdir D:\ai-learning\issue-43不要把 JSON 提交到公开仓库如果里面有私人事项。先备份已有todos.json。五、核心原理列表适合保留显示顺序任务对象用字典表示编号用稳定的整数。这里不用列表下标充当编号删除第一项后下标会整体前移用户手里的“任务 3”可能突然指向另一条。新增编号应取已有最大值加一完成动作只改done不悄悄删除数据。路径有三个概念Path.cwd()是进程从哪里启动__file__是脚本在哪里数据文件是产品约定存在哪里。写Path(todos.json)等于把产品约定交给启动方式从 IDE、任务计划和终端启动可能落到三个目录。学习项目可把数据目录作为参数传入入口再默认选择脚本旁的data。这样测试能使用临时目录生产入口也不依赖用户当前站在哪儿。直接覆盖还有第二个风险序列化完成前进程退出会留下半截 JSON。更稳妥的次序是“同目录临时文件 -flush-fsync-os.replace”。同一文件系统内替换能避免读者看见半成品但它不等于数据库事务也不能解决两个进程同时写。解析失败更不能返回空列表后保存那会把仍可人工抢救的残骸变成合法空文件。错误写法与正确写法的差别不是语法风格# 错路径随 cwd 变化坏文件被伪装成空数据try:itemsjson.loads(Path(todos.json).read_text())exceptException:items[]# 对路径由调用者给出JSON 损坏向上报告itemsload_items(data_path)六、完整项目目录结构issue-43/ ├── todo.py └── data/ # 首次保存时自动创建todo.py是可直接运行的完整版本from__future__importannotationsfromdataclassesimportasdict,dataclassfrompathlibimportPathimportargparseimportjsonimportosimporttempfile DEFAULT_DATAPath(__file__).resolve().parent/data/todos.jsondataclassclassItem:id:inttitle:strdone:boolFalsedefload(path:Path)-list[Item]:从 JSON 加载任务文件不存在表示首次使用文件损坏则失败。ifnotpath.exists():return[]rawjson.loads(path.read_text(encodingutf-8))ifnotisinstance(raw,list):raiseValueError(todos.json 结构必须是列表)items[Item(**row)forrowinraw]iflen({item.idforiteminitems})!len(items):raiseValueError(任务编号不能重复)returnitemsdefsave(path:Path,items:list[Item])-None:先完整写入同目录临时文件再原子替换目标文件。payloadjson.dumps([asdict(x)forxinitems],ensure_asciiFalse,indent2)path.parent.mkdir(parentsTrue,exist_okTrue)fd,tmptempfile.mkstemp(dirpath.parent,suffix.tmp)try:withos.fdopen(fd,w,encodingutf-8)asf:f.write(payload)f.flush()os.fsync(f.fileno())os.replace(tmp,path)exceptException:ifos.path.exists(tmp):os.remove(tmp)raisedefadd(title:str,items:list[Item])-list[Item]:titletitle.strip()ifnottitle:raiseValueError(标题不能为空)nidmax((x.idforxinitems),default0)1items.append(Item(idnid,titletitle))returnitemsdefdone(item_id:int,items:list[Item])-list[Item]:forxinitems:ifx.iditem_id:x.doneTruereturnitemsraiseValueError(f没有编号{item_id})defmain()-int:解析子命令成功返回 0用户输入错误返回 2。parserargparse.ArgumentParser(description可恢复的命令行待办)parser.add_argument(--data,typePath,defaultDEFAULT_DATA)commandsparser.add_subparsers(destcommand,requiredTrue)add_parsercommands.add_parser(add)add_parser.add_argument(title)commands.add_parser(list)done_parsercommands.add_parser(done)done_parser.add_argument(id,typeint)argsparser.parse_args()try:itemsload(args.data)ifargs.commandadd:save(args.data,add(args.title,items))elifargs.commanddone:save(args.data,done(args.id,items))else:foriteminitems:markxifitem.doneelse print(f[{mark}]{item.id}:{item.title})return0except(OSError,ValueError,json.JSONDecodeError)asexc:print(f错误{exc})return2if__name____main__:raiseSystemExit(main())在脚本目录执行python todo.py add复核第43期python todo.py add备份 todos.jsonpython todo.py list python todo.py done 1 python todo.py list预期最后两行是[x] 1: 复核第43期和[ ] 2: 备份 todos.json。再关闭终端、从另一个目录执行python D:\ai-learning\issue-43\todo.py list结果应保持不变。需要隔离测试数据时传--data .\sandbox\todos.json不要拿真实待办做破坏实验。七、可复现失败案例项目记录环节记录——构造方式把DEFAULT_DATA临时改成Path(todos.json)分别从两个目录启动故障现象两次添加都成功list却各看到一条不同任务影响用户误以为保存丢失继续操作可能覆盖错误文件最初误判json.dumps没有真正写盘排查顺序打印 cwd - 打印脚本目录 - 搜索同名文件 - 对比绝对路径根因相对路径由 cwd 解释同名文件实际落在两个目录修复默认路径锚定脚本目录同时保留--data显式注入能力复验从两个目录运行都显示相同任务坏 JSON 返回退出码 2 且原文件不变这是“可复现失败案例”不是虚构的生产事故。旧文件若已经散落先备份并对比内容再合并不要看到同名文件就删除。八、实验设计与数据实验使用临时目录不接触真实待办。控制变量是同一脚本和同一输入只改变启动目录、输入合法性或文件内容指标是退出码、目标文件路径、写入前后 SHA-256 与任务条数。用例预期结果成功判据首次list空输出、退出码 0不要求预先创建文件添加中文标题写入 1 条文件为 UTF-8重启可读空标题退出码 2文件哈希与条数不变done 999退出码 2其他任务状态不变内容改成半截[{JSON 解码错误残骸未被覆盖为空列表从两个 cwd 启动读取同一绝对路径输出任务编号和标题一致功能实验必须同时覆盖正常、边界和失败。原子替换的“进程在任意时刻崩溃”不能由一次手工强杀充分证明因此本文只验证临时文件与替换顺序不把它写成绝对不丢数据。生产系统仍需备份、锁和恢复策略。九、常见问题与避坑把所有异常都当首次运行。文件不存在可以返回空列表权限拒绝、JSON 损坏、结构错误必须报告。三者语义不同。用eval解析命令。用户输入会进入代码执行环境。这里用 argparse 的固定子命令未知参数自动拒绝。把完成动作做成删除。输错编号时难以恢复。先改状态真正删除应另设命令和确认。让 ID 等于下标。删除会导致身份漂移。ID 稳定、顺序可变这两个概念要分开。多个进程同时写。os.replace只避免半文件不防最后写入者覆盖前一个。并发需求出现时换 SQLite 或加可靠文件锁。十、平台、系统与库的差异pathlib会处理 Windows 与 POSIX 分隔符代码里不要手拼反斜杠。Windows 上目标文件被某些编辑器占用时os.replace可能因共享模式失败Linux 通常允许替换仍被读取的文件名。两边都必须捕获OSError并保留旧文件。PowerShell、CMD、IDE 和任务计划的 cwd 都可能不同所以路径契约不能依赖终端。单用户单进程 JSON 足够教学多进程写、条件查询和事务出现时应换 SQLite而不是继续给 JSON 打补丁。十一、验证清单执行两次add后运行list应看到稳定编号 1、2。从其他目录以绝对脚本路径运行输出应与脚本目录启动一致。执行空标题和done 999退出码应为 2原文件内容不变。把测试副本改成[{应报告 JSON 错误文件不能被覆盖成[]。完成编号 1 后重启编号 2 不应变成编号 1。用中文标题保存并重开终端与文件中都应显示原汉字。搜索工作区内todos.json除显式测试路径外只应存在约定文件。从单文件练习推导存储边界JSON 方案成立有三个前提单用户、单进程、数据量可一次读入内存。只要其中一个变化设计就要重新评估。两个终端同时加载相同版本各自追加任务再保存后保存者会覆盖前者原子替换只能保证文件完整不能合并两份并发修改。最小改进可以在文件中增加版本号保存前重新读取并做乐观并发检查真正需要并发、条件查询或事务时SQLite 比自制锁协议更可靠。格式也会演进。今天任务只有 id、title、done明天可能增加 created_at、priority。加载器若直接Item(**row)未知字段和缺失字段都会失败。学习阶段可以严格失败迫使开发者写迁移生产升级则应给文件增加schema_version按版本逐级迁移并在迁移前备份。不要用“字段没有就给默认值”悄悄吞掉所有差异因为拼写错误也会被当成旧格式。恢复策略要回答三个问题旧文件保留多久如何识别最近一次有效备份恢复后如何验证。可以在替换前把目标复制为带时间戳的备份但备份本身也要有数量上限且复制失败时不应继续覆盖。本文没有实现轮换是因为备份策略与用户价值、磁盘空间有关这属于适用边界不该用一句“原子写入所以安全”掩盖。命令行接口也有稳定性。退出码 0 表示成功2 表示输入或数据错误人类提示写到标准错误会更便于管道区分机器可读输出可以另加--json不要让脚本解析彩色中文句子。--data是依赖注入点也是测试边界正式路径、测试临时路径和导入旧文件都经同一入口。路径可配置不等于路径随意程序仍应打印解析后的绝对路径让排障者知道正在操作哪份数据。最后用一次恢复演练收口复制测试文件故意截断末尾确认加载失败且原内容没有被写空恢复备份后再次 list编号、标题和完成状态全部一致。演练的证据是文件哈希、退出码和任务数不是“看起来好了”。做到这里单文件工具才真正具备可恢复性而不是只有保存按钮。测试时如何观察“不写盘”失败路径最重要的断言不是错误文案而是状态未被改变。测试可以在临时目录写入一份已知 JSON记录原始字节调用空标题、越界编号或坏结构后再逐字节读取并比较。只比较内存 items 不够因为错误实现可能先把空列表写盘再抛异常。目标文件内容、临时文件残留、退出码三者都要观察。对原子保存的正常路径验证新文件可以再次反序列化、任务数一致、中文未转义丢失并确认目录里没有遗留.tmp。对模拟替换失败的路径可以把 replace 封装成可注入函数由测试让它抛 OSError断言旧文件仍在、临时文件被清理、异常没有被吞。本文代码直接调用标准库读者扩展测试时再做这个小重构避免为了测试把生产逻辑复制一份。数据边界还包括文件权限。只读目录、磁盘空间不足和防病毒软件占用都可能让保存失败。程序应把 OSError 转成人能理解的失败并返回非零退出码绝不能先删除旧文件再尝试新建。Windows 文件占用行为与 Linux 不同这正是为什么“同目录写临时文件并替换”要在目标系统实测。最后区分业务失败与程序缺陷空标题、编号不存在、JSON 损坏属于预期失败可以给简洁提示AttributeError、断言失败等未知缺陷不应被宽泛捕获后伪装成输入错误。捕获范围越小调试线索越完整。稳定的 CLI 不是永不失败而是失败类别清楚、旧数据仍可恢复、自动化调用能从退出码判断下一步。还要验证帮助信息本身python todo.py --help应列出三个子命令和--data未知命令应由 argparse 返回非零退出码且不创建数据文件。命令行契约一旦被脚本调用参数名与退出码就是公开接口修改时要同步 README并保留兼容迁移而不是只让交互演示能跑。十二、面试题与追问为什么不用数据库单人单文件足够先把失败路径走完。追问什么时候必须换存储并发和查询变复杂时。相对路径为什么危险它相对 cwd 不是脚本。追问__file__在 notebook 里可靠吗不可靠要注入数据目录。坏 JSON 该崩溃还是当空应崩溃或备份后提示不能当空。追问为什么当空等于删数据。如何测试 CLI把 add/done/load/save 做成纯函数测试不走 input。追问要不要自动化按键盘先测函数。id 用列表下标行不行删除后错位。追问自增 id 重启后怎么办取 max1。十三、小蓝伞的工程金句写到哪里不能取决于你站在哪里。静默把坏文件当成空列表比崩溃更危险。能关终端再打开还在才叫持久化。十四、本篇技术清单与下一期下一期AI-0132 网页电影信息抓取把第 40 期 requests、第 37 期 JSON、本期文件保存和合规边界接起来。关注合集继续。你的 CLI 数据文件丢过吗是 cwd 还是写到一半先定文件契约再谈功能列表。官方资料https://docs.python.org/zh-cn/3/library/json.htmlhttps://docs.python.org/zh-cn/3/library/pathlib.htmlhttps://docs.python.org/zh-cn/3/library/tempfile.html适用边界学习用单人工具。生产还要备份、权限和并发控制。