Tangkap stdout dari skrip?

94

misalkan ada skrip yang melakukan hal seperti ini:

# module writer.py
import sys

def write():
    sys.stdout.write("foobar")

Sekarang misalkan saya ingin menangkap output dari writefungsi dan menyimpannya dalam variabel untuk diproses lebih lanjut. Solusi naifnya adalah:

# module mymodule.py
from writer import write

out = write()
print out.upper()

Tapi ini tidak berhasil. Saya datang dengan solusi lain dan berhasil, tapi tolong, beri tahu saya jika ada cara yang lebih baik untuk menyelesaikan masalah. Terima kasih

import sys
from cStringIO import StringIO

# setup the environment
backup = sys.stdout

# ####
sys.stdout = StringIO()     # capture output
write()
out = sys.stdout.getvalue() # release output
# ####

sys.stdout.close()  # close the stream 
sys.stdout = backup # restore original stdout

print out.upper()   # post processing
Paolo
sumber

Jawaban:

51

Setting stdoutadalah cara yang masuk akal untuk melakukannya. Cara lainnya adalah menjalankannya sebagai proses lain:

import subprocess

proc = subprocess.Popen(["python", "-c", "import writer; writer.write()"], stdout=subprocess.PIPE)
out = proc.communicate()[0]
print out.upper()
Matthew Flaschen
sumber
4
check_output secara langsung menangkap keluaran dari perintah yang dijalankan dalam subproses: <br> value = subprocess.check_output (perintah, shell = True)
Arthur
1
Versi yang diformat :value = subprocess.check_output(command, shell=True)
Nae
51

Berikut adalah versi pengelola konteks dari kode Anda. Ini menghasilkan daftar dua nilai; yang pertama adalah stdout, yang kedua adalah stderr.

import contextlib
@contextlib.contextmanager
def capture():
    import sys
    from cStringIO import StringIO
    oldout,olderr = sys.stdout, sys.stderr
    try:
        out=[StringIO(), StringIO()]
        sys.stdout,sys.stderr = out
        yield out
    finally:
        sys.stdout,sys.stderr = oldout, olderr
        out[0] = out[0].getvalue()
        out[1] = out[1].getvalue()

with capture() as out:
    print 'hi'
Jason Grout
sumber
Suka solusi ini. Saya memodifikasi, agar tidak kehilangan barang-barang dari aliran yang tidak saya harapkan hasilnya, misalnya kesalahan tak terduga. Dalam kasus saya, capture () dapat menerima sys.stderr atau sys.stdout sebagai parameter, menunjukkan untuk hanya menangkap aliran itu.
Joshua Richards
StringIO tidak mendukung unicode dengan cara apa pun, jadi Anda dapat mengintegrasikan jawabannya di sini untuk membuat karakter non-ASCII dukungan di atas: stackoverflow.com/a/1819009/425050
mafrosis
3
Memodifikasi nilai yang dihasilkan pada akhirnya benar-benar agak aneh - with capture() as out:akan berperilaku berbeda denganwith capture() as out, err:
Eric
1
Dukungan unicode / stdout.buffer dapat dicapai dengan menggunakan modul io. Lihat jawaban saya .
JonnyJD
3
Solusi ini rusak jika Anda menggunakan subprocessdan mengalihkan keluaran ke sys.stdout / stderr. Ini karena StringIObukan objek file nyata dan melewatkan fileno()fungsinya.
letmaik
47

Untuk pengunjung di masa mendatang: Contextlib Python 3.4 menyediakan ini secara langsung (lihat bantuan contextlib Python ) melalui redirect_stdoutpengelola konteks:

from contextlib import redirect_stdout
import io

f = io.StringIO()
with redirect_stdout(f):
    help(pow)
s = f.getvalue()
noder
sumber
2
Ini tidak menyelesaikan masalah saat mencoba menulis ke sys.stdout.buffer (seperti yang perlu Anda lakukan saat menulis byte). StringIO tidak memiliki atribut buffer, sedangkan TextIOWrapper memilikinya. Lihat jawaban dari @JonnyJD.
penenun
10

Ini adalah bagian dekorator dari kode asli saya.

writer.py tetap sama:

import sys

def write():
    sys.stdout.write("foobar")

mymodule.py sligthly dimodifikasi:

from writer import write as _write
from decorators import capture

@capture
def write():
    return _write()

out = write()
# out post processing...

Dan inilah dekoratornya:

def capture(f):
    """
    Decorator to capture standard output
    """
    def captured(*args, **kwargs):
        import sys
        from cStringIO import StringIO

        # setup the environment
        backup = sys.stdout

        try:
            sys.stdout = StringIO()     # capture output
            f(*args, **kwargs)
            out = sys.stdout.getvalue() # release output
        finally:
            sys.stdout.close()  # close the stream 
            sys.stdout = backup # restore original stdout

        return out # captured output wrapped in a string

    return captured
Paolo
sumber
10

Atau mungkin menggunakan fungsionalitas yang sudah ada ...

from IPython.utils.capture import capture_output

with capture_output() as c:
    print('some output')

c()

print c.stdout
dgrigonis.dll
sumber
9

Dimulai dengan Python 3 Anda juga dapat menggunakan sys.stdout.buffer.write()untuk menulis (sudah) string byte yang dikodekan ke stdout (lihat stdout di Python 3 ). Ketika Anda melakukan itu, StringIOpendekatan sederhana tidak berhasil karena tidak sys.stdout.encodingjuga sys.stdout.bufferakan tersedia.

