ler subprocesso stdout linha por linha

235

Meu script python usa subprocesso para chamar um utilitário linux que é muito barulhento. Eu quero armazenar toda a saída em um arquivo de log e mostrar algumas delas para o usuário. Eu pensei que o seguinte funcionaria, mas a saída não aparece no meu aplicativo até que o utilitário tenha produzido uma quantidade significativa de saída.

#fake_utility.py, just generates lots of output over time
import time
i = 0
while True:
   print hex(i)*512
   i += 1
   time.sleep(0.5)

#filters output
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
for line in proc.stdout:
   #the real code does filtering here
   print "test:", line.rstrip()

O comportamento que eu realmente quero é que o script de filtro imprima cada linha conforme é recebida do subprocesso. Tipo como o que teefaz, mas com código python.

o que estou perdendo? Isso é possível?


Atualizar:

Se a sys.stdout.flush()for adicionado ao fake_utility.py, o código terá o comportamento desejado no python 3.1. Estou usando o python 2.6. Você pensaria que o uso proc.stdout.xreadlines()funcionaria da mesma forma que o py3k, mas não.


Atualização 2:

Aqui está o código de trabalho mínimo.

#fake_utility.py, just generates lots of output over time
import sys, time
for i in range(10):
   print i
   sys.stdout.flush()
   time.sleep(0.5)

#display out put line by line
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
#works in python 3.0+
#for line in proc.stdout:
for line in iter(proc.stdout.readline,''):
   print line.rstrip()
deft_code
fonte
4
você pode usar em print line,vez de print line.rstrip()(nota: vírgula no final).
JFS
2
A atualização 2 afirma que funciona com o python 3.0+, mas usa a declaração de impressão antiga, portanto, não funciona com o python 3.0+.
Rooky
Nenhuma das respostas listadas aqui funcionou para mim, mas o stackoverflow.com/questions/5411780/… funcionou!
encaixotado

Respostas:

179

Já faz muito tempo desde a última vez que trabalhei com Python, mas acho que o problema está na declaração for line in proc.stdout, que lê toda a entrada antes de iterá-la. A solução é usar readline():

#filters output
import subprocess
proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)
while True:
  line = proc.stdout.readline()
  if not line:
    break
  #the real code does filtering here
  print "test:", line.rstrip()

É claro que você ainda precisa lidar com o buffer do subprocesso.

Nota: de acordo com a documentação, a solução com um iterador deve ser equivalente ao uso readline(), exceto pelo buffer de leitura antecipada, mas (ou exatamente por isso) a alteração proposta produziu resultados diferentes para mim (Python 2.5 no Windows XP).

Rômulo Ceccon
fonte
11
para file.readline()vs. for line in filever bugs.python.org/issue3907 (em suma: ele funciona em Python3; uso io.open()em Python 2.6+)
jfs
5
O teste mais pitônico para um EOF, de acordo com as "Recomendações de Programação" no PEP 8 ( python.org/dev/peps/pep-0008 ), seria 'se não for a linha:'.
Jason Mock
14
@naxa: para tubos: for line in iter(proc.stdout.readline, ''):.
JFS
3
@ Jan-PhilipGehrcke: sim. 1. você pode usar for line in proc.stdoutno Python 3 (não existe o bug de leitura antecipada) 2. '' != b''no Python 3 - não copie e cole o código cegamente - pense no que ele faz e como funciona.
jfs
2
@ JFSebastian: claro, a iter(f.readline, b'')solução é bastante óbvia (e também funciona no Python 2, se alguém estiver interessado). O objetivo do meu comentário não foi culpar sua solução (desculpe se ela apareceu assim, eu li isso também agora!), Mas descrever a extensão dos sintomas, que são bastante graves nesse caso (a maioria dos Py2 / 3 questões resultam em exceções, enquanto aqui um loop bem-comportado mudou para ser infinito, e a coleta de lixo luta para combater a inundação de objetos recém-criados, produzindo oscilações no uso da memória com longo período e grande amplitude).
Dr. Jan-Philip Gehrcke
45

Um pouco atrasado para a festa, mas fiquei surpreso ao não ver o que acho a solução mais simples aqui:

import io
import subprocess

