使用子流程获取实时输出


135

我正在尝试为命令行程序(svnadmin verify)编写包装脚本,该脚本将显示该操作的良好进度指示器。这要求我能够立即看到包装程序输出的每一行。

我认为我只是使用subprocess.Popen,use 来执行程序stdout=PIPE,然后读取其中的每一行并据此进行操作。但是,当我运行以下代码时,输​​出似乎被缓冲在某处,导致它出现在两个块中,第1到332行,然后是333到439行(输出的最后一行)

from subprocess import Popen, PIPE, STDOUT

p = Popen('svnadmin verify /var/svn/repos/config', stdout = PIPE, 
        stderr = STDOUT, shell = True)
for line in p.stdout:
    print line.replace('\n', '')

在稍微了解一下子流程的文档之后,我发现了bufsize参数Popen,因此我尝试将bufsize设置为1(缓冲每行)和0(没有缓冲),但是两个值似乎都没有改变行的传递方式。

在这一点上,我开始精通吸管,因此编写了以下输出循环:

while True:
    try:
        print p.stdout.next().replace('\n', '')
    except StopIteration:
        break

但是得到了相同的结果。

是否可以获取使用子进程执行的程序的“实时”程序输出?Python中还有其他向前兼容的选项(不是exec*)吗?


1
您是否尝试过省略sydout=PIPE子进程以使子进程绕过父进程直接写入控制台?
S.Lott

5
问题是我想读取输出。如果直接输出到控制台,我该怎么做?另外,我不希望用户看到包装程序的输出,而只看到我的输出。
克里斯·利布

那为什么要“实时”显示呢?我没有用例。
S.Lott

8
不要使用shell = True。它不必要地调用您的shell。使用p = Popen(['svnadmin','verify','/ var / svn / repos / config'],stdout = PIPE,stderr = STDOUT)代替
nosklo

2
@ S.Lott基本上,svnadmin verify为每个已验证的修订版输出一行输出。我想做一个不错的进度指示器,它不会导致过多的输出。例如,有点像wget
克里斯·利勃

Answers:


82

我尝试了这个,由于某种原因,代码

for line in p.stdout:
  ...

积极地缓冲,变体

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

才不是。显然,这是一个已知的错误:http : //bugs.python.org/issue3907(从2018年8月29日开始,此问题已“关闭”)


这不是旧的Python IO实现中的唯一麻烦。这就是为什么Py2.6和Py3k拥有全新的IO库的原因。
蒂姆·林

3
如果子进程返回空行,则此代码将中断。更好的解决方案是使用while p.poll() is None代替while True,并删除if not line
exhuma

6
@exhuma:很好。readline在空行上返回“ \ n”,其结果不为true。它仅在管道关闭时(即子进程终止时)返回空字符串。
爱丽丝·珀赛尔

1
@Dave供将来参考:使用在py2 +中打印utf-8行print(line.decode('utf-8').rstrip())
乔纳森·科马尔

3
同样,为了实时实时读取进程的输出,您需要告诉python您不需要任何缓冲。亲爱的Python,直接给我输出。方法如下:您需要设置环境变量PYTHONUNBUFFERED=1。这对于无穷大的输出特别有用
George Pligoropoulos

38
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()

1
@nbro可能是因为p.stdout.close()不清楚。
anatoly techtonik

1
@nbro可能是因为没有给出解释就给出了代码...:/
亚伦·霍尔

3
这个b''是什么意思?
ManuelSchneid3r

29

您可以将子流程的输出直接定向到流。简化示例:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)

这是否允许您在事实发生后也获得内容.communicate()?还是内容丢失到父stderr / stdout流?
theferrit32 '19

不,communicate()对返回的方法没有方法CompletedProcess。另外,capture_outputstdout和是互斥的stderr
艾丹·费尔德曼

20

您可以尝试以下方法:

import subprocess
import sys

process = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

while True:
    out = process.stdout.read(1)
    if out == '' and process.poll() != None:
        break
    if out != '':
        sys.stdout.write(out)
        sys.stdout.flush()

如果您使用readline而不是read,则在某些情况下不会打印输入消息。尝试使用需要内联输入的命令并亲自查看。


是的,使用readline()将停止打印(即使调用sys.stdout.flush())
Mark Ma

