子进程命令的实时输出

新手上路,请多包涵

我正在使用 python 脚本作为流体动力学代码的驱动程序。 When it comes time to run the simulation, I use subprocess.Popen to run the code, collect the output from stdout and stderr into a subprocess.PIPE --- 然后我可以打印(并保存到日志文件)输出信息,并检查是否有任何错误。问题是,我不知道代码是如何进行的。如果我直接从命令行运行它,它会给我输出关于它在什么时候进行的迭代、什么时间、下一个时间步是什么等等。

有没有一种方法既可以存储输出(用于日志记录和错误检查),又可以生成实时流输出?

我的代码的相关部分:

 ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
output, errors = ret_val.communicate()
log_file.write(output)
print output
if( ret_val.returncode ):
    print "RUN failed\n\n%s\n\n" % (errors)
    success = False

if( errors ): log_file.write("\n\n%s\n\n" % errors)

最初我通过管道将 run_command 通过 tee 以便副本直接进入日志文件,并且流仍然直接输出到终端 - 但那样我不能存储任何错误(据我所知)。


到目前为止我的临时解决方案:

 ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
    log_file.flush()

然后,在另一个终端中,运行 tail -f log.txt (st log_file = 'log.txt' )。

原文由 DilithiumMatrix 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 448
2 个回答

Python 3 的 TLDR:

 import subprocess
import sys

with open("test.log", "wb") as f:
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for c in iter(lambda: process.stdout.read(1), b""):
        sys.stdout.buffer.write(c)
        f.buffer.write(c)


您有两种方法可以做到这一点,通过从 readreadline 函数创建一个迭代器并执行:

 import subprocess
import sys

# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    # replace "" with b'' for Python 3
    for c in iter(lambda: process.stdout.read(1), ""):
        sys.stdout.write(c)
        f.write(c)

要么

import subprocess
import sys

# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    # replace "" with b"" for Python 3
    for line in iter(process.stdout.readline, ""):
        sys.stdout.write(line)
        f.write(line)

或者您可以创建一个 reader 和一个 writer 文件。将 writer 传递给 Popen 并从 reader 读取

import io
import time
import subprocess
import sys

filename = "test.log"
with io.open(filename, "wb") as writer, io.open(filename, "rb", 1) as reader:
    process = subprocess.Popen(command, stdout=writer)
    while process.poll() is None:
        sys.stdout.write(reader.read())
        time.sleep(0.5)
    # Read the remaining
    sys.stdout.write(reader.read())

这样,您就可以将数据写入 test.log 以及标准输出。

文件方法的唯一优点是您的代码不会阻塞。因此,您可以在此期间做任何您想做的事,并随时以非阻塞方式从 reader 读取。当您使用 PIPEreadreadline 函数将阻塞,直到一个字符写入管道或一行分别写入管道。

原文由 Viktor Kerkez 发布,翻译遵循 CC BY-SA 4.0 许可协议

执行摘要(或“tl;dr”版本):当最多有一个 subprocess.PIPE 时很容易,否则很难。

可能是时候解释一下 subprocess.Popen 是如何工作的了。

(警告:这是针对 Python 2.x 的,尽管 3.x 是相似的;而且我对 Windows 变体很模糊。我对 POSIX 的理解要好得多。)

Popen 函数需要同时处理零到三个 I/O 流。它们像往常一样表示为 stdinstdoutstderr

