你可以用subprocess来完成这项工作,但这并不简单。如果查看文档中的Frequently Used Arguments,您将看到可以将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命令为您执行此操作可能会更简单。
对于快速脏的解决方案,您可以使用外壳来通过管道。像这样的:subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True,
stdout=file_out)
但我不想调试shell管道;让我们在Python中进行,如in the docs: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-sh,shell,shell_command,shellout,iterpipes,sarge,cmd_utils,commandwrapper等子进程和/或shell上有十几个更高级别的包装器。搜索“shell”,“subprocess”,“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。如果数据太多,可能导致管道溢出,子进程阻塞。但是,即使没有发生这种情况,很明显,在stdout提供某些信息之前,您将无法读取和记录stderr数据。
如果使用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/eppe/EOF到达的顺序。我不认为所有这些都会影响到我们,因为我们不会发送任何输入……但是不要不经过思考和/或测试就相信我。)3.3+中的^{}函数可以正确地获得所有的细节。但是,您可能会发现使用PyPI和ActiveState上的异步子进程包装器实现,甚至使用像Twisted这样的成熟异步框架中的子进程实现要简单得多。
*文档并没有真正解释什么是管道,就像他们希望您是一个老的Unix C hand一样……但是一些示例,特别是在Replacing Older Functions with the ^{} Module部分中,显示了它们是如何使用的,而且非常简单。
**最困难的部分是对两个或多个管道进行正确的排序。如果你在一个管道上等待,另一个管道可能溢出并堵塞,从而阻止你在另一个管道上的等待。解决这个问题的唯一简单方法是创建一个线程来为每个管道提供服务。(在大多数*nix平台上,您可以使用一个select或poll反应器,但是使这个跨平台变得异常困难。)The source到模块,特别是communicate及其助手,展示了如何做到这一点。(我链接到3.3,因为在早期版本中,communicate本身会出错……)这就是为什么,只要可能,如果需要多个管道,就要使用communicate。在您的例子中,您不能使用communicate,但幸运的是,您不需要多个管道。