Python醒目日志实战:从logging到彩色输出与rich库
2026/9/11 3:46:19 网站建设 项目流程

1. 为什么要花心思做一份醒目的日志

先问个实在问题:你写Python跑程序的时候,控制台刷出来的那一大片白底黑字,你真的逐行看过吗?我猜大概率是扫一眼有没有Traceback,然后就开始在输出里搜关键字。这就是问题所在——常规日志在排错时根本不够用,尤其是程序跑了几百行、几千行之后,错误信息淹没在普通输出里,你想定位一个警告,眼睛都得瞪花了。

我最早接触“醒目日志”这个概念,是接手一个跑数据分析的脚本。那个脚本每天定时跑,偶尔会在某个数据源上翻车,但控制台只输出普通的print。有次线上数据出问题,我硬是盯着一屏一屏滚动的输出看了十分钟,才从几百行里找到那条红色的报错。从那时候起,我就下决心把所有脚本的日志都改成“有颜色、带级别、一眼能分清内外”的样式。后来发现,这个习惯带来的收益远超预期:不仅自己排错快了,同事拿我写的工具去跑,也能第一时间看出哪一步出了问题,不再需要对着白花花的控制台发呆。

做醒目日志并不复杂,核心就三件事:结构化输出按级别着色关键信息高亮。这篇文章我把完整的方案拆开来讲,从Python自带的logging模块起步,讲到ANSI颜色、colorama、rich这些工具,最后给你一套可以直接抄走的封装代码。不管你是刚学Python的入门用户,还是已经在写脚本的工程党,都能从中拿到一套立刻能用的方案。

2. 先把logging用明白

2.1 别再用print临时凑数了

很多人写脚本习惯用print来打印过程信息,打印完就完事。说实话,临时调试用print没问题,但只要是“要跑一段时间”“可能出问题需要排查”的脚本,我强烈建议换成logging。为啥?print天然有四个短板:

  • 没有级别概念,分不清“信息、警告、错误”的优先级。
  • 输出位置单一,只能去控制台看,没法同时写文件、发远程。
  • 回溯排查困难,你不知道这条输出是哪一行代码打的,哪个模块传过来的。
  • 没有标准格式,打印内容五花八门,脚本一多就混乱。

logging模块是Python标准库的一部分,从Python 2.3開始就有了,稳定性毋庸置疑。它提供了Logger、Handler、Formatter三个核心角色:Logger负责接收代码里的日志调用,Handler负责决定日志送哪儿去(控制台、文件、Socket等),Formatter负责决定日志长什么样。三者配合,你可以把日志同时输出到控制台和文件,还能按级别分开处理。

2.2 一套最基础的logging配置

看一个最简单的标准写法:

import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) log = logging.getLogger("demo") log.info("程序启动") log.warning("磁盘空间不足,注意检查") log.error("数据库连接失败")

运行之后,控制台会输出类似这样规整的行:

2024-01-15 14:22:31 | INFO | demo | 程序启动 2024-01-15 14:22:31 | WARNING | demo | 磁盘空间不足,注意检查 2024-01-15 14:22:31 | ERROR | demo | 数据库连接失败

%(levelname)-8s这行格式化串很多人第一次看会懵,其实就是把级别名左对齐并占满8个字符宽度,这样INFO、WARNING、ERROR在视觉上能对齐,输出整齐很多。格式化字段还有很多,常用的有:

字段含义
%(asctime)s时间戳
%(levelname)s日志级别名
%(name)sLogger名称
%(message)s日志正文
%(filename)s打印日志的文件名
%(lineno)d打印日志的行号
%(funcName)s打印日志的函数名
%(process)d进程ID
%(threadName)s线程名

真正排错的时候,filenamelineno特别有用。比如程序跑到第108行报错,有行号你直接定位,没有你就得自己在代码里搜那句日志在哪儿。所以我的建议是:凡是脚本要保持长期运行的,format里至少带上文件名和行号。

3. 颜色不是花架子,是信息分层

3.1 ANSI转义序列的底层原理

要让控制台输出彩色文字,靠的就是ANSI转义序列。简单理解,这一类特殊字符不会被直接打印出来,而是告诉终端“接下来的文字换成什么颜色”。常见的写法是:

