Python 关于标准流(stdin/out/err)重定向与控制台日志记录的相关内容整理

Python 关于标准流(stdin / out / err)重定向与控制台日志记录的相关内容整理

最近自己写 Python 控制台程序的时候突发奇想(突然犯病),如果要把程序运行过程中控制台显示的所有内容保存到日志文件该怎么办?
在每个涉及到显示内容的语句加上with open(logPath, "a")...? 或者重写全局input()print()方法? 又或者逐行读取控制台内容然后保存?
第一种方法思路过于清奇,不仅徒增许多代码,而且太过凌乱不便维护
第二种方法需要考虑对不同类型数据分别做处理,因为 file.write()方法只能传入字符串, 而且调用起来有些繁琐
第三种虽然可行,但不方便定位每次运行的输出范围,记录的日志中可能会出现一些多余的内容,比如上次运行显示的内容
这篇文章不会使用上面的任何一种方法,而是 将系统标准流重定向到自定义类 来实现所需功能

这篇文章能帮到你什么

  • Python 标准流的重定向
  • inputprint 函数的工作流程和特点
  • 控制台输入输出内容的日志记录
  • 保存日志前对内容的预处理

正文

1. inputprint 函数的工作流程

想要理解为何重定向标准流之后就能将 inputprint 函数输入输出的内容做处理,我们需要先了解这两个函数的工作流程,官方在文档注释中写的很清楚

[ input ]
Read a string from standard input. The trailing newline is stripped. 从标准输入读取一个字符串,将去掉后面的换行符
The prompt string, if given, is printed to standard output without a trailing newline before reading input. 如果给出了提示字符串,则在读取输入之前将其打印到标准输出,末尾不带换行符

[ print ]
Prints the values to a stream, or to sys.stdout by default. 将值打印到一个流或默认的标准输出流 sys.stdout (自动删除末尾的换行符)
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout. file 参数:类文件对象或流,默认为标准输出流 sys.stdout
sep: string inserted between values, default a space. sep 参数:多值之间的填充分隔符,默认为空格
end: string appended after the last value, default a newline. 输出结尾添加的字符,默认为换行符( ‘\n’ )
flush: whether to forcibly flush the stream. 是否强制刷新流

可以看到在使用 print 时就可以对输出流进行设置了,但是每次都需要设置,所以不推荐

对于 inp = input("tip") 来说,函数首先调用标准输出流的 sys.stdout.write("tip") 方法将提示内容写入输出流的缓冲区 (此时控制台并未显示提示内容),然后调用 sys.stdout.flush() 方法刷新缓冲区 (提示内容在这时显示到控制台),接着程序将等待用户的输入,用户输入完成按下回车,此时调用标准输入流的 sys.stdin.readline() 方法读取我们输入的内容并赋值给变量 inp[详细演示请见附录1]

# 以下两段代码的运行效果等价
inp = input("tip")

sys.stdout.write("tip")
sys.stdout.flush()
inp = sys.stdin.readline()

对于 print("msg") 来说,函数首先调用标准输出流的 sys.stdout.write("msg") 方法将信息写入输出流的缓冲区 (此时控制台并未显示信息),然后调用 sys.stdout.write("\n") 来执行一次换行 (此时控制台已经显示出信息内容)。可是到此为止并未调用 sys.stdout.flush() 方法,为什么控制台却输出了信息呢?其实换行不同于普通文本,它是一种特殊行为,会自动刷新缓冲区的内容,所以你看到了信息的输出。[详细演示请见附录2]

# 以下两段代码的运行效果等价
print("msg")

sys.stdout.write("msg")
sys.stdout.write("\n")
# 也可能是 sys.stdout.write("msg" + "\n")

2. 自定义类用于流的重定向

通过前面的内容,我们已经能理解 inputprint 两个方法之所以能从控制台读取内容以及让控制台显示内容,本质上是调用了标准输入输出流相关的方法,那么我们能不能将输入输出流替换为非标准的自定义流呢?答案是肯定的。

