(十)完整快照与原子替换
1. 先形成新内容,再切换文件名
直接用 w 打开书架文件,旧内容立刻清空。若写到一半程序出错,留下的就可能是半份 JSON。可以先在目标目录写完一份临时文件,关闭后再把它替换到目标名字。
下面的函数会在本章项目中复用:
# 项目文件:persistence_project/bookdata/atomic.py
# 临时文件与目标放在同一个目录,写完并关闭后再执行替换。
import os
from pathlib import Path
from tempfile import NamedTemporaryFile
def write_text_atomic(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = None
try:
with NamedTemporaryFile(
mode="w", encoding="utf-8", newline="", dir=path.parent,
prefix=f".{path.name}.", suffix=".tmp", delete=False
) as stream:
temporary = Path(stream.name)
stream.write(text)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
# 成功时临时名字已被移走;失败时清理本次创建的临时文件。
if temporary is not None:
temporary.unlink(missing_ok=True)NamedTemporaryFile 为本次保存创建独立名字,delete=False 让文件关闭后仍可用于替换。flush 把 Python 缓冲交给底层,fileno 取得系统文件描述符,fsync 请求把这份文件的数据同步到存储。退出 with 关闭句柄后再 os.replace,也适合 Windows 的文件使用方式。
替换这一步使读者按目标路径取得旧的完整内容或新的完整内容。保存顺序是“先写新文件,再替换名字”:写入或转换阶段失败时,旧目标还保留原来的记录,临时文件由收尾代码清理。临时文件、路径替换
2. 格式损坏与初次创建分别处理
第一次使用还没有文件,存储器可以建立空书架;已有文件内容损坏,则应把解析错误交回调用者。如果把二者都处理成空列表,下一次保存就会把损坏文件直接覆盖成空记录,原始错误也难以排查。
本章的 JSON 存储器只在捕获 FileNotFoundError 时返回空列表。JSONDecodeError、编码错误与字段错误保留各自的信息,不在载入过程中悄悄清空已有数据。记录结构另外带 version,后面读到不同结构时,可以据此安排转换。