\033[显示方式;前景色;背景色m

比如\033[91m代表亮红色前景,\033[0m代表重置所有样式。Python里字符串可以直接拼这些控制符:

print("\033[91m" + "这是亮红色文字" + "\033[0m")

在支持ANSI的终端(macOS终端、Linux终端、Windows 10以上的PowerShell/Windows Terminal)里,渲染出来的就是红色文字,后面的\033[0m会把颜色复位,避免“后续所有输出都变红”的串色问题。

常用的前景色编号我整理了一下:

颜色普通亮度高亮度
黑色3090
红色3191
绿色3292
黄色3393
蓝色3494
品红3595
青色3696
白色3797

还有一个很多人忽略的点:除了颜色,ANSI还支持加粗(1)、下划线(4)、闪烁(5)、反白(7)。我自己用下来,加粗在关键报错上的效果比单纯换色更醒目。比如一个错误级别日志用“亮红+加粗”,警告用“亮黄”即可,不要全都加粗,否则视觉权重就没了。

3.2 colorama让跨平台不再头疼

直接拼ANSI有一个坑:老版本的Windows命令行终端(cmd.exe)默认不解析ANSI序列,你输出一坨\033[91m,屏幕上直接显示乱码。虽然Windows 10之后的系统大多默认开启支持,但兼容性总归是个隐患,尤其是你要把脚本分发给其他同事的时候。

colorama这个库就是来解决这个问题的。它会检测当前平台,在Windows下自动把ANSI序列转换为相应的Windows API调用,从而让颜色在所有终端里都能正常显示。用法也极其简单:

pip install colorama
import colorama from colorama import Fore, Back, Style colorama.init(autoreset=True) print(Fore.RED + "红色文字") print(Fore.GREEN + "绿色文字") print(Fore.YELLOW + "黄色文字") print(Style.BRIGHT + Fore.RED + "亮红色加粗")

autoreset=True这个参数等于每段文本之后自动加一个\033[0m,这样你就不用每个字符串结尾都手动拼重置符了。如果不设置autoreset,一旦打印了红色,后面所有日志都是红色,排查起来反而容易懵。

3.3 用rich库把日志输出颜值拉满

如果你觉得colorama还只是“上色”,那rich算是把日志输出带到了一个全新的层次。它是一个终端富文本渲染库,支持彩色、粗体、表格、进度条、语法高亮,甚至还能把整个traceback格式化成非常漂亮的彩色堆栈。安装同样很简单:

pip install rich

rich自带一个logging的Handler,可以直接接入标准logging:

from rich.logging import RichHandler import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[RichHandler(rich_tracebacks=True, markup=True)] ) log = logging.getLogger("rich_demo") log.info("这是一条普通信息") log.warning("这是一条警告") log.error("这是一条错误")

运行之后,你会看到带颜色和缩进的日志块逐行显示,警告、错误各有一套配色体系,视觉效果非常清晰。而且rich_tracebacks=True之后,如果代码抛异常,控制台会给出一个排版清晰、关键字带高亮的堆栈信息,比默认的Python Traceback好读非常多。

rich还支持在日志文本里插入标记语法,比如你想把一段内容特别标红:

log.info("[bold red]关键[/bold red] 任务执行完成,耗时 [cyan]12.3s[/cyan]")

这个markup=True开启之后,中括号内就是样式控制符。我自己在实际项目里,经常用这种内嵌标记来强调耗时、路径、行数等关键数据,比全行统一颜色更有针对性。

4. 实战封装:一套可以直接用的醒目日志模块

4.1 设计思路与完整代码

讲了这么多工具,最终要落到一个能直接复制的方案上。我的做法是封装一个独立的logger.py,提供三个功能:

  • 默认输出到控制台,同时可选输出到文件。
  • 控制台按级别着色:DEBUG灰、INFO青、WARNING黄、ERROR亮红加粗。
  • 抛异常时能记录完整的traceback。

直接看代码:

import logging import sys from logging.handlers import RotatingFileHandler from colorama import Fore, Style, init # 兼容Windows终端颜色 init(autoreset=True) # 自定义Formatter,重写format方法实现级别着色 class ColoredFormatter(logging.Formatter): COLORS = { "DEBUG": Fore.CYAN, "INFO": Fore.GREEN, "WARNING": Fore.YELLOW, "ERROR": Fore.RED, "CRITICAL": Fore.RED + Style.BRIGHT, } def format(self, record): # 先按标准格式生成文本,再根据级别套颜色 msg = super().format(record) color = self.COLORS.get(record.levelname, "") if color: return f"{color}{msg}{Style.RESET_ALL}" return msg def setup_logger( name: str = "app", log_file: str | None = None, level: int = logging.DEBUG, ): logger = logging.getLogger(name) logger.setLevel(level) logger.handlers.clear() # 避免重复添加handler fmt = "%(asctime)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s" datefmt = "%Y-%m-%d %H:%M:%S" # 控制台handler,带颜色 console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(level) console_handler.setFormatter(ColoredFormatter(fmt, datefmt)) logger.addHandler(console_handler) # 文件handler,不带颜色(文件里存颜色码没有意义) if log_file: file_handler = RotatingFileHandler( log_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8" ) file_handler.setLevel(level) file_handler.setFormatter(logging.Formatter(fmt, datefmt)) logger.addHandler(file_handler) return logger # 默认logger log = setup_logger()

这里有一个细节:文件日志我刻意不加颜色。原因是日志文件如果存入了ANSI颜色码,用编辑器打开会看到一堆\033[91m之类的乱码,反而干扰查看。所以文件handler用普通的Formatter,保留干净的结构化文本;控制台handler用带颜色的Formatter,方便肉眼识别。

4.2 核心机制解读:为什么代码这样写

有读者可能会问,ColoredFormatter里为什么要调用super().format(record),而不是直接在格式化串里拼颜色?这涉及到logging的调用链:Formatter.format()内部会先执行formatMessage(),把%(xxx)s占位符替换成真实字段,生成纯文本日志,然后format()方法还负责处理异常信息(当exc_info=True时,会把traceback追加到消息尾部)。如果我只改formatMessage()而忽略format(),那么在记录异常的时候traceback就不会被正确附加。

我的写法是在format()里先拿到完整的、已经包含traceback的文本,再统一套颜色。这样无论是普通日志还是异常日志,颜色都能正确加上。

再看logger.handlers.clear()这行。很多同学在模块里多次调用setup_logger()时,会发现日志被重复打印,原因就是同名的Logger对象会累积Handler。每次调用前清空已有Handler可以避免这个问题,但要注意:如果程序里其他地方也往这个Logger上挂了自定义Handler,clear会一并清掉。所以更稳妥的做法是每次调用都返回一个新的Logger实例,或者加一个全局字典缓存已创建的Logger。

4.3 实际使用效果与扩展

封装之后的使用方式就很顺了:

from logger import log log.debug("这是一条调试信息,通常不显示") log.info("任务开始执行") log.warning("磁盘空间低于20%,建议清理") log.error("连接第三方API失败") try: 1 / 0 except ZeroDivisionError: log.exception("计算过程出现异常")

log.exception()是logging里的一个隐藏利器。它等价于log.error("...", exc_info=True),会在日志正文后面自动附上完整的traceback。有了这个方法,你捕获到异常后直接写它,保存到文件里的日志就能完整记录崩溃现场,对远程排查特别有用。

再举一个可视化场景的例子。很多Python GUI工具(比如tkinter、PyQt)里会放一个“开始”按钮,点击后触发后台任务,界面上需要一个日志展示区域。这种情况可以直接把stdio重定向到文本框:

import sys import tkinter as tk class TextRedirector: def __init__(self, widget, tag="stdout"): self.widget = widget self.tag = tag def write(self, msg): self.widget.insert("end", msg) self.widget.see("end") self.widget.tag_add(self.tag, "end-1c", "end-1l") def flush(self): pass root = tk.Tk() text = tk.Text(root, height=20, width=80) text.pack() sys.stdout = TextRedirector(text, "stdout") sys.stderr = TextRedirector(text, "stderr") log.info("点击按钮后,日志会实时显示在文本框里")

这样就把控制台的日志同步显示到了GUI界面,点击按钮执行任务时,用户能实时看到运行状态。如果你接的是PyQt5,还可以用QTextEdit配合QPlainTextEdit,效果类似。

5. 实际排错里最常见的日志坑

5.1 Windows下颜色不生效怎么办

我在真实工作中遇到最多的情况是:脚本在开发机上(macOS/Linux)跑得好好的,一放到客户Windows服务器上,颜色就变成了一堆乱码。解决办法分两种:

  • 情况一:你的环境是Windows 10以上,且使用PowerShell或Windows Terminal。这种已经默认支持ANSI,直接用colorama初始化一次就能正常。
  • 情况二:还是老的cmd.exe,或者某些被组策略锁定的环境,颜色始终无法正常显示。这时候就别追求彩色了,改用纯文本格式,并依靠级别前缀和分隔符来区分日志轻重。

如果你用colorama之后发现老cmd还是乱码,可以考虑启用Windows的虚拟终端处理。在Python里可以这样做:

import ctypes kernel32 = ctypes.windll.kernel32 kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)

-11代表STD_OUTPUT_HANDLE7表示ENABLE_VIRTUAL_TERMINAL_PROCESSING,把这个权限打开,老终端也能识别ANSI序列。不过这个操作需要Windows 10 1703以上才生效,更老的系统就只能放弃彩色了。

5.2 日志重复输出,越排查越晕

大量新手在配置logging时都会踩这个坑:明明只打了一条日志,控制台却出现两遍、三遍,甚至越来越多。原因是root logger和自定义logger都在工作。basicConfig()默认配置的是root logger,而当你通过logging.getLogger("my_module")获取logger时,如果它没有设置propagate为False,日志事件会同时被当前logger的handler和root的handler各处理一次,造成重复。

最简单的修复办法:

logger = logging.getLogger("my_module") logger.propagate = False

我在实际项目里更推荐另一种思路:整个程序只保留一个根logger,所有模块都通过logging.getLogger(__name__)获取子logger,根logger上挂handler,子logger只管发事件,传播到根logger统一输出。这样日志的流向清晰,不会重复。

5.3 控制台显示正常,日志文件里中文乱码

这个问题在Windows平台特别容易出。Python 3.9之后open()的默认编码随平台变化,Windows默认是GBK,如果你的日志里有中文字符,写到文件时没指定encoding="utf-8",要么写不进去,要么打开文件时全是乱码。解决办法是在FileHandlerRotatingFileHandler创建时显式传入encoding="utf-8"

file_handler = RotatingFileHandler( log_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8" )

这事让我在客户那边丢过一次数据:脚本日志文件在中文Windows下用GBK编码写了一半,程序崩溃后日志文件里全是问号,最后只能靠“再说一遍业务过程”来复盘。从那以后,所有跟文件读写相关的场景,我都坚持显式指定UTF-8编码。

5.4 日志文件在程序崩溃时丢数据

这个坑比较隐蔽。logging模块默认使用缓冲写入,程序如果遇到os._exit()直接硬退出、或者断电、进程被杀,缓冲区的日志还没落盘就没了。解决方式有两种:

  • 在关键流程结束后显式调用logging.shutdown(),它会刷新所有handler并清理资源。
  • 给FileHandler指定delay=False,默认就会立即打开文件,但写入是否实时还取决于操作系统的缓冲策略。

更为稳妥的做法是改用QueueHandler+QueueListener的异步日志方案,把日志写入放到独立线程,主线程只管put,不阻塞业务。不过日常脚本通常用不到这么重,我一般只在长任务的末尾加一句logging.shutdown()就够用了。

6. 我把这套方案用在了哪些场景里

说了这么多,分享几个实际落地场景,方便你对号入座。

跑数据脚本时,一个健壮的日志输出能让你一眼看出哪个环节耗时最长。我的习惯是:

  • 任务开始打INFO级日志,标注“开始执行”,带上任务ID。
  • 每个阶段结束打INFO,带上耗时秒数。
  • 数据量异常、依赖缺缺失打WARNING。
  • 捕获异常打ERROR,并手动加一条“建议检查XX配置文件”的提示。

配合彩色输出,调度平台上展示的日志就是一块一块的色块:绿色代表正常流转,黄色代表风险提示,红色代表故障点。运维同事看到红色直接跳过去定位,不再需要逐行读日志。

GUI工具场景里,我给内部一个批量文件处理工具加过日志面板。用户点击“开始”按钮,后台线程用logging记录进度,界面用tkinter的Text文本框实时刷新。以前用户反馈“点完按钮没反应”,现在自己能通过日志看到走到哪一步、在哪一步卡住,工单质量明显提升。

还有一个特别好用的扩展:把日志同时发到远程。logging的Handler机制天然支持扩展,你只需要写一个继承logging.Handler的类,在emit()方法里用HTTP、Redis、Kafka等方式把日志转发出去。这个玩法可以让多台机器的日志统一汇总到一张看板上,对排查分布式任务特别有用。我之前用一个简单的requests.post就把日志直接转发到了企业微信机器人,脚本出差错了微信群立刻收到红色告警,连盯控制台都省了。

7. 给你的建议

做日志这件事,方向比工具重要。很多新人一上来就想用最炫的rich库,配了一堆颜色,结果真正出问题时,该记录的上下文信息没记录全,花哨的界面反而掩盖了关键数据。我在实际使用中的体会是:醒目日志的精髓不是颜色多好看,而是信息分层清晰、关键上下文不遗漏、不同级别一眼可辨

  • 控制台输出用颜色分层,文件日志保持干净文本。
  • 线上排错依赖的是filenamelinenoexc_info这些细节,别省。
  • Windows环境务必考虑colorama或手动开启虚拟终端支持。
  • 长任务记得必须把日志写到文件里,别只靠控制台。
  • 学会log.exception(),这可能是你排错时最常用的一个方法。

最后再分享一个小技巧:你可以给关键的WARNING和ERROR日志配一个固定的前缀,比如[CHECK][DATA_ERR],后续用grep或者编辑器搜索时,一条命令就能把所有异常点拉出来。我自己的脚本里,凡是需要人工介入的日志,统一用[ACTION_REQUIRED]前缀,这样每天扫一眼日志,哪些问题必须处理,一目了然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询