proc = subprocess.Popen(["prog", "arg"], stdout=subprocess.PIPE)
for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):  # or another encoding
    # do something with line

(Isso requer Python 3.)

jbg
fonte
25
Eu gostaria de usar essa resposta, mas eu estou recebendo: AttributeError: 'file' object has no attribute 'readable' py2.7
Dan Garthwaite
3
Funciona com python 3
matanster
É evidente que este código não é válido para múltiplas razões PY3 / PY3 compatibilidade e risco real de ficar ValueError: Eu operação de E / S em arquivo fechado
Sorin
3
@sorin nenhuma dessas coisas o torna "inválido". Se você estiver escrevendo uma biblioteca que ainda precisa oferecer suporte ao Python 2, não use esse código. Mas muitas pessoas têm o luxo de poder usar o software lançado mais recentemente, há uma década. Se você tentar ler em um arquivo fechado, receberá essa exceção, independentemente de usar TextIOWrapperou não. Você pode simplesmente lidar com a exceção.
jbg
1
talvez você esteja atrasado para a festa, mas você responde está atualizado com a versão atual do Python, ty
Dusan Gligoric 16/01
20

De fato, se você selecionou o iterador, o armazenamento em buffer agora pode ser seu problema. Você poderia dizer ao python no subprocesso para não armazenar em buffer sua saída.

proc = subprocess.Popen(['python','fake_utility.py'],stdout=subprocess.PIPE)

torna-se

proc = subprocess.Popen(['python','-u', 'fake_utility.py'],stdout=subprocess.PIPE)

Eu preciso disso ao chamar python de dentro de python.

Steve Carter
fonte
14

Você deseja passar esses parâmetros extras para subprocess.Popen:

bufsize=1, universal_newlines=True

Então você pode iterar como no seu exemplo. (Testado com Python 3.5)

user1747134
fonte
2
@nicoulaj Deverá funcionar se estiver usando o pacote subprocess32.
Quantum7
4

Função que permite iterar simultaneamente stdoute stderrsimultaneamente, em tempo real, linha por linha

Caso precise obter o fluxo de saída para ambos stdoute stderrao mesmo tempo, você pode usar a seguinte função.

A função usa Filas para mesclar os dois pipes do Popen em um único iterador.

Aqui criamos a função read_popen_pipes():

from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
            except Empty:
                pass
            try:
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

read_popen_pipes() em uso:

import subprocess as sp


with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):

        # Do stuff with each line, e.g.:
        print(out_line, end='')
        print(err_line, end='')

    return p.poll() # return status-code
Rotareti
fonte
2

Você também pode ler linhas sem loop. Funciona em python3.6.

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
list_of_byte_strings = process.stdout.readlines()
aiven
fonte
1
Ou para converter em strings:list_of_strings = [x.decode('utf-8').rstrip('\n') for x in iter(process.stdout.readlines())]
ndtreviv 28/11/19
1

Eu tentei isso com python3 e funcionou, fonte

def output_reader(proc):
    for line in iter(proc.stdout.readline, b''):
        print('got line: {0}'.format(line.decode('utf-8')), end='')


def main():
    proc = subprocess.Popen(['python', 'fake_utility.py'],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)

    t = threading.Thread(target=output_reader, args=(proc,))
    t.start()

    try:
        time.sleep(0.2)
        import time
        i = 0

        while True:
        print (hex(i)*512)
        i += 1
        time.sleep(0.5)
    finally:
        proc.terminate()
        try:
            proc.wait(timeout=0.2)
            print('== subprocess exited with rc =', proc.returncode)
        except subprocess.TimeoutExpired:
            print('subprocess did not terminate in time')
    t.join()
shakram02
fonte
1

A seguinte modificação da resposta de Rômulo funciona para mim no Python 2 e 3 (2.7.12 e 3.6.1):

import os
import subprocess

process = subprocess.Popen(command, stdout=subprocess.PIPE)
while True:
  line = process.stdout.readline()
  if line != '':
    os.write(1, line)
  else:
    break
mdh
fonte
0

Não sei quando isso foi adicionado ao módulo de subprocesso, mas com o Python 3 você deve usar bem proc.stdout.splitlines():

for line in proc.stdout.splitlines():
   print "stdout:", line
StefanQ
fonte