Перенаправить стандартный вывод в файл в Python?

315

Как перенаправить стандартный вывод в произвольный файл в Python?

Когда длительно работающий скрипт Python (например, веб-приложение) запускается изнутри сеанса ssh и отключается, а сеанс ssh закрывается, приложение вызывает IOError и завершает работу в тот момент, когда оно пытается записать в stdout. Мне нужно было найти способ, чтобы приложение и модули выводили в файл, а не в stdout, чтобы предотвратить сбой из-за IOError. В настоящее время я использую nohup для перенаправления вывода в файл, и это делает работу, но мне было интересно, есть ли способ сделать это без использования nohup, из любопытства.

Я уже пробовал sys.stdout = open('somefile', 'w'), но это не мешает некоторым внешним модулям по-прежнему выводить на терминал (или, возможно, sys.stdout = ...линия вообще не сработала). Я знаю, что он должен работать с более простыми скриптами, на которых я тестировал, но у меня еще не было времени для тестирования веб-приложения.

Эрик Лещинский
источник
8
Это не вещь Python, это функция оболочки. Просто запустите свой сценарий, какscript.p > file
Falmarri
В настоящее время я
1
@foxbunny: nohup? Почему просто someprocess | python script.py? Зачем привлекать nohup?
С.Лотт
3
Перепишите printоператоры, чтобы применить loggingмодуль из stdlib. После этого вы можете перенаправить вывод везде, есть контроль над тем, сколько выходом вы хотите и т.д. В большинстве случаев кода производства не должен , printно log.
erikbwork
2
Возможно, лучшим решением для этой проблемы является команда screen, которая сохранит ваш сеанс bash и позволит вам получить к нему доступ с разных запусков.
Райан Амос

Ответы:

404

Если вы хотите сделать перенаправление внутри скрипта Python, установка sys.stdoutна объект файла делает свое дело:

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

Гораздо более распространенным методом является использование перенаправления оболочки при выполнении (то же самое в Windows и Linux):

$ python foo.py > file
moinudin
источник
3
Если вы работаете в Windows, следите за ошибкой Windows - невозможно перенаправить вывод, когда я запускаю скрипт Python в Windows, используя только имя скрипта
Петр Доброгост,
7
Это не работает from sys import stdout, возможно, потому что это создает локальную копию. Также вы можете использовать его with, например, с with open('file', 'w') as sys.stdout: functionThatPrints(). Теперь вы можете реализовать functionThatPrints()с помощью обычных printоператоров.
мгольд
41
Лучше всего хранить локальную копию, stdout = sys.stdoutчтобы вы могли положить ее обратно, когда закончите sys.stdout = stdout. Таким образом, если вы вызываетесь из функции, которая использует, printвы не облажаетесь.
мгольд
4
@Jan: buffering=0отключает буферизацию (это может негативно повлиять на производительность (10-100 раз)). buffering=1разрешает буферизацию строки, чтобы вы могли использовать ее tail -fдля линейно-ориентированного вывода.
Jfs
41
@mgold или вы можете использовать, sys.stdout = sys.__stdout__чтобы получить его обратно.
Клемтой
176

В Python 3.4 есть contextlib.redirect_stdout()функция :

from contextlib import redirect_stdout

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

Это похоже на:

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

это можно использовать в более ранних версиях Python. Последняя версия не подлежит повторному использованию . Это может быть сделано один при желании.

Он не перенаправляет стандартный вывод на уровне файловых дескрипторов, например:

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' и 'echo this also is not redirected' не перенаправляются в output.txtфайл.

Чтобы перенаправить на уровне дескриптора файла, os.dup2() можно использовать:

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

Тот же самый пример работает теперь, если stdout_redirected()используется вместо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')

Вывод, который ранее был напечатан на stdout, теперь идет в output.txt тех пор, stdout_redirected()пока активен менеджер контекста.

