将子进程输出显示到标准输出并重定向

17

我正在使用 Python 的 subprocess 模块运行一个脚本。目前我使用的代码是:

p = subprocess.Popen('/path/to/script', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = p.communicate()

我随后将结果输出到stdout。虽然这样做没问题,但由于脚本需要很长时间才能完成,我想要从脚本实时地输出到stdout。我管道化输出的原因是为了解析它。


将Python子进程的输出同时输出到文件和终端? - jfs
如果您不需要访问Popen的所有低级选项,可以尝试使用subprocess.call(['/path/to/script'])。默认情况下,输出应流式传输到stdout。 - Lukeclh
@Lukeclh: call('/path/to/script') 会显示输出,但您无法同时捕获它(以后像 OP 要求的那样解析它)。 - jfs
4个回答

17
为了将子进程的标准输出保存到变量中以进行进一步处理,并在子进程运行时实时显示到达的输出
#!/usr/bin/env python3
from io import StringIO
from subprocess import Popen, PIPE

with Popen('/path/to/script', stdout=PIPE, bufsize=1,
           universal_newlines=True) as p, StringIO() as buf:
    for line in p.stdout:
        print(line, end='')
        buf.write(line)
    output = buf.getvalue()
rc = p.returncode

如果要同时保存子进程的标准输出和标准错误流,就需要更复杂的处理方式,因为你应该同时消耗两个流以避免死锁

stdout_buf, stderr_buf = StringIO(), StringIO()
rc =  teed_call('/path/to/script', stdout=stdout_buf, stderr=stderr_buf,
                universal_newlines=True)
output = stdout_buf.getvalue()
...

teed_call()在这里定义。


更新:这里是一个更简单的asyncio版本


旧版:

这是一个基于 tulipchild_process.py 示例 的单线程解决方案:

import asyncio
import sys
from asyncio.subprocess import PIPE

@asyncio.coroutine
def read_and_display(*cmd):
    """Read cmd's stdout, stderr while displaying them as they arrive."""
    # start process
    process = yield from asyncio.create_subprocess_exec(*cmd,
            stdout=PIPE, stderr=PIPE)

    # read child's stdout/stderr concurrently
    stdout, stderr = [], [] # stderr, stdout buffers
    tasks = {
        asyncio.Task(process.stdout.readline()): (
            stdout, process.stdout, sys.stdout.buffer),
        asyncio.Task(process.stderr.readline()): (
            stderr, process.stderr, sys.stderr.buffer)}
    while tasks:
        done, pending = yield from asyncio.wait(tasks,
                return_when=asyncio.FIRST_COMPLETED)
        assert done
        for future in done:
            buf, stream, display = tasks.pop(future)
            line = future.result()
            if line: # not EOF
                buf.append(line)    # save for later
                display.write(line) # display in terminal
                # schedule to read the next line
                tasks[asyncio.Task(stream.readline())] = buf, stream, display

    # wait for the process to exit
    rc = yield from process.wait()
    return rc, b''.join(stdout), b''.join(stderr)

该脚本运行 '/path/to/script 命令,并同时读取其标准输出和标准错误流中的每一行。这些行分别打印到父进程的标准输出和标准错误流中,并保存为字节串以供未来处理。要运行 read_and_display() 协程,我们需要一个事件循环:
import os

if os.name == 'nt':
    loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()
try:
    rc, *output = loop.run_until_complete(read_and_display("/path/to/script"))
    if rc:
        sys.exit("child failed with '{}' exit code".format(rc))
finally:
    loop.close()

1

p.communicate()等待子进程完成,然后一次性返回其整个输出。

您尝试过像这样的方式吗?逐行读取子进程的输出。

p = subprocess.Popen('/path/to/script', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in p.stdout:
  # do something with this individual line
  print line

2
如果子进程生成足够的输出以填充操作系统stderr管道缓冲区(在我的机器上为65K),则它会挂起。您还应该同时消耗p.stderr。由于预读错误,for line in p.stdout将会突发打印。您可以改用for line in iter(p.stdout.readline, b'')print line将打印双重换行符。您可以使用print line,(注意:逗号)来避免这种情况。 - jfs
关于消耗stderr的观点很好。我一直以为在一个长数据流中缓冲几行不会有问题,但这也是需要考虑的事情。 - Dan Lenski
1
脚本需要很长时间才能完成 - 这意味着如果脚本将进度写入stderr,则可能会停顿。 - jfs

0

Popen.communicate文档明确说明:

Note: The data read is buffered in memory, so do not use this method if the data size is large or unlimited.

https://docs.python.org/2/library/subprocess.html#subprocess.Popen.communicate

因此,如果您需要实时输出,您需要使用类似于以下的东西:

stream_p = subprocess.Popen('/path/to/script', stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while stream_line in stream_p:
    #Parse it the way you want
    print stream_line

0

这将stdout和stderr都打印到终端,同时将它们保存到一个变量中:

from subprocess import Popen, PIPE, STDOUT

with Popen(args, stdout=PIPE, stderr=STDOUT, text=True, bufsize=1) as p:
    output = "".join([print(buf, end="") or buf for buf in p.stdout])

然而,根据您具体的操作,这可能很重要:通过使用 stderr=STDOUT,我们无法再区分 stdout 和 stderr,并且在调用 print 时,无论输出来自 stdout 还是 stderr,您的输出将始终打印到 stdout。

对于 Python < 3.7,您需要使用 universal_newlines 而非 text

3.7 版本中新增了 text 作为 universal_newlines 更易读的别名。

来源:https://docs.python.org/3/library/subprocess.html#subprocess.Popen


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接