【问题标题】:How to safely write to a file?如何安全地写入文件?
【发布时间】:2010-12-21 04:36:34
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

    标签: python windows file


    【解决方案1】:

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

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

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

    如您所见,您拥有 abc.xml.bak,如果有任何与 tmp 文件相关的问题以及将其复制回来,您可以使用它来恢复。

    【讨论】:

    • 这与 S.Lott 的回答类似,只是删除了备份文件。似乎这是最好的方法。谢谢。
    • 我实际上在 ZODB(Zope 对象数据库)打包其数据库文件(Data.fs)的方式中看到了这种实现,即从数据库文件中删除旧事务的未使用空间。代码是普通的python代码,打包在一个临时文件中,然后进行类似上述的步骤。 ZODB 已经存在了这么多年,并且在 Windows 和 POSIX 平台上都运行良好,所以我相信这种方法应该可行。
    • Python 不能保证重命名是原子的。据我所知,它只是调用操作系统的系统调用。不过,您提供的程序效果很好。
    • 为什么不将 abc.xml 重命名为 abc.xml.bak 并立即将新文件创建为 abc.xml?这会更短,我认为它只会面临同样的弱点,即假设如果您成功地将 abc.xml 重命名为 abc.xml.bak,如果写入失败,您绝对可以将其重命名回 abc.xml。跨度>
    • 除了上述内容之外,同步文件也很重要(正如 u0b34a0f6ae 在答案中提到的那样)。关闭文件或重命名文件都不意味着任何类型的同步到磁盘。所以在新文件到位后,实际文件数据可能还没有到达磁盘盘片,如果机器立即崩溃,可能会导致数据丢失。 f.flush()os.fsync(f.fileno()) 也是如此,其中 f 是您的文件句柄。
    【解决方案2】:

    如果您想要 POSIXly 正确并保存,您必须:

    1. 写入临时文件
    2. 刷新并fsync 文件(或fdatasync
    3. 重命名原始文件

    请注意,调用 fsync 会对性能产生不可预知的影响 - ext3 上的 Linux 可能会因此在磁盘 I/O 整数秒内停顿,这取决于其他未完成的 I/O。

    请注意,rename 不是 POSIX 中的原子操作——至少与您期望的文件数据无关。但是,大多数操作系统和文件系统都会以这种方式工作。但是您似乎错过了关于 Ext4 和文件系统保证原子性的非常大的 linux 讨论。我不知道在哪里链接,但这里是一个开始:ext4 and data loss

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

    通过写入临时文件,然后重命名临时文件,人们会期望操作是依赖的,并且会按顺序执行。

    然而,问题是大多数(如果不是全部)文件系统将元数据和数据分开。重命名只是元数据。这对您来说可能听起来很可怕,但是文件系统重视元数据而不是数据(以 HFS+ 或 Ext3,4 中的日志记录为例)!原因是元数据更轻,如果元数据损坏,则整个文件系统都损坏了——文件系统当然必须自己保存,然后按顺序保存用户的数据。

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

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

    【讨论】:

    • 也许我理解错了。但是,如果您在 POSIX 系统上进行重命名,如果重命名失败,您不保证目的地不会被修改吗? “如果 rename() 函数因 [EIO] 以外的任何原因失败,则任何以 new 命名的文件均不受影响。”我想它仍然会留下损坏的数据。
    • 问题是重命名成功,只是重命名并不能保证整个操作的原子性。
    • 你的意思是 rename() 没有检查点。它肯定是原子的,请参阅:opengroup.org/onlinepubs/009695399/functions/rename.html
    【解决方案3】:

    在 Win API 中,我发现了相当不错的函数 ReplaceFile,即使有可选的备份,它也能做到顾名思义。 DeleteFileMoveFile 组合总有办法。

    总的来说,你想做的事情真的很好。而且我想不出更好的写作方案。

    【讨论】:

    • 如果您使用调用 MS 库 API 的适当 Python 代码来说明这一点会更好。
    • 我没有意识到 ReplaceFile 存在。阅读文档,它似乎做的不仅仅是重命名。它保留了被替换文件的许多属性,因此它似乎是专门为此目的而设计的。
    【解决方案4】:

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

    请注意,rename 在文件系统中不是原子的。为了真正安全,您必须采取一些轻微的解决方法(例如目标文件系统上的临时文件,然后重命名)。

    要锁定文件,请参阅portalocker

    【讨论】:

    • 如果 tempfile 是在目标文件系统之外的另一个文件系统中创建的,那么最终的重命名要么不起作用,要么不是原子的。
    • @tzot,没错,但解决方案很简单:将文件复制到目标文件系统上的临时文件,然后重命名。
    • 是的,完全正确;关心更新答案吗? :)
    • @tzot,很抱歉在将近 12 年后才跟进这个线程:)
    【解决方案5】:

    标准解决方案是这样的。

    1. 编写一个具有相似名称的新文件。以 X.ext# 为例。

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

      • X.ext(原)转X.ext~

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

    3. (仅适用于疯狂的偏执狂)调用操作系统同步函数来强制写入脏缓冲区。

    任何时候都不会丢失或损坏任何东西。唯一的故障可能发生在重命名期间。但是你没有丢失任何东西或损坏任何东西。在最终重命名之前,原件都可以恢复。

    【讨论】:

    • 但是,如果您不重命名两次(如您所做的那样创建备份),如果重命名不是原子的,您可能会丢失数据,对吧?
    • 重命名 is 在许多操作系统中都是原子的。但是,备份比担心操作的原子性更重要。请记住:崩溃的几率(Windows 除外)非常小。在重命名过程中发生崩溃的几率(只有几条指令加上一次同步)非常非常小。
    【解决方案6】:

    现在有一个经过编码的纯 Python,我敢在boltons utility libraryboltons.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

    【讨论】:

      【解决方案7】:

      根据 RedGlyph 的建议,我添加了一个 ReplaceFile 实现,它使用 ctypes 来访问 Windows API。我首先将它添加到 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

      【讨论】:

        【解决方案8】:

        您可以使用 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
        

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

        【讨论】:

          猜你喜欢
          • 2020-02-18
          • 1970-01-01
          • 2015-12-10
          • 2012-11-06
          • 2015-06-26
          • 2011-09-05
          • 1970-01-01
          • 1970-01-01
          • 2019-07-02
          相关资源
          最近更新 更多