capturando stdout em tempo real do subprocesso


87

Quero subprocess.Popen()rsync.exe no Windows e imprimir o stdout em Python.

Meu código funciona, mas não detecta o progresso até que uma transferência de arquivo seja concluída! Quero imprimir o progresso de cada arquivo em tempo real.

Usando Python 3.1 agora, desde que ouvi dizer que deve ser melhor para lidar com IO.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()


1
(Vindo do google?) Todos os PIPEs entrarão em conflito quando um dos bufferes dos PIPEs ficar cheio e não for lido. ex .: impasse de stdout quando stderr é preenchido. Nunca passe um PIPE que você não pretende ler.
Nasser Al-Wohaibi

Alguém poderia explicar por que você não pode simplesmente definir stdout como sys.stdout em vez de subprocess.PIPE?
Mike

Respostas:


98

Algumas regras básicas para subprocess.

  • Nunca use shell=True. Ele invoca desnecessariamente um processo shell extra para chamar seu programa.
  • Ao chamar processos, os argumentos são transmitidos como listas. sys.argvem python é uma lista, e também argvem C. Então, você passa uma lista para Popenpara chamar subprocessos, não uma string.
  • Não redirecione stderrpara um PIPEquando não estiver lendo.
  • Não redirecione stdinquando você não estiver escrevendo.

Exemplo:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

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

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Dito isso, é provável que o rsync armazene sua saída ao detectar que está conectado a um pipe em vez de um terminal. Este é o comportamento padrão - quando conectado a um pipe, os programas devem limpar explicitamente o stdout para resultados em tempo real, caso contrário, a biblioteca C padrão irá armazenar em buffer.

Para testar isso, tente executar o seguinte:

cmd = [sys.executable, 'test_out.py']

e crie um test_out.pyarquivo com o conteúdo:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

A execução desse subprocesso deve dar a você "Hello" e esperar 10 segundos antes de dar "World". Se isso acontecer com o código Python acima e não com rsync, isso significa que rsyncele próprio está armazenando a saída em buffer, então você está sem sorte.

Uma solução seria conectar-se diretamente a um pty, usando algo parecido pexpect.


12
shell=Falseé a coisa certa quando você constrói uma linha de comando, especialmente a partir de dados inseridos pelo usuário. Mas, no entanto, também shell=Trueé útil quando você obtém toda a linha de comando de uma fonte confiável (por exemplo, codificada no script).
Denis Otkidach,

10
@Denis Otkidach: Não acho que justifique o uso de shell=True. Pense nisso - você está invocando outro processo em seu sistema operacional, envolvendo alocação de memória, uso de disco, programação de processador, apenas para dividir uma string ! E um você se juntou !! Você pode dividir em python, mas é mais fácil escrever cada parâmetro separadamente de qualquer maneira. Além disso, usando uma lista significa que você não tem que escapar caracteres especiais do escudo: espaços, ;, >, <, &.. seus parâmetros podem conter esses caracteres e você não precisa se preocupar! Não vejo razão para usar shell=True, realmente, a menos que você esteja executando um comando somente shell.
nosklo

nosklo, que deve ser: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick: Não sei por que você faria essas operações como processos separados ... você pode cortar o conteúdo do arquivo e extrair o primeiro campo facilmente em python usando o csvmódulo. Mas, por exemplo, seu pipeline em python seria: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultObserve que você pode trabalhar com nomes de arquivos longos e caracteres especiais de shell sem ter que escapar, agora que o shell não está envolvido. Além disso, é muito mais rápido, pois há um processo a menos.
nosklo

11
use em for line in iter(p.stdout.readline, b'')vez de for line in p.stdoutno Python 2, caso contrário, as linhas não são lidas em tempo real, mesmo se o processo de origem não buffer sua saída.
jfs

41

Eu sei que este é um assunto antigo, mas agora há uma solução. Chame o rsync com a opção --outbuf = L. Exemplo:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
Isso funciona e deve ser votado para evitar que futuros leitores percorram toda a caixa de diálogo acima.
VectorVictor

1
@VectorVictor Não explica o que está acontecendo e por que está acontecendo. Pode ser que seu programa funcione, até: 1. você adicionar preexec_fn=os.setpgrppara fazer o programa sobreviver ao script pai 2. você pular a leitura do canal do processo 3. o processo produzir muitos dados, preenchendo o tubo 4. você ficar preso por horas , tentando descobrir por que o programa que você está executando é encerrado após um período de tempo aleatório . A resposta de @nosklo me ajudou muito.
danuker

15

No Linux, tive o mesmo problema para me livrar do buffer. Finalmente usei "stdbuf -o0" (ou, sem buffer de expect) para me livrar do buffer PIPE.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Eu poderia então usar select.select em stdout.

Veja também /unix/25372/


2
Para qualquer um que esteja tentando obter o código C stdout do Python, posso confirmar que essa solução foi a única que funcionou para mim. Para ser claro, estou falando sobre adicionar 'stdbuf', '-o0' à minha lista de comandos existente no Popen.
Reckless

Obrigado! stdbuf -o0provou ser realmente útil com um monte de testes pytest / pytest-bdd que escrevi para gerar um aplicativo C ++ e verificar se ele emite certas instruções de log. Sem stdbuf -o0, esses testes precisavam de 7 segundos para obter a saída (em buffer) do programa C ++. Agora eles funcionam quase que instantaneamente!
evadeflow

11

Dependendo do caso de uso, você também pode desabilitar o armazenamento em buffer no próprio subprocesso.

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

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

Ou, alternativamente, passe isso no envargumento para Popen.

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

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

