Imprime constantemente a saída do subprocesso enquanto o processo está em execução

202

Para iniciar programas dos meus scripts Python, estou usando o seguinte método:

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

Então, quando inicio um processo como Process.execute("mvn clean install"), meu programa aguarda até que o processo termine e só então recebo a saída completa do meu programa. Isso é chato se eu estiver executando um processo que demora um pouco para terminar.

Posso deixar meu programa escrever a saída do processo linha por linha, pesquisando a saída do processo antes que ela termine em um loop ou algo assim?

** [EDIT] Desculpe, não pesquisei muito bem antes de postar esta pergunta. Rosquear é realmente a chave. Encontrei um exemplo aqui que mostra como fazê-lo: ** Python Subprocess.Popen from a thread

Ingo Fischer
fonte
Thread em vez de subprocesso, eu acho #
Ant
9
Não, você não precisa de threads. Toda a ideia de tubulação funciona porque você pode obter leitura / gravação dos processos enquanto eles estão em execução.
tokland

Respostas:

264

Você pode usar o iter para processar linhas assim que as saídas de comandá-los: lines = iter(fd.readline, ""). Aqui está um exemplo completo mostrando um caso de uso típico (obrigado a @jfs por ajudar):

from __future__ import print_function # Only Python 2.x
import subprocess

def execute(cmd):
    popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
    for stdout_line in iter(popen.stdout.readline, ""):
        yield stdout_line 
    popen.stdout.close()
    return_code = popen.wait()
    if return_code:
        raise subprocess.CalledProcessError(return_code, cmd)

# Example
for path in execute(["locate", "a"]):
    print(path, end="")
tokland
fonte
24
Eu tentei esse código (com um programa que leva um tempo significativo para ser executado) e posso confirmar que ele gera linhas conforme são recebidas, em vez de esperar pela conclusão da execução. Esta é a resposta superior imo.
Andrew Martin
11
Nota: No Python 3, você pode usar for line in popen.stdout: print(line.decode(), end=''). Para dar suporte ao Python 2 e 3, use os bytes literais: b''caso contrário, lines_iteratornunca termina no Python 3.
jfs
3
O problema dessa abordagem é que, se o processo parar um pouco sem escrever nada no stdout, não haverá mais entrada para leitura. Você precisará de um loop para verificar se o processo terminou ou não. Eu tentei isso usando subprocess32 em Python 2.7
Har
7
deve funcionar. Para ele polonês, você pode adicionar bufsize=1(pode melhorar o desempenho em Python 2), perto do popen.stdouttubo explicitamente (sem esperar que a coleta de lixo para cuidar dele) e aumento subprocess.CalledProcessError(como check_call(), check_output()fazer). A printdeclaração é diferente no Python 2 e 3: você pode usar o softspace hack print line,(nota: vírgula) para evitar duplicar todas as novas linhas como seu código e transmitir universal_newlines=Trueno Python 3, para obter texto em vez de resposta relacionada a bytes .
JFS
6
@binzhang Isso não é um erro, o stdout é armazenado em buffer por padrão nos scripts Python (também para muitas ferramentas Unix). Tente execute(["python", "-u", "child_thread.py"]). Mais informações: stackoverflow.com/questions/14258500/…
tokland 6/6
84

Ok, eu consegui resolvê-lo sem threads (qualquer sugestão de por que usar threads seria melhor é apreciada) usando um trecho desta pergunta Interceptando o stdout de um subprocesso enquanto ele está sendo executado

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    # Poll process for new output until finished
    while True:
        nextline = process.stdout.readline()
        if nextline == '' and process.poll() is not None:
            break
        sys.stdout.write(nextline)
        sys.stdout.flush()

    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)
