Redirect stdout ke file dengan Python?

315

Bagaimana cara mengarahkan stdout ke file sewenang-wenang dengan Python?

Ketika skrip Python yang berjalan lama (misalnya, aplikasi web) dimulai dari dalam sesi ssh dan backgounded, dan sesi ssh ditutup, aplikasi akan menaikkan IOError dan gagal saat mencoba menulis ke stdout. Saya perlu menemukan cara untuk membuat aplikasi dan modul keluaran ke file daripada stdout untuk mencegah kegagalan karena IOError. Saat ini, saya menggunakan nohup untuk mengarahkan output ke file, dan itu menyelesaikan pekerjaan, tetapi saya bertanya-tanya apakah ada cara untuk melakukannya tanpa menggunakan nohup, karena penasaran.

Saya sudah mencoba sys.stdout = open('somefile', 'w'), tetapi ini tampaknya tidak mencegah beberapa modul eksternal dari masih keluaran ke terminal (atau mungkin sys.stdout = ...garis tidak menyala sama sekali). Saya tahu itu harus bekerja dari skrip sederhana yang telah saya uji, tetapi saya juga belum punya waktu untuk menguji pada aplikasi web.

Eric Leschinski
sumber
8
Itu bukan hal python, itu fungsi shell. Jalankan skrip Anda sepertiscript.p > file
Falmarri
Saat ini saya memecahkan masalah menggunakan nohup, tapi saya pikir mungkin ada sesuatu yang lebih pintar ...
1
@foxbunny: nohup? Kenapa sederhana someprocess | python script.py? Mengapa melibatkan nohup?
S.Lott
3
Tulis ulang printpernyataan untuk menerapkan loggingmodul dari stdlib. Kemudian Anda dapat mengarahkan output di mana-mana, memiliki kontrol atas berapa banyak output yang Anda inginkan dll Dalam kebanyakan kasus kode produksi tidak boleh printtapi log.
erikbwork
2
Mungkin solusi yang lebih baik untuk masalah ini adalah perintah layar, yang akan menghemat sesi bash Anda dan memungkinkan Anda untuk mengaksesnya dari berbagai proses.
Ryan Amos

Jawaban:

404

Jika Anda ingin melakukan pengalihan dalam skrip Python, pengaturan sys.stdoutke objek file melakukan trik:

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

Metode yang jauh lebih umum adalah dengan menggunakan pengalihan shell ketika menjalankan (sama pada Windows dan Linux):

$ python foo.py > file
moinudin
sumber
3
Jika Anda menggunakan Windows, berhati
hatilah dengan
7
Tidak berfungsi from sys import stdout, mungkin karena itu membuat salinan lokal. Anda juga dapat menggunakannya dengan with, mis with open('file', 'w') as sys.stdout: functionThatPrints(). Anda sekarang dapat menerapkan functionThatPrints()menggunakan printpernyataan normal .
mgold
41
Lebih baik menyimpan salinan lokal, stdout = sys.stdoutsehingga Anda dapat mengembalikannya setelah selesai sys.stdout = stdout,. Dengan begitu jika Anda dipanggil dari fungsi yang menggunakan printAnda tidak mengacaukannya.
mgold
4
@ Jan: buffering=0menonaktifkan buffering (ini dapat mempengaruhi kinerja secara negatif (10-100 kali)). buffering=1memungkinkan penyangga garis sehingga Anda bisa menggunakan tail -funtuk keluaran berorientasi garis.
jfs
41
@mgold atau Anda dapat menggunakannya sys.stdout = sys.__stdout__untuk mendapatkannya kembali.
clemtoy
176

