用 Python 标准库更新 JSON:临时文件与 os.replace

举报
yd_210400787 发表于 2026/11/02 09:19:55 2026/11/02
【摘要】 使用 Python 标准库 json、tempfile 和 os.replace 更新小型 JSON 配置:先写同目录临时文件,再替换目标路径。附可运行脚本、失败保留旧文件的验证与并发、持久性边界。

写配置文件时,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 配置;大文件、多人并发修改或需要回滚历史的场景,应采用更完整的存储方案。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。