Dimulai dengan Python 2.6 Anda dapat menggunakan TextIOBaseAPI , yang menyertakan atribut yang hilang:

import sys
from io import TextIOWrapper, BytesIO

# setup the environment
old_stdout = sys.stdout
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)

# do some writing (indirectly)
write("blub")

# get output
sys.stdout.seek(0)      # jump to the start
out = sys.stdout.read() # read output

# restore stdout
sys.stdout.close()
sys.stdout = old_stdout

# do stuff with the output
print(out.upper())

Solusi ini bekerja untuk Python 2> = 2.6 dan Python 3. Harap dicatat bahwa kami sys.stdout.write()hanya menerima string unicode dan sys.stdout.buffer.write()hanya menerima string byte. Ini mungkin bukan kasus untuk kode lama, tetapi sering kali kasus kode yang dibuat untuk berjalan di Python 2 dan 3 tanpa perubahan.

Jika Anda perlu mendukung kode yang mengirimkan string byte ke stdout secara langsung tanpa menggunakan stdout.buffer, Anda dapat menggunakan variasi ini:

class StdoutBuffer(TextIOWrapper):
    def write(self, string):
        try:
            return super(StdoutBuffer, self).write(string)
        except TypeError:
            # redirect encoded byte strings directly to buffer
            return super(StdoutBuffer, self).buffer.write(string)

Anda tidak perlu menyetel pengkodean buffer sys.stdout.encoding, tetapi ini membantu saat menggunakan metode ini untuk menguji / membandingkan keluaran skrip.

JonnyJD
sumber
4

Pertanyaan di sini (contoh bagaimana mengarahkan keluaran, bukan teebagian) digunakan os.dup2untuk mengarahkan aliran pada tingkat OS. Itu bagus karena ini akan berlaku untuk perintah yang Anda buat dari program Anda juga.

Jeremiah Willcock
sumber
4

Saya pikir Anda harus melihat empat objek ini:

from test.test_support import captured_stdout, captured_output, \
    captured_stderr, captured_stdin

Contoh:

from writer import write

with captured_stdout() as stdout:
    write()
print stdout.getvalue().upper()

UPD: Seperti yang Eric katakan dalam komentar, seseorang tidak boleh menggunakannya secara langsung, jadi saya menyalin dan menempelkannya.

# Code from test.test_support:
import contextlib
import sys

@contextlib.contextmanager
def captured_output(stream_name):
    """Return a context manager used by captured_stdout and captured_stdin
    that temporarily replaces the sys stream *stream_name* with a StringIO."""
    import StringIO
    orig_stdout = getattr(sys, stream_name)
    setattr(sys, stream_name, StringIO.StringIO())
    try:
        yield getattr(sys, stream_name)
    finally:
        setattr(sys, stream_name, orig_stdout)

def captured_stdout():
    """Capture the output of sys.stdout:

       with captured_stdout() as s:
           print "hello"
       self.assertEqual(s.getvalue(), "hello")
    """
    return captured_output("stdout")

def captured_stderr():
    return captured_output("stderr")

def captured_stdin():
    return captured_output("stdin")
Oleksandr Fedorov
sumber
4

Saya suka solusi contextmanager namun jika Anda membutuhkan buffer yang disimpan dengan file terbuka dan dukungan fileno, Anda dapat melakukan sesuatu seperti ini.

import six
from six.moves import StringIO


class FileWriteStore(object):
    def __init__(self, file_):
        self.__file__ = file_
        self.__buff__ = StringIO()

    def __getattribute__(self, name):
        if name in {
            "write", "writelines", "get_file_value", "__file__",
                "__buff__"}:
            return super(FileWriteStore, self).__getattribute__(name)
        return self.__file__.__getattribute__(name)

    def write(self, text):
        if isinstance(text, six.string_types):
            try:
                self.__buff__.write(text)
            except:
                pass
        self.__file__.write(text)

    def writelines(self, lines):
        try:
            self.__buff__.writelines(lines)
        except:
            pass
        self.__file__.writelines(lines)

    def get_file_value(self):
        return self.__buff__.getvalue()

menggunakan

import sys
sys.stdout = FileWriteStore(sys.stdout)
print "test"
buffer = sys.stdout.get_file_value()
# you don't want to print the buffer while still storing
# else it will double in size every print
sys.stdout = sys.stdout.__file__
print buffer
Nathan Buckner
sumber
2

Berikut ini adalah manajer konteks yang mengambil inspirasi dari jawaban @ JonnyJD yang mendukung penulisan byte ke bufferatribut tetapi juga memanfaatkan referensi dunder -io sys untuk penyederhanaan lebih lanjut.

import io
import sys
import contextlib


@contextlib.contextmanager
def capture_output():
    output = {}
    try:
        # Redirect
        sys.stdout = io.TextIOWrapper(io.BytesIO(), sys.stdout.encoding)
        sys.stderr = io.TextIOWrapper(io.BytesIO(), sys.stderr.encoding)
        yield output
    finally:
        # Read
        sys.stdout.seek(0)
        sys.stderr.seek(0)
        output['stdout'] = sys.stdout.read()
        output['stderr'] = sys.stderr.read()
        sys.stdout.close()
        sys.stderr.close()

        # Restore
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__


with capture_output() as output:
    print('foo')
    sys.stderr.buffer.write(b'bar')

print('stdout: {stdout}'.format(stdout=output['stdout']))
print('stderr: {stderr}'.format(stderr=output['stderr']))

Outputnya adalah:

stdout: foo

stderr: bar
Ryne Everett
sumber