440 votes

Rediriger stdout vers un fichier en Python?

Comment rediriger stdout vers un fichier arbitraire en Python?

Lorsqu'un script Python long en cours d'exécution (par exemple, une application web) est lancé depuis une session ssh et mis en arrière-plan, et que la session ssh est fermée, l'application va lever une IOError et échouer dès qu'elle tente d'écrire dans stdout. J'ai dû trouver un moyen de faire en sorte que l'application et les modules produisent une sortie vers un fichier plutôt que vers stdout pour éviter l'échec en raison de IOError. Actuellement, j'utilise nohup pour rediriger la sortie vers un fichier, et cela fonctionne bien, mais je me demandais s'il y avait un moyen de le faire sans utiliser nohup, par curiosité.

J'ai déjà essayé sys.stdout = open('somefile', 'w'), mais cela ne semble pas empêcher certains modules externes de continuer à produire une sortie sur le terminal (ou peut-être que la ligne sys.stdout = ... n'a pas été exécutée du tout). Je sais que cela devrait fonctionner avec des scripts plus simples que j'ai testés, mais je n'ai pas encore eu le temps de le tester sur une application web.

12 votes

Ce n'est pas vraiment une chose en python, c'est une fonction shell. Il suffit d'exécuter votre script comme script.p > file

0 votes

Je résous actuellement le problème en utilisant nohup, mais je pensais qu'il pourrait y avoir quelque chose de plus intelligent...

1 votes

@foxbunny: pourquoi simplement someprocess | python script.py? Pourquoi impliquer nohup?

571voto

marcog Points 39356

Si vous souhaitez effectuer la redirection à l'intérieur du script Python, le fait de définir sys.stdout sur un objet fichier fait l'affaire :

# pour python3
import sys
with open('file', 'w') as sys.stdout:
    print('test')

Une méthode beaucoup plus courante est d'utiliser la redirection de shell lors de l'exécution (identique sur Windows et Linux) :

$ python3 foo.py > file

5 votes

9 votes

Ça ne fonctionne pas avec from sys import stdout, peut-être parce que cela crée une copie locale. Vous pouvez également l'utiliser avec with, par exemple with open('file', 'w') as sys.stdout: functionThatPrints(). Vous pouvez maintenant implémenter functionThatPrints() en utilisant des déclarations print normales.

61 votes

Il est préférable de conserver une copie locale, stdout = sys.stdout afin de la remettre en place une fois que vous avez terminé, sys.stdout = stdout. De cette façon, si vous êtes appelé depuis une fonction qui utilise print, vous ne les perturbez pas.

280voto

J.F. Sebastian Points 102961

Il existe la fonction contextlib.redirect_stdout() en Python 3.4+:

from contextlib import redirect_stdout

with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        print('il imprime maintenant dans `help.text`')

Il est similaire à :

import sys
from contextlib import contextmanager

@contextmanager
def redirect_stdout(new_target):
    old_target, sys.stdout = sys.stdout, new_target # Remplacer sys.stdout
    try:
        yield new_target # Exécuter du code avec le stdout remplacé
    finally:
        sys.stdout = old_target # Restaurer à la valeur précédente

qui peut être utilisé sur des versions antérieures de Python. La version ultérieure n'est pas réutilisable. Elle peut être rendue réutilisable si désiré.

Cela ne redirige pas le stdout au niveau des descripteurs de fichiers par exemple :

import os
from contextlib import redirect_stdout

