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

18 投票
4 回答
19307 浏览
提问于 2025-04-20 17:16

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

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

然后我把结果打印到标准输出(stdout)。这样做没问题,但因为这个脚本需要很长时间才能完成,我希望能实时看到脚本的输出。之所以要把输出通过管道传递,是因为我想对它进行解析。

4 个回答

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 后,我们就无法区分标准输出和错误输出了。而且在调用 print 时,你的输出总是会被打印到标准输出,不管它是来自标准输出还是错误输出。

对于 Python 版本低于 3.7 的情况,你需要使用 universal_newlines,而不是 text

在 3.7 版本中新增:text 被作为 universal_newlines 的一个更易读的别名。

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

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
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
17

要把子进程的输出(stdout)保存到一个变量里,以便后续处理,同时在子进程运行时实时显示这些输出,可以参考以下内容:

#!/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

如果想同时保存子进程的输出和错误信息(stderr),事情就复杂一些,因为你需要同时处理这两个输出流,以避免出现死锁的情况,可以查看这个链接了解更多:

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()

撰写回答