¿Redirigir stdout a un archivo en Python?

315

¿Cómo redirijo stdout a un archivo arbitrario en Python?

Cuando se inicia un script de Python de larga duración (por ejemplo, una aplicación web) desde la sesión ssh y retrocede, y la sesión ssh se cierra, la aplicación generará IOError y fallará en el momento en que intente escribir en stdout. Necesitaba encontrar una manera de hacer que la aplicación y los módulos salieran a un archivo en lugar de stdout para evitar fallas debido a IOError. Actualmente, uso nohup para redirigir la salida a un archivo, y eso hace el trabajo, pero me preguntaba si había una manera de hacerlo sin usar nohup, por curiosidad.

Ya lo he intentado sys.stdout = open('somefile', 'w'), pero esto no parece evitar que algunos módulos externos sigan saliendo al terminal (o tal vez la sys.stdout = ...línea no se disparó en absoluto). Sé que debería funcionar a partir de scripts más simples que he probado, pero todavía no tuve tiempo para probar en una aplicación web todavía.

Eric Leschinski
fuente
8
Eso no es realmente una cosa de Python, es una función de shell. Simplemente ejecute su script comoscript.p > file
Falmarri el
Actualmente resolver el problema con nohup, pero pensé que podría haber algo más inteligente ...
1
@foxbunny: ¿no? ¿Por qué simplemente someprocess | python script.py? ¿Por qué involucrarse nohup?
S.Lott
3
Reescribe las printdeclaraciones para aplicar el loggingmódulo desde stdlib. A continuación, puede redirigir la salida de todo el mundo, tener control sobre la cantidad de salida que desea etc. En la mayoría de casos el código de producción no debería printpero log.
erikbwork
2
Quizás una mejor solución para este problema es el comando de pantalla, que guardará su sesión bash y le permitirá acceder desde diferentes ejecuciones.
Ryan Amos

Respuestas:

404

Si desea hacer la redirección dentro de la secuencia de comandos de Python, la configuración sys.stdoutde un objeto de archivo hace el truco:

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

Un método mucho más común es utilizar la redirección de shell al ejecutar (lo mismo en Windows y Linux):

$ python foo.py > file
moinudin
fuente
3
Si está en Windows, tenga cuidado con el error de Windows: no se puede redirigir la salida cuando ejecuto el script Python en Windows usando solo el nombre del script
Piotr Dobrogost el
77
No funciona con from sys import stdout, quizás porque crea una copia local. También puedes usarlo con with, por ejemplo with open('file', 'w') as sys.stdout: functionThatPrints(). Ahora puede implementar functionThatPrints()usando printdeclaraciones normales .
mgold
41
Es mejor mantener una copia local, stdout = sys.stdoutpara que pueda volver a guardarla cuando haya terminado sys.stdout = stdout. De esa manera, si te llaman desde una función que usa print, no los arruinas.
mgold
44
@ Jan: buffering=0deshabilita el almacenamiento en búfer (puede afectar negativamente el rendimiento (10-100 veces)). buffering=1habilita el almacenamiento en línea para que pueda usarlo tail -fpara una salida orientada a línea.
jfs
41
@mgold o puede usar sys.stdout = sys.__stdout__para recuperarlo.
clemtoy
176

