Python: subprocess.call,将标准输出重定向到文件,将标准错误重定向到文件,并实时在屏幕上显示标准错误信息

34

我正在为Python编写一个命令行工具(实际上是几个),并对其进行封装。

该工具通常是这样使用的:

 $ path_to_tool -option1 -option2 > file_out

用户将输出写入文件“file_out”,并且可以在运行工具时看到各种状态消息。

我希望复制这种行为,同时将stderr(状态消息)记录到文件中。

我拥有的是:

from subprocess import call
call(['path_to_tool','-option1','option2'], stdout = file_out, stderr = log_file)

除了stderr没有写入到屏幕之外,这个代码可以正常工作。 我可以添加代码将log_file的内容打印到屏幕上,但用户会在所有操作完成后才看到它,而不是在操作进行时。

总之,期望的行为是:

  1. 使用 call() 或 subprocess()
  2. 将stdout导向文件
  3. 将stderr导向文件,同时实时地将stderr写入屏幕,就像从命令行直接调用该工具一样。

我感觉我可能漏掉了一些非常简单的东西,或者这比我想象的要复杂得多...感谢任何帮助!

编辑:这只需要在Linux上工作。


你的代码需要在Windows(或其他非POSIX平台)上运行吗?如果不需要,在这里有一个更简单的答案。 - abarnert
3个回答

69
您可以使用subprocess来完成此操作,但这并不容易。如果您查看文档中的常用参数,您会发现可以将PIPE作为stderr参数传递,这将创建一个新的管道,将管道的一端传递给子进程,并使另一端可用作stderr属性*。
因此,您需要服务于该管道,向屏幕和文件写入内容。通常情况下,这方面的细节非常棘手**。在您的情况下,只有一个管道,并且您计划同步地对其进行服务,因此情况并不糟糕。
import subprocess
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
for line in proc.stderr:
    sys.stdout.write(line)
    log_file.write(line)
proc.wait()

(请注意,在使用for line in proc.stderr:时可能会出现一些问题 - 基本上,如果由于任何原因你正在读取的内容不是按行缓冲的,即使实际上有一半行的数据需要处理,你也可能会坐在那里等待换行符。如果需要,可以一次读取一定数量的数据块,比如说,read(128),甚至是read(1),以使数据更加平稳地流动。如果你需要在每个字节到达时立即获取每个字节,并且无法承担read(1)的成本,则需要将管道设置为非阻塞模式并进行异步读取。)

但是,如果你在Unix系统上,使用tee命令可能会更简单。

对于一个快速而简单的解决方案,你可以使用shell通过它进行管道传输。像这样:

subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True,
                stdout=file_out)

但我不想调试shell的管道;让我们用Python来做,如文档中所示:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stderr)
tool.stderr.close()
tee.communicate()

最后,在PyPI上有十几个或更多的高级包装器,可以处理subprocesses和/或shell——shshellshell_commandshelloutiterpipessargecmd_utilscommandwrapper等。搜索关键词“shell”、“subprocess”、“process”、“command line”等,找到一个你喜欢的,使问题变得微不足道。


如果您需要收集stderr和stdout怎么办?

简单的方法是将其中一个重定向到另一个,就像Sven Marnach在评论中建议的那样。只需更改Popen参数,如下所示:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

然后在所有用到tool.stderr的地方,改为使用tool.stdout,例如最后一个示例:

tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout)
tool.stdout.close()
tee.communicate()

但这也有一些权衡。最明显的是,混合两个流意味着你无法将stdout记录到file_out并将stderr记录到log_file,或者将stdout复制到你的stdout并将stderr复制到你的stderr。但它也意味着顺序可能是不确定的 - 如果子进程在写任何东西到stdout之前总是先写两行到stderr,那么一旦你混合这些流,你可能会在这两行之间得到一堆stdout。这也意味着它们必须共享stdout的缓冲模式,因此如果你依赖于linux / glibc保证stderr为行缓冲(除非子过程明确更改它),那可能不再成立。

如果需要分别处理这两个进程,情况就变得更加困难了。早些时候,我说只要您只有一个管道且可以同步服务它,则即时处理管道很容易。如果你有两个管道,那显然不再成立。想象一下,你正在等待tool.stdout.read(),然后从tool.stderr收到新数据。如果数据太多,它会导致管道溢出并使子过程阻塞。但即使没有发生这种情况,你也显然无法读取并记录stderr数据,直到有些东西从stdout进来。

如果使用pipe-through-tee解决方案,那就避免了最初的问题...但只是通过创建一个同样糟糕的新项目来实现。你有两个tee实例,在调用一个上的communicate时,另一个会一直闲置等待。