Veja também aqui sobre stdbufou outras opções.


1
Você salva o meu dia, obrigado por PYTHONUNBUFFERED = 1
diewland

9
for line in p.stdout:
  ...

sempre bloqueia até o próximo avanço de linha.

Para comportamento em "tempo real", você deve fazer algo assim:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

O loop while é deixado quando o processo filho fecha seu stdout ou sai. read()/read(-1)bloquearia até que o processo filho fechasse seu stdout ou saísse.


1
incharnunca é Noneusado if not inchar:( read()retorna uma string vazia em EOF). a propósito, é pior for line in p.stdoutnão imprimir nem mesmo linhas completas em tempo real no Python 2 ( for line in iter (p.stdout.readline, '') `pode ser usado no lugar).
jfs

1
Eu testei isso com python 3.4 no osx e não funciona.
qed

1
@qed: for line in p.stdout:funciona no Python 3. Certifique-se de entender a diferença entre ''(string Unicode) e b''(bytes). Consulte Python: leia a entrada de streaming de subprocess.communicate ()
jfs

8

Seu problema é:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

o próprio iterador tem buffer extra.

Tente fazer assim:

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

5

Você não pode fazer com que o stdout imprima sem buffer em um pipe (a menos que você possa reescrever o programa que imprime no stdout), então aqui está minha solução:

Redirecione stdout para sterr, que não é armazenado em buffer. '<cmd> 1>&2'deve fazer isso. Abra o processo da seguinte maneira: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Você não pode distinguir de stdout ou stderr, mas obtém todas as saídas imediatamente.

Espero que isso ajude alguém a lidar com este problema.


4
Tentaste? Porque não funciona .. Se o stdout for armazenado em buffer nesse processo, ele não será redirecionado para o stderr da mesma forma que não é redirecionado para um PIPE ou arquivo ..
Filipe Pina

5
Isso é totalmente errado. O buffer stdout ocorre dentro do próprio programa. A sintaxe do shell 1>&2apenas muda para quais arquivos os descritores de arquivo apontam antes de iniciar o programa. O próprio programa não consegue distinguir entre redirecionar stdout para stderr ( 1>&2) ou vice-versa ( 2>&1), portanto, isso não terá efeito no comportamento de buffer do programa. E de qualquer forma, a 1>&2sintaxe é interpretada pelo shell. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)iria falhar porque você não especificou shell=True.
Will Manley

No caso de as pessoas estarem lendo isso: eu tentei usar stderr em vez de stdout, ele mostra exatamente o mesmo comportamento.
martinthenext

3

Altere o stdout do processo rsync para sem buffer.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

3
O armazenamento em buffer acontece no lado rsync, alterar o atributo bufsize no lado python não ajudará.
nosklo

14
Para qualquer pessoa que esteja procurando, a resposta de nosklo está completamente errada: a tela de progresso do rsync não é armazenada em buffer; o verdadeiro problema é que o subprocesso retorna um objeto de arquivo e a interface do iterador de arquivo tem um buffer interno mal documentado, mesmo com bufsize = 0, exigindo que você chame readline () repetidamente se precisar de resultados antes que o buffer seja preenchido.
Chris Adams

3

Para evitar o cache de saída, você pode tentar o pexpect,

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : Eu sei que essa questão é bem antiga, ainda fornecendo a solução que funcionou para mim.

PPS : obtive esta resposta de outra pergunta


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Estou escrevendo uma GUI para rsync em python e tenho os mesmos problemas. Esse problema me incomodou por vários dias, até que eu encontrei isso no pyDoc.

Se universal_newlines for True, os objetos de arquivo stdout e stderr são abertos como arquivos de texto no modo de nova linha universal. As linhas podem ser encerradas por qualquer uma das '\ n', convenção de fim de linha do Unix, '\ r', a antiga convenção do Macintosh ou '\ r \ n', a convenção do Windows. Todas essas representações externas são vistas como '\ n' pelo programa Python.

Parece que o rsync irá imprimir '\ r' quando a tradução estiver acontecendo.


1

Percebi que não há menção ao uso de um arquivo temporário como intermediário. A seguir, contorna os problemas de buffer, gerando um arquivo temporário e permite que você analise os dados vindos do rsync sem conectar a um pty. Testei o seguinte em uma máquina Linux, e a saída do rsync tende a ser diferente entre as plataformas, portanto, as expressões regulares para analisar a saída podem variar:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

não é em tempo real. Um arquivo não resolve o problema de buffer no lado do rsync.
jfs

tempfile.TemporaryFile pode deletar a si mesmo para facilitar a limpeza em caso de exceções
jfs

3
while not p.poll()leva a um loop infinito se o subprocesso sair com sucesso com 0, use em p.poll() is Nonevez disso
jfs

O Windows pode proibir a abertura de arquivos já abertos, portanto, open(file_name)pode falhar
jfs

1
Acabei de encontrar esta resposta, infelizmente apenas para Linux, mas funciona como um charme link. Então, eu apenas estendo meu comando da seguinte maneira: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argve chamo: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) e agora posso ler sem nenhum buffer
Arvid Terzibaschian

0

se você executar algo assim em um thread e salvar a propriedade ffmpeg_time em uma propriedade de um método para que você possa acessá-lo, funcionaria muito bem, eu recebo saídas como esta: a saída será como se você usar threading no tkinter

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

No Python 3, aqui está uma solução, que tira um comando da linha de comando e entrega strings decodificadas em tempo real à medida que são recebidas.

Receptor ( receiver.py):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Exemplo de programa simples que pode gerar saída em tempo real ( dummy_out.py):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Resultado:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.