让子进程输出到文件和 stdout/stderr 的最简单接口?

Wan*_*ang 5 python subprocess

我想要cmd > >(tee -a {{ out.log }}) 2> >(tee -a {{ err.log }} >&2)在 python 子进程中具有类似效果的东西,而不需要调用tee. 基本上将 stdout 写入 stdout 和 out.log 文件,并将 stderr 写入 stderr 和 err.log 文件。我知道我可以使用循环来处理它。但由于我的代码中已经有很多 Popen、subprocess.run 调用,而且我不想重写整个事情,我想知道是否有某个包提供的更简单的接口可以让我做类似的事情:

subprocess.run(["ls", "-l"], stdout=some_magic_file_object(sys.stdout, 'out.log'), stderr=some_magic_file_object(sys.stderr, 'out.log') )
Run Code Online (Sandbox Code Playgroud)

Wil*_*lva 2

据我所知,这不是简单的方法,但这是一种方法:

import os


class Tee:
    def __init__(self, *files, bufsize=1):
        files = [x.fileno() if hasattr(x, 'fileno') else x for x in files]
        read_fd, write_fd = os.pipe()
        pid = os.fork()
        if pid:
            os.close(read_fd)
            self._fileno = write_fd
            self.child_pid = pid
            return
        os.close(write_fd)
        while buf := os.read(read_fd, bufsize):
            for f in files:
                os.write(f, buf)
        os._exit(0)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def fileno(self):
        return self._fileno

    def close(self):
        os.close(self._fileno)
        os.waitpid(self.child_pid, 0)
Run Code Online (Sandbox Code Playgroud)

Tee对象采用文件对象列表(即,要么是整数文件描述符,要么具有fileno方法的对象)。它创建一个子进程,该子进程从自己的进程中读取内容fileno(这就是subprocess.run将要写入的内容)并将该内容写入它提供的所有文件中。

需要一些生命周期管理,因为它的文件描述符必须关闭,并且之后必须等待子进程。为此,您必须通过调用Tee对象的close方法来手动管理它,或者将其用作上下文管理器,如下所示。

用法:

import subprocess
import sys


logfile = open('out.log', 'w')
stdout_magic_file_object = Tee(sys.stdout, logfile)
stderr_magic_file_object = Tee(sys.stderr, logfile)

# Use the file objects with as many subprocess calls as you'd like here
subprocess.run(["ls", "-l"], stdout=stdout_magic_file_object, stderr=stderr_magic_file_object)

# Close the files after you're done with them.
stdout_magic_file_object.close()
stderr_magic_file_object.close()
logfile.close()
Run Code Online (Sandbox Code Playgroud)

一种更简洁的方法是使用上下文管理器,如下所示。但它需要更多的重构,因此您可能更喜欢手动关闭文件。

import subprocess
import sys


with open('out.log', 'w') as logfile:
    with Tee(sys.stdout, logfile) as stdout, Tee(sys.stderr, logfile) as stderr:
        subprocess.run(["ls", "-l"], stdout=stdout, stderr=stderr)
Run Code Online (Sandbox Code Playgroud)

这种方法的一个问题是子进程立即写入 stdout,因此 Python 自己的输出经常会混合在其中。Tee您可以通过使用临时文件和日志文件来解决此问题,然后在Tee退出上下文块后打印临时文件的内容(并将其删除)。创建一个Tee自动执行此操作的子类很简单,但使用它会有点麻烦,因为现在您需要退出上下文块(或以其他方式让它运行一些代码)以打印出子进程的输出。