Obtendo saída em tempo real usando subprocesso

135

Estou tentando escrever um script de wrapper para um programa de linha de comando (svnadmin Verifique) que exibirá um bom indicador de progresso para a operação. Isso exige que eu seja capaz de ver cada linha de saída do programa empacotado assim que for lançada.

Imaginei que apenas executaria o programa usando subprocess.Popen, use stdout=PIPEe depois leria cada linha conforme ela chegasse e atuaria de acordo. No entanto, quando executei o código a seguir, a saída parecia estar armazenada em buffer em algum lugar, fazendo com que aparecesse em dois blocos, linhas 1 a 332 e depois 333 a 439 (a última linha de saída)

from subprocess import Popen, PIPE, STDOUT

p = Popen('svnadmin verify /var/svn/repos/config', stdout = PIPE, 
        stderr = STDOUT, shell = True)
for line in p.stdout:
    print line.replace('\n', '')

Depois de examinar um pouco a documentação do subprocesso, descobri o bufsizeparâmetroPopen , então tentei definir o bufsize como 1 (buffer cada linha) e 0 (sem buffer), mas nenhum valor parecia mudar a maneira como as linhas estavam sendo entregues.

Nesse ponto, eu estava começando a procurar por canudos, então escrevi o seguinte loop de saída:

while True:
    try:
        print p.stdout.next().replace('\n', '')
    except StopIteration:
        break

mas obteve o mesmo resultado.

É possível obter a saída do programa 'em tempo real' de um programa executado usando subprocesso? Existe alguma outra opção no Python que seja compatível com a frente (não exec*)?

Chris Lieb
fonte
1
Você tentou omitir a sydout=PIPEgravação para que o subprocesso grave diretamente no console, ignorando o processo pai?
S.Lott
5
A questão é que eu quero ler a saída. Se for enviado diretamente para o console, como eu poderia fazer isso? Além disso, não quero que o usuário veja a saída do programa empacotado, apenas minha saída.
31510 Chris Lieb
Então, por que uma exibição "em tempo real"? Eu não entendo o caso de uso.
S.Lott
8
Não use shell = True. Desnecessariamente invoca seu shell. Use p = Popen (['svnadmin', 'verifique', '/ var / svn / repos / config']], stdout = PIPE, stderr = STDOUT)
nosklo 30/04/2009
2
@ S.Lott Basicamente, o svnadmin Verifique imprime uma linha de saída para cada revisão que é verificada. Eu queria fazer um bom indicador de progresso que não causasse quantidades excessivas de produção. Mais ou menos como o wget, por exemplo
Chris Lieb

Respostas:

82

Eu tentei isso e, por algum motivo, enquanto o código

for line in p.stdout:
  ...

buffers agressivamente, a variante

while True:
  line = p.stdout.readline()
  if not line: break
  ...

não. Aparentemente, este é um bug conhecido: http://bugs.python.org/issue3907 (o problema agora está "fechado" em 29 de agosto de 2018)

Dave
fonte
Essa não é a única bagunça nas implementações antigas do Python IO. É por isso que o Py2.6 e o ​​Py3k acabaram com uma biblioteca IO completamente nova.
Tim Lin
3
Esse código será interrompido se o subprocesso retornar uma linha vazia. Uma solução melhor seria usar while p.poll() is Noneem vez de while True, e remover oif not line
exhuma
6
@ Exhuma: funciona bem. readline retorna "\ n" em uma linha vazia, que não é avaliada como verdadeira. ele retorna apenas uma string vazia quando o canal é fechado, que será quando o subprocesso terminar.
Alice Purcell #
1
@Dave Para referência futura: imprima linhas utf-8 em py2 + com print(line.decode('utf-8').rstrip()).
27616 Jonathan Komar
3
Também para ter uma leitura em tempo real da saída do processo, você precisará informar ao python que você NÃO deseja nenhum buffer. Caro Python, apenas me dê a saída diretamente. E aqui está como: Você precisa definir a variável de ambiente PYTHONUNBUFFERED=1. Isto é especialmente útil para as saídas que são infinita
George Pligoropoulos
38
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()
Corey Goldberg
fonte
1
@ Nbro provavelmente porque p.stdout.close()não está claro.
Anatoly techtonik
1
@nbro provavelmente porque o código foi fornecido sem explicação ...: /
Aaron Hall
3
O que é esse b ''?
ManuelSchneid3r
29

