如何使文件创建成为原子操作?

81

我正在使用Python一次性将文本块写入文件:

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

如果脚本被中断以致于文件写入未能完成,我希望得到的是没有文件而不是部分完成的文件。这可行吗?


7个回答

123

将数据写入临时文件,当数据成功写入后,将文件重命名为正确的目标文件,例如:

with open(tmpFile, 'w') as f:
    f.write(text)
    # make sure that all data is on disk
    # see https://dev59.com/cGs05IYBdhLWcg3wIObS
    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存在且是一个文件,则只有用户有权限时才会被静默替换。如果src和dst在不同的文件系统上,则操作可能失败。如果成功,重命名将是原子操作(这是POSIX的要求)。
注意:
如果src和dest位置不在同一文件系统上,则可能不是原子操作。 在像断电、系统崩溃等情况下,如果性能/响应比数据完整性更重要,则可以跳过os.fsync步骤。

11
为了完整性,tempfile 模块提供了一种简单而安全的方法来创建临时文件。 - itsadok
10
为了更完整:在 POSIX 中,rename 仅在同一文件系统内是原子的,因此最简单的方法是在 myFile 目录中创建 tmpFile - darkk
2
虽然如果你担心操作系统突然关闭(例如断电或内核恐慌),os.fsync是必要的,但对于仅关注进程被中断的情况来说,这已经过度了。 - R Samuel Klatchko
1
@AnuragUniyal - 是否会影响性能取决于原子写操作的频率。os.fsync 可能非常缓慢,因为它必须等待内核刷新其缓冲区。如果有人使用此代码来写入多个文件,则肯定会导致可测量的减速。 - R Samuel Klatchko
2
@J.F.Sebastian 注意,SQLite 添加了 fsync(opendir(filename)) 以确保重命名也写入磁盘。这不会影响此修改的原子性,只会影响此操作与另一个文件上的前一个/后一个操作的相对顺序。 - Dima Tisnek
显示剩余10条评论

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)

1
临时文件需要与要替换的文件位于同一文件系统上。在具有多个文件系统的系统上,此代码将无法可靠地工作。NamedTemporaryFile调用需要一个dir=参数。 - textshell
感谢您的评论,我最近已经更改了这个片段,以便在os.rename失败时回退到shutil.move。这使得它可以跨越文件系统边界工作。 - Nils Werner
3
运行时似乎可以解决问题,但是shutil.move使用的是不具有原子性的copy2。如果想要copy2具有原子性,它需要在与目标文件相同的文件系统中创建临时文件。因此,回退到shutil.move只是掩盖了问题。这就是为什么大多数代码片段将临时文件放置在与目标文件相同的目录中。这也可以使用tempfile.NamedTemporaryFile使用dir命名参数来实现。由于在不可写的目录中移动文件无论如何都不起作用,因此这似乎是最简单和最稳健的解决方案。 - textshell
正确的做法是,我认为shutils.move()不是原子操作,因为连续调用了shutils.copy2()shutils.remove()。新的实现(请参见编辑)将在当前目录中创建文件,并更好地处理异常。 - Nils Werner
@BorisVerkhovskiy:你没有传递 dir=,所以它将其放在临时目录中。你需要传递 dir=os.path.dirname(path_to_orig_file),将其放在与原始文件相同的目录中,从而允许在同一文件系统中进行原子重命名。 - ShadowRanger
显示剩余4条评论

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

现在它已经不再维护。 - Alexandr Zarubkin
@AlexandrZarubkin 哦,那真遗憾。你有什么其他的推荐吗? - vog

6

我正在使用以下代码来原子性地替换/写入文件:

import os
from contextlib import contextmanager

@contextmanager
def atomic_write(filepath, binary=False, fsync=False):
    """ Writeable file object that atomically updates a file (using a temporary file).

    :param filepath: the file path to be opened
    :param binary: whether to open the file in a binary mode instead of textual
    :param fsync: whether to force write the file to disk
    """

    tmppath = filepath + '~'
    while os.path.isfile(tmppath):
        tmppath += '~'
    try:
        with open(tmppath, 'wb' if binary else 'w') as file:
            yield file
            if fsync:
                file.flush()
                os.fsync(file.fileno())
        os.rename(tmppath, filepath)
    finally:
        try:
            os.remove(tmppath)
        except (IOError, OSError):
            pass

使用方法:

with atomic_write('path/to/file') as f:
    f.write("allons-y!\n")

这是基于这个示例的。


1
while循环存在竞争条件,可能是两个并发进程打开同一个文件。使用tempfile.NamedTemporaryFile可以解决这个问题。 - Mic92
2
我认为像这样的tmppath会更好:'.{filepath}~{random}',如果两个进程执行相同的操作,这可以避免竞争条件。这并不能解决竞争条件,但至少你不会得到一个包含两个进程内容的文件。 - guettli

3

完成后只需链接文件:

with tempfile.NamedTemporaryFile(mode="w") as f:
    f.write(...)
    os.link(f.name, final_filename)

如果你想做得更好:

@contextlib.contextmanager
def open_write_atomic(filename: str, **kwargs):
    kwargs['mode'] = 'w'
    with tempfile.NamedTemporaryFile(**kwargs) as f:
        yield f
        os.link(f.name, filename)

2

这个页面上的答案相当旧了,现在有一些库可以帮助你完成这个任务。

特别是 safer 是一个设计用于防止程序员错误破坏文件、套接字连接或通用流的库。它非常灵活,除了其他功能外,它还可以使用内存或临时文件,甚至在失败的情况下也可以保留临时文件。

他们的示例正是你想要的:

# dangerous
with open(filename, 'w') as fp:
    json.dump(data, fp)
    # If an exception is raised, the file is empty or partly written

# safer
with safer.open(filename, 'w') as fp:
    json.dump(data, fp)
    # If an exception is raised, the file is unchanged.

这个库已经在PyPI上了,您可以使用 pip install --user safer 命令进行安装,或者在https://github.com/rec/safer获取最新版本。


-2

Windows的原子解决方案,用于循环文件夹和重命名文件。已测试,原子化自动化,您可以增加概率以最小化风险,避免出现相同的文件名。您可以使用随机.choice方法来使用字母符号组合的随机库,对于数字str(random.random.range(50,999999999,2)。您可以根据需要变化数字范围。

import os import random

path = "C:\\Users\\ANTRAS\\Desktop\\NUOTRAUKA\\"

def renamefiles():
    files = os.listdir(path)
    i = 1
    for file in files:
        os.rename(os.path.join(path, file), os.path.join(path, 
                  random.choice('ABCDEFGHIJKL') + str(i) + str(random.randrange(31,9999999,2)) + '.jpg'))
        i = i+1

for x in range(30):
    renamefiles()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接