目录
Python 关于标准流(stdin / out / err)重定向与控制台日志记录的相关内容整理
最近自己写 Python 控制台程序的时候突发奇想(突然犯病),如果要把程序运行过程中控制台显示的所有内容保存到日志文件该怎么办?
在每个涉及到显示内容的语句加上with open(logPath, "a")...
? 或者重写全局input()
和 print()
方法? 又或者逐行读取控制台内容然后保存?
第一种方法思路过于清奇,不仅徒增许多代码,而且太过凌乱不便维护
第二种方法需要考虑对不同类型数据分别做处理,因为 file.write()
方法只能传入字符串, 而且调用起来有些繁琐
第三种虽然可行,但不方便定位每次运行的输出范围,记录的日志中可能会出现一些多余的内容,比如上次运行显示的内容
这篇文章不会使用上面的任何一种方法,而是 将系统标准流重定向到自定义类 来实现所需功能
这篇文章能帮到你什么
- Python 标准流的重定向
input
和print
函数的工作流程和特点- 控制台输入输出内容的日志记录
- 保存日志前对内容的预处理
正文
1. input
和 print
函数的工作流程
想要理解为何重定向标准流之后就能将 input
和 print
函数输入输出的内容做处理,我们需要先了解这两个函数的工作流程,官方在文档注释中写的很清楚
[ 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. 自定义类用于流的重定向
通过前面的内容,我们已经能理解 input
和 print
两个方法之所以能从控制台读取内容以及让控制台显示内容,本质上是调用了标准输入输出流相关的方法,那么我们能不能将输入输出流替换为非标准的自定义流呢?答案是肯定的。
在我们开始编写自己的自定义流之前有必要先想一下 input
和 print
都调用了哪些标准输入输出方法,列举出来如下:
[ input ]
sys.stdout.write()
sys.stdout.flush()
sys.stdin.readline()
[ print ]
sys.stdout.write()
sys.stdout.flush()
可以发现,标准输入输出流中含有的方法包括:write
、 flush
以及 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()