Как воспроизвести поведение тройника в Python при использовании подпроцесса?

Я ищу решение Python, которое позволит мне сохранить вывод команды в файл, не скрывая его с консоли.

К вашему сведению: я спрашиваю о tee (в качестве утилиты командной строки Unix) и а не одноименная функция из модуля Python intertools.

Подробности

  • Решение Python (не вызывает tee, недоступно в Windows)
  • Мне не нужно вводить какие-либо данные в stdin для вызываемого процесса
  • Я не контролирую вызываемую программу. Все, что я знаю, это то, что он выведет что-то в stdout и stderr и вернется с кодом выхода.
  • Работать при вызове внешних программ (подпроцесс)
  • Работать как для stderr, так и для stdout
  • Возможность различать stdout и stderr, потому что я могу захотеть отобразить только один из них на консоли или я могу попытаться вывести stderr, используя другой цвет - это означает, что stderr = subprocess.STDOUT не будет работать.
  • Живой вывод (прогрессивный) - процесс может работать долго, и я не могу дождаться его завершения.
  • Код, совместимый с Python 3 (важно)

использованная литература

Вот несколько неполных решений, которые я нашел на данный момент:

http://blog.i18n.ro/wp-content/uploads/2010/06/Drawing_tee_py.png

Текущий код (вторая попытка)

#!/usr/bin/python
from __future__ import print_function

import sys, os, time, subprocess, io, threading
cmd = "python -E test_output.py"

from threading import Thread
class StreamThread ( Thread ):
    def __init__(self, buffer):
        Thread.__init__(self)
        self.buffer = buffer
    def run ( self ):
        while 1:
            line = self.buffer.readline()
            print(line,end="")
            sys.stdout.flush()
            if line == '':
                break

proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdoutThread = StreamThread(io.TextIOWrapper(proc.stdout))
stderrThread = StreamThread(io.TextIOWrapper(proc.stderr))
stdoutThread.start()
stderrThread.start()
proc.communicate()
stdoutThread.join()
stderrThread.join()

print("--done--")

#### test_output.py ####

#!/usr/bin/python
from __future__ import print_function
import sys, os, time

for i in range(0, 10):
    if i%2:
        print("stderr %s" % i, file=sys.stderr)
    else:
        print("stdout %s" % i, file=sys.stdout)
    time.sleep(0.1)
Real output
stderr 1
stdout 0
stderr 3
stdout 2
stderr 5
stdout 4
stderr 7
stdout 6
stderr 9
stdout 8
--done--

Ожидаемый результат заключался в том, чтобы строки были упорядочены. Замечание, изменение Popen для использования только одного PIPE не допускается, потому что в реальной жизни я хочу делать разные вещи с помощью stderr и stdout.

Кроме того, даже во втором случае мне не удалось получить подобие в реальном времени, фактически все результаты были получены по завершении процесса. По умолчанию Popen не должен использовать буферы (bufsize = 0).


person Community    schedule 08.06.2010    source источник
comment
comment
Возможный дубликат Python Popen: одновременная запись в стандартный вывод И файл журнала Голосование таким образом, потому что это вики сообщества :-)   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 30.08.2018


Ответы (6)


Я вижу, что это довольно старый пост, но на всякий случай кто-то все еще ищет способ сделать это:

proc = subprocess.Popen(["ping", "localhost"], 
                        stdout=subprocess.PIPE, 
                        stderr=subprocess.PIPE)

with open("logfile.txt", "w") as log_file:
  while proc.poll() is None:
     line = proc.stderr.readline()
     if line:
        print "err: " + line.strip()
        log_file.write(line)
     line = proc.stdout.readline()
     if line:
        print "out: " + line.strip()
        log_file.write(line)
