如何将文件创建变为原子操作?

81 投票
7 回答
45423 浏览
提问于 2025-04-15 19:43

我正在用Python一次性把一段段文字写入文件:

open(file, 'w').write(text)

如果脚本在写文件的时候被中断,导致文件没有写完,我希望这个文件根本不存在,而不是一个写了一半的文件。这样可以做到吗?

7 个回答

19

因为处理这些细节很容易出错,所以我建议使用一个小型的库来帮忙。使用库的好处是,它可以处理所有这些繁琐的细节,而且这个库是由社区不断审查和改进的。

其中一个这样的库是 python-atomicwrites,由 untitaker 开发,它甚至支持 Windows 系统:

注意(截至2023年):

这个库目前没有维护。作者的评论:

[...] 我觉得现在是时候不再支持这个包了。Python 3 有 os.replace 和 os.rename,这两个功能对于大多数使用场景来说已经足够好了。

最初的推荐:

来自 README 文件:

from atomicwrites import atomic_write

with atomic_write('foo.txt', overwrite=True) as f:
    f.write('Hello world.')
    # "foo.txt" doesn't exist yet.

# Now it does.

通过 PIP 安装:

pip install atomicwrites
28

这是一个简单的代码片段,使用Python的tempfile模块来实现原子写入,也就是确保写入操作的安全性。

with open_atomic('test.txt', 'w') as f:
    f.write("huzza")

甚至可以在同一个文件中进行读写操作:

with open('test.txt', 'r') as src:
    with open_atomic('test.txt', 'w') as dst:
        for line in src:
            dst.write(line)

这里使用了两个简单的上下文管理器。

import os
import tempfile as tmp
from contextlib import contextmanager

@contextmanager
def tempfile(suffix='', dir=None):
    """ Context for temporary file.

    Will find a free temporary filename upon entering
    and will try to delete the file on leaving, even in case of an exception.

    Parameters
    ----------
    suffix : string
        optional file suffix
    dir : string
        optional directory to save temporary file in
    """

    tf = tmp.NamedTemporaryFile(delete=False, suffix=suffix, dir=dir)
    tf.file.close()
    try:
        yield tf.name
    finally:
        try:
            os.remove(tf.name)
        except OSError as e:
            if e.errno == 2:
                pass
            else:
                raise

@contextmanager
def open_atomic(filepath, *args, **kwargs):
    """ Open temporary file object that atomically moves to destination upon
    exiting.

    Allows reading and writing to and from the same filename.

    The file will not be moved to destination in case of an exception.

    Parameters
    ----------
    filepath : string
        the file path to be opened
    fsync : bool
        whether to force write the file to disk
    *args : mixed
        Any valid arguments for :code:`open`
    **kwargs : mixed
        Any valid keyword arguments for :code:`open`
    """
    fsync = kwargs.pop('fsync', False)

    with tempfile(dir=os.path.dirname(os.path.abspath(filepath))) as tmppath:
        with open(tmppath, *args, **kwargs) as file:
            try:
                yield file
            finally:
                if fsync:
                    file.flush()
                    os.fsync(file.fileno())
        os.rename(tmppath, filepath)
124

先把数据写到一个临时文件里,等数据成功写入后,再把这个临时文件改名为目标文件,比如:

with open(tmpFile, 'w') as f:
    f.write(text)
    # make sure that all data is on disk
    # see http://stackoverflow.com/questions/7433057/is-rename-without-fsync-safe
    f.flush()
    os.fsync(f.fileno())    
os.replace(tmpFile, myFile)  # os.rename pre-3.3, but os.rename won't work on Windows

根据文档 http://docs.python.org/library/os.html#os.replace

把文件或文件夹 src 改名为 dst。如果 dst 是一个非空的文件夹,就会出现 OSError 错误。如果 dst 已经存在并且是一个文件,且用户有权限的话,它会被静默替换。如果 srcdst 在不同的文件系统上,这个操作可能会失败。如果成功,重命名操作会是一个原子操作(这是 POSIX 的要求)。

注意:

  • 如果 src 和目标位置 dst 不在同一个文件系统上,可能就不是原子操作了。

  • 如果在像停电、系统崩溃等情况下,性能或响应速度比数据完整性更重要,可以跳过 os.fsync 这一步。

撰写回答