【python 实时打印/记录子进程中的输出到主进程 巧妙地实现】

 共三段关键代码

import logging
import sys


def init_logger(log_path=None):
    # 打印并记载日志
    logger = logging.getLogger(__file__)
    logger.setLevel(logging.INFO)
    if logger.hasHandlers():
        logger.handlers.clear()
    formatter = logging.Formatter("%(asctime)s - %(message)s")

    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)

    if log_path is not None:
        file_handler = logging.FileHandler(log_path, mode="a")
        file_handler.setLevel(logging.INFO)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

    return logger
import threading


def start_reading_threads(process, logger):
    # 异步读取子进程的输出,并使用logger进行记录
    
    def read_output(stream, log_func):
        # 构建一个迭代器,读取子进程中的输出,直到读取到空
        for line in iter(stream.readline, ""):
            if line:
                log_func(line.rstrip())
        stream.close()

    stdout_thread = threading.Thread(
        target=read_output, args=(process.stdout, logger.info)
    )
    stderr_thread = threading.Thread(
        target=read_output, args=(process.stderr, logger.error)
    )
    stdout_thread.start()
    stderr_thread.start()
    return stdout_thread, stderr_thread

 解读1:

1. 函数定义

def start_reading_threads(process, logger):
  • process: 这是一个外部进程对象,通常是通过 subprocess.Popen 创建的。这个对象包含了该进程的标准输出(stdout)和标准错误输出(stderr)流。

  • logger: 这是一个日志记录器对象,通常是通过 logging 模块创建的。它用于记录从进程中读取的输出。

  • 2. 内部函数 read_output

    def read_output(stream, log_func):
        for line in iter(stream.readline, ''):
            if line:
                log_func(line.rstrip())
        stream.close()
  • read_output 是一个内部函数,用于从指定的流(stream)中读取数据,并将每一行数据传递给指定的日志函数(log_func)。

  • stream.readline: 这是一个方法,用于从流中读取一行数据。iter(stream.readline, '') 创建了一个迭代器,它会不断调用 stream.readline,直到读取到空字符串(即流结束)。

  • if line:: 检查读取到的行是否为空。如果非空,则继续处理。

  • log_func(line.rstrip()): 调用指定的日志函数(如 logger.info 或 logger.error),并将行数据传递给它。rstrip() 用于去除行末尾的换行符。

  • stream.close(): 当流读取完毕后,关闭流。

  • 3. 创建并启动线程

    stdout_thread = threading.Thread(target=read_output, args=(process.stdout, logger.info))
    stderr_thread = threading.Thread(target=read_output, args=(process.stderr, logger.error))
    stdout_thread.start()
    stderr_thread.start()
  • threading.Thread: 创建一个新的线程对象。target 参数指定线程要执行的函数(这里是 read_output),args 参数指定传递给该函数的参数。

  • stdout_thread: 创建一个线程,用于读取 process.stdout(标准输出),并将每一行数据记录到 logger.info

  • stderr_thread: 创建另一个线程,用于读取 process.stderr(标准错误输出),并将每一行数据记录到 logger.error

  • stdout_thread.start() 和 stderr_thread.start(): 启动这两个线程,使它们开始执行 read_output 函数。

  • 4. 返回线程对象

    return stdout_thread, stderr_thread
  • 函数返回两个线程对象 stdout_thread 和 stderr_thread。这样调用者可以进一步操作这些线程,例如等待它们完成(join)。

  • 总结

    这个函数的主要作用是异步地读取外部进程的标准输出和标准错误输出,并将这些输出记录到指定的日志记录器中。通过使用线程,可以避免阻塞主程序,同时确保输出被及时记录。

    import subprocess
    
    logger = init_logger("log.log")
    command = [
        "python",
        "a.py",  # 你的脚本代码
    ]
    logger.info(f"Running command: {' '.join(command)}")
    try:
        process = subprocess.Popen(
            command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
        )
    
        stdout_thread, stderr_thread = start_reading_threads(process, logger)
        process.wait()
        stdout_thread.join()
        stderr_thread.join()
    
        if process.returncode != 0:
            logger.error(f"Bash script exited with return code {process.returncode}")
        else:
            logger.info("Bash script executed successfully.")
    except Exception as e:
        logger.error(f"Error occurred while executing bash script: {e}")

    解读2:

    1. 启动外部进程

    process = subprocess.Popen(
        command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
    )
  • subprocess.Popen: 这是一个用于启动外部进程的函数。它返回一个 Popen 对象,该对象代表启动的进程。

  • command: 这是一个字符串或字符串列表,表示要执行的命令。例如,["ls", "-l"] 或 "ls -l"

  • stdout=subprocess.PIPE: 将标准输出重定向到一个管道(PIPE),这样可以通过 process.stdout 读取输出。

  • stderr=subprocess.PIPE: 将标准错误输出重定向到一个管道(PIPE),这样可以通过 process.stderr 读取错误输出。

  • text=True: 指定输出为文本模式。这意味着读取的输出将是字符串而不是字节。

  • 2. 启动读取线程

    stdout_thread, stderr_thread = start_reading_threads(process, logger)
  • start_reading_threads: 这是前面定义的函数,用于启动两个线程分别读取 process.stdout 和 process.stderr,并将输出记录到 logger 中。

  • stdout_thread: 这是一个线程对象,用于读取并记录标准输出。

  • stderr_thread: 这是一个线程对象,用于读取并记录标准错误输出。

  • 3. 等待进程完成

    process.wait()
  • process.wait(): 这是一个阻塞调用,它会等待外部进程执行完毕。这意味着主程序会在这里暂停,直到外部进程结束。

  • 4. 等待线程完成

    stdout_thread.join()
    stderr_thread.join()
  • stdout_thread.join(): 这是一个阻塞调用,它会等待 stdout_thread 线程执行完毕。这意味着主程序会在这里暂停,直到 stdout_thread 线程完成读取和记录标准输出的工作。

  • stderr_thread.join(): 这是一个阻塞调用,它会等待 stderr_thread 线程执行完毕。这意味着主程序会在这里暂停,直到 stderr_thread 线程完成读取和记录标准错误输出的工作。

  • 总结

    这段代码的整体流程如下:

    1. 使用 subprocess.Popen 启动一个外部进程,并将标准输出和标准错误输出重定向到管道。

    2. 启动两个线程,分别读取并记录标准输出和标准错误输出。

    3. 等待外部进程执行完毕。

    4. 等待两个读取线程完成工作。

    通过这种方式,可以确保外部进程的输出被异步读取并记录,同时主程序会等待所有工作完成后再继续执行。

    学习自

    MM-Self-Improve/self_train.py at master · njucckevin/MM-Self-Improve

    作者:放飞自我的Coder

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【python 实时打印/记录子进程中的输出到主进程 巧妙地实现】

    发表回复