person Community    schedule 27.07.2012
comment
У меня это сработало, хотя я нашел stdout, stderr = proc.communicate() более простым в использовании. - person Chase Seibert; 31.10.2012
comment
-1: это решение приводит к тупиковой ситуации для любого подпроцесса, который может генерировать достаточный вывод на stdout или stderr и где stdout / stderr не полностью синхронизированы. - person jfs; 18.02.2014
comment
@ J.F.Sebastian: Верно, но вы можете решить эту проблему, заменив readline() на readline(size). Я сделал нечто подобное на других языках. Ссылка: docs.python.org/3/library/io. html # io.TextIOBase.readline - person kevinarpe; 06.06.2015
comment
@kevinarpe ошибается. readline(size) не исправит тупик. stdout / stderr следует читать одновременно. См. Ссылки под вопросом, в которых показаны решения с использованием потоков или asyncio. - person jfs; 06.06.2015
comment
@ J.F.Sebastian: существует ли эта проблема, если меня интересует только чтение одного из потоков? - person ThorSummoner; 26.09.2015
comment
@ThorSummoner: естественно, нет проблем, если только один поток перенаправляется на канал. - person jfs; 13.02.2016
comment
Неужели это действительно гарантированно, чтобы не пропустить ни одного stdout? Допустим, proc выводит две последние строки в стандартный вывод в течение периода времени двух последующих proc.poll() вызовов: 1. proc.poll() == None - ›чтение одной строки -› еще одна строка существует в stdout, но процесс завершен - › 2. proc.poll() == returncode, и цикл while прерывается (пока в stdout остаются оставшиеся строки). Также рассмотрите возможность установки stderr на subprocess.STDOUT, чтобы избежать взаимоблокировок. - person dfrib; 05.04.2018

Если требование python 3.6 не является проблемой, теперь есть способ сделать это с помощью asyncio. Этот метод позволяет вам захватывать stdout и stderr по отдельности, но при этом оба потока будут передаваться на tty без использования потоков. Вот примерный план:

class RunOutput():
    def __init__(self, returncode, stdout, stderr):
        self.returncode = returncode
        self.stdout = stdout
        self.stderr = stderr

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

async def _stream_subprocess(cmd, stdin=None, quiet=False, echo=False) -> RunOutput:
    if isWindows():
        platform_settings = {'env': os.environ}
    else:
        platform_settings = {'executable': '/bin/bash'}

    if echo:
        print(cmd)

    p = await asyncio.create_subprocess_shell(cmd,
                                              stdin=stdin,
                                              stdout=asyncio.subprocess.PIPE,
                                              stderr=asyncio.subprocess.PIPE,
                                              **platform_settings)
    out = []
    err = []

    def tee(line, sink, pipe, label=""):
        line = line.decode('utf-8').rstrip()
        sink.append(line)
        if not quiet:
            print(label, line, file=pipe)

    await asyncio.wait([
        _read_stream(p.stdout, lambda l: tee(l, out, sys.stdout)),
        _read_stream(p.stderr, lambda l: tee(l, err, sys.stderr, label="ERR:")),
    ])

    return RunOutput(await p.wait(), out, err)


def run(cmd, stdin=None, quiet=False, echo=False) -> RunOutput:
    loop = asyncio.get_event_loop()
    result = loop.run_until_complete(
        _stream_subprocess(cmd, stdin=stdin, quiet=quiet, echo=echo)
    )

    return result

Приведенный выше код основан на этом сообщении в блоге: https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/

person Community    schedule 26.11.2019

Это простой перенос tee на Python.

import sys
sinks = sys.argv[1:]
sinks = [open(sink, "w") for sink in sinks]
sinks.append(sys.stderr)
while True:
  input = sys.stdin.read(1024)
  if input:
    for sink in sinks:
      sink.write(input)
  else:
    break

Я сейчас работаю на Linux, но это должно работать на большинстве платформ.


Теперь что касается части subprocess, я не знаю, как вы хотите «связать» stdin, stdout и stderr подпроцесса с вашими stdin, stdout, stderr и приемниками файлов, но я знаю, что вы можете сделать это:

import subprocess
callee = subprocess.Popen( ["python", "-i"],
                           stdin = subprocess.PIPE,
                           stdout = subprocess.PIPE,
                           stderr = subprocess.PIPE
                         )

Теперь вы можете получить доступ к callee.stdin, _ 12_ и _ 13_ как обычные файлы, позволяя вышеуказанному" решению "работать. Если вы хотите получить callee.returncode, вам необходимо сделайте дополнительный вызов callee.poll().

Будьте осторожны при записи в callee.stdin: если процесс завершился, когда вы это сделаете, может возникнуть ошибка (в Linux я получаю IOError: [Errno 32] Broken pipe).

