web-dev-qa-db-fra.com

Capturer la sortie standard d’un script dans Python

supposons qu'il y ait un script faisant quelque chose comme ceci:

# module writer.py
import sys

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

Supposons maintenant que je veuille capturer le résultat de la fonction write et le stocker dans une variable pour un traitement ultérieur. La solution naïve était:

# module mymodule.py
from writer import write

out = write()
print out.upper()

Mais ça ne marche pas. Je propose une autre solution et cela fonctionne, mais s'il vous plaît, laissez-moi savoir s'il existe un meilleur moyen de résoudre le problème. Merci

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
77
Paolo

Définir stdout est un moyen raisonnable de le faire. Une autre consiste à l'exécuter en tant que processus différent:

import subprocess

proc = subprocess.Popen(["python", "-c", "import writer; writer.write()"], stdout=subprocess.PIPE)
out = proc.communicate()[0]
print out.upper()
44
Matthew Flaschen

Voici une version de gestionnaire de contexte de votre code. Cela donne une liste de deux valeurs; le premier est stdout, le second est 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'
41
Jason Grout

Pour les futurs visiteurs: Python 3.4 contextlib fournit cela directement (voir aide de Python contextlib) ) via le redirect_stdout gestionnaire de contexte:

from contextlib import redirect_stdout
import io

f = io.StringIO()
with redirect_stdout(f):
    help(pow)
s = f.getvalue()
31
nodesr

Ou peut-être utiliser des fonctionnalités déjà présentes ...

from IPython.utils.capture import capture_output

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

c()

print c.stdout
10
dgrigonis

Ceci est la contrepartie décoratrice de mon code original.

writer.py reste le même:

import sys

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

mymodule.py légèrement modifié

from writer import write as _write
from decorators import capture

@capture
def write():
    return _write()

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

Et voici le décorateur:

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
9
Paolo

À partir de Python 3, vous pouvez également utiliser sys.stdout.buffer.write() pour écrire des chaînes d'octets (déjà) sur stdout (voir stdout dans Python 3 ). Lorsque vous faites cela, l'approche simple StringIO ne fonctionne pas car ni sys.stdout.encoding Ni sys.stdout.buffer Ne seraient disponibles.

En commençant par Python 2.6, vous pouvez utiliser l'API TextIOBase , qui inclut les attributs manquants:

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

Cette solution fonctionne pour Python 2> = 2.6 et Python 3. Veuillez noter que notre sys.stdout.write() n'accepte que les chaînes unicode et que sys.stdout.buffer.write() n'accepte que chaînes d'octets. Ce n'est peut-être pas le cas pour l'ancien code, mais c'est souvent le cas pour le code conçu pour être exécuté sur Python 2 et 3 sans modification.

Si vous devez prendre en charge un code qui envoie des chaînes d'octets à stdout directement sans utiliser stdout.buffer, vous pouvez utiliser cette variante:

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)

Vous n'avez pas besoin de définir le codage du tampon sys.stdout.encoding, mais cela vous aide lorsque vous utilisez cette méthode pour tester/comparer la sortie du script.

6
JonnyJD

La question ici (l'exemple montrant comment rediriger la sortie, pas la partie tee) utilise os.dup2 pour rediriger un flux au niveau du système d'exploitation. C'est bien, car cela s'appliquera également aux commandes que vous créez à partir de votre programme.

3
Jeremiah Willcock

J'aime la solution contextmanager, mais si vous avez besoin du tampon stocké avec le fichier ouvert et le support fileno, vous pouvez faire quelque chose comme ceci.

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

utilisation

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
3
Nathan Buckner

Je pense que vous devriez regarder ces quatre objets:

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

Exemple:

from writer import write

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

UPD: Comme Eric l'a dit dans un commentaire, il ne faut pas les utiliser directement, alors je l'ai copié et collé.

# 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")
3
Oleksandr Fedorov