您可以提供:

  • None ,表示您不想重定向流。它将像往常一样继承这些。请注意,至少在 POSIX 系统上,这并不意味着它将使用 Python 的 sys.stdout ,只是 Python 的 实际 标准输出;见最后的演示。
  • 一个 int 值。这是一个“原始”文件描述符(至少在 POSIX 中)。 (旁注: PIPESTDOUT 实际上是 int 内部,但是是“不可能的”和-2描述符,-
  • 流——实际上,任何具有 fileno 方法的对象。 Popen 将使用 stream.fileno() 找到该流的描述符,然后继续执行 int 值。
  • subprocess.PIPE ,表示 Python 应该创建一个管道。
  • subprocess.STDOUT (仅限 stderr ):告诉Python使用与 stdout 相同的描述符。这只有在您为 --- 提供(非 None )值 stdout ,即便如此,只有在设置 stdout=subprocess.PIPE 时才 _需要_(否则,您可以只提供与 stdout 相同的参数,例如 Popen(..., stdout=stream, stderr=stream) 。)

最简单的情况(没有管道)

如果您什么都不重定向(将所有三个保留为默认值 None 值或提供显式 None ), Pipe 很容易。它只需要剥离子进程并让它运行。或者,如果您重定向到非 PIPE —an int 或流的 fileno() 仍然很容易,因为它的所有工作。 Python 只需要分离子进程,将其标准输入、标准输出和/或标准错误连接到提供的文件描述符。

仍然简单的情况:一根管子

如果您只重定向一个流, Pipe 仍然很容易。让我们一次选择一个流并观看。

假设您要提供一些 stdin ,但让 stdoutstderr 去重定向,或转到文件描述符。作为父进程,您的 Python 程序只需要使用 write() 将数据发送到管道。你可以自己做,例如:

 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc

或者您可以将标准输入数据传递给 proc.communicate() ,然后执行上面显示的 stdin.write 。没有输出返回,所以 communicate() 只有一个真正的工作:它也为你关闭了管道。 (如果您不调用 proc.communicate() 您必须调用 proc.stdin.close() 关闭管道,以便子进程知道没有更多数据通过。)

假设您要捕获 stdout 但单独留下 stdinstderr 。同样,这很简单:只需调用 proc.stdout.read() (或等价物)直到没有更多输出。因为 proc.stdout() 是一个普通的 Python I/O 流,你可以在它上面使用所有普通的结构,比如:

 for line in proc.stdout:

或者,您也可以使用 proc.communicate() ,它只是为您执行 read()

如果您只想捕获 stderr ,它的工作原理与 stdout 相同。

在事情变得困难之前还有一个技巧。假设您要捕获 stdout ,并且还要捕获 stderr与标准输出在同一管道上:

 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

在这种情况下, subprocess “作弊”!好吧,它必须这样做,所以它并不是真正的作弊:它启动子进程时将其 stdout 和 stderr 定向到(单个)管道描述符,该描述符反馈给其父(Python)进程。在父端,再次只有一个管道描述符用于读取输出。所有“stderr”输出都显示在 proc.stdout 中,如果调用 proc.communicate() ,stderr 结果(元组中的第二个值)将是 None 一个字符串。

困难案例:两个或更多管道

当您想要使用至少两个管道时,所有问题都会出现。事实上, subprocess 代码本身有这个位:

 def communicate(self, input=None):
    ...
    # Optimization: If we are only using one pipe, or no pipe at
    # all, using select() or threads is unnecessary.
    if [self.stdin, self.stdout, self.stderr].count(None) >= 2:

但是,唉,在这里我们至少制作了两个,也许三个,不同的管道,所以 count(None) 返回 1 或 0。我们必须以艰难的方式做事。

On Windows, this uses threading.Thread to accumulate results for self.stdout and self.stderr , and has the parent thread deliver self.stdin input data (and then关闭管道)。

在 POSIX 上,这使用 poll 如果可用,否则使用 select 来累积输出并提供标准输入输入。所有这些都在(单个)父进程/线程中运行。

这里需要线程或者poll/select来避免死锁。例如,假设我们已将所有三个流重定向到三个单独的管道。进一步假设在写入过程暂停之前,可以将多少数据填充到管道中有一个小限制,等待读取过程从另一端“清理”管道。让我们将这个小限制设置为单个字节,只是为了说明。 (实际上这就是事情的运作方式,只是限制远大于一个字节。)

如果父 (Python) 进程试图写入几个字节——比如, 'go\n'proc.stdin ,第一个字节进入,然后第二个字节导致 Python 进程挂起,等待子进程读取第一个字节,清空管道。

同时,假设子进程决定打印一个友好的“Hello!Don’t Panic!”。问候。 H 进入其标准输出管道,但 e 使其暂停,等待其父级读取 H 清空标准输出管道。

现在我们被困住了:Python 进程处于睡眠状态,等待说完“go”,子进程也处于睡眠状态,等待说完“Hello!Don’t Panic!”。

subprocess.Popen 代码通过线程或选择/轮询避免了这个问题。当字节可以通过管道时,它们就会通过。当它们不能时,只有一个线程(不是整个进程)必须休眠——或者,在选择/轮询的情况下,Python 进程同时等待“可以写入”或“数据可用”,写入进程的标准输入仅当有空间时,并且仅当数据准备就绪时才读取其 stdout 和/或 stderr。 proc.communicate() 代码(实际上是 _communicate 处理毛茸茸的情况)在发送所有标准输入数据(如果有)并且累积所有标准输出和/或标准错误数据后返回。

如果你想在两个不同的管道上同时阅读 stdoutstderr (不管任何 stdin ),你也需要 - 重定向。这里的死锁场景是不同的——它发生在子进程向 stderr 写入长内容时,而你从 stdout 中提取数据,反之亦然——但它仍然存在。


演示

我答应证明,未重定向,Python subprocess es 写入底层标准输出,而不是 sys.stdout 。所以,这里有一些代码:

 from cStringIO import StringIO
import os
import subprocess
import sys

def show1():
   print 'start show1'
   save = sys.stdout
   sys.stdout = StringIO()
   print 'sys.stdout being buffered'
   proc = subprocess.Popen(['echo', 'hello'])
   proc.wait()
   in_stdout = sys.stdout.getvalue()
   sys.stdout = save
   print 'in buffer:', in_stdout

def show2():
   print 'start show2'
   save = sys.stdout
   sys.stdout = open(os.devnull, 'w')
   print 'after redirect sys.stdout'
   proc = subprocess.Popen(['echo', 'hello'])
   proc.wait()
   sys.stdout = save

show1()
show2()

运行时:

 $ python out.py
start show1
hello
in buffer: sys.stdout being buffered

start show2
hello

请注意,如果添加 stdout=sys.stdout ,第一个例程将失败,因为 StringIO 对象没有 fileno 。 The second will omit the hello if you add stdout=sys.stdout since sys.stdout has been redirected to os.devnull .

(如果您重定向 Python 的 file-descriptor-1,子进程 遵循该重定向 open(os.devnull, 'w') 调用生成一个流,其 fileno() 大于 2。)

原文由 torek 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题