3
这应该无限期地挂起吗?我希望给定的解决方案也包括样板代码,以在完成初始子过程时编辑循环。抱歉,无论我研究多少次,子流程等都是我无法工作的东西。
ThorSummoner

1
为什么要测试''”,如果在Python中我们可以不使用就可以使用?
格雷格·贝尔

2
这是长期工作的最佳解决方案。但是它应该使用的不是None而不是!= None。您不应该将!=与None一起使用。
卡里,2015年

这也显示stderr吗?
Pieter Vogelaar

7

流子stdin和stdout与ASYNCIO在Python的博客文章凯文·麦卡锡显示了如何ASYNCIO做到这一点:

import asyncio
from asyncio.subprocess import PIPE
from asyncio import create_subprocess_exec


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


async def run(command):
    process = await create_subprocess_exec(
        *command, stdout=PIPE, stderr=PIPE
    )

    await asyncio.wait(
        [
            _read_stream(
                process.stdout,
                lambda x: print(
                    "STDOUT: {}".format(x.decode("UTF8"))
                ),
            ),
            _read_stream(
                process.stderr,
                lambda x: print(
                    "STDERR: {}".format(x.decode("UTF8"))
                ),
            ),
        ]
    )

    await process.wait()


async def main():
    await run("docker build -t my-docker-image:latest .")


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

这个作品有轻微修改的代码发布
Jeef

嗨,@ Jeef,您能指出解决方法,以便我更新答案吗?
Pablo

嗨,这对我import nest_asyncio; nest_asyncio.apply()有用,但是我必须添加以下内容来消除一些错误消息:并使用shell命令,即process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)代替process = await create_subprocess_exec(...)。干杯!
user319436 '19

4

实时输出问题已解决:在捕获c程序的实时输出时,我在Python中确实遇到了类似的问题。我添加了“ fflush(stdout) ;” 在我的C代码中 它为我工作。这是代码片段

<< C程序>>

#include <stdio.h>
void main()
{
    int count = 1;
    while (1)
    {
        printf(" Count  %d\n", count++);
        fflush(stdout);
        sleep(1);
    }
}

<< Python程序>>

#!/usr/bin/python

import os, sys
import subprocess