Примечание: stdout.flush()не очищает буферы C stdio на Python 3, где ввод / вывод реализован непосредственно при вызовах read()/ write()system. Чтобы очистить все открытые потоки вывода C stdio, вы можете libc.fflush(None)явно вызвать, если какое-то расширение C использует ввод / вывод на основе 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

Вы можете использовать stdoutпараметр для перенаправления других потоков, не только, sys.stdoutнапример, для объединения sys.stderrи sys.stdout:

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

Пример:

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)

Примечание: stdout_redirected()смешивает буферизованный ввод / вывод ( sys.stdoutобычно) и небуферизованный ввод / вывод (операции с дескрипторами файлов напрямую). Осторожно, могут быть проблемы с буферизацией .

Чтобы ответить, отредактируйте: вы можете использовать, python-daemonчтобы демонизировать ваш скрипт и использовать loggingмодуль (как предложено @ erikb85 ) вместо printоператоров и просто перенаправить stdout для вашего долгосрочного скрипта Python, который вы nohupсейчас используете .

JFS
источник
3
stdout_redirectedполезно Имейте в виду, что это не работает внутри doctest, так как специальный SpoofOutобработчик doctest, используемый для замены sys.stdout, не имеет filenoатрибута.
Крис Джонсон
@ChrisJohnson: Если это не поднять, ValueError("Expected a file (`.fileno()`) or a file descriptor")то это ошибка. Вы уверены, что это не поднимает это?
Jfs
Это действительно вызывает эту ошибку, что делает ее не пригодной для использования в ходе тестирования. Чтобы использовать вашу функцию в рамках doctest, необходимо указать, doctest.sys.__stdout__где мы обычно будем использовать sys.stdout. Это не проблема с вашей функцией, просто приспособление, необходимое для doctest, так как он заменяет стандартный вывод объектом, который не имеет всех атрибутов, которые имел бы истинный файл.
Крис Джонсон
stdout_redirected()Имеет stdoutпараметр, вы можете установить его, sys.__stdout__если хотите перенаправить исходный стандартный вывод Python ( .fileno()в большинстве случаев он должен быть действительным ). Он ничего не делает для тока, sys.stdoutесли они разные. Не используйте doctest.sys; это доступно случайно.
Jfs
Это действительно хорошо работает, то есть перенаправить stdout и stderr на fd: with stdout_redirected(to=fd): with merged_stderr_stdout(): print('...'); print('...', file=sys.stderr)
neok
91

ты можешь попробовать это намного лучше

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
Юда Правира
источник
Любые предложения для труб loggerили syslog?
dsummersl
Если вы хотите редактировать файл, это не очень полезно. В любом случае +1 за хороший трюк
aIKid
10
Это будет иметь последствия для кода, который предполагает, что sys.stdout является полноценным файловым объектом с такими методами, как fileno () (который включает код в стандартной библиотеке python). Я бы добавил метод __getattr __ (self, attr) к тому, который откладывает поиск атрибутов до self.terminal. def __getattr__(self, attr): return getattr(self.terminal, attr)
Пибоди
4
Вы также должны добавить def flush(self):метод в класс Logger.
loretoparisi
1
@loretoparisi, но что на самом деле происходит в методе, который вы создаете?
elkshadow5
28

Другие ответы не охватывали случай, когда вы хотите, чтобы разветвленные процессы использовали ваш новый стандартный вывод.

Для этого:

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
Ям Маркович
источник
3
необходимо заменить атрибут 'w' на, os.O_WRONLY | os.O_CREATE ... нельзя отправлять строки в команды "os"!
Ч'марр
3
Вставьте оператор sys.stdout.flush()before, close(1)чтобы убедиться, что 'file'файл перенаправления получает выходные данные. Кроме того, вы можете использовать tempfile.mkstemp()файл вместо 'file'. И будьте осторожны, у вас не запущены другие потоки, которые могут украсть первый дескриптор файла ОС после, os.close(1)но до 'file'открытия, чтобы использовать дескриптор.
Алекс Робинсон
2
его os.O_WRONLY | os.O_CREAT ... там нет E
Джефф Шеффилд
@ Ch'marr Это O_CREAT, а не O_CREATE.
Quant_Dev
28

