Запустите команду и получите ее stdout, stderr отдельно в режиме реального времени, как в терминале

были предоставлены два ответа, один из которых касается первых двух критериев и будет хорошо работать, когда вам просто нужны как stdout, так и stderr с использованием потоков и очереди. Другой ответ использует select, неблокирующий метод для чтения файловых дескрипторов, и pty, метод "обмануть" порожденный процесс, полагая, что он работает в реальном терминале, как если бы он был запущен из Bash напрямую, но может или не может иметь побочных эффектов. Я хотел бы принять оба ответа, потому что "правильный" метод действительно зависит от ситуации и от того, почему вы обрабатываете в первую очередь, но, увы, я мог принять только один.

Я пытаюсь найти способ в Python, чтобы запускать другие программы таким образом, что:

  1. stdout и stderr запускаемой программы могут быть зарегистрированы отдельно.
  2. stdout и stderr запускаемой программы могут быть просматривается в почти реальном времени, так что если дочерний процесс зависает, пользователь может видеть. (т. е. мы не ждем завершения исполнения печать stdout / stderr пользователю)
  3. бонусные критерии: запускаемая программа не знает, что она запускается через python, и, таким образом не будет делать неожиданных вещей (например, chunk его выход вместо печатать его в реальное временя, или выход потому что он требует стержня к просмотр его вывода). Этот маленький критерий в значительной степени означает, что нам понадобится чтобы использовать pty, я думаю.

вот что у меня есть до сих пор... Метод 1:

def method1(command):
    ## subprocess.communicate() will give us the stdout and stderr sepurately, 
    ## but we will have to wait until the end of command execution to print anything.
    ## This means if the child process hangs, we will never know....
    proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable='/bin/bash')
    stdout, stderr = proc.communicate() # record both, but no way to print stdout/stderr in real-time
    print ' ######### REAL-TIME ######### '
    ########         Not Possible
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print stdout
    print 'STDOUT:'
    print stderr

Способ 2

def method2(command):
    ## Using pexpect to run our command in a pty, we can see the child's stdout in real-time,
    ## however we cannot see the stderr from "curl google.com", presumably because it is not connected to a pty?
    ## Furthermore, I do not know how to log it beyond writing out to a file (p.logfile). I need the stdout and stderr
    ## as strings, not files on disk! On the upside, pexpect would give alot of extra functionality (if it worked!)
    proc = pexpect.spawn('/bin/bash', ['-c', command])
    print ' ######### REAL-TIME ######### '
    proc.interact()
    print ' ########## RESULTS ########## '
    ########         Not Possible

Способ 3:

def method3(command):
    ## This method is very much like method1, and would work exactly as desired
    ## if only proc.xxx.read(1) wouldn't block waiting for something. Which it does. So this is useless.
    proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable='/bin/bash')
    print ' ######### REAL-TIME ######### '
    out,err,outbuf,errbuf = '','','',''
    firstToSpeak = None
    while proc.poll() == None:
            stdout = proc.stdout.read(1) # blocks
            stderr = proc.stderr.read(1) # also blocks
            if firstToSpeak == None:
                if stdout != '': firstToSpeak = 'stdout'; outbuf,errbuf = stdout,stderr
                elif stderr != '': firstToSpeak = 'stderr'; outbuf,errbuf = stdout,stderr
            else:
                if (stdout != '') or (stderr != ''): outbuf += stdout; errbuf += stderr
                else:
                    out += outbuf; err += errbuf;
                    if firstToSpeak == 'stdout': sys.stdout.write(outbuf+errbuf);sys.stdout.flush()
                    else: sys.stdout.write(errbuf+outbuf);sys.stdout.flush()
                    firstToSpeak = None
    print ''
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print out
    print 'STDERR:'
    print err

чтобы попробовать эти методы, вам понадобится import sys,subprocess,pexpect

pexpect является pure-python и может быть с