Ingo Fischer
fonte
3
Mesclar o código do ifischer e do tokland funciona muito bem (eu tive que mudar print line,para sys.stdout.write(nextline); sys.stdout.flush(). Caso contrário, ele imprimiria a cada duas linhas. Então, novamente, isso está usando a interface do Notebook do IPython, então talvez algo mais estivesse acontecendo - independentemente de chamar explicitamente as flush()obras
eacousineau 14/10
3
senhor você é meu salva-vidas !! muito estranho que este tipo de coisas não são build-in na biblioteca em si .. porque se eu escrever cliapp, eu quero mostrar tudo o que está processando em malha instantaneamente .. s'rsly ..
holms
3
Esta solução pode ser modificada para imprimir constantemente a saída e os erros? Se eu mudar stderr=subprocess.STDOUTpara stderr=subprocess.PIPEe depois ligar process.stderr.readline()de dentro do loop, parece que estou em conflito com o próprio conflito que é alertado na documentação do subprocessmódulo.
Davidrmcharles
7
@DavidCharles Acho que o que você está procurando é stdout=subprocess.PIPE,stderr=subprocess.STDOUTcapturar o stderr e acredito (mas ainda não testei) que ele também captura o stdin.
Andrew Martin
obrigado por aguardar o código de saída. Não sabia como trabalhar com isso
Vitaly Isaev
68

Para imprimir a saída do subprocesso linha por linha assim que seu buffer stdout for liberado no Python 3:

from subprocess import Popen, PIPE, CalledProcessError

with Popen(cmd, stdout=PIPE, bufsize=1, universal_newlines=True) as p:
    for line in p.stdout:
        print(line, end='') # process line here

if p.returncode != 0:
    raise CalledProcessError(p.returncode, p.args)

Aviso: você não precisa p.poll()- o loop termina quando eof é atingido. E você não precisaiter(p.stdout.readline, '') - o bug de leitura antecipada foi corrigido no Python 3.

Veja também, Python: leia a entrada de streaming de subprocess.communicate () .

jfs
fonte
3
Esta solução funcionou para mim. A solução aceita fornecida acima continuou imprimindo linhas em branco para mim.
Codename
3
Eu tive que adicionar sys.stdout.flush () para obter impressões imediatamente.
Codename
3
@ Nome do código: você não precisa sys.stdout.flush()no pai - o stdout é buffer de linha se não for redirecionado para um arquivo / canal e, portanto, a impressão linelibera o buffer automaticamente. Você também não precisa sys.stdout.flush()do filho - passe -ua opção de linha de comando.
JFS
1
@ Nome do código: se você deseja usar >, execute python -u your-script.py > some-file. Aviso: -uopção que eu mencionei acima (não precisa usar sys.stdout.flush()).
JFS
1
@mvidelgauz não há necessidade de ligar p.wait()- é chamado na saída do withbloco. Use p.returncode.
JFS
8

Na verdade, existe uma maneira muito simples de fazer isso quando você deseja imprimir a saída:

import subprocess
import sys

def execute(command):
    subprocess.check_call(command, stdout=sys.stdout, stderr=subprocess.STDOUT)

Aqui, estamos simplesmente apontando o subprocesso para nosso próprio stdout e usando a API de êxito ou exceção existente.

Andrew Ring
fonte
1
Esta solução é mais simples e mais limpa que a solução da @ tokland, para Python 3.6. Notei que o argumento shell = True não é necessário.
Good Will
Boa captura, boa vontade. Removidoshell=True
Andrew Ring
Muito astucioso e funciona perfeitamente com pouco código. Talvez você deva redirecionar o subprocesso stderr para sys.stderr também?
Manu
Manu você certamente pode. Aqui não, porque a tentativa na pergunta estava redirecionando o stderr para o stdout.
Andrew Ring
Você pode explicar qual é a diferença entre sys.stdout e subprocess.STDOUT?
Ron Serruya
7

@tokland

tentei seu código e o corrigi para 3,4 e o Windows dir.cmd é um comando dir simples, salvo como arquivo cmd

import subprocess
c = "dir.cmd"

def execute(command):
    popen = subprocess.Popen(command, stdout=subprocess.PIPE,bufsize=1)
    lines_iterator = iter(popen.stdout.readline, b"")
    while popen.poll() is None:
        for line in lines_iterator:
            nline = line.rstrip()
            print(nline.decode("latin"), end = "\r\n",flush =True) # yield line

execute(c)
user3759376
fonte
3
você pode simplificar seu código . iter()e end='\r\n'são desnecessários. O Python usa o modo de novas linhas universais por padrão, ou seja, qualquer um '\n'é convertido '\r\n'durante a impressão. 'latin'provavelmente é uma codificação errada, você pode usar universal_newlines=Truepara obter saída de texto no Python 3 (decodificado usando a codificação preferida da localidade). Não pare .poll(), pode haver dados não lidos em buffer. Se o script Python estiver sendo executado em um console, sua saída será com buffer de linha; você pode forçar o buffer de linha usando a -uopção - você não precisa flush=Trueaqui.
jfs
4

Caso alguém queira ler ambos stdoute stderrao mesmo tempo usando threads, é isso que eu criei:

import threading
import subprocess
import Queue

class AsyncLineReader(threading.Thread):
    def __init__(self, fd, outputQueue):
        threading.Thread.__init__(self)

        assert isinstance(outputQueue, Queue.Queue)
        assert callable(fd.readline)

        self.fd = fd
        self.outputQueue = outputQueue

    def run(self):
        map(self.outputQueue.put, iter(self.fd.readline, ''))

    def eof(self):
        return not self.is_alive() and self.outputQueue.empty()

    @classmethod
    def getForFd(cls, fd, start=True):
        queue = Queue.Queue()
        reader = cls(fd, queue)

        if start:
            reader.start()

        return reader, queue


process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdoutReader, stdoutQueue) = AsyncLineReader.getForFd(process.stdout)
(stderrReader, stderrQueue) = AsyncLineReader.getForFd(process.stderr)