因此,无论哪种方式,你都需要某种异步机制。你可以使用线程、select反应器、类似于gevent的东西等来实现这一点。

以下是一个简单而粗略的示例:

proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)
t1 = threading.Thread(target=tee_pipe, args=(proc.stdout, file_out, sys.stdout))
t2 = threading.Thread(target=tee_pipe, args=(proc.stderr, log_file, sys.stderr))
t3 = threading.Thread(proc.wait)
t1.start(); t2.start(); t3.start()
t1.join(); t2.join(); t3.join()

然而,在一些边缘情况下,这种方法可能行不通。(问题在于SIGCHLD和SIGPIPE/EPIPE/EOF到达的顺序。我认为这不会影响我们,因为我们没有发送任何输入…但是在没有经过思考和/或测试之前,请不要轻易相信我。)从3.3版本开始,subprocess.communicate函数可以正确处理所有棘手的细节。但你可能会发现使用PyPI和ActiveState上可以找到的async-subprocess包装器实现,甚至使用像Twisted这样的完整异步框架中的子进程工具更加简单。


* 文档并没有真正解释管道是什么,好像他们希望你是一个老的Unix C程序员...但是一些示例,特别是在Replacing Older Functions with the subprocess Module部分中,展示了如何使用它们,而且很简单。

** 困难的部分是正确地对两个或多个管道进行排序。如果你等待一个管道,另一个管道可能会溢出并阻塞,导致你对另一个管道的等待永远无法完成。唯一简单的方法是创建一个线程来为每个管道提供服务。(在大多数*nix平台上,你可以使用select或poll反应器,但是使其跨平台非常困难。)该模块的源代码,特别是communicate及其辅助程序,展示了如何做到这一点。(我链接到3.3版本,因为在早期版本中,communicate本身会出现一些重要的错误...)这就是为什么,尽可能地使用communicate如果你需要多个管道。在你的情况下,你不能使用communicate,但幸运的是你不需要多个管道。


1
感谢您抽出时间回答我的问题。我现在已经自己找到了答案——在man stderr中:“流stderr是无缓冲的。” - Sven Marnach
1
使用stdout=subprocess.PIPEstderr=subprocess.STDOUT。请注意,混合两个流会导致不确定的输出,并且stdout可能会完全缓冲。如果您可以控制调用的子进程,则可以在那里禁用缓冲。 - Sven Marnach
1
@user2063292:您可以创建两个单独的管道并分别从它们中读取...但是,然后您会遇到我提到的处理两个管道的问题。我可以在答案中添加更多相关信息。但是,如果像Sven Marnach建议的那样只将stderr重定向到stdout,那么这更加容易。 - abarnert
2
在第一个代码示例中,data = proc.stderr.read()会一直阻塞,直到所有数据都被读取。 - jfs
@J.F.Sebastian:感谢你发现了这个问题。我最初使用的是read(1),并解释了不同选项之间的权衡...我不确定为什么我改变了它。无论如何,解释不同选项和权衡比为OP选择一个选项更重要,并且没有解释... - abarnert
显示剩余13条评论

1

我对@abarnert的Python 3答案进行了一些修改。这似乎可以工作:

def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)

proc = subprocess.Popen(["/bin/echo", "hello"],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)

# Open the output files for stdout/err in unbuffered mode.
out_file = open("stderr.log", "wb", 0)
err_file = open("stdout.log", "wb", 0)

stdout = sys.stdout
stderr = sys.stderr

# On Python3 these are wrapped with BufferedTextIO objects that we don't
# want.
if sys.version_info[0] >= 3:
    stdout = stdout.buffer
    stderr = stderr.buffer

# Start threads to duplicate the pipes.
out_thread = threading.Thread(target=tee_pipe,
                              args=(proc.stdout, out_file, stdout))
err_thread = threading.Thread(target=tee_pipe,
                              args=(proc.stderr, err_file, stderr))

out_thread.start()
err_thread.start()

# Wait for the command to finish.
proc.wait()

# Join the pipe threads.
out_thread.join()
err_thread.join()

1
我认为你在寻找的是类似于以下内容:

我想您需要的是这样的东西:

import sys, subprocess
p = subprocess.Popen(cmdline,
                     stdout=sys.stdout,
                     stderr=sys.stderr)

为了将输出/日志写入文件,我会修改我的cmdline以包括通常的重定向,就像在普通的Linux bash/shell上一样。例如,我会在命令行中添加teecmdline += ' | tee -a logfile.txt' 希望这有所帮助。

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