sudo pip установить pexpect

Я думаю, что решение будет включать модуль Pty python , который является чем-то вроде черного искусства, которое я не могу найти никого, кто знает, как использовать. Возможно так знает :) В качестве хедз-апа я рекомендую вам использовать ' curl www.google.com-в качестве тестовой команды, потому что она почему-то выводит свой статус на stderr :D

обновление: Хорошо, поэтому библиотека pty не подходит для потребления человеком. Документы, по сути, являются исходным кодом. Любое представленное решение, которое блокирует, а не асинхронно, не будет работать здесь. Метод Threads / Queue от Padraic Cunningham отлично работает, хотя добавление поддержки pty невозможно - и это "грязно" (процитировать #python Freenode). Кажется, это единственное решение, подходящее для производственный стандартный код использует Twisted framework, который даже поддерживает pty как логический переключатель для запуска процессов точно так же, как если бы они были вызваны из оболочки. Но добавление Twisted в проект требует полной перезаписи всего кода. Это полный облом :/

3 ответов


stdout и stderr запускаемой программы могут регистрироваться отдельно.

вы не можете использовать pexpect потому что оба stdout и stderr идут к тому же pty и после этого их невозможно разделить.

stdout и stderr запускаемой программы можно просматривать в почти реальном времени, так что если дочерний процесс зависает, пользователь может видеть. (т. е. мы не ждем выполнения завершения печати stdout / stderr пользователю)

если выход подпроцесса не является tty, то вероятно, что он использует буферизацию блоков и поэтому, если он не производит много выхода, то это не будет "реальное время" например, если буфер равен 4K, то ваш родительский процесс Python ничего не увидит, пока дочерний процесс не напечатает символы 4K и буфер не переполнится или не будет сброшен явно (внутри подпроцесса). Этот буфер находится внутри дочернего процесса и нет стандартных способов управлять им извне. Вот изображение, которое показывает буферы stdio и буфер канала для command 1 | command2 shell pipeline:

pipe/stdio buffers

запускаемая программа не знает, что она запускается через python, и, таким образом, не будет делать неожиданных вещей (например, chunk его вывод вместо печати в режиме реального времени или выхода, потому что он требует терминала для просмотра его вывода).

кажется, вы имели в виду скорее всего, ваш дочерний процесс блокирует свой вывод, а не промывает каждую выходную строку как можно скорее, если вывод перенаправляется в канал (когда вы используете stdout=PIPE в Python). Это означает, что по умолчанию резьбонарезной или asyncio solutions не будет работать, как в вашем случае.

есть несколько вариантов, чтобы обойти его:

  • команда может принять аргумент командной строки, например grep --line-buffered или python -u, чтобы отключить буферизацию блоков.

  • stdbuf работает для некоторых программ то есть, вы можете запустить ['stdbuf', '-oL', '-eL'] + command используя решение threading или asyncio выше, и вы должны получить stdout, stderr отдельно, и строки должны появиться в почти реальном времени:

    #!/usr/bin/env python3
    import os
    import sys
    from select import select
    from subprocess import Popen, PIPE
    
    with Popen(['stdbuf', '-oL', '-e0', 'curl', 'www.google.com'],
               stdout=PIPE, stderr=PIPE) as p:
        readable = {
            p.stdout.fileno(): sys.stdout.buffer, # log separately
            p.stderr.fileno(): sys.stderr.buffer,
        }
        while readable:
            for fd in select(readable, [], [])[0]:
                data = os.read(fd, 1024) # read available
                if not data: # EOF
                    del readable[fd]
                else: 
                    readable[fd].write(data)
                    readable[fd].flush()
    
  • наконец, вы можете попробовать pty + select решение с двумя ptys:

    #!/usr/bin/env python3
    import errno
    import os
    import pty
    import sys
    from select import select
    from subprocess import Popen
    
    masters, slaves = zip(pty.openpty(), pty.openpty())
    with Popen([sys.executable, '-c', r'''import sys, time
    print('stdout', 1) # no explicit flush
    time.sleep(.5)
    print('stderr', 2, file=sys.stderr)
    time.sleep(.5)
    print('stdout', 3)
    time.sleep(.5)
    print('stderr', 4, file=sys.stderr)
    '''],
               stdin=slaves[0], stdout=slaves[0], stderr=slaves[1]):
        for fd in slaves:
            os.close(fd) # no input
        readable = {
            masters[0]: sys.stdout.buffer, # log separately
            masters[1]: sys.stderr.buffer,
        }
        while readable:
            for fd in select(readable, [], [])[0]:
                try:
                    data = os.read(fd, 1024) # read available
                except OSError as e:
                    if e.errno != errno.EIO:
                        raise #XXX cleanup
                    del readable[fd] # EIO means EOF on some systems
                else:
                    if not data: # EOF
                        del readable[fd]
                    else:
                        readable[fd].write(data)
                        readable[fd].flush()
    for fd in masters:
        os.close(fd)
    

    я не знаю, каковы побочные эффекты использования различных ptys на стандартный вывод, стандартный вывод. Вы можете попробовать, достаточно ли одного pty в вашем случае, например, set stderr=PIPE и использовать p.stderr.fileno() вместо masters[1]. комментарий sh источник предполагает, что есть проблемы, если stderr not in {STDOUT, pipe}


Если вы хотите прочитать из stderr и stdout и получить вывод отдельно, вы можете использовать поток с очередью, не слишком проверенный, но что-то вроде следующего:

import threading
import queue

def run(fd, q):
    for line in iter(fd.readline, ''):
        q.put(line)
    q.put(None)


def create(fd):
    q = queue.Queue()
    t = threading.Thread(target=run, args=(fd, q))
    t.daemon = True
    t.start()
    return q, t


process = Popen(["curl","www.google.com"], stdout=PIPE, stderr=PIPE,
                universal_newlines=True)

std_q, std_out = create(process.stdout)
err_q, err_read = create(process.stderr)

while std_out.is_alive() or err_read.is_alive():
        for line in iter(std_q.get, None):
            print(line)
        for line in iter(err_q.get, None):
            print(line)

в то время как ответ Дж.Ф. Себастьяна, безусловно, решает суть проблемы, я запускаю python 2.7 (который не был в исходных критериях), поэтому я просто бросаю это всем другим усталым путешественникам, которые просто хотят вырезать/вставить какой-то код. Я еще не тестировал это, но по всем командам, которые я пробовал, он работает отлично :) возможно, ты захочешь измениться .декодировать ('ascii') до .decode ('utf-8') - im все еще тестирует этот бит.

#!/usr/bin/env python2.7
import errno
import os
import pty
import sys
from select import select
import subprocess
stdout = ''
stderr = ''
command = 'curl google.com ; sleep 5 ; echo "hey"'
masters, slaves = zip(pty.openpty(), pty.openpty())
p = subprocess.Popen(command, stdin=slaves[0], stdout=slaves[0], stderr=slaves[1], shell=True, executable='/bin/bash')
for fd in slaves: os.close(fd)

readable = { masters[0]: sys.stdout, masters[1]: sys.stderr }
try:
    print ' ######### REAL-TIME ######### '
    while readable:
        for fd in select(readable, [], [])[0]:
            try: data = os.read(fd, 1024)
            except OSError as e:
                if e.errno != errno.EIO: raise
                del readable[fd]
            finally:
                if not data: del readable[fd]
                else:
                    if fd == masters[0]: stdout += data.decode('ascii')
                    else: stderr += data.decode('ascii')
                    readable[fd].write(data)
                    readable[fd].flush()
except: pass
finally:
    p.wait()
    for fd in masters: os.close(fd)
    print ''
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print stdout
    print 'STDERR:'
    print stderr