【问题标题】:Python: subprocess.call, stdout to file, stderr to file, display stderr on screen in real timePython:subprocess.call,stdout to file,stderr to file,在屏幕上实时显示stderr
【发布时间】:2013-08-23 02:06:01
【问题描述】:

我有一个命令行工具(实际上是几个),我正在用 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. 将标准输出定向到文件
  3. 将 stderr 直接写入文件,同时将 stderr 实时写入屏幕,就好像 工具已直接从命令行调用。

我有一种感觉,要么我错过了一些非常简单的东西,要么这比我想象的要复杂得多...感谢您的帮助!

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

【问题讨论】:

标签: python subprocess stderr


【解决方案1】:

可以使用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 有十几个或更多更高级别的包装器——shshellshell_commandshelloutiterpipessarge、@ 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 平台上,您可以使用 selectpoll 反应器,但要实现跨平台非常困难。)The source 到模块,尤其是 communicate 及其助手,展示了如何做。 (我链接到 3.3,因为在早期版本中,communicate 本身会出现一些重要的错误……)这就是为什么如果您需要多个管道,请尽可能使用communicate。在您的情况下,您不能使用communicate,但幸运的是您不需要多个管道。

【讨论】:

  • @user2063292:对不起,那是tooltee。跟随示例代码有点过于密切。 :) 感谢您的关注。
  • 感谢您抽出宝贵时间回答我的问题。实际上,我现在自己找到了答案——在man stderr:“流 stderr 没有缓冲。”
  • @user2063292:使用stdout=subprocess.PIPEstderr=subprocess.STDOUT。请注意,混合两个流会导致不确定的输出,并且 stdout 可能会完全缓冲。如果你可以控制你调用的子进程,你可以在那里禁用缓冲。
  • @user2063292:您可以创建两个单独的管道并分别从它们中读取......但是您遇到了我提到的处理两个管道的问题。我可以在答案中添加更多内容。但是,如果像 Sven Marnach 建议的那样将 stderr 重定向到 stdout 是可以接受的,那么它会很多更容易。
  • data = proc.stderr.read() 在第一个代码示例中阻塞,直到读取 所有 数据。
【解决方案2】:

我认为您正在寻找的是:

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

要将输出/日志写入文件,我将修改我的cmdline 以包含通常的重定向,就像在普通的 linux bash/shell 上完成的那样。例如,我会将tee 附加到命令行:cmdline += ' | tee -a logfile.txt'

希望对您有所帮助。

【讨论】:

    【解决方案3】:

    我不得不对@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()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-10
      • 1970-01-01
      • 1970-01-01
      • 2020-02-09
      • 1970-01-01
      相关资源
      最近更新 更多