Rediriger stdout vers un fichier en Python?

315

Comment rediriger stdout vers un fichier arbitraire en Python?

Lorsqu'un script Python de longue durée (par exemple, une application Web) est démarré à partir de la session ssh et rétrogradé, et que la session ssh est fermée, l'application déclenche IOError et échoue au moment où elle essaie d'écrire sur stdout. J'avais besoin de trouver un moyen de faire sortir l'application et les modules dans un fichier plutôt que stdout pour éviter les échecs dus à IOError. Actuellement, j'utilise nohup pour rediriger la sortie vers un fichier, et cela fait le travail, 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 à sortir vers le terminal (ou peut-être que la sys.stdout = ...ligne ne s'est pas déclenchée du tout). Je sais que cela devrait fonctionner à partir de scripts plus simples sur lesquels j'ai testé, mais je n'ai pas encore eu le temps de tester sur une application Web.

Eric Leschinski
la source
8
Ce n'est pas vraiment une chose python, c'est une fonction shell. Exécutez simplement votre script commescript.p > file
Falmarri
Je résous actuellement le problème en utilisant nohup, mais je pensais qu'il pourrait y avoir quelque chose de plus intelligent ...
1
@foxbunny: nohup? Pourquoi tout simplement someprocess | python script.py? Pourquoi impliquer nohup?
S.Lott
3
Réécrivez les printinstructions pour appliquer le loggingmodule à partir de stdlib. Ensuite, vous pouvez rediriger la sortie partout, contrôler la quantité de sortie souhaitée, etc. Dans la plupart des cas, le code de production ne devrait pas printmais log.
erikbwork
2
Une meilleure solution à ce problème est peut-être la commande screen, qui enregistrera votre session bash et vous permettra d'y accéder à partir de différentes exécutions.
Ryan Amos

Réponses:

404

Si vous souhaitez effectuer la redirection dans le script Python, la définition sys.stdoutd'un objet fichier fait l'affaire:

import sys
sys.stdout = open('file', 'w')
print('test')

Une méthode beaucoup plus courante consiste à utiliser la redirection de shell lors de l'exécution (idem sous Windows et Linux):

$ python foo.py > file
moinudin
la source
3
Si vous êtes sur Windows, faites attention au bogue Windows - Impossible de rediriger la sortie lorsque j'exécute un script Python sur Windows en utilisant uniquement le nom du script
Piotr Dobrogost
7
Cela ne fonctionne pas avec from sys import stdout, peut-être parce qu'il 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 en functionThatPrints()utilisant des printinstructions normales .
mgold
41
Il est préférable de garder une copie locale, stdout = sys.stdoutafin que vous puissiez le remettre lorsque vous avez terminé, sys.stdout = stdout. De cette façon, si vous êtes appelé à partir d'une fonction qui l'utilise print, ne les gâchez pas.
mgold
4
@Jan: buffering=0désactive la mise en mémoire tampon (cela peut affecter négativement les performances (10 à 100 fois)). buffering=1active la mise en mémoire tampon des lignes afin que vous puissiez l'utiliser tail -fpour une sortie orientée ligne.
jfs
41
@mgold ou vous pouvez utiliser sys.stdout = sys.__stdout__pour le récupérer.
clemtoy
176

Il y a une contextlib.redirect_stdout()fonction dans Python 3.4:

from contextlib import redirect_stdout

with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        print('it now prints to `help.text`')

C'est similaire à:

import sys
from contextlib import contextmanager

@contextmanager
def redirect_stdout(new_target):
    old_target, sys.stdout = sys.stdout, new_target # replace sys.stdout
    try:
        yield new_target # run some code with the replaced stdout
    finally:
        sys.stdout = old_target # restore to the previous value

qui peut être utilisé sur les versions antérieures de Python. Cette dernière version n'est pas réutilisable . Il peut être fait si vous le souhaitez.

Il ne redirige pas la sortie standard 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('redirected to a file')
    os.write(stdout_fd, b'not redirected')
    os.system('echo this also is not redirected')

b'not redirected'et 'echo this also is not redirected'ne sont pas redirigés vers le output.txtfichier.

Pour rediriger au niveau du descripteur de fichier, os.dup2()pourrait ê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("Expected a file (`.fileno()`) or a file descriptor")
    return fd

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

    stdout_fd = fileno(stdout)
    # copy stdout_fd before it is overwritten
    #NOTE: `copied` is inheritable on Windows when duplicating a standard stream
    with os.fdopen(os.dup(stdout_fd), 'wb') as copied: 
        stdout.flush()  # flush library buffers that dup2 knows nothing about
        try:
            os.dup2(fileno(to), stdout_fd)  # $ exec >&to
        except ValueError:  # filename
            with open(to, 'wb') as to_file:
                os.dup2(to_file.fileno(), stdout_fd)  # $ exec > to
        try:
            yield stdout # allow code to be run with the redirected stdout
        finally:
            # restore stdout to its previous value
            #NOTE: dup2 makes stdout_fd inheritable unconditionally
            stdout.flush()
            os.dup2(copied.fileno(), stdout_fd)  # $ exec >&copied

Le même exemple fonctionne maintenant si stdout_redirected()est utilisé à la place de redirect_stdout():

import os
import sys

stdout_fd = sys.stdout.fileno()
with open('output.txt', 'w') as f, stdout_redirected(f):
    print('redirected to a file')
    os.write(stdout_fd, b'it is redirected now\n')
    os.system('echo this is also redirected')
print('this is goes back to stdout')

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

Remarque: stdout.flush()ne vide pas les tampons stdio C sur Python 3 où les E / S sont implémentées directement sur read()/ les write()appels système. Pour vider tous les flux de sortie stdio C ouverts, vous pouvez appeler libc.fflush(None)explicitement si une extension C utilise des E / S basées 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 # unsupported

Vous pouvez utiliser un stdoutparamètre pour rediriger d'autres flux, non seulement sys.stdoutpar exemple, pour fusionner sys.stderret 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('this is printed on stdout')
     print('this is also printed on stdout', file=sys.stderr)

Remarque: stdout_redirected()mélange les E / S tamponnées ( sys.stdoutgénéralement) et les E / S non tamponnées (opérations sur les descripteurs de fichiers directement). Attention, il pourrait y avoir des problèmes de mise en mémoire tampon .

Pour répondre, votre modification: vous pouvez utiliser python-daemonpour démonifier votre script et utiliser le loggingmodule (comme @ erikb85 l'a suggéré ) au lieu d' printinstructions et simplement rediriger stdout pour votre script Python de longue durée que vous exécutez nohupmaintenant.

jfs
la source
3
stdout_redirectedest utile. Sachez que cela ne fonctionne pas dans les doctests, car le SpoofOutgestionnaire spécial que doctest utilise pour remplacer sys.stdoutn'a pas d' filenoattribut.
Chris Johnson
@ChrisJohnson: S'il ne se lève pas, ValueError("Expected a file (`.fileno()`) or a file descriptor")c'est un bug. Êtes-vous sûr que cela ne le soulève pas?
jfs
Cela soulève cette erreur, ce qui la rend inutilisable dans un doctorat. Pour utiliser votre fonction au sein d'un doctorat, il semble nécessaire de spécifier doctest.sys.__stdout__où nous utiliserions normalement sys.stdout. Ce n'est pas un problème avec votre fonction, juste un accommodement requis pour doctest car il remplace stdout par un objet qui n'a pas tous les attributs d'un vrai fichier.
Chris Johnson
stdout_redirected()a un stdoutparamètre, vous pouvez le définir sys.__stdout__si vous souhaitez rediriger la sortie standard de python (qui devrait avoir une valeur valide .fileno()dans la plupart des cas). Cela ne fait rien pour le courant sys.stdouts'ils sont différents. Ne pas utiliser doctest.sys; il est disponible par accident.
jfs
Cela fonctionne vraiment bien, c.-à-d. Redirige stdout et stderr vers un fd: with stdout_redirected(to=fd): with merged_stderr_stdout(): print('...'); print('...', file=sys.stderr)
neok
91

vous pouvez essayer cela 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 !" # this is should be saved in yourlogfilename.txt
Yuda Prawira
la source
Des suggestions de tuyauterie vers loggerou syslog?
dsummersl
Si vous souhaitez modifier un fichier, cela n'est pas très utile. Quoi qu'il en soit +1 pour le bon tour
aIKid
10
Cela aura des conséquences pour le code qui suppose que sys.stdout est un objet fichier à part entière avec des méthodes telles que fileno () (qui inclut du code dans la bibliothèque standard python). J'ajouterais une méthode __getattr __ (self, attr) à celle qui reporte la recherche d'attribut à self.terminal. def __getattr__(self, attr): return getattr(self.terminal, attr)
peabody
4
Vous devez également ajouter une def flush(self):méthode à la classe Logger.
loretoparisi
1
@loretoparisi mais qu'est-ce qui se passe réellement dans la méthode que vous créez?
elkshadow5
28

Les autres réponses ne couvraient pas le cas où vous souhaitez que les processus fourchus partagent votre nouvelle sortie standard.

Pour faire ça:

from os import open, close, dup, O_WRONLY

old = dup(1)
close(1)
open("file", O_WRONLY) # should open on 1

..... do stuff and then restore

close(1)
dup(old) # should dup to 1
close(old) # get rid of left overs
Yam Marcovic
la source
3
il faut remplacer l'attribut 'w' par, os.O_WRONLY | os.O_CREATE ... ne peut pas envoyer de chaînes dans les commandes "os"!
Ch'marr
3
Insérez un sys.stdout.flush()avant l' close(1)instruction pour vous assurer que le 'file'fichier de redirection obtient la sortie. Vous pouvez également utiliser un tempfile.mkstemp()fichier à la place de 'file'. Et soyez prudent, vous n'avez pas d'autres threads en cours d'exécution qui peuvent voler le premier handle de fichier du système d'exploitation après le os.close(1)mais avant que le 'file'soit ouvert pour utiliser le handle.
Alex Robinson
2
son os.O_WRONLY | os.O_CREAT ... il n'y a pas de E là-bas.
Jeff Sheffield
@ Ch'marr C'est O_CREAT, pas O_CREATE.
quant_dev
28

Cité du PEP 343 - La déclaration "with" (déclaration d'importation ajoutée):

Rediriger temporairement la sortie standard:

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 "Hello world"

Ce n'est pas thread-safe, bien sûr, mais ne fait pas cette même danse manuellement. Dans les programmes à thread unique (par exemple dans les scripts), c'est une façon populaire de faire les choses.

Gerli
la source
1
+1. Note: il ne fonctionne pas pour les sous - processus , par exemple, os.system('echo not redirected'). Ma réponse montre comment rediriger une telle sortie
jfs
à partir de Python 3.4 , il est redirect_stdoutencontextlib
Walter Tross
12
import sys
sys.stdout = open('stdout.txt', 'w')
Cat Plus Plus
la source
3

Voici une variante de la réponse de Yuda Prawira :

  • implémenter flush()et tous les attributs de fichier
  • l'écrire en tant que gestionnaire de contexte
  • capturer stderraussi

.

import contextlib, sys

@contextlib.contextmanager
def log_print(file):
    # capture all outputs to a log file while still printing it
    class Logger:
        def __init__(self, file):
            self.terminal = sys.stdout
            self.log = file

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

        def __getattr__(self, attr):
            return getattr(self.terminal, attr)

    logger = Logger(file)

    _stdout = sys.stdout
    _stderr = sys.stderr
    sys.stdout = logger
    sys.stderr = logger
    try:
        yield logger.log
    finally:
        sys.stdout = _stdout
        sys.stderr = _stderr


with log_print(open('mylogfile.log', 'w')):
    print('hello world')
    print('hello world on stderr', file=sys.stderr)

# you can capture the output to a string with:
# with log_print(io.StringIO()) as log:
#   ....
#   print('[captured output]', log.getvalue())
damio
la source
2

Sur la base de cette réponse: https://stackoverflow.com/a/5916874/1060344 , voici une autre façon dont j'ai compris que j'utilise dans l'un de mes projets. Pour tout ce que vous remplacez sys.stderrou sys.stdoutavec, vous devez vous assurer que le remplacement est compatible avec l' fileinterface, surtout si c'est quelque chose que vous faites parce que stderr / stdout sont utilisés dans une autre bibliothèque qui n'est pas sous votre contrôle. Cette bibliothèque peut utiliser d'autres méthodes d'objet fichier.

Découvrez de cette façon où je laisse toujours tout faire stderr / stdout (ou n'importe quel fichier d'ailleurs) et envoie également le message à un fichier journal à l'aide de la fonction de journalisation de Python (mais vous pouvez vraiment faire quoi que ce soit avec cela):

class FileToLogInterface(file):
    '''
    Interface to make sure that everytime anything is written to stderr, it is
    also forwarded to a file.
    '''

    def __init__(self, *args, **kwargs):
        if 'cfg' not in kwargs:
            raise TypeError('argument cfg is required.')
        else:
            if not isinstance(kwargs['cfg'], config.Config):
                raise TypeError(
                    'argument cfg should be a valid '
                    'PostSegmentation configuration object i.e. '
                    'postsegmentation.config.Config')
        self._cfg = kwargs['cfg']
        kwargs.pop('cfg')

        self._logger = logging.getlogger('access_log')

        super(FileToLogInterface, self).__init__(*args, **kwargs)

    def write(self, msg):
        super(FileToLogInterface, self).write(msg)
        self._logger.info(msg)
vaidik
la source
2

Vous avez besoin d'un multiplexeur de terminal comme tmux ou écran GNU

Je suis surpris qu'un petit commentaire de Ryan Amos à la question d'origine soit la seule mention d'une solution de loin préférable à toutes les autres proposées, peu importe à quel point la supercherie en python peut être intelligente et combien de votes positifs ils ont reçus. Suite au commentaire de Ryan, tmux est une belle alternative à l'écran GNU.

Mais le principe est le même: si jamais vous vous retrouvez à vouloir quitter un travail terminal pendant que vous vous déconnectez, rendez-vous au café pour un sandwich, passez aux toilettes, rentrez chez vous (etc.), puis plus tard, reconnectez-vous à votre session de terminal depuis n'importe où ou depuis n'importe quel ordinateur comme si vous n'aviez jamais été absent, les multiplexeurs de terminaux sont la réponse. Considérez-les comme VNC ou bureau à distance pour les sessions de terminal. Tout le reste est une solution de contournement. En bonus, lorsque le patron et / ou le partenaire arrive et que vous ctrl-w / cmd-w par inadvertance votre fenêtre de terminal au lieu de la fenêtre de votre navigateur avec son contenu douteux, vous n'aurez pas perdu les 18 dernières heures de traitement !

duncan
la source
4
alors que c'est une bonne réponse pour la partie de la question apparue après l'édition; il ne répond pas à la question dans le titre (la plupart des gens viennent de google pour le titre)
jfs
0

Les programmes écrits dans d'autres langages (par exemple C) doivent faire une magie spéciale (appelée double forking) pour se détacher expressément du terminal (et pour empêcher les processus zombies). Donc, je pense que la meilleure solution est de les émuler.

Un plus de la réexécution de votre programme est que vous pouvez choisir des redirections sur la ligne de commande, par exemple /usr/bin/python mycoolscript.py 2>&1 1>/dev/null

Voir cet article pour plus d'informations: Quelle est la raison d'effectuer un double fork lors de la création d'un démon?

jpaugh
la source
Eh ... je ne peux pas dire que je suis un fan des processus gérant leur propre double fourche. C'est un idiome si courant et si facile à coder si vous n'y faites pas attention. Mieux vaut écrire votre processus pour qu'il s'exécute au premier plan et utiliser un gestionnaire de tâches en arrière-plan du système ( systemd, upstart) ou un autre utilitaire ( daemon(1)) pour gérer le passe-partout de la fourche.
Lucretiel