Você pode direcionar a saída do subprocesso para os fluxos diretamente. Exemplo simplificado:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Aidan Feldman
fonte
Isso permite que você também obtenha o conteúdo após o fato .communicate()? Ou o conteúdo é perdido para os fluxos stderr / stdout pai?
precisa saber é o seguinte
Não, nenhum communicate()método retornado CompletedProcess. Além disso, capture_outputé mutuamente exclusivo com stdoute stderr.
Aidan Feldman
20

Você pode tentar isso:

import subprocess
import sys

process = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

while True:
    out = process.stdout.read(1)
    if out == '' and process.poll() != None:
        break
    if out != '':
        sys.stdout.write(out)
        sys.stdout.flush()

Se você usar a linha de leitura em vez da leitura, haverá alguns casos em que a mensagem de entrada não será impressa. Experimente com um comando que requer uma entrada embutida e veja por si mesmo.

Nadia Alramli
fonte
Sim, usando readline () irá interromper a impressão (mesmo com a chamada sys.stdout.flush ())
Mark Ma
3
Isso deveria travar indefinidamente? Eu gostaria que uma determinada solução também incluísse o código padrão para editar o loop quando o subprocesso inicial for concluído. Desculpe, não importa quantas vezes eu investigue, o subprocesso etc. é algo que simplesmente não consigo trabalhar.
ThorSummoner
1
Por que testar '' quando em Python podemos usar apenas se não estiver fora?
Greg Sino
2
Esta é a melhor solução para trabalhos de longa duração. mas deve usar is not None e not! = None. Você não deve usar! = Com Nenhum.
Cari
O stderr também é exibido por isso?
Pieter Vogelaar
7

O subprocesso de streaming stdin e stdout com assíncio na postagem do blog em Python por Kevin McCarthy mostra como fazer isso com asyncio:

import asyncio
from asyncio.subprocess import PIPE
from asyncio import create_subprocess_exec


async def _read_stream(stream, callback):
    while True:
        line = await stream.readline()
        if line:
            callback(line)
        else:
            break


async def run(command):
    process = await create_subprocess_exec(
        *command, stdout=PIPE, stderr=PIPE
    )

    await asyncio.wait(
        [
            _read_stream(
                process.stdout,
                lambda x: print(
                    "STDOUT: {}".format(x.decode("UTF8"))
                ),
            ),
            _read_stream(
                process.stderr,
                lambda x: print(
                    "STDERR: {}".format(x.decode("UTF8"))
                ),
            ),
        ]
    )

    await process.wait()


async def main():
    await run("docker build -t my-docker-image:latest .")


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
Pablo
fonte
isso funciona com uma ligeira modificação no código postado
Jeef
Oi @Jeef, você pode apontar a correção para que eu possa atualizar a resposta?
Pablo
Oi, isso funcionou para mim, mas eu tive que adicionar o seguinte para se livrar de algumas mensagens de erro: import nest_asyncio; nest_asyncio.apply()e usar o comando shell, ou seja, em process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)vez de process = await create_subprocess_exec(...). Felicidades!
User319436
4

Problema de saída em tempo real resolvido: Encontrei um problema semelhante no Python, enquanto capturava a saída em tempo real do programa c. Eu adicionei " fflush (stdout) ;" no meu código C. Funcionou para mim. Aqui está o snip do código

<< Programa C >>