# Keep checking queues until there is no more output.
while not stdoutReader.eof() or not stderrReader.eof():
   # Process all available lines from the stdout Queue.
   while not stdoutQueue.empty():
       line = stdoutQueue.get()
       print 'Received stdout: ' + repr(line)

       # Do stuff with stdout line.

   # Process all available lines from the stderr Queue.
   while not stderrQueue.empty():
       line = stderrQueue.get()
       print 'Received stderr: ' + repr(line)

       # Do stuff with stderr line.

   # Sleep for a short time to avoid excessive CPU use while waiting for data.
   sleep(0.05)

print "Waiting for async readers to finish..."
stdoutReader.join()
stderrReader.join()

# Close subprocess' file descriptors.
process.stdout.close()
process.stderr.close()

print "Waiting for process to exit..."
returnCode = process.wait()

if returnCode != 0:
   raise subprocess.CalledProcessError(returnCode, command)

Eu só queria compartilhar isso, pois acabei tentando fazer algo semelhante, mas nenhuma das respostas resolveu meu problema. Espero que ajude alguém!

Observe que, no meu caso de uso, um processo externo mata o processo que nós Popen().

Vai
fonte
1
Eu tive que usar algo quase exatamente assim para python2. Embora algo como isso deva ter sido fornecido no python2, não é assim que algo assim esteja absolutamente correto.
precisa
3

Para quem tenta as respostas a esta pergunta para obter o stdout de um script Python, observe que o Python armazena em buffer seu stdout e, portanto, pode demorar um pouco para vê-lo.

Isso pode ser corrigido adicionando o seguinte após cada gravação stdout no script de destino:

sys.stdout.flush()
user1379351
fonte
1
Mas rodar o Python como um subprocesso do Python é uma loucura em primeiro lugar. Seu script deve simplesmente importo outro script; examine multiprocessingou threadingse precisar de execução paralela.
Tripleee
3
@triplee Existem vários cenários em que a execução do Python como um subprocesso do Python é apropriada. Eu tenho vários scripts em lote python que desejo executar sequencialmente, diariamente. Eles podem ser orquestrados por um script Python mestre que inicia a execução e são enviados por e-mail se o script filho falhar. Cada script é protegido por sandbox do outro - sem conflitos de nomes. Não estou paralelizando, portanto, o multiprocessamento e o encadeamento não são relevantes.
precisa saber é o seguinte
Você também pode iniciar outro programa de python usando um executável python diferente do que a de que o programa python principal está sendo executado em, por exemplo,subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine
3

Em Python> = 3.5, o subprocess.runWorks funciona para mim:

import subprocess

cmd = 'echo foo; sleep 1; echo foo; sleep 2; echo foo'
subprocess.run(cmd, shell=True)

