9

我有一个长时间运行的进程,它在一个文件中写入了很多东西。结果应该是全部或全部,所以我正在写入一个临时文件并将其重命名为最后的真实名称。目前,我的代码是这样的:

filename = 'whatever'
tmpname = 'whatever' + str(time.time())

with open(tmpname, 'wb') as fp:
    fp.write(stuff)
    fp.write(more stuff)

if os.path.exists(filename):
    os.unlink(filename)
os.rename(tmpname, filename)

我对此不满意有几个原因:

  • 如果发生异常,它不会正确清理
  • 它忽略了并发问题
  • 它不可重复使用(我需要在程序的不同位置使用它)

任何建议如何改进我的代码?有图书馆可以帮助我吗?

4

4 回答 4

11

你可以使用 Python 的tempfile模块给你一个临时文件名。它可以以线程安全的方式创建一个临时文件,而不是使用time.time()如果同时在多个线程中使用可能返回相同名称的临时文件。

正如对您问题的评论中所建议的那样,这可以与上下文管理器的使用相结合。tempfile.py通过查看 Python源代码,您可以了解如何实现您想要做的事情。

下面的代码片段可能会做你想做的事。它使用从tempfile.

  • 临时文件的创建是线程安全的。
  • 成功完成后重命名文件是原子的,至少在 Linux 上是这样。和之间没有单独的检查os.path.exists()os.rename()这可能会引入竞争条件。对于 Linux 上的原子重命名,源和目标必须位于同一文件系统上,这就是此代码将临时文件与目标文件放在同一目录中的原因。
  • 对于大多数用途,RenamedTemporaryFile该类的行为应该像 a NamedTemporaryFile,除非使用上下文管理器关闭它,文件被重命名。

样本:

import tempfile
import os

class RenamedTemporaryFile(object):
    """
    A temporary file object which will be renamed to the specified
    path on exit.
    """
    def __init__(self, final_path, **kwargs):
        tmpfile_dir = kwargs.pop('dir', None)

        # Put temporary file in the same directory as the location for the
        # final file so that an atomic move into place can occur.

        if tmpfile_dir is None:
            tmpfile_dir = os.path.dirname(final_path)

        self.tmpfile = tempfile.NamedTemporaryFile(dir=tmpfile_dir, **kwargs)
        self.final_path = final_path

    def __getattr__(self, attr):
        """
        Delegate attribute access to the underlying temporary file object.
        """
        return getattr(self.tmpfile, attr)

    def __enter__(self):
        self.tmpfile.__enter__()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.tmpfile.delete = False
            result = self.tmpfile.__exit__(exc_type, exc_val, exc_tb)
            os.rename(self.tmpfile.name, self.final_path)
        else:
            result = self.tmpfile.__exit__(exc_type, exc_val, exc_tb)

        return result

然后你可以像这样使用它:

with RenamedTemporaryFile('whatever') as f:
    f.write('stuff')

在写入过程中,内容转到一个临时文件,退出时文件被重命名。这段代码可能需要一些调整,但总体思路应该可以帮助您入门。

于 2012-08-17T14:40:14.153 回答
5

可靠地将全部或全部写入文件:

import os
from contextlib import contextmanager
from tempfile   import NamedTemporaryFile

if not hasattr(os, 'replace'):
    os.replace = os.rename #NOTE: it won't work for existing files on Windows

@contextmanager
def FaultTolerantFile(name):
    dirpath, filename = os.path.split(name)
    # use the same dir for os.rename() to work
    with NamedTemporaryFile(dir=dirpath, prefix=filename, suffix='.tmp') as f:
        yield f
        f.flush()   # libc -> OS
        os.fsync(f) # OS -> disc (note: on OSX it is not enough)
        f.delete = False # don't delete tmp file if `replace()` fails
        f.close()
        os.replace(f.name, name)

另请参阅没有 fsync() 的 rename() 是否安全?@Mihai Stan提到)

用法

with FaultTolerantFile('very_important_file') as file:
    file.write('either all ')
    file.write('or nothing is written')

要实现缺失os.replace(),您可以在 Windows 上调用MoveFileExW(src, dst, MOVEFILE_REPLACE_EXISTING)(通过 win32file 或 ctypes 模块)。

如果有多个线程,您可以queue.put(data)从不同的线程调用并在专用线程中写入文件:

 for data in iter(queue.get, None):
     file.write(data)

queue.put(None)打破循环。

作为替代方案,您可以使用锁(线程、多处理、文件锁)来同步访问:

def write(self, data):
    with self.lock:
        self.file.write(data)
于 2012-08-17T20:34:03.243 回答
2

with构造对于在退出时进行清理很有用,但对于您想要的提交/回滚系统却没有。可以为此使用try /except/else块。

您还应该使用标准方法来创建临时文件名,例如使用tempfile模块。

并记得在重命名之前进行 fsync

以下是修改后的完整代码:

import time, os, tempfile

def begin_file(filepath):
    (filedir, filename) = os.path.split(filepath)
    tmpfilepath = tempfile.mktemp(prefix=filename+'_', dir=filedir)
    return open(os.path.join(filedir, tmpfilepath), 'wb') 

def commit_file(f):
    tmppath = f.name
    (filedir, tmpname) = os.path.split(tmppath)
    origpath = os.path.join(filedir,tmpname.split('_')[0])

    os.fsync(f.fileno())
    f.close()

    if os.path.exists(origpath):
        os.unlink(origpath)
    os.rename(tmppath, origpath)

def rollback_file(f):
    tmppath = f.name
    f.close()
    os.unlink(tmppath)


fp = begin_file('whatever')
try:
    fp.write('stuff')
except:
    rollback_file(fp)
    raise
else:
    commit_file(fp)
于 2012-08-17T14:47:16.840 回答
1

您可以在写入文件时使用lockfile 模块锁定文件。任何后续锁定它的尝试都会阻塞,直到前一个进程/线程的锁被释放。

from lockfile import FileLock
with FileLock(filename):
    #open your file here....

这样,您可以避免并发问题,并且在发生异常时不必清理任何剩余文件。

于 2012-08-17T10:31:37.460 回答