person badp    schedule 08.06.2010
comment
Это неоптимально для Linux, поскольку Linux предоставляет специальный tee(f_in, f_out, len, flags) API, но это не в том дело, правда? - person badp; 08.06.2010
comment
Я обновил вопрос, проблема в том, что я не смог найти, как использовать подпроцесс, чтобы получать данные из двух каналов постепенно, а не все сразу в конце процесса. - person sorin; 08.06.2010
comment
Я знаю, что ваш код должен работать, но есть небольшое требование, которое нарушает всю логику: я хочу иметь возможность различать stdout и stderr, а это означает, что мне нужно читать их оба, но я не знаю, что будет получить новые данные. Взгляните на пример кода. - person sorin; 09.06.2010
comment
@Sorin, это означает, что вам придется либо использовать два потока. Один читает stdout, один читает stderr. Если вы собираетесь записать оба значения в один и тот же файл, вы можете установить блокировку приемников, когда вы начинаете чтение, и освободить ее после записи признака конца строки. : / - person badp; 09.06.2010
comment
Мне кажется, что использование потоков для этого не слишком привлекательно, может быть, мы найдем что-нибудь еще. Странно, что это обычная проблема, но полного решения для нее никто не предложил. - person sorin; 09.06.2010
comment
@badp Я пробовал использовать потоки, но это не сработало. Я обновляю вопрос, чтобы включить новый пример. - person sorin; 30.06.2010
comment
@Sorin Опубликованный вами результат упорядочен. У вас было line1 line3 line5 line7 line9 на stderr, line0 line2 line4 line6 line8 на stdout. Конечно, в этом прогоне поток stderr первым получил результат, что означало, что у вас было line1 line0 line3 line2 line5 line4... вместо line0 line1 line2 line3 line4 line5..., но вы не получили line0 line3 line5 line1 line2..., line4 line2 line1 line0 line6... или line0 liline1 line3 linne2 line3e5.... Боюсь, что для программы, которая должна принимать произвольный ввод, подобный недетерминизм неизбежен, если даже не необходим. - person badp; 30.06.2010

Вот как это можно сделать

import sys
from subprocess import Popen, PIPE

with open('log.log', 'w') as log:
    proc = Popen(["ping", "google.com"], stdout=PIPE, encoding='utf-8')
    while proc.poll() is None:
        text = proc.stdout.readline() 
        log.write(text)
        sys.stdout.write(text)
person Community    schedule 06.06.2019
comment
Для всех, кому интересно, ДА, вы можете использовать print() вместо sys.stdout.write(). :-) - person progyammer; 20.07.2019
comment
@progyammer print добавит дополнительный символ новой строки, который вам не нужен, когда вам нужно точно воспроизвести вывод. - person ivan_pozdeev; 27.10.2020
comment
Да, но print(line, end='') может решить проблему - person Danylo Zhydyk; 23.11.2020

Если вы не хотите взаимодействовать с процессом, вы можете использовать модуль подпроцесса.

Пример:

tester.py

import os
import sys

for file in os.listdir('.'):
    print file

sys.stderr.write("Oh noes, a shrubbery!")
sys.stderr.flush()
sys.stderr.close()

testing.py

import subprocess

p = subprocess.Popen(['python', 'tester.py'], stdout=subprocess.PIPE,
                     stdin=subprocess.PIPE, stderr=subprocess.PIPE)

stdout, stderr = p.communicate()
print stdout, stderr

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

person Wayne Werner    schedule 08.06.2010
comment
Это не показывает вам сообщения об ошибках в STDERR в контексте STDOUT, что может сделать отладку сценариев оболочки и т. Д. Практически невозможной. - person RobM; 01.07.2010
comment
Значение...? В этом скрипте все, что доставляется через STDERR, выводится на экран вместе с STDOUT. Если вы имеете в виду коды возврата, просто используйте p.poll() для их получения. - person Wayne Werner; 01.07.2010
comment
Это не удовлетворяет прогрессивному условию. - person ivan_pozdeev; 18.10.2019

Мое решение не изящно, но работает.

Вы можете использовать powershell, чтобы получить доступ к «тройнику» под WinOS.

import subprocess
import sys

cmd = ['powershell', 'ping', 'google.com', '|', 'tee', '-a', 'log.txt']

if 'darwin' in sys.platform:
    cmd.remove('powershell')

p = subprocess.Popen(cmd)
p.wait()
person Community    schedule 05.06.2019
comment
Выдает недопустимое сообщение об ошибке командной строки от ping в MacOS. - person ivan_pozdeev; 18.10.2019