web-dev-qa-db-fra.com

écriture atomique dans un fichier avec Python

J'utilise Python pour écrire des morceaux de texte dans des fichiers en une seule opération:

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

Si le script est interrompu afin qu'une écriture de fichier ne se termine pas, je veux n'avoir aucun fichier plutôt qu'un fichier partiellement complet. Cela peut-il être fait?

56
hoju

Écrivez les données dans un fichier temporaire et lorsque les données ont été écrites avec succès, renommez le fichier dans le fichier de destination correct, par exemple

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)

Selon doc http://docs.python.org/library/os.html#os.rename

En cas de succès, le changement de nom sera une opération atomique (il s'agit d'une exigence POSIX). Sous Windows, si dst existe déjà, OSError sera déclenché même s'il s'agit d'un fichier; il n'y a aucun moyen d'implémenter un renommage atomique lorsque dst nomme un fichier existant

aussi

L'opération peut échouer sur certaines versions d'Unix si src et dst sont sur des systèmes de fichiers différents.

Remarque:

  • Il peut ne pas s'agir d'une opération atomique si les emplacements src et dest ne sont pas sur le même système de fichiers

  • os.fsync l'étape peut être ignorée si les performances/la réactivité sont plus importantes que l'intégrité des données dans des cas comme une panne de courant, une panne du système, etc.

88
Anurag Uniyal

Un simple extrait qui implémente l'écriture atomique en utilisant Python tempfile.

with open_atomic('test.txt', 'w') as f:
    f.write("huzza")

ou même lire et écrire vers et depuis le même fichier:

with open('test.txt', 'r') as src:
    with open_atomic('test.txt', 'w') as dst:
        for line in src:
            dst.write(line)

en utilisant deux gestionnaires de contexte simples

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)
17
Nils Werner

Il existe un simple assistant AtomicFile: https://github.com/sashka/atomicfile

6
Alexander Saltanov

J'utilise ce code pour remplacer/écrire atomiquement un fichier:

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

Usage:

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

C'est basé sur cette recette .

5
Jakub Jirutka

Puisqu'il est très facile de gâcher les détails, je recommande d'utiliser une petite bibliothèque pour cela. L'avantage d'une bibliothèque est qu'elle prend en charge tous ces détails, et est révisée et améliorée par une communauté.

L'une de ces bibliothèques est python-atomicwrites par Untitaker qui a même un support Windows approprié:

Du 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.
5
vog