stdout_fd = sys.stdout.fileno()
with open('output.txt', 'w') as f, redirect_stdout(f):
    print('redirigé vers un fichier')
    os.write(stdout_fd, b'n'est pas redirigé')
    os.system('echo ceci non plus n'est pas redirigé')

b'n'est pas redirigé' et 'echo ceci non plus n'est pas redirigé' ne sont pas redirigés vers le fichier output.txt.

Pour rediriger au niveau du descripteur de fichier, os.dup2() peut être utilisé :

import os
import sys
from contextlib import contextmanager

def fileno(file_or_fd):
    fd = getattr(file_or_fd, 'fileno', lambda: file_or_fd)()
    if not isinstance(fd, int):
        raise ValueError("Un fichier (`.fileno()`) ou un descripteur de fichier était attendu")
    return fd

@contextmanager
def stdout_redirected(to=os.devnull, stdout=None):
    if stdout is None:
       stdout = sys.stdout

    stdout_fd = fileno(stdout)
    # Copier stdout_fd avant qu'il soit écrasé
    #NOTE: `copied` est héritable sur Windows lors de la duplication d'un flux standard
    with os.fdopen(os.dup(stdout_fd), 'wb') as copied: 
        stdout.flush()  # Vider les buffers de librairie que dup2 ne connait pas
        try:
            os.dup2(fileno(to), stdout_fd)  # $ exec >&to
        except ValueError:  # nom de fichier
            with open(to, 'wb') as to_file:
                os.dup2(to_file.fileno(), stdout_fd)  # $ exec > to
        try:
            yield stdout # permettre l'exécution de code avec le stdout redirigé
        finally:
            # restaurer stdout à sa valeur précédente
            #NOTE: dup2 rend stdout_fd héritable inconditionnellement
            stdout.flush()
            os.dup2(copied.fileno(), stdout_fd)  # $ exec >&copied

Le même exemple fonctionne maintenant si stdout_redirected() est utilisé au lieu de redirect_stdout():

import os
import sys

stdout_fd = sys.stdout.fileno()
with open('output.txt', 'w') as f, stdout_redirected(f):
    print('redirigé vers un fichier')
    os.write(stdout_fd, b'c'est maintenant redirigé\n')
    os.system('echo ceci est aussi redirigé')
print('cela revient vers stdout')

La sortie précédemment imprimée sur stdout va maintenant dans output.txt tant que le gestionnaire de contexte stdout_redirected() est actif.

Note: stdout.flush() ne vide pas les buffers C stdio sur Python 3 où l'I/O est implémentée directement sur les appels système read()/write(). Pour vider tous les flux de sortie C stdio ouverts, vous pourriez appeler libc.fflush(None) explicitement si une extension C utilise une I/O basée sur stdio :

try:
    import ctypes
    from ctypes.util import find_library
except ImportError:
    libc = None
else:
    try:
        libc = ctypes.cdll.msvcrt # Windows
    except OSError:
        libc = ctypes.cdll.LoadLibrary(find_library('c'))

def flush(stream):
    try:
        libc.fflush(None)
        stream.flush()
    except (AttributeError, ValueError, IOError):
        pass # non supporté

Vous pourriez utiliser le paramètre stdout pour rediriger d'autres flux, pas seulement sys.stdout par ex., pour fusionner sys.stderr et sys.stdout:

def merged_stderr_stdout():  # $ exec 2>&1
    return stdout_redirected(to=sys.stdout, stdout=sys.stderr)

Exemple :

from __future__ import print_function
import sys

with merged_stderr_stdout():
     print('ceci est imprimé sur stdout')
     print('ceci est aussi imprimé sur stdout', file=sys.stderr)

Note: stdout_redirected() mélange l'I/O bufferisée (sys.stdout habituellement) et l'I/O non bufferisée (opérations sur les descripteurs de fichiers directement). Attention, il pourrait y avoir des problèmes de buffering .

Pour répondre, à votre édition : vous pourriez utiliser python-daemon pour mettre en service votre script et utiliser le module logging (comme @erikb85 a suggéré) au lieu des déclarations print et simplement rediriger stdout pour votre script Python long en cours d'exécution que vous exécutez maintenant en utilisant nohup.

3 votes

stdout_redirected est utile. Sachez que cela ne fonctionne pas à l'intérieur des doctests, car le gestionnaire spécial SpoofOut que doctest utilise pour remplacer sys.stdout n'a pas d'attribut fileno.

0 votes

@ChrisJohnson: Si cela ne génère pas ValueError("Expected a file (`.fileno()`) or a file descriptor") alors c'est un bug. Êtes-vous sûr que cela ne le génère pas?

0 votes

Cela soulève cette erreur, ce qui rend impossible son utilisation dans un doctest. Pour utiliser votre fonction dans un doctest, il semble nécessaire de spécifier doctest.sys.__stdout__ là où nous utiliserions normalement sys.stdout. Ce n'est pas un problème avec votre fonction, juste une adaptation nécessaire pour doctest, puisqu'il remplace stdout par un objet qui n'a pas tous les attributs d'un vrai fichier.

103voto

Gunslinger_ Points 2293

Vous pouvez essayer ceci beaucoup mieux

import sys

class Logger(object):
    def __init__(self, filename="Default.log"):
        self.terminal = sys.stdout
        self.log = open(filename, "a")

    def write(self, message):
        self.terminal.write(message)
        self.log.write(message)

sys.stdout = Logger("yourlogfilename.txt")
print "Hello world !" # cela devrait être enregistré dans votrelogfilename.txt

0 votes

Avez-vous des suggestions pour rediriger vers logger ou syslog ?

0 votes

Si vous souhaitez modifier un fichier, ce n'est pas très utile. Quoi qu'il en soit, +1 pour la jolie astuce

14 votes

Cela aura des conséquences pour le code qui suppose que sys.stdout est un objet de fichier complet avec des méthodes telles que fileno() (ce qui inclut du code dans la bibliothèque standard de Python). Je voudrais ajouter une méthode __getattr__(self,attr) à cela qui diffère la recherche d'attribut à self.terminal. def __getattr__(self,attr): return getattr(self.terminal,attr)

33voto

Yam Marcovic Points 1566

Les autres réponses ne couvraient pas le cas où vous souhaitez que des processus forkés partagent votre nouveau stdout.

Pour ce faire :

from os import open, close, dup, O_WRONLY

old = dup(1)
close(1)
open("file", O_WRONLY) # devrait ouvrir sur 1

..... faire quelque chose puis restaurer

close(1)
dup(old) # devrait dupliquer vers 1
close(old) # se débarrasser des restes

3 votes

Il faut remplacer l'attribut 'w' par os.O_WRONLY|os.O_CREATE ... On ne peut pas envoyer des chaînes de caractères dans les commandes "os" !

3 votes

Insérez un sys.stdout.flush() avant l'instruction close(1) pour vous assurer que le fichier redirigé 'file' reçoit la sortie. Vous pouvez également utiliser un fichier tempfile.mkstemp() à la place de 'file'. Et veillez à ce qu'aucun autre thread ne soit en cours d'exécution pour voler la première poignée de fichier os après le os.close(1) mais avant que le 'file' ne soit ouvert pour utiliser la poignée.

2 votes

Sa os.O_WRONLY | os.O_CREAT ... il n'y a pas de E là-dedans.

31voto

Gerli Points 161

Cité de PEP 343 -- L'instruction "with" (ajout d'une déclaration d'importation):

Rediriger temporairement stdout:

import sys
from contextlib import contextmanager
@contextmanager
def stdout_redirected(new_stdout):
    save_stdout = sys.stdout
    sys.stdout = new_stdout
    try:
        yield None
    finally:
        sys.stdout = save_stdout

Utilisé comme suit:

with open(filename, "w") as f:
    with stdout_redirected(f):
        print "Bonjour le monde"

Cela n'est pas sûr en cas de thread, bien sûr, mais faire manuellement cette même danse ne l'est pas non plus. Dans les programmes à un seul thread (par exemple dans des scripts), c'est une façon populaire de faire les choses.

2 votes

+1. Remarque : cela ne fonctionne pas pour les sous-processus, par exemple, os.system('echo not redirected'). Ma réponse montre comment rediriger une telle sortie

1 votes

À partir de Python 3.4, il y a redirect_stdout in contextlib

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X