您可以使用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 命令为您执行此操作可能会更简单。
对于快速而肮脏的解决方案,您可以使用 shell 来通过它进行管道传输。像这样的:
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 上围绕子进程和/或 shell 有十几个或更多更高级别的包装器——sh、shell、shell_command、shellout、iterpipes、sarge、@ 987654344@、commandwrapper 等。搜索“shell”、“子进程”、“进程”、“命令行”等,找到您喜欢的,让问题变得简单。
如果您需要同时收集 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 上找到的异步子流程包装器实现之一,甚至是来自像 Twisted 这样成熟的异步框架的子流程的东西会简单得多。
* 文档并没有真正解释管道是什么,就好像他们希望你是一个老 Unix C 手……但是一些例子,特别是在 Replacing Older Functions with the subprocess Module 部分,展示了它们是如何使用的,而且很简单。
** 困难的部分是正确地对两个或多个管道进行排序。如果您在一个管道上等待,另一个可能会溢出并阻塞,从而阻止您对另一个管道的等待完成。解决这个问题的唯一简单方法是创建一个线程来服务每个管道。 (在大多数 *nix 平台上,您可以使用 select 或 poll 反应器,但要实现跨平台非常困难。)The source 到模块,尤其是 communicate 及其助手,展示了如何做。 (我链接到 3.3,因为在早期版本中,communicate 本身会出现一些重要的错误……)这就是为什么如果您需要多个管道,请尽可能使用communicate。在您的情况下,您不能使用communicate,但幸运的是您不需要多个管道。