#include <stdio.h>
void main()
{
    int count = 1;
    while (1)
    {
        printf(" Count  %d\n", count++);
        fflush(stdout);
        sleep(1);
    }
}

<< Programa Python >>

#!/usr/bin/python

import os, sys
import subprocess


procExe = subprocess.Popen(".//count", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

while procExe.poll() is None:
    line = procExe.stdout.readline()
    print("Print:" + line)

<< SAÍDA >> Impressão: Contagem 1 Impressão: Contagem 2 Impressão: Contagem 3

Espero que ajude.

~ sairam

sairam
fonte
1
Esta foi a única coisa que realmente ajudou. Eu usei o mesmo código ( flush(stdout)) em C ++. Obrigado!
Gerhard Hagerer
Eu estava tendo o mesmo problema com um script python chamando outro script python como um subprocesso. Nas impressões do subprocesso, "flush" era necessário (print ("hello", flush = True) no python 3). Além disso, muitos exemplos ainda existem (2020) python 2, este é python 3, então +1
smajtkst 20/03
3

Encontrei o mesmo problema há algum tempo. Minha solução foi abandonar a iteração para o readmétodo, que retornará imediatamente, mesmo que seu subprocesso não termine a execução, etc.

Eli Courtwright
fonte
3

Dependendo do caso de uso, também convém desativar o buffer no próprio subprocesso.

Se o subprocesso for um processo Python, você poderá fazer isso antes da chamada:

os.environ["PYTHONUNBUFFERED"] = "1"

Ou, alternativamente, passe isso no envargumento paraPopen .

Caso contrário, se você estiver no Linux / Unix, poderá usar a stdbufferramenta. Por exemplo, como:

cmd = ["stdbuf", "-oL"] + cmd

Veja também aqui sobrestdbuf ou outras opções.

(Veja também aqui a mesma resposta.)

Albert
fonte
2

Usei esta solução para obter saída em tempo real em um subprocesso. Esse loop será interrompido assim que o processo for concluído, sem a necessidade de uma declaração de interrupção ou possível loop infinito.

sub_process = subprocess.Popen(my_command, close_fds=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while sub_process.poll() is None:
    out = sub_process.stdout.read(1)
    sys.stdout.write(out)
    sys.stdout.flush()
Jason Hedlund
fonte
5
é possível que isso saia do loop sem que o buffer stdout esteja vazio?
Jayjay
Procurei muito uma resposta adequada que não parasse após a conclusão! Eu encontrei isso como uma solução, adicionando if out=='': breakdepoisout = sub_process...
Sos
2

Encontrei esta função "plug-and-play" aqui . Trabalhou como um encanto!

import subprocess

def myrun(cmd):
    """from http://blog.kagesenshi.org/2008/02/teeing-python-subprocesspopen-output.html
    """
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = []
    while True:
        line = p.stdout.readline()
        stdout.append(line)
        print line,
        if line == '' and p.poll() != None:
            break
    return ''.join(stdout)
Deena
fonte
1
A adição de stderr=subprocess.STDOUTrealmente ajuda muito na captura de dados de streaming. Eu estou votando.
Khan
1
A principal carne aqui parece vir da resposta aceita
tripleee
2

Você pode usar um iterador sobre cada byte na saída do subprocesso. Isso permite a atualização embutida (as linhas que terminam com '\ r' substituem a linha de saída anterior) do subprocesso:

from subprocess import PIPE, Popen

command = ["my_command", "-my_arg"]

# Open pipe to subprocess
subprocess = Popen(command, stdout=PIPE, stderr=PIPE)


# read each byte of subprocess
while subprocess.poll() is None:
    for c in iter(lambda: subprocess.stdout.read(1) if subprocess.poll() is None else {}, b''):
        c = c.decode('ascii')
        sys.stdout.write(c)
sys.stdout.flush()

if subprocess.returncode != 0:
    raise Exception("The subprocess did not terminate correctly.")
rhyno183
fonte
2

No Python 3.x, o processo pode travar porque a saída é uma matriz de bytes em vez de uma sequência de caracteres. Certifique-se de decodificá-lo em uma string.

A partir do Python 3.6, você pode fazer isso usando o parâmetro encodingno Popen Constructor . O exemplo completo:

process = subprocess.Popen(
    'my_command',
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    shell=True,
    encoding='utf-8',
    errors='replace'
)

while True:
    realtime_output = process.stdout.readline()

    if realtime_output == '' and process.poll() is not None:
        break

    if realtime_output:
        print(realtime_output.strip(), flush=True)

Observe que esse código redirecionamentos stderr para stdoute manipula erros de saída .

pavelnazimok
fonte
1

O uso do pexpect [ http://www.noah.org/wiki/Pexpect ] com linhas de leitura sem bloqueio resolverá esse problema. Isso decorre do fato de que os pipes são armazenados em buffer e, portanto, a saída do aplicativo é armazenada em buffer pelo pipe; portanto, você não pode chegar a essa saída até que o buffer seja preenchido ou o processo morra.

Gabe
fonte
0

Solução completa:

import contextlib
import subprocess

# Unix, Windows and old Macintosh end-of-line
newlines = ['\n', '\r\n', '\r']
def unbuffered(proc, stream='stdout'):
    stream = getattr(proc, stream)
    with contextlib.closing(stream):
        while True:
            out = []
            last = stream.read(1)
            # Don't loop forever
            if last == '' and proc.poll() is not None:
                break
            while last not in newlines:
                # Don't loop forever
                if last == '' and proc.poll() is not None:
                    break
                out.append(last)
                last = stream.read(1)
            out = ''.join(out)
            yield out

def example():
    cmd = ['ls', '-l', '/']
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        # Make all end-of-lines '\n'
        universal_newlines=True,
    )
    for line in unbuffered(proc):
        print line

example()
Andres Restrepo
fonte
1
Como você está usando universal_newlines=Truea Popen()ligação, provavelmente não precisa colocar seu próprio tratamento também - esse é o objetivo da opção.
martineau
1
parece desnecessário complicado. Não resolve problemas de buffer. Veja os links na minha resposta .
JFS
Esta é a única maneira de obter o progresso do rsync em tempo real (- outbuf = L)! obrigado
Mohammadhzp
0

Este é o esqueleto básico que eu sempre uso para isso. Isso facilita a implementação de tempos limites e é capaz de lidar com processos de interrupção inevitáveis.

import subprocess
import threading
import Queue

def t_read_stdout(process, queue):
    """Read from stdout"""

    for output in iter(process.stdout.readline, b''):
        queue.put(output)

    return

process = subprocess.Popen(['dir'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           bufsize=1,
                           cwd='C:\\',
                           shell=True)

queue = Queue.Queue()
t_stdout = threading.Thread(target=t_read_stdout, args=(process, queue))
t_stdout.daemon = True
t_stdout.start()

while process.poll() is None or not queue.empty():
    try:
        output = queue.get(timeout=.5)

    except Queue.Empty:
        continue

    if not output:
        continue

    print(output),

t_stdout.join()
Badslacks
fonte
0

(Esta solução foi testada com o Python 2.7.15)
Você só precisa do sys.stdout.flush () após cada linha de leitura / gravação:

while proc.poll() is None:
    line = proc.stdout.readline()
    sys.stdout.write(line)
    # or print(line.strip()), you still need to force the flush.
    sys.stdout.flush()
dan
fonte
0

Poucas respostas sugerindo python 3.x ou pthon 2.x, o código abaixo funcionará para ambos.

 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,)
    stdout = []
    while True:
        line = p.stdout.readline()
        if not isinstance(line, (str)):
            line = line.decode('utf-8')
        stdout.append(line)
        print (line)
        if (line == '' and p.poll() != None):
            break
Djai
fonte