Ada contextlib.redirect_stdout()fungsi dalam 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`')

Ini mirip dengan:

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

yang dapat digunakan pada versi Python sebelumnya. Versi terakhir tidak dapat digunakan kembali . Itu bisa dibuat satu jika diinginkan.

Itu tidak mengarahkan stdout di tingkat deskriptor file misalnya:

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'dan 'echo this also is not redirected'tidak diarahkan ke output.txtfile.

Untuk mengarahkan ulang di tingkat deskriptor file, os.dup2()dapat digunakan:

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

Contoh yang sama berfungsi sekarang jika stdout_redirected()digunakan alih-alih 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')

Output yang sebelumnya dicetak pada stdout sekarang pergi output.txtselama stdout_redirected()manajer konteks aktif.

Catatan: stdout.flush()tidak menyiram buffer C stdio di Python 3 di mana I / O diimplementasikan langsung pada read()/ write()system calls. Untuk menyiram semua aliran output C stdio yang terbuka, Anda dapat memanggil libc.fflush(None)secara eksplisit jika beberapa ekstensi C menggunakan I / O berbasis 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

Anda bisa menggunakan stdoutparameter untuk mengarahkan aliran lain, tidak hanya sys.stdoutmis., Untuk menggabungkan sys.stderrdan sys.stdout:

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

Contoh:

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)

Catatan: stdout_redirected()mencampur I / O buffer ( sys.stdoutbiasanya) dan I / O unbuffered (operasi pada deskriptor file secara langsung). Hati-hati, mungkin ada masalah buffering .

Untuk menjawab, hasil edit Anda: Anda dapat menggunakan python-daemonuntuk daemonisasi skrip Anda dan menggunakan loggingmodul (seperti yang disarankan @ erikb85 ) alih-alih printpernyataan dan hanya mengarahkan stdout untuk skrip Python Anda yang sudah berjalan lama yang Anda jalankan gunakan nohupsekarang.

jfs
sumber
3
stdout_redirectedmembantu. Ketahuilah bahwa ini tidak berfungsi di dalam dokumen, karena dokumen khusus yang SpoofOutdigunakan doctest untuk menggantikan sys.stdouttidak memiliki filenoatribut.
Chris Johnson
@ ChrisJohnson: Jika tidak naik ValueError("Expected a file (`.fileno()`) or a file descriptor")maka itu adalah bug. Apakah Anda yakin itu tidak menaikkannya?
jfs
Itu memang memunculkan kesalahan itu, yang membuatnya tidak bisa digunakan dalam dokumen. Untuk menggunakan fungsi Anda dalam dokumen, tampaknya perlu menentukan doctest.sys.__stdout__tempat yang biasanya kami gunakan sys.stdout. Ini bukan masalah dengan fungsi Anda, hanya akomodasi yang diperlukan untuk doctest karena ia menggantikan stdout dengan objek yang tidak memiliki semua atribut yang akan dimiliki oleh file yang sebenarnya.
Chris Johnson
stdout_redirected()memiliki stdoutparameter, Anda dapat mengaturnya sys.__stdout__jika Anda ingin mengarahkan ulang stdout python asli (yang seharusnya berlaku .fileno()pada kebanyakan kasus). Itu tidak melakukan apa pun untuk arus sys.stdoutjika mereka berbeda. Jangan gunakan doctest.sys; ini tersedia secara tidak sengaja.
jfs
Ini benar-benar bekerja dengan baik, yaitu mengarahkan stdout dan stderr ke fd: with stdout_redirected(to=fd): with merged_stderr_stdout(): print('...'); print('...', file=sys.stderr)
neok
91

Anda dapat mencoba ini jauh lebih baik

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
sumber
Ada saran untuk perpipaan ke loggeratau syslog?
dsummersl
Jika Anda ingin mengedit file ini tidak terlalu berguna. Pokoknya +1 untuk trik yang bagus
aIKid
10
Ini akan memiliki konsekuensi untuk kode yang mengasumsikan sys.stdout adalah objek file yang lengkap dengan metode seperti fileno () (yang termasuk kode dalam pustaka standar python). Saya akan menambahkan metode __getattr __ (self, attr) ke metode yang menolak pencarian atribut ke self.terminal. def __getattr__(self, attr): return getattr(self.terminal, attr)
peabody
4
Anda harus menambahkan def flush(self):metode ke kelas Logger.
loretoparisi
1
@loretoparisi tetapi apa yang sebenarnya terjadi pada metode yang Anda buat?
elkshadow5
28

Jawaban lain tidak mencakup kasus di mana Anda ingin proses bercabang untuk berbagi stdout baru Anda.

Untuk melakukannya:

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
sumber
3
kita perlu mengganti atribut 'w' dengan, os.O_WRONLY | os.O_CREATE ... tidak dapat mengirim string ke perintah "os"!
Ch'marr
3
Masukkan sys.stdout.flush()sebelum close(1)pernyataan untuk memastikan 'file'file redirect mendapatkan output. Anda juga dapat menggunakan tempfile.mkstemp()file sebagai pengganti 'file'. Dan berhati-hatilah, Anda tidak memiliki utas lain yang berjalan yang dapat mencuri file pertama os menangani setelah os.close(1)tetapi sebelum 'file'dibuka untuk menggunakan pegangan.
Alex Robinson
2
itu os.O_WRONLY | os.O_CREAT ... tidak ada E di sana.
Jeff Sheffield
@ Ch'marr Ini O_CREAT, bukan O_CREATE.
quant_dev
28

Dikutip dari PEP 343 - Pernyataan "with" (pernyataan impor ditambahkan):

Redirect stdout sementara:

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

Digunakan sebagai berikut:

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

Ini bukan thread-safe, tentu saja, tetapi tidak ada yang melakukan tarian yang sama ini secara manual. Dalam program single-threaded (misalnya dalam skrip) ini adalah cara populer untuk melakukan sesuatu.

Gerli
sumber
1
+1. Catatan: tidak berfungsi untuk subproses misalnya os.system('echo not redirected'),. Jawaban saya menunjukkan bagaimana mengarahkan output seperti itu
jfs
mulai dari Python 3.4 ada redirect_stdoutdicontextlib
Walter Tross
12
import sys
sys.stdout = open('stdout.txt', 'w')
Cat Plus Plus
sumber
3

Berikut adalah variasi jawaban Yuda Prawira :

  • mengimplementasikan flush()dan semua atribut file
  • tulis sebagai manajer konteks
  • tangkap stderrjuga

.

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

Berdasarkan jawaban ini: https://stackoverflow.com/a/5916874/1060344 , berikut adalah cara lain yang saya tahu yang saya gunakan di salah satu proyek saya. Untuk apa pun yang Anda ganti sys.stderratau sys.stdoutdengan, Anda harus memastikan bahwa penggantian sesuai dengan fileantarmuka, terutama jika ini adalah sesuatu yang Anda lakukan karena stderr / stdout digunakan di beberapa perpustakaan lain yang tidak di bawah kendali Anda. Perpustakaan itu mungkin menggunakan metode lain dari objek file.

Lihat cara ini di mana saya masih membiarkan semuanya berjalan stderr / stdout (atau file apa pun dalam hal ini) dan juga mengirim pesan ke file log menggunakan fasilitas logging Python (tetapi Anda benar-benar dapat melakukan apa pun dengan ini):

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

Anda memerlukan terminal multiplexer seperti tmux atau layar GNU

Saya terkejut bahwa komentar kecil oleh Ryan Amos 'untuk pertanyaan asli adalah satu-satunya penyebutan solusi yang jauh lebih disukai daripada yang lain yang ditawarkan, tidak peduli seberapa pintar tipu muslihat python dan berapa banyak upvotes yang telah mereka terima. Lebih jauh dari komentar Ryan, tmux adalah alternatif yang bagus untuk layar GNU.

Tetapi prinsipnya sama: jika Anda mendapati diri Anda ingin meninggalkan pekerjaan terminal yang sedang berjalan saat Anda log-out, pergilah ke kafe untuk makan sandwich, mampir ke kamar mandi, pulang ke rumah (dll) dan kemudian, sambungkan kembali ke Anda sesi terminal dari mana saja atau komputer manapun seolah-olah Anda tidak pernah pergi, multiplexer terminal adalah yang jawabannya. Anggap mereka sebagai VNC atau desktop jarak jauh untuk sesi terminal. Yang lainnya adalah solusi. Sebagai bonus, ketika bos dan / atau mitra masuk dan Anda secara tidak sengaja ctrl-w / cmd-w jendela terminal Anda alih-alih jendela browser Anda dengan konten yang cerdik, Anda tidak akan kehilangan pemrosesan selama 18 jam terakhir yang bernilai !

duncan
sumber
4
sementara itu adalah jawaban yang bagus untuk bagian pertanyaan yang muncul setelah diedit; itu tidak menjawab pertanyaan dalam judul (kebanyakan orang datang ke sini dari google untuk judul)
jfs
0

Program yang ditulis dalam bahasa lain (misalnya C) harus melakukan sihir khusus (disebut forking ganda) secara tegas untuk melepaskan diri dari terminal (dan untuk mencegah proses zombie). Jadi, saya pikir solusi terbaik adalah meniru mereka.

Kelebihan dari menjalankan kembali program Anda adalah, Anda dapat memilih pengalihan pada baris perintah, misalnya /usr/bin/python mycoolscript.py 2>&1 1>/dev/null

Lihat posting ini untuk info lebih lanjut: Apa alasan melakukan garpu ganda saat membuat daemon?

jpaugh
sumber
Eh ... tidak bisa mengatakan saya penggemar proses mengelola fork ganda mereka sendiri. Ini adalah ungkapan yang sangat umum, dan sangat mudah untuk kode yang salah jika Anda tidak berhati-hati. Lebih baik menulis proses Anda untuk berjalan di latar depan, dan menggunakan task manager latar belakang sistem ( systemd, upstart) atau utilitas lain ( daemon(1)) untuk menangani pelat tengkuk forking.
Lucretiel