Атомная запись в файл с помощью Python

Я использую Python для записи фрагментов текста в файлы за одну операцию:

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

Если script прерван, поэтому запись файла не завершена. Я хочу, чтобы у меня не было файла, а не частично полного файла. Это можно сделать?

Ответ 1

Запись данных во временный файл и когда данные были успешно записаны, переименуйте файл в правильный файл назначения, например

f = open(tmpFile, 'w')
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()) 
f.close()

os.rename(tmpFile, myFile)

В соответствии с документом http://docs.python.org/library/os.html#os.rename

В случае успеха переименование будет атомной операцией (это POSIX). В Windows, если dst уже существует, OSError будет поднят даже если это файл; не может быть способ реализовать атомное переименование, когда dst называет существующий файл

и

Операция может завершиться неудачей при некоторых вариантах Unix, если src и dst находятся в разных файловых системах.

Примечание:

  • Это может быть не атомная операция, если местоположения src и dest не находятся в одной файловой системе.

  • os.fsync шаг может быть пропущен, если производительность/отзывчивость важнее целостности данных в таких случаях, как сбой питания, сбой системы и т.д.

Ответ 2

Простой фрагмент, который реализует атомарную запись с использованием 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:
            f.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.get('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)

Ответ 4

Im, используя этот код для автоматической замены/записи файла:

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")

На основе этот рецепт.

Ответ 5

Так как очень легко испортить детали, я рекомендую использовать для этого крошечную библиотеку. Преимущество библиотеки заключается в том, что она заботится обо всех этих подробных деталях и просматривает и улучшает сообщество.

Одна такая библиотека python-atomicwrites от unitaker, которая даже имеет правильную поддержку Windows:

Из 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.