(obter a saída durante a execução também funciona sem shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run

user7017793
fonte
2
Isso não é "durante a execução". A subprocess.run()chamada retornará apenas quando o subprocesso terminar a execução.
tripleee
1
Você pode explicar como não é "durante a execução"? Algo como >>> import subprocess; subprocess.run('top')também parece imprimir "durante a execução" (e o topo nunca termina). Talvez eu não esteja entendendo alguma diferença sutil?
user7017793
Se você redirecionar a saída de volta para Python, por exemplo, stdout=subprocess.PIPEvocê poderá lê-la somente após a topconclusão. Seu programa Python é bloqueado durante a execução do subprocesso.
Tripleee
1
Certo, isso faz sentido. O runmétodo ainda funciona se você estiver interessado apenas em ver a saída conforme ela é gerada. Se você deseja fazer algo com a saída em python de forma assíncrona, está certo de que não funciona.
user7017793
3

Para responder à pergunta original, a melhor maneira de o IMO é apenas redirecionar o subprocesso stdoutdiretamente para o programa stdout(opcionalmente, o mesmo pode ser feito stderr, como no exemplo abaixo)

p = Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
p.communicate()
Alleo
fonte
3
Não especificando nada para stdoute stderrfaz a mesma coisa com menos código. Embora eu suponha que explícito é melhor que implícito.
tripleee
1

Esse PoC lê constantemente a saída de um processo e pode ser acessado quando necessário. Somente o último resultado é mantido, todas as outras saídas são descartadas, impedindo o PIPE de ficar sem memória:

import subprocess
import time
import threading
import Queue


class FlushPipe(object):
    def __init__(self):
        self.command = ['python', './print_date.py']
        self.process = None
        self.process_output = Queue.LifoQueue(0)
        self.capture_output = threading.Thread(target=self.output_reader)

    def output_reader(self):
        for line in iter(self.process.stdout.readline, b''):
            self.process_output.put_nowait(line)

    def start_process(self):
        self.process = subprocess.Popen(self.command,
                                        stdout=subprocess.PIPE)
        self.capture_output.start()

    def get_output_for_processing(self):
        line = self.process_output.get()
        print ">>>" + line


if __name__ == "__main__":
    flush_pipe = FlushPipe()
    flush_pipe.start_process()

    now = time.time()
    while time.time() - now < 10:
        flush_pipe.get_output_for_processing()
        time.sleep(2.5)

    flush_pipe.capture_output.join(timeout=0.001)
    flush_pipe.process.kill()

print_date.py

#!/usr/bin/env python
import time

if __name__ == "__main__":
    while True:
        print str(time.time())
        time.sleep(0.01)

saída: Você pode ver claramente que só há saída a partir do intervalo ~ 2,5s, nada entre elas.

>>>1520535158.51
>>>1520535161.01
>>>1520535163.51
>>>1520535166.01
Robert Nagtegaal
fonte
0

Isso funciona pelo menos em Python3.4

import subprocess

process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
for line in process.stdout:
    print(line.decode().strip())
arod
fonte
1
Isso tem o problema que ele bloqueia no loop até que o processo termine a execução.
tripleee
0

Nenhuma das respostas aqui abordou todas as minhas necessidades.

  1. Sem threads para stdout (sem filas etc.)
  2. Sem bloqueio, pois preciso verificar se há outras coisas acontecendo
  3. Use o PIPE conforme necessário para fazer várias coisas, por exemplo, saída de fluxo, gravação em um arquivo de log e retorno de uma cópia em seqüência da saída.

Um pouco de experiência: estou usando um ThreadPoolExecutor para gerenciar um pool de threads, cada um iniciando um subprocesso e executando-os simultaneamente. (No Python2.7, mas isso também deve funcionar na versão 3.x mais recente). Eu não quero usar threads apenas para reunir a saída, pois quero o maior número possível de outras coisas (um pool de 20 processos usaria 40 threads apenas para executar; 1 para o processo e 1 para stdout ... e mais se você quiser stderr eu acho)

Estou retirando muitas exceções e outras aqui, então isso se baseia no código que funciona na produção. Espero que não estraguei tudo na cópia e colar. Além disso, feedback muito bem-vindo!

import time
import fcntl
import subprocess
import time

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

# Make stdout non-blocking when using read/readline
proc_stdout = proc.stdout
fl = fcntl.fcntl(proc_stdout, fcntl.F_GETFL)
fcntl.fcntl(proc_stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)

def handle_stdout(proc_stream, my_buffer, echo_streams=True, log_file=None):
    """A little inline function to handle the stdout business. """
    # fcntl makes readline non-blocking so it raises an IOError when empty
    try:
        for s in iter(proc_stream.readline, ''):   # replace '' with b'' for Python 3
            my_buffer.append(s)

            if echo_streams:
                sys.stdout.write(s)

            if log_file:
                log_file.write(s)
    except IOError:
        pass

# The main loop while subprocess is running
stdout_parts = []
while proc.poll() is None:
    handle_stdout(proc_stdout, stdout_parts)

    # ...Check for other things here...
    # For example, check a multiprocessor.Value('b') to proc.kill()

    time.sleep(0.01)

# Not sure if this is needed, but run it again just to be sure we got it all?
handle_stdout(proc_stdout, stdout_parts)

stdout_str = "".join(stdout_parts)  # Just to demo

Tenho certeza de que há custos adicionais sendo adicionados aqui, mas isso não é uma preocupação no meu caso. Funcionalmente, ele faz o que eu preciso. A única coisa que não resolvi é por que isso funciona perfeitamente para mensagens de log, mas vejo algumas printmensagens aparecerem mais tarde e de uma só vez.

Rafe
fonte
-2

No Python 3.6, usei isso:

import subprocess

cmd = "command"
output = subprocess.call(cmd, shell=True)
print(process)
Rajiv Sharma
fonte
1
Esta não é uma resposta para esta pergunta em particular. Aguardar a conclusão do subprocesso antes de obter sua saída é específica e precisamente o que o OP está tentando evitar. A função legada antiga subprocess.call()possui algumas verrugas que são corrigidas pelas funções mais recentes; no Python 3.6 você geralmente usaria subprocess.run()para isso; por conveniência, a função de invólucro antiga subprocess.check_output()também está disponível - ela retorna a saída real do processo (esse código retornaria apenas o código de saída, mas mesmo assim imprimia algo indefinido).
Tripleee