Python 保存 JSON 配置,怎样避免写到一半留下损坏文件
Python 保存 JSON 配置,怎样避免写到一半留下损坏文件
一个小工具经常只需要一份 JSON 配置。直接用 open(path, 'w') 写入虽然简单,但文件会先被截断:如果随后发生序列化错误、磁盘写入失败或进程退出,原来可用的配置也可能丢失。下面用标准库实现一个更稳妥的保存方式,并说明它能解决的问题及限制。
一、先写临时文件,再替换目标
基本顺序是:在目标目录创建临时文件,将完整 JSON 写入临时文件,刷新缓冲区,最后使用 os.replace 替换目标。这里强调“目标目录”,因为原子重命名要求源和目标位于支持该语义的同一文件系统;把临时文件随便放到另一个分区,可能得到跨设备错误。
在常见的 Linux 本地文件系统上,替换操作成功后,新打开目标路径的读取者会读到完整的新文件;替换前已经打开旧文件的读取者可以继续读旧内容。因此,这个方法适合避免读取者遇到截断或半写入的配置。
二、一个可直接使用的标准库实现
import json
import os
import tempfile
from pathlib import Path
def save_json(path, data):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
temp_path = None
try:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=path.parent,
prefix=f".{path.name}.", suffix=".tmp", delete=False,
) as stream:
temp_path = Path(stream.name)
json.dump(data, stream, ensure_ascii=False, indent=2)
stream.flush()
os.fsync(stream.fileno())
os.replace(temp_path, path)
finally:
if temp_path is not None:
temp_path.unlink(missing_ok=True)
示例使用 Python 3.8 及以上版本。ensure_ascii=False 让中文保持可读;flush 把 Python 缓冲区交给操作系统,fsync 请求同步临时文件的数据。只有写入及同步成功后才进行替换。序列化抛错时,程序会进入 finally 清理临时文件,目标文件保持原样;异常继续向调用方传播,便于记录或显示保存失败。
三、验证失败时旧配置是否还在
不要只验证正常保存。下面在一个专门的测试目录中检查失败分支;请使用空测试目录,避免覆盖已有设置。
import json
from pathlib import Path
path = Path("./json-save-demo/settings.json")
save_json(path, {"状态": "旧值"})
save_json(path, {"状态": "新值", "重试次数": 3})
assert json.loads(path.read_text(encoding="utf-8"))["状态"] == "新值"
try:
save_json(path, {"不支持": object()})
except TypeError:
pass
else:
raise AssertionError("无效数据应触发序列化错误")
assert json.loads(path.read_text(encoding="utf-8"))["状态"] == "新值"
assert not list(path.parent.glob(".settings.json.*.tmp"))
本文实现已在 Linux 测试目录实际运行,验证了正常替换、序列化失败时保留旧配置,以及常规异常路径清理临时文件。这些检查没有模拟突然断电,也不能代替目标设备上的文件系统兼容测试。
四、原子替换之外,还要注意什么
原子性与断电持久性不同。 示例同步了文件内容,但没有同步父目录项。对断电恢复有严格要求时,还需按目标系统的规范同步父目录,并验证底层存储行为;不能把一次 os.replace 理解成所有硬件故障下的数据保证。
并发更新仍可能互相覆盖。 两个进程分别读取旧配置并修改,即使各自保存都是原子的,最后一次替换仍会覆盖前一次修改。存在多个写入者时,应使用锁、单一写入进程,或改用支持事务的数据库。
权限会随新文件变化。 临时文件通常使用较严格的权限,替换后不会自动继承旧文件的权限、属主和扩展属性。若其他用户运行的服务需要读取配置,应明确设置并核验权限;不要为了省事把敏感配置设为任何人可读。
强制终止可能留下临时文件。 finally 能处理正常异常,却无法在进程被强制结束或系统崩溃时运行。可以在工具启动时清理已确认属于自己的过期临时文件,清理前要避开仍在进行的写入。
对于单进程、小体积、低频更新的配置,这个办法足够简洁。数据关系复杂、写入频繁或需要多步骤一致性时,SQLite 事务通常更适合。选择方式时应依据数据要求,而不是只依据文件大小。
- 点赞
- 收藏
- 关注作者
评论(0)