Hay una contextlib.redirect_stdout()función en 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`')

Esto es similar a:

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

que se puede usar en versiones anteriores de Python. La última versión no es reutilizable . Se puede hacer uno si se desea.

No redirige el stdout en el nivel de descriptores de archivo, por ejemplo:

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'y 'echo this also is not redirected'no se redirigen al output.txtarchivo.

Para redirigir en el nivel de descriptor de archivo, os.dup2()podría usarse:

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

El mismo ejemplo funciona ahora si stdout_redirected()se usa en lugar 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')

El resultado que antes se imprimía en stdout ahora se mantiene output.txtmientras el stdout_redirected()administrador de contexto esté activo.

Nota: stdout.flush()no vacía las memorias intermedias de C stdio en Python 3 donde la E / S se implementa directamente en las llamadas read()/ write()system. Para vaciar todas las secuencias de salida de C stdio abiertas, puede llamar libc.fflush(None)explícitamente si alguna extensión de C usa E / S basadas en 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

Puede usar el stdoutparámetro para redirigir otras secuencias, no solo sys.stdout, por ejemplo, para fusionar sys.stderry sys.stdout:

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

Ejemplo:

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)

Nota: stdout_redirected()mezcla E / S con búfer ( sys.stdoutgeneralmente) y E / S sin búfer (operaciones en descriptores de archivo directamente). Cuidado, podría haber problemas de almacenamiento en búfer .

Para responder, su edición: puede usar python-daemonpara demonizar su script y usar el loggingmódulo (como sugirió @ erikb85 ) en lugar de printdeclaraciones y simplemente redireccionar stdout para su script Python de larga ejecución que ejecuta nohupahora.

jfs
fuente
3
stdout_redirectedes útil. Tenga en cuenta que esto no funciona dentro de doctests, ya que el SpoofOutcontrolador especial que doctest utiliza para reemplazar sys.stdoutno tiene un filenoatributo.
Chris Johnson
@ChrisJohnson: Si no aumenta, ValueError("Expected a file (`.fileno()`) or a file descriptor")entonces es un error. ¿Estás seguro de que no lo eleva?
jfs
Realiza ese error, que es lo que lo hace no utilizable dentro de un doctest. Para usar su función dentro de un doctest, parece necesario especificar doctest.sys.__stdout__dónde usaríamos normalmente sys.stdout. Esto no es un problema con su función, solo un ajuste requerido para doctest ya que reemplaza stdout con un objeto que no tiene todos los atributos que tendría un archivo verdadero.
Chris Johnson
stdout_redirected()tiene un stdoutparámetro, puede configurarlo sys.__stdout__si desea redirigir la stdout original de Python (que debería tener una validez .fileno()en la mayoría de los casos). No hace nada por la corriente sys.stdoutsi son diferentes. No utilizar doctest.sys; Está disponible por accidente.
jfs
Esto realmente funciona bien, es decir, redirigir stdout y stderr a un fd: with stdout_redirected(to=fd): with merged_stderr_stdout(): print('...'); print('...', file=sys.stderr)
neok
91

puedes probar esto mucho mejor

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
fuente
¿Alguna sugerencia para canalizar loggero syslog?
dsummersl
Si desea editar un archivo, esto no es muy útil. De todos modos +1 por el buen truco
aIKid
10
Esto tendrá consecuencias para el código que supone que sys.stdout es un objeto de archivo completo con métodos como fileno () (que incluye código en la biblioteca estándar de Python). Agregaría un método __getattr __ (self, attr) al que difiere la búsqueda de atributos en self.terminal. def __getattr__(self, attr): return getattr(self.terminal, attr)
peabody
44
Tienes que agregar el def flush(self):método también a la clase Logger.
loretoparisi
1
@loretoparisi pero, ¿qué pasa realmente en el método que creas?
elkshadow5
28

Las otras respuestas no cubrieron el caso en el que desea que los procesos bifurcados compartan su nuevo stdout.

Para hacer eso:

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
Ñame Marcovic
fuente
3
uno necesita reemplazar el atributo 'w' con, os.O_WRONLY | os.O_CREATE ... ¡no puede enviar cadenas a los comandos "os"!
Ch'marr
3
Inserte un sys.stdout.flush()antes de la close(1)declaración para asegurarse de que el 'file'archivo de redireccionamiento obtenga el resultado. Además, puede usar un tempfile.mkstemp()archivo en lugar de 'file'. Y tenga cuidado de no tener otros subprocesos en ejecución que puedan robar el primer identificador de archivo del sistema operativo después del os.close(1)pero antes de 'file'abrirlo para usar el identificador.
Alex Robinson el
2
es os.O_WRONLY | os.O_CREAT ... no hay E ahí.
Jeff Sheffield
@ Ch'marr Es O_CREAT, no O_CREATE.
quant_dev
28

Citado de PEP 343 - La declaración "con" (declaración de importación agregada):

Redireccionar stdout temporalmente:

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

Usado de la siguiente manera:

with open(filename, "w") as f:
    with stdout_redirected(f):
        print "Hello world"

Esto no es seguro para subprocesos, por supuesto, pero tampoco lo está haciendo este mismo baile manualmente. En los programas de subproceso único (por ejemplo, en los scripts) es una forma popular de hacer las cosas.

Gerli
fuente
1
+1. Nota: no funciona para los subprocesos por ejemplo, os.system('echo not redirected'). Mi respuesta muestra cómo redirigir dicha salida
jfs
a partir de Python 3.4 existe redirect_stdoutencontextlib
Walter Tross
12
import sys
sys.stdout = open('stdout.txt', 'w')
Cat Plus Plus
fuente
3

Aquí hay una variación de la respuesta de Yuda Prawira :

  • implementar flush()y todos los atributos del archivo
  • escríbelo como gestor de contexto
  • capturar stderrtambién

.

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
fuente
2

En base a esta respuesta: https://stackoverflow.com/a/5916874/1060344 , aquí hay otra forma que descubrí que utilizo en uno de mis proyectos. Para lo que sea que reemplace sys.stderro sys.stdoutcon, debe asegurarse de que el reemplazo cumpla con la fileinterfaz, especialmente si esto es algo que está haciendo porque stderr / stdout se usan en alguna otra biblioteca que no está bajo su control. Esa biblioteca puede estar utilizando otros métodos de objeto de archivo.

Eche un vistazo de esta manera donde todavía dejo que todo vaya a hacer stderr / stdout (o cualquier archivo para el caso) y también envíe el mensaje a un archivo de registro utilizando la función de registro de Python (pero realmente puede hacer cualquier cosa con esto):

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
fuente
2

Necesita un multiplexor de terminal como tmux o pantalla GNU

Me sorprende que un pequeño comentario de Ryan Amos a la pregunta original sea la única mención de una solución mucho más preferible a todas las demás que se ofrecen, sin importar cuán inteligente sea el truco de Python y cuántos votos positivos hayan recibido. Además del comentario de Ryan, tmux es una buena alternativa a la pantalla GNU.

Pero el principio es el mismo: si alguna vez te encuentras con ganas de dejar un trabajo en la terminal mientras cierras la sesión, ve al café a comer un sándwich, ve al baño, ve a casa (etc.) y luego, vuelve a conectarte con tu sesión terminal desde cualquier lugar o cualquier computadora como si nunca hubiera estado fuera, los multiplexores terminales son la respuesta. Piense en ellos como VNC o escritorio remoto para sesiones de terminal. Cualquier otra cosa es una solución alternativa. Como beneficio adicional, cuando entra el jefe y / o el socio y, sin darse cuenta, ctrl-w / cmd-w su ventana de terminal en lugar de la ventana de su navegador con su contenido dudoso, no habrá perdido las últimas 18 horas de procesamiento !

Duncan
fuente
44
si bien es una buena respuesta para la parte de la pregunta que apareció después de la edición; no responde la pregunta en el título (la mayoría de la gente viene de google por el título)
jfs
0

Los programas escritos en otros lenguajes (por ejemplo, C) tienen que hacer magia especial (llamada doble bifurcación) expresamente para desconectarse de la terminal (y para evitar procesos zombis). Entonces, creo que la mejor solución es emularlos.

Una ventaja adicional de volver a ejecutar su programa es que puede elegir redirecciones en la línea de comandos, p. Ej. /usr/bin/python mycoolscript.py 2>&1 1>/dev/null

Consulte esta publicación para obtener más información: ¿Cuál es la razón para realizar una doble bifurcación al crear un demonio?

jpaugh
fuente
Eh ... no puedo decir que soy un fanático de los procesos que gestionan su propia bifurcación. Es un idioma tan común y tan fácil codificar mal si no tienes cuidado. Es mejor escribir su proceso para que se ejecute en primer plano, y usar un administrador de tareas en segundo plano del sistema ( systemd, upstart) u otra utilidad ( daemon(1)) para manejar la placa de horquilla.
Lucretiel