Redirigir stdout a un archivo en Python?


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

Cuando un script Python de larga duración (por ejemplo, una aplicación web) se inicia desde dentro de la sesión ssh y se retroalimenta, 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, empleo nohup para redirigir la salida a un archivo, y que hace el trabajo, pero me preguntaba si había una manera de hacerlo sin usar nohup, por curiosidad.

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

 235
Author: Eric Leschinski, 2011-01-13

10 answers

Si desea hacer la redirección dentro del script Python, establecer sys.stdout a un objeto file hace el truco:

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

Un método mucho más común es usar la redirección de shell cuando se ejecuta (lo mismo en Windows y Linux):

$ python foo.py > file
 304
Author: marcog,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-13 00:53:28

Hay 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`')

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 a 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 son redirigidos al archivo output.txt.

Para redirigir a 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 el mismo ejemplo funciona ahora si se usa stdout_redirected() 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')

La salida que anteriormente se imprimía en stdout ahora va a output.txt siempre y cuando stdout_redirected() context manager esté activo.

Nota: stdout.flush() no se descarga C stdio buffers en Python 3 donde la E/S se implementa directamente en read()/write() llamadas del sistema. Para vaciar todos los flujos de salida C stdio abiertos, puede llamar a libc.fflush(None) explícitamente si alguna extensión 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

Podría usar el parámetro stdout para redirigir otros flujos, no solo sys.stdout por ejemplo, para combinar sys.stderr y 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.stdout normalmente) y E/S sin búfer (operaciones directamente en descriptores de fichero). Cuidado, podría haber buffering issues .

Para responder, su edición: podría usar python-daemon para demonizar tu script y usar el módulo logging (como @erikb85 sugirió ) en lugar de las instrucciones print y simplemente redireccionando stdout para su script Python de larga ejecución que ejecuta usando nohup ahora.

 125
Author: jfs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:43

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
 83
Author: Yuda Prawira,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-10 00:35:10

Las otras respuestas no cubrieron el caso en el que desea que los procesos bifurcados compartan su nueva salida estándar.

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
 29
Author: Yam Marcovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-15 00:04:01

Citado de PEP 343 {La Declaración "con" (declaración de importación añadida):

Redirigir 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

Utilizado como sigue:

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

Esto no es seguro para hilos, por supuesto, pero tampoco lo es hacer esta misma danza manualmente. En programas de subproceso único (por ejemplo en scripts) es una forma popular de hacer las cosas.

 21
Author: Gerli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-06 13:07:10
import sys
sys.stdout = open('stdout.txt', 'w')
 10
Author: Cat Plus Plus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-13 00:52:51

Basado en esta respuesta: https://stackoverflow.com/a/5916874/1060344 , aquí hay otra forma que descubrí que uso en uno de mis proyectos. Para lo que sea que reemplace sys.stderr o sys.stdout, debe asegurarse de que el reemplazo cumpla con la interfaz file, especialmente si esto es algo que está haciendo porque stderr/stdout se utilizan en alguna otra biblioteca que no está bajo su control. Esa biblioteca puede estar usando otros métodos de file object.

Echa un vistazo de esta manera donde I todavía deje 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 instalació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)
 2
Author: vaidik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:33:26

Necesita un multiplexor de terminal como tmux o GNU screen

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

Pero el principio es el mismo: si alguna vez te encuentras con ganas de dejar un terminal de trabajo en ejecución mientras se cierra la sesión, la cabeza a la cafetería para un sándwich, pop al baño, ir a casa (etc) y luego más tarde, volver a conectar a su sesión de terminal desde cualquier lugar o cualquier ordenador como si nunca había estado lejos, multiplexores de terminal son la respuesta. Piense en ellos como VNC o escritorio remoto para sesiones de terminal. Cualquier otra cosa es una solución. Como bono, cuando el jefe y / o socio entra y sin darse cuenta ctrl-w / cmd-w su ventana de terminal en lugar de su navegador ventana con su contenido dudoso, no habrá perdido las últimas 18 horas de procesamiento!

 2
Author: duncan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-23 05:17:02

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

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

Ver este post para más información: ¿Cuál es la razón para realizar una bifurcación doble al crear un demonio?

 1
Author: jpaugh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:54

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

  • implementar flush() y todos los atributos del archivo
  • escríbalo como un contextmanager
  • captura stderr tambié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())
 -1
Author: damio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-05 13:41:11