在我们开始编写自己的自定义流之前有必要先想一下 inputprint 都调用了哪些标准输入输出方法,列举出来如下:

[ input ]
sys.stdout.write()
sys.stdout.flush()
sys.stdin.readline()

[ print ]
sys.stdout.write()
sys.stdout.flush()

可以发现,标准输入输出流中含有的方法包括:writeflush 以及 readline 其中 readline 带有返回值。
这时我们算是对标准输入输出流的构成有了比较深入的理解,下面开始动手编写自定义类来对输入输出功能进行扩充,这也是本文介绍的日志记录方法的基础

import sys
from typing import TextIO

class CustomStream(object):
    """自定义流, 将标准输入输出流进行重新封装, 对输入输出功能进行扩充

    Attribute:
        stream (TextIO): 输入输出流
        message (str): 读取的文本信息

    """
    stream: TextIO
    message: str

    def __init__(self, stream: TextIO):
        self.stream = stream

    def write(self, message):
        self.stream.write(message)

    def readline(self):
        message = self.stream.readline()
        return message

    def flush(self):
        self.stream.flush()  # 网上其它教程可能会把此处设置为 pass 但仔细看完附录你会明白这个刷新有多重要
       						 # 使用调试模式可能不会有问题,但是当你脱离调试模式问题就会暴露

以上代码搭建了一个对标准流的封装框架,如果此时使用 sys.stdout = CustomStream(sys.stdout) ... 省略 stdin/err 的替换 将其实例化并替换原来的标准流,你的程序将和之前没有任何区别,输出也照常输出,输入也照常输入

接下来步入正题,如何在上面框架的基础上添加功能,比如记录控制台输出日志?就只要修改这几个方法就行了。下面我们来添加日志记录的功能。

import sys
from typing import TextIO
from io import TextIOWrapper  # 新增

class CustomStream(object):
    """自定义流, 将标准输入输出流进行重新封装, 对输入输出功能进行扩充

    Attribute:
        stream (TextIO): 输入输出流
        log (TextIOWrapper): 日志文件对象  # 新增
        message (str): 读取的文本信息

    """
    stream: TextIO
    log: TextIOWrapper  # 新增
    message: str

    def __init__(self, filename, stream: TextIO):  # 新增 filename 参数
        self.stream = stream
        self.log = open(filename, 'a', encoding="UTF-8")  # 新增

    def write(self, message):
        self.stream.write(message)
        self.log.write(message)  # 新增
        self.log.flush()  # 新增

    def readline(self):
        message = self.stream.readline()
        self.log.write(message)  # 新增
        self.log.flush()  # 新增
        return message

    def flush(self):
        self.stream.flush()

此时的自定义流已经具备了日志记录的功能,注意标有新增注释的行,这些行服务于日志记录功能。

使用 sys.stdout = CustomStream(log_file_path, sys.stdout) 可将输出流替换为自定义流,在不影响控制台输出的情况下能够将输出内容保存到 log_file_path 对应的日志文件,但是用户从控制台输入的内容无法记录到日志,因为输入流还没替换啊亲,继续使用 sys.stdin = CustomStream(log_file_path, sys.stdin) 将输入流替换,此时在不影响正常输入的情况下也能将输入内容写入日志文件了。还有一个流 sys.stderr ?替换方式相同,使用 sys.stderr = CustomStream(log_file_path, sys.stderr) 即可。

你也可以使用三个不同的 log_file_path 将输出、输入以及错误分别记录到三个不同日志文件

3. 其他的修饰

如果你只是单纯使用黑底白字控制台并记录日志,那么前面的内容已经足够了,并且效果很好
但如果你使用了 ANSI 转义序列来对控制台进行特殊操作(如修改输出颜色、光标位移等),记录的日志就会是下面这样…
请添加图片描述请添加图片描述那些乱码的地方就是你在字符串中添加的 ANSI 转义序列,.log 日志文件无法识别这些序列,会显示为乱码。解决方法为:信息写入日志之前将转义序列删除,我使用的是 re 模块进行正则表达式匹配(之前想用 str.replace 使用正则表达式时遇到点问题所以没用它),其他好办法欢迎在评论区讨论。