procExe = subprocess.Popen(".//count", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

while procExe.poll() is None:
    line = procExe.stdout.readline()
    print("Print:" + line)

<<输出>>打印:计数1打印:计数2打印:计数3

希望能帮助到你。

〜塞拉姆


1
这是唯一真正有用的东西。我flush(stdout)在C ++中使用了相同的代码()。谢谢!
Gerhard Hagerer '18

我在调用另一个Python脚本作为子进程的python脚本遇到相同的问题。在子进程打印中,“ flush”是必需的(python 3中的print(“ hello”,flush = True))。另外,那里的许多示例仍然是(2020)python 2,这是python 3,所以+1
smajtkst


3

根据使用情况,您可能还想禁用子流程本身中的缓冲。

如果子进程将是Python进程,则可以在调用之前执行此操作:

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

或者将其作为env参数传递给Popen

否则,如果您使用的是Linux / Unix,则可以使用该stdbuf工具。例如:

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

另请参见这里stdbuf或其他选项。

(有关相同答案,请参见此处。)


2

我使用此解决方案在子流程上获得实时输出。该过程完成后,该循环将立即停止,不再需要break语句或可能的无限循环。

sub_process = subprocess.Popen(my_command, close_fds=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while sub_process.poll() is None:
    out = sub_process.stdout.read(1)
    sys.stdout.write(out)
    sys.stdout.flush()

5
有没有可能在没有标准输出缓冲区为空的情况下退出循环?
jayjay 2014年

我已经寻找了很多合适的答案,而这些答案并没有在完​​成后就挂了!我发现这是一个解决方案,加入if out=='': breakout = sub_process...
索斯

2

在此处找到此“即插即用”功能。像魅力一样工作!

import subprocess

def myrun(cmd):
    """from http://blog.kagesenshi.org/2008/02/teeing-python-subprocesspopen-output.html
    """
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = []
    while True:
        line = p.stdout.readline()
        stdout.append(line)
        print line,
        if line == '' and p.poll() != None:
            break
    return ''.join(stdout)

1
添加的stderr=subprocess.STDOUT实际上有很大帮助捕捉流数据。我对此表示赞同。
可汗

1
这里的主要牛肉似乎来自于被接受的答案
Tripleee '18

2

您可以在子进程的输出中的每个字节上使用迭代器。这允许从子进程进行内联更新(以'\ r'结尾的行覆盖先前的输出行):

from subprocess import PIPE, Popen

command = ["my_command", "-my_arg"]

# Open pipe to subprocess
subprocess = Popen(command, stdout=PIPE, stderr=PIPE)


# read each byte of subprocess
while subprocess.poll() is None:
    for c in iter(lambda: subprocess.stdout.read(1) if subprocess.poll() is None else {}, b''):
        c = c.decode('ascii')
        sys.stdout.write(c)
sys.stdout.flush()

if subprocess.returncode != 0:
    raise Exception("The subprocess did not terminate correctly.")

2

在Python 3.x中,该过程可能会挂起,因为输出是字节数组而不是字符串。确保将其解码为字符串。

从Python 3.6开始,您可以使用Popen Constructor中的参数encoding来实现。完整的例子:

process = subprocess.Popen(
    'my_command',
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    shell=True,
    encoding='utf-8',
    errors='replace'
)

while True:
    realtime_output = process.stdout.readline()

    if realtime_output == '' and process.poll() is not None:
        break

    if realtime_output:
        print(realtime_output.strip(), flush=True)

请注意,此代码重定向 stderrstdout处理输出错误


1

将pexpect [ http://www.noah.org/wiki/Pexpect ]与非阻塞的阅读行一起使用将解决此问题。这是由于管道是缓冲的,因此您的应用程序的输出将被管道缓冲,因此,直到缓冲填满或进程终止,您才能获得该输出。


0

完整的解决方案:

import contextlib
import subprocess

# Unix, Windows and old Macintosh end-of-line
newlines = ['\n', '\r\n', '\r']
def unbuffered(proc, stream='stdout'):
    stream = getattr(proc, stream)
    with contextlib.closing(stream):
        while True:
            out = []
            last = stream.read(1)
            # Don't loop forever
            if last == '' and proc.poll() is not None:
                break
            while last not in newlines:
                # Don't loop forever
                if last == '' and proc.poll() is not None:
                    break
                out.append(last)
                last = stream.read(1)
            out = ''.join(out)
            yield out

def example():
    cmd = ['ls', '-l', '/']
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        # Make all end-of-lines '\n'
        universal_newlines=True,
    )
    for line in unbuffered(proc):
        print line

example()

1
由于您使用universal_newlines=TruePopen()电话,你可能并不需要把自己的这些处理中,太-这是选择的关键所在。
martineau

1
似乎不必要的复杂。它不能解决缓冲问题。看到我的答案中的链接
jfs 2014年

这是我可以实时获取rsync进度输出的唯一方法(--outbuf = L)!谢谢
Mohammadhzp 2015年

0

这是我经常使用的基本骨架。它使实现超时变得容易,并且能够处理不可避免的挂起过程。

import subprocess
import threading
import Queue

def t_read_stdout(process, queue):
    """Read from stdout"""

    for output in iter(process.stdout.readline, b''):
        queue.put(output)

    return

process = subprocess.Popen(['dir'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           bufsize=1,
                           cwd='C:\\',
                           shell=True)

queue = Queue.Queue()
t_stdout = threading.Thread(target=t_read_stdout, args=(process, queue))
t_stdout.daemon = True
t_stdout.start()

while process.poll() is None or not queue.empty():
    try:
        output = queue.get(timeout=.5)

    except Queue.Empty:
        continue

    if not output:
        continue

    print(output),

t_stdout.join()

0

(此解决方案已通过Python 2.7.15进行了测试
),每行读/写后只需要sys.stdout.flush():

while proc.poll() is None:
    line = proc.stdout.readline()
    sys.stdout.write(line)
    # or print(line.strip()), you still need to force the flush.
    sys.stdout.flush()

0

很少有建议使用python 3.x或pthon 2.x的答案,下面的代码对两者都适用。

 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,)
    stdout = []
    while True:
        line = p.stdout.readline()
        if not isinstance(line, (str)):
            line = line.decode('utf-8')
        stdout.append(line)
        print (line)
        if (line == '' and p.poll() != None):
            break
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.