您可以使用
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——sh
、shell
、shell_command
、shellout
、iterpipes
、sarge
、cmd_utils
、commandwrapper
等。搜索关键词“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,但幸运的是你不需要多个管道。