添加的代码如下

from re import compile, sub

comp = compile(r"\033\[.*?m|\033\[.*?A|\033\[.*?C|")  # 这里匹配的东西以你用到的转义序列为准
message = sub(comp, "", message)

添加后的完整代码如下

import sys
from typing import TextIO
from io import TextIOWrapper
from re import compile, sub

class CustomStream(object):
    """自定义流, 将标准输入输出流进行重新封装, 对输入输出功能进行扩充

    Attribute:
        stream (TextIO): 输入输出流
        log (TextIOWrapper): 日志文件对象
        message (str): 读取的文本信息

    """
    stream: TextIO
    log: TextIOWrapper
    message: str

    def __init__(self, filename, stream: TextIO):
        self.stream = stream
        self.log = open(filename, 'a', encoding="UTF-8")

    def write(self, message):
        self.stream.write(message)
        comp = compile(r"\033\[.*?m|\033\[.*?A|\033\[.*?C|")  # 这里匹配的东西以你用到的转义序列为准
        message = sub(comp, "", message)
        self.log.write(message)
        self.log.flush()

    def readline(self):
        message = self.stream.readline()
        self.log.write(message)
        self.log.flush()
        return message

    def flush(self):
        self.stream.flush()

那如果我想在程序运行过程中通过一些操作取消日志记录功能怎么办?使用 sys.stdout = sys.stdout ?总觉得哪里怪怪的,实际上你将 sys.stdout 重定向到其他地方后原本的 sys.stdout 已经不复存在,所以你需要事先将标准流进行存档,采用如下代码

old_stdout = sys.stdout
old_stdin = sys.stdin
old_stderr = sys.stderr

下面两个方法方便我们时刻切换日志功能是否启用

def setLogger(log_file_path: str):
    """应用日志记录器

    Args:
        logfpath (str): 日志文件路径
    """
    sys.stdout = CustomStream(log_file_path, stream=old_stdout)
    sys.stderr = CustomStream(log_file_path, stream=old_stderr)
    sys.stdin = CustomStream(log_file_path, stream=old_stdin)


def removeLogger():
    """取消日志记录器, 将系统标准流重定向到最初的标准流
    """
    sys.stdout = old_stdout
    sys.stdin = old_stdin
    sys.stderr = old_stderr

完整的日志模块内容 @ log.py

import sys
from typing import TextIO
from io import TextIOWrapper
from re import compile, sub

old_stdout = sys.stdout
old_stdin = sys.stdin
old_stderr = sys.stderr

class CustomStream(object):
    """自定义流, 将标准输入输出流进行重新封装, 对输入输出功能进行扩充

    Attribute:
        stream (TextIO): 输入输出流
        log (TextIOWrapper): 日志文件对象
        message (str): 读取的文本信息

    """
    stream: TextIO
    log: TextIOWrapper
    message: str

    def __init__(self, filename, stream: TextIO):
        self.stream = stream
        self.log = open(filename, 'a', encoding="UTF-8")

    def write(self, message):
        self.stream.write(message)
        comp = compile(r"\033\[.*?m|\033\[.*?A|\033\[.*?C|")  # 这里匹配的东西以你用到的转义序列为准
        message = sub(comp, "", message)
        self.log.write(message)
        self.log.flush()

    def readline(self):
        message = self.stream.readline()
        self.log.write(message)
        self.log.flush()
        return message

    def flush(self):
        self.stream.flush()


def setLogger(log_file_path: str):
    """应用日志记录器

    Args:
        logfpath (str): 日志文件路径
    """
    sys.stdout = CustomStream(log_file_path, stream=old_stdout)
    sys.stderr = CustomStream(log_file_path, stream=old_stderr)
    sys.stdin = CustomStream(log_file_path, stream=old_stdin)


def removeLogger():
    """取消日志记录器, 将系统标准流重定向到最初的标准流
    """
    sys.stdout = old_stdout
    sys.stdin = old_stdin
    sys.stderr = old_stderr

