整体目标
涉及的库均为Python3自带库实现
- logging
- sys
- enum
终端显示彩色基本原理参考👉Terminal里的颜色的那些事
Python打印日志可以直接借用logging
自带的库实现,但是默认的打印实在太丑了,长下面这样
这只是一条日志看着还好比较清爽,如果多了之后可想而知,大片红色看着由多么糟糕,借鉴SpringBoot的处理怎么做到SpringBoot那样的彩色日志输出呢?
这里突然想起来,Java自带的日志框架(JUL)打印也是一片红色,可能原生的东西都比较呆正。
日志框架的组成
日志框架无论哪种语言基本都是遵循一样的做法,包括以下组件:
- Logger:日志记录器,用于记录打印日志,我们操作的就是这个对象
- Handler:处理器,有些也叫Appender,就是实际处理日志记录的东西,比如说我们生成一条日志字符串,这个字符串是输出到控制台,还是输出到文件呢?或者说两者都输出呢?这就是处理器做的事情,如果存在多个处理器还可以重复输出。
- Formatter:格式化器,有些框架也叫Layout,就是格式化信息的,比如我们看到上面工工整整的时间,日志级别,就是我们定义好的格式,交给他处理。
正常打印一条日志,都是由日志记录器开始,日志记录器存在属性Handler处理器,处理器Handler存在属性Formatter。因此我们需要操作的对象就是Formatter。
import logging
if __name__ == '__main__':
# Logger Handler Formatter 三者之间的关系,相互链接建立联系
formatter = logging.Formatter(logging.BASIC_FORMAT)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
因此我们如果要自定义处理器或者自定义格式化器,都是从上面的setFormatter和addHandler入手的。
如何自定义这些组件? 答案是继承,Python中我们无法实现接口,但是可以继承基类,只要是儿子,自然也就是处理器,或者格式化器。
class CustomFormatter(logging.Formatter):
# 重写父类的方法,再调用父类,相当于增强方法
def format(self, record):
# 这里就可以处理记录record的了
s = super().format(record)
return s
我们的基本思路就是拿到需要格式化的数据,%(asctime)s
时间字符串,%(levelname)s
等级字符串这些,拿到这些数据那就可以进行彩色处理了。
终端彩色显示
ANSI转义序列(ANSI escape sequences)是一种带内信号的转义序列标准,用于控制视频文本终端上的光标位置、颜色和其他选项。在文本中嵌入确定的字节序列,大部分以ESC转义字符和"["字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。
大体原理就是类似\r
代表回到开头,\n
代表换行这种,具有一定的转义含义的东西,在终端不同的转义含义就可以控制输出的字体颜色,背景颜色,字体加粗,划线等等。
基本格式为:
\033[XXXm
其中\033[
以及后面的m
都是固定的,只有XXX代表不同含义,分别由;
分割,比如下面的例子
print("\033[31;9;3mHello Python\033[m")
其中31表示前景色(字体颜色)为红色,9表示划除线,3表示斜体
效果图
注意:并不是说第一个数字一定要控制颜色,也可以完全没有颜色控制的!
具体的ANSI转义表参考复杂正规版
简单的网上搜一下就好,就是说每个数字代表什么含义的码表,比如你想要粗体应该是数字1。
我们的目的是颜色,标准颜色的数字是30~37分别如下表示:
颜色 | 前景色代码 |
---|---|
黑色 | 30 |
红色 | 31 |
绿色 | 32 |
黄色 | 33 |
蓝色 | 34 |
品红 | 35 |
青色 | 36 |
白色 | 37 |
print("\033[31mINFO\033[m")
print("\033[32mINFO\033[m")
print("\033[33mINFO\033[m")
print("\033[34mINFO\033[m")
print("\033[35mINFO\033[m")
print("\033[36mINFO\033[m")
print("\033[37mINFO\033[m")
效果图
所以我们只需要获取到指定字符串然后包裹封装就可以了,问题是怎么获取呢?
完整实现
from enum import Enum
import logging
# 枚举类用来记录颜色
class Color(Enum):
BLACK = 30
RED = 31
GREEN = 32
YELLOW = 33
BLUE = 34
CYAN = 36
WHITE = 37
DEFAULT = 39
# ANSI转义颜色包裹需要颜色显示的字符串
def color_text(text: str, color: Color) -> str:
return "\033[{}m{}\033[0m".format(str(color.value), text)
# 自定义格式化器继承Formatter,在format格式化之前拿到需要变化的字段字符串,这里主要就是日志级别和记录器名称
class CustomFormatter(logging.Formatter):
def format(self, record):
if record.levelname == "INFO":
level_name_color = Color.GREEN
elif record.levelname == "ERROR":
level_name_color = Color.RED
elif record.levelname == "WARNING":
level_name_color = Color.YELLOW
else:
level_name_color = Color.DEFAULT
record.levelname = color_text(record.levelname, level_name_color)
record.name = color_text(record.name, Color.CYAN)
s = super().format(record)
return s
# 设置自定义格式化器,这里封装一个方法用来获取日志记录器,正好在这里添加格式化器,控制台输出处理器Handler默认是StreamHandler
def getLogger(name: str = __name__):
logger = logging.getLogger(name)
handler = logging.StreamHandler()
handler.setLevel("DEBUG")
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handler.setFormatter(CustomFormatter(fmt=format))
logger.addHandler(handler)
logger.propagate = 0
return logger
效果图
注意:这里需要配置记录器的日志级别,记录器的日志级别向下传递给处理器,所以其实处理器本质不设置日志级别也是可以的,由记录器传递。
上面还有个关键问题,为什么整体颜色还是红色?
答案是对于终端的输出分为stdout和stderr,而StreamHandler.stream输出流默认输出为stderr,因为我们还需要加一行关键的代码。
在getLogger方法中添加
handler.stream = sys.stdout
最终效果图
Note:
- 为了更接近SpringBoot效果,对格式化内容做了调整
- 日志级别其实最高也就WARNING共7个字符,但是由于加了转义序列,实际长度并不是7,因此如果太小则会出现对不齐的情况,这里采用
%(levelname)18s
占位18个字符右对齐。
格式化标准:
format = "%(asctime)s - %(levelname)18s - [%(name)15s] - %(message)s"