用 Python 标准库更新 JSON:临时文件与 os.replace
写配置文件时,Path.write_text() 很方便,但它会先打开并截断目标文件。如果程序在写入一半时异常退出,原文件可能只剩下一段不完整的 JSON。对需要反复更新的小型配置文件,可以先在同一目录写好临时文件,确认内容写完后再用 os.replace() 替换目标路径。
下面的例子只用 Python 标准库。它演示成功更新,以及数据无法序列化时原文件仍然保持完整。
可运行脚本
将下面内容保存为 atomic_json_write.py,用 Python 3.8 或更新版本执行:
#!/usr/bin/env python3
"""Demonstrate replacing a JSON file without exposing a partial write."""
import json
import os
import tempfile
from pathlib import Path
def write_json_atomic(path: Path, data: object) -> None:
"""Write JSON to a sibling temporary file, then replace the destination."""
temporary_path = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
newline="\n",
dir=path.parent,
prefix=f".{path.name}.",
suffix=".tmp",
delete=False,
) as handle:
temporary_path = Path(handle.name)
json.dump(data, handle, ensure_ascii=False, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary_path, path)
except Exception:
if temporary_path is not None:
temporary_path.unlink(missing_ok=True)
raise
def main() -> None:
with tempfile.TemporaryDirectory() as directory:
destination = Path(directory) / "config.json"
destination.write_text('{"version": 1}\n', encoding="utf-8")
write_json_atomic(destination, {"version": 2, "features": ["校验", "备份"]})
print("成功更新:", json.loads(destination.read_text(encoding="utf-8")))
try:
write_json_atomic(destination, {"invalid": {1, 2}})
except TypeError as error:
print("序列化失败:", error)
print("失败后仍是:", json.loads(destination.read_text(encoding="utf-8")))
print("遗留临时文件:", len(list(Path(directory).glob("*.tmp"))))
if __name__ == "__main__":
main()
运行 python3 atomic_json_write.py,我在本地得到:
成功更新: {'features': ['校验', '备份'], 'version': 2}
序列化失败: Object of type set is not JSON serializable
失败后仍是: {'features': ['校验', '备份'], 'version': 2}
遗留临时文件: 0
第二次调用传入了 Python set,json.dump() 不支持它,因此抛出 TypeError。代码进入 except 删除尚未完成的临时文件;因为没有执行到 os.replace(),目标文件仍然是第一次成功写入的版本。
每一步解决什么问题
NamedTemporaryFile(delete=False, dir=path.parent) 在目标目录创建临时文件。放在同一目录可避免跨文件系统替换,也让临时文件和目标文件处于相同的目录环境。显式关闭临时文件后再替换,对不同平台都更稳妥。
json.dump(..., ensure_ascii=False) 直接输出中文,而不是写成 \\u 转义序列;sort_keys=True 让键顺序稳定,便于人工比较版本。flush() 把 Python 缓冲区内容交给操作系统,随后 os.fsync() 请求同步文件内容。最后的 os.replace() 将目标路径一次切换到新文件:其他进程按路径打开文件时,通常会看到旧版或新版,不会看到正在写入的半成品。
使用边界
这里保证的是替换前不破坏旧文件,并不等于所有场景都有完整事务语义。两个进程同时写同一个文件仍可能发生“后写覆盖先写”,需要额外的锁或版本检查。它也不是备份:替换成功后,旧内容不再由这个路径提供。若需要针对突然断电讨论更严格的持久性,还要考虑目录项同步及目标文件系统的保证,不能只凭这里的 fsync() 宣称万无一失。
新文件来自临时文件,权限与原文件可能不同,尤其在已有文件上覆盖时应按应用需求设置权限和所有者。示例适合小型 JSON 配置;大文件、多人并发修改或需要回滚历史的场景,应采用更完整的存储方案。
- 点赞
- 收藏
- 关注作者
评论(0)