Перенаправити stdout на файл у Python?


314

Як перенаправити stdout до довільного файлу в Python?

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

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


8
Це насправді не пітон, це оболонка. Просто запустіть свій сценарій, якscript.p > file
Falmarri

Я зараз вирішую проблему за допомогою nohup, але я подумав, що може бути щось розумніше ...

1
@foxbunny: nohup? Чому просто someprocess | python script.py? Навіщо залучати nohup?
С.Лотт

3
Перезапишіть printзаяви, щоб застосувати loggingмодуль із stdlib. Тоді ви можете перенаправляти вихід скрізь, мати контроль над тим, який обсяг випуску ви хочете і т. Д. У більшості випадків виробничий код не повинен, printале log.
erikbwork

2
Можливо, кращим рішенням цієї проблеми є екранна команда, яка збереже ваш баш-сеанс і дозволить отримати доступ до нього з різних запусків.
Райан Амос

Відповіді:


403

Якщо ви хочете зробити перенаправлення в сценарії Python, налаштування sys.stdoutна файл-об’єкт виконує фокус:

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

Набагато більш поширеним методом є використання перенаправлення оболонки під час виконання (те саме в Windows та Linux):

$ python foo.py > file


7
Це не працює from sys import stdout, можливо, тому що він створює локальну копію. Крім того, ви можете використовувати його з with, наприклад with open('file', 'w') as sys.stdout: functionThatPrints(). Тепер ви можете реалізувати, functionThatPrints()використовуючи звичайні printоператори.
мгдол

41
Найкраще , щоб зберегти локальну копію, stdout = sys.stdoutтак що ви можете помістити його назад , коли ви закінчите, sys.stdout = stdout. Таким чином, якщо вам дзвонять з функції, яка використовує, printви не нагнітаєте їх.
mgold

4
@Jan: buffering=0вимикає буферизацію (це може негативно вплинути на продуктивність (10-100 разів)). buffering=1уключає буферизацію ліній, щоб ви могли використовувати tail -fдля виводу, орієнтованого на лінію.
jfs

41
@mgold або ви можете використовувати його, sys.stdout = sys.__stdout__щоб повернути його.
clemtoy

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. Остання версія не використовується повторно . Його можна зробити за бажанням.

Він не перенаправляє stdout на рівні дескрипторів файлів, наприклад:

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()системних викликах. Для очищення всіх відкритих вихідних потоків 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зараз використовуєте .


3
stdout_redirectedкорисно. Пам’ятайте, що це не працює всередині doctests, оскільки спеціальний SpoofOutобробник doctest, який використовується для заміни sys.stdout, не має filenoатрибута.
Кріс Джонсон

@ChrisJohnson: Якщо вона не підвищується, ValueError("Expected a file (`.fileno()`) or a file descriptor")то це помилка. Ви впевнені, що це не підвищує?
jfs

Це дійсно підвищує цю помилку, і саме це робить її непридатною в межах документа. Щоб використовувати свою функцію в рамках доктесту, видається необхідним вказати, doctest.sys.__stdout__де ми зазвичай будемо використовувати sys.stdout. Це не проблема з вашою функцією, просто розміщення, необхідне для doctest, оскільки воно замінює stdout об'єктом, який не має всіх атрибутів, які мали би справжній файл.
Кріс Джонсон

stdout_redirected()має stdoutпараметр, ви можете встановити його, sys.__stdout__якщо ви хочете переспрямувати вихідний python stdout (який .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)
peabody

4
Ви також повинні додати def flush(self):метод до класу Logger.
loretoparisi

1
@loretoparisi, але що насправді йде у створеному вами методі?
elkshadow5

28

Інші відповіді не стосувались випадків, коли ви хочете, щоб роздвоєні процеси поділилися новою версією stdout.

Для цього:

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"!
Ch'marr

3
Введіть a sys.stdout.flush()перед close(1)оператором, щоб переконатися, що 'file'файл переадресації отримує вихід. Також ви можете використовувати tempfile.mkstemp()файл замість 'file'. І будьте обережні, що у вас немає інших потоків, які можуть вкрасти першу ручку файлу OS після, os.close(1)але перед 'file'відкриттям, щоб використовувати ручку.
Алекс Робінсон

2
його os.O_WRONLY | os.O_CREAT ... там немає Е.
Джефф Шеффілд


@ Ch'marr Це O_CREAT, а не O_CREATE.
quant_dev

28

Цитується з PEP 343 - Заява "з" (додана заява про імпорт):

Тимчасове перенаправлення 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

Використовується наступним чином:

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

Це, звичайно, не є безпечним для потоків, але жоден не виконує цей самий танець вручну. У однопотокових програмах (наприклад, у скриптах) це популярний спосіб робити речі.


1
+1. Примітка: це не працює для подпроцессов , наприклад, os.system('echo not redirected'). Моя відповідь показує, як перенаправити такий вихід
jfs

Починаючи з Python 3.4 там, redirect_stdoutвcontextlib
Walter Tross


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())

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 годин, варті обробки !


4
хоча це є гарною відповіддю на частину питання, яке з’явилося після редагування; він не відповідає на запитання в назві (більшість людей приходить сюди з google за заголовком)
jfs

0

Програми, написані іншими мовами (наприклад, C), повинні виконувати спеціальну магію (так звану подвійну форкінг), щоб чітко відірватися від терміналу (і запобігти зомбі-процесам). Отже, я думаю, що найкращим рішенням є їх наслідування.

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

Дивіться цю публікацію для отримання додаткової інформації: Що є причиною виконання подвійної вилки під час створення демона?


Ех ... не можу сказати, що я фанат процесів управління власним подвійним розкручуванням. Це така поширена ідіома, і так легко кодувати неправильно, якщо ви не обережні. Краще записати свій процес для запуску на передньому плані та використовувати системний менеджер фонових завдань ( systemd, upstart) або іншу утиліту ( daemon(1)) для обробки котлованної вилки.
Лукретьєль
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.