正文到此结束

附录

1. 关于标准流的详细演示

注意: 以下演示均在 PowerShell 7.2.1 中使用 python.exe 运行 python 程序,而非 VSCode、PyCharm 等开发工具的调试模式,调试模式可能会对输出过程进行干涉,使 sys.stdout.write() 方法向缓冲区写入内容后不需要调用 sys.stdout.flush() 刷新缓冲区即可输出,这显然是不对的

使用标准流方法模拟 inp = input("tip > ") & print(inp) 的执行过程

# 说明: 使用标准流方法模拟 inp = input("tip > ") & print(inp) 的执行过程
import sys

sys.stdout.write("tip > ")
sys.stdout.flush()
inp = sys.stdin.readline()

sys.stdout.write(inp)
sys.stdout.write("\n")

请添加图片描述
请添加图片描述

不进行流刷新时的效果

# 说明: 不进行流刷新时的效果
import sys

sys.stdout.write("tip > ")
sys.stdin.readline()  # 使用 readline() 暂停程序方便观察效果

请添加图片描述
请添加图片描述

程序执行完毕退出时会自动刷新标准流

# 说明: 程序执行完毕退出时会自动刷新标准流
import sys

sys.stdout.write("tip > ")

请添加图片描述

标准输出方法单次输出的内容中含有换行符(\n)时(无论几个), 都会自动刷新缓冲区

# 说明: 标准输出方法单次输出的内容中含有换行符(\n)时(无论几个), 都会自动刷新缓冲区
import sys

sys.stdout.write("msg1\nmsg2\nmsg3")
sys.stdin.readline()  # 暂停程序

sys.stdout.write("msg1\n")
sys.stdout.write("msg2\n")
sys.stdout.write("msg3")
sys.stdin.readline()  # 暂停程序

请添加图片描述
请添加图片描述
请添加图片描述

2. 关于 print 方法的详细演示

关键字参数的使用@end & sep

# 说明: 关键字参数的使用
# 基本输出
print("-" * 60)
print("msg1")
print("msg2")
print("msg3")

# end 参数, 每次输出内容结尾追加内容, 默认为换行符("\n")
print("-" * 60)
print("msg1")
print("msg2", end="")
print("msg3", end="\n")

# sep 参数, 多值输出时每个值之间的分隔符, 默认为空格
print("-" * 60)
print("msg1", "msg2", "msg3")
print("msg1", "msg2", "msg3", sep="")
print("msg1", "msg2", "msg3", sep="+")

请添加图片描述

关键字参数的使用@flush

# 说明: flush 参数, 控制是否强制刷新流, 默认为 False
import sys

print("msg1")
sys.stdin.readline()  # 暂停程序
print("msg2", end="")
sys.stdin.readline()  # 暂停程序
print("msg3", end="", flush=True)
sys.stdin.readline()  # 暂停程序

请添加图片描述
请添加图片描述
请添加图片描述

关键字参数的使用@file

# 说明: file 参数, 设置输出流, 默认为标准输出流
import sys

f = open(".\\test.txt", "w", encoding="UTF-8")  # 打开文件

print("msg1\nmsg2\nmsg3", file=f, flush=True)
sys.stdin.readline()  # 暂停程序

f.close()  # 关闭文件
f = open(".\\test.txt", "w", encoding="UTF-8")  # 重新打开文件, 由于模式为 "w" 打开时会清空已有内容

print("msg1\nmsg2\nmsg3", file=f)
sys.stdin.readline()  # 暂停程序

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

print("\ntest\n") 会自动删除末尾的换行符

# 说明: print("\ntest\n") 会自动删除末尾的换行符
print("\ntest\n", end="")

请添加图片描述

print() 方法不会自动调用 sys.stdout.flush() 除非设置 flush=True 前面已经提到,这里省略运行截图

# 说明: print() 方法不会自动调用 sys.stdout.flush()
import sys

print("test", end="")
sys.stdin.readline()
sys.stdout.flush()
sys.stdin.readline()
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NEKO!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值