如何安全地写入文件?

时间:2009-11-28 09:46:39

标签: python windows file

想象一下,您有一个用于处理某种XML文件或配置文件的库。该库将整个文件读入内存并提供编辑内容的方法。操作完内容后,可以调用write将内容保存回文件。问题是如何以安全的方式做到这一点。

覆盖现有文件(开始写入原始文件)显然不安全。如果write方法在完成之前失败,则最终会得到一个半文件并且您丢失了数据。

更好的选择是在某处写入临时文件,当write方法完成后,您临时文件复制到原始文件文件。

现在,如果副本以某种方式失败,您仍然可以在临时文件中正确保存数据。如果复制成功,您可以删除临时文件。

在POSIX系统上,我猜你可以使用rename系统调用,这是一个原子操作。但是你如何在Windows系统上做到最好?特别是,如何使用 Python 来处理这个问题?

另外,还有另一种安全写入文件的方案吗?

8 个答案:

答案 0 :(得分:14)

如果你看到Python的文档,它清楚地提到os.rename()是一个原子操作。因此,在您的情况下,将数据写入临时文件然后将其重命名为原始文件将是非常安全的。

另一种方式可以这样工作:

  • 让原始文件为abc.xml
  • 创建abc.xml.tmp并向其写入新数据
  • 将abc.xml重命名为abc.xml.bak
  • 将abc.xml.tmp重命名为abc.xml
  • 在新的abc.xml正确放置后,删除abc.xml.bak

正如您所看到的那样,如果存在与tmp文件相关的任何问题并将其复制回来,您可以使用abc.xml.bak进行恢复。

答案 1 :(得分:11)

如果你想要POSIX正确并保存,你必须:

  1. 写入临时文件
  2. 刷新并fsync文件(或fdatasync
  3. 重命名原始文件
  4. 请注意,调用fsync会对性能产生不可预测的影响 - 因此,ext3上的Linux可能因磁盘I / O整数秒而停顿,具体取决于其他未完成的I / O.

    请注意rename 不是 POSIX中的原子操作 - 至少与您期望的文件数据无关。但是,大多数操作系统和文件系统都将以这种方式工作。但似乎你错过了关于Ext4和文件系统保证原子性的大型Linux讨论。我不知道确切的链接位置,但这是一个开头:ext4 and data loss

    请注意,在许多系统上,重命名在实践中与您期望的一样安全。然而,在所有可能的Linux配置中,它不可能同时获得性能和可靠性!

    通过写入临时文件,然后重命名临时文件,可以预期操作是依赖的,并且将按顺序执行。

    然而,问题是大多数(如果不是全部)文件系统将元数据和数据分开。重命名只是元数据。这对您来说可能听起来很糟糕,但是文件系统会重视元数据而不是数据(例如,在HFS +或Ext3,4中使用日记)!原因是元数据较轻,如果元数据损坏,整个文件系统都会损坏 - 文件系统当然必须自行保存,然后按顺序保留用户的数据。

    Ext4在第一次出现时确实打破了rename的期望,但是添加了启发式算法来解决它。问题是重命名失败,但成功重命名。 Ext4可能会成功注册重命名,但如果此后不久发生崩溃,则无法写出文件数据。结果是一个0长度的文件,既没有orignal也没有新数据。

    简而言之,POSIX没有这样的保证。阅读链接的Ext4文章了解更多信息!

答案 2 :(得分:5)

在Win API中,我找到了相当不错的函数ReplaceFile,即使使用可选的备份也可以做出名称。始终使用DeleteFileMoveFile组合。

一般来说,你想做的事情真的很棒。我想不出更好的写作方案。

答案 3 :(得分:4)

标准解决方案就是这个。

  1. 编写一个名称相似的新文件。例如X.ext#。

  2. 当该文件已关闭(甚至可能是读取和校验和)时,您将两次重命名。

    • X.ext(原文)到X.ext~

    • X.ext#(新的)到X.ext

  3. (仅限疯狂的偏执狂)调用OS同步功能强制进行脏缓冲区写入。

  4. 任何时候都不会丢失或腐败。在重命名期间可能发生唯一的故障。但你没有丢失任何东西或损坏任何东西。原始版本可以恢复,直到最终重命名。

答案 4 :(得分:3)

一个简单的解决方案。使用tempfile创建临时文件,如果写入成功,则只需将文件重命名为原始配置文件。

要锁定文件,请参阅portalocker

答案 5 :(得分:3)

现在有一个编纂的纯Python,我敢在boltons utility library boltons.fileutils.atomic_save中说Pythonic解决方案。

只需pip install boltons,然后:

from boltons.fileutils import atomic_save

with atomic_save('/path/to/file.txt') as f:
    f.write('this will only overwrite if it succeeds!\n')

有很多实用的选择all well-documented。完全披露,我是boltons的作者,但这个特殊部分是在很多社区帮助下构建的。如果不清楚,请不要犹豫drop a note

答案 6 :(得分:2)

根据RedGlyph的建议,我添加了一个使用ctypes访问Windows API的ReplaceFile实现。我首先将其添加到jaraco.windows.api.filesystem。

ReplaceFile = windll.kernel32.ReplaceFileW
ReplaceFile.restype = BOOL
ReplaceFile.argtypes = [
    LPWSTR,
    LPWSTR,
    LPWSTR,
    DWORD,
    LPVOID,
    LPVOID,
    ]

REPLACEFILE_WRITE_THROUGH = 0x1
REPLACEFILE_IGNORE_MERGE_ERRORS = 0x2
REPLACEFILE_IGNORE_ACL_ERRORS = 0x4

然后,我使用此脚本测试了行为。

from jaraco.windows.api.filesystem import ReplaceFile
import os

open('orig-file', 'w').write('some content')
open('replacing-file', 'w').write('new content')
ReplaceFile('orig-file', 'replacing-file', 'orig-backup', 0, 0, 0)
assert open('orig-file').read() == 'new content'
assert open('orig-backup').read() == 'some content'
assert not os.path.exists('replacing-file')

虽然这仅适用于Windows,但它似乎有许多其他替换例程缺少的很好的功能。有关详细信息,请参阅API docs

答案 7 :(得分:0)

您可以使用fileinput模块为您处理备份和就地写作:

import fileinput
for line in fileinput.input(filename,inplace=True, backup='.bak'):
    # inplace=True causes the original file to be moved to a backup
    # standard output is redirected to the original file.
    # backup='.bak' specifies the extension for the backup file.

    # manipulate line
    newline=process(line)
    print(newline)

如果您需要先阅读全部内容,然后才能编写换行符, 然后你可以先做,然后用

打印全新的内容
newcontents=process(contents)
for line in fileinput.input(filename,inplace=True, backup='.bak'):
    print(newcontents)
    break

如果脚本突然结束,您仍然可以备份。