Цитируется из PEP 343 - Заявление «с» (добавлен оператор импорта):

Временно перенаправить стандартный вывод:

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

Используется следующим образом:

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

Конечно, это не потокобезопасно, но ни один из них не выполняет этот же танец вручную. В однопоточных программах (например, в скриптах) это популярный способ работы.

Gerli
источник
1
+1. Примечание: это не работает для подпроцессов , например, os.system('echo not redirected'). Мой ответ показывает, как перенаправить такой вывод
JFS
начиная с Python 3.4 есть redirect_stdoutвcontextlib
Уолтер Тросс
12
import sys
sys.stdout = open('stdout.txt', 'w')
Cat Plus Plus
источник
3

Вот вариант ответа Юда Правира :

  • реализовать flush()и все атрибуты файла
  • напишите это как контекстный менеджер
  • захватить stderrтакже

,

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
источник
2

Основываясь на этом ответе: https://stackoverflow.com/a/5916874/1060344 , вот еще один способ, который я выяснил, который я использую в одном из моих проектов. Что бы вы ни заменяли sys.stderrили чем бы то ни было sys.stdout, вы должны убедиться, что замена соответствует fileинтерфейсу, особенно если вы этим занимаетесь, потому что stderr / stdout используются в какой-то другой библиотеке, которая не находится под вашим контролем. Эта библиотека может использовать другие методы объекта файла.

Посмотрите на этот способ, где я все еще позволяю всему делать stderr / stdout (или любой файл в этом отношении), а также отправлять сообщение в файл журнала с помощью средства ведения журнала Python (но вы действительно можете сделать что-нибудь с этим):

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

Вам нужен терминальный мультиплексор, такой как tmux или экран GNU

Я удивлен тем, что небольшой комментарий Райана Амоса к первоначальному вопросу - единственное упоминание о решении, гораздо более предпочтительном, чем все остальные, предлагаемые, независимо от того, насколько хитрым может быть обман с питоном и сколько голосов они получили. В дополнение к комментарию Райана, tmux - хорошая альтернатива экрану GNU.

Но принцип тот же: если вам захочется оставить работу терминала во время выхода из системы, отправляйтесь в кафе за сэндвичем, загляните в ванную, идите домой (и т. Д.), А затем снова подключитесь к терминальная сессия из любого места или любого компьютера , как если бы вы никогда не были далеки, терминальные мультиплексоры ответ. Думайте о них как о VNC или удаленном рабочем столе для терминальных сессий. Все остальное - обходной путь. В качестве бонуса, когда к вам придет босс и / или партнер и вы по неосторожности нажмете ctrl-w / cmd-w в окне терминала, а не в окне браузера с его хитрым контентом, вы не потеряете обработку за последние 18 часов. !

Duncan
источник
4
пока это хороший ответ на ту часть вопроса, которая появилась после редактирования; это не отвечает на вопрос в названии (большинство людей приходят сюда из Google для названия)
JFS
0

Программы, написанные на других языках (например, C), должны выполнять специальную магию (называемую двойным разветвлением), чтобы отсоединиться от терминала (и предотвратить процессы зомби). Поэтому я думаю, что лучшим решением будет подражать им.

Плюс повторного выполнения вашей программы, вы можете выбрать перенаправления в командной строке, например /usr/bin/python mycoolscript.py 2>&1 1>/dev/null

См. Этот пост для получения дополнительной информации: Какова причина выполнения двойной вилки при создании демона?

jpaugh
источник
Эх ... не могу сказать, что я фанат процессов, управляющих собственным двойным ветвлением. Это такая распространенная идиома, которую легко кодировать неправильно, если вы не будете осторожны. Лучше написать свой процесс для запуска на переднем плане, а также использовать диспетчер фоновых задач системы ( systemd, upstart) или другую утилиту ( daemon(1)) для обработки шаблона разветвления.
Лукретиэль