
本文详解为何 logging.filter 无法实现“仅终端着色”,并提供基于自定义 formatter 的正确解决方案,确保颜色代码只作用于 streamhandler,完全隔离文件日志的纯净性。
在 Python 日志系统中,一个常见误区是试图用 logging.Filter 实现日志内容的样式化(如为 levelname 添加 ANSI 颜色)。但正如示例所揭示的:Filter 在日志记录被格式化前就已修改 record 对象本身——而 record 是跨所有 Handler 共享的。因此,即使你只将 ColourLevelNameFilter 添加到 StreamHandler,它仍会污染 FileHandler 所用的同一 LogRecord 实例,导致颜色转义序列(如 \x1b[031m)意外写入日志文件。
根本原因在于:
- Filter.filter(record) 接收的是原始 LogRecord 对象;
- record.levelname = ... 是原地修改(in-place mutation);
- 同一 record 会被依次传递给所有已注册的 Handler(包括 FileHandler),此时 levelname 已含颜色码。
✅ 正确解法是避免修改 record,改在格式化阶段动态注入样式——即继承 logging.Formatter,重写 format() 方法,在生成最终字符串时精准替换目标字段。
以下是一个健壮、生产就绪的着色 Formatter 示例:
立即学习“Python免费学习笔记(深入)”;
import logging
class ColourFormatter(logging.Formatter):
FORMATS = {
logging.DEBUG: '\x1b[36m{0}\x1b[0m', # cyan
logging.INFO: '\x1b[32m{0}\x1b[0m', # green
logging.WARNING: '\x1b[33m{0}\x1b[0m', # yellow
logging.ERROR: '\x1b[31m{0}\x1b[0m', # red
logging.CRITICAL: '\x1b[41m{0}\x1b[0m', # white-on-red
}
def format(self, record: logging.LogRecord) -> str:
# 先调用父类获取标准格式化字符串
msg = super().format(record)
# 获取对应级别的着色 levelname
colour_levelname = self.FORMATS.get(
record.levelno, '{0}'
).format(record.levelname)
# 精准替换:仅替换独立出现的 levelname(避免误染 message 中同名文本)
# 使用 f-string 模式匹配时间+级别组合,进一步提升安全性
timestamp_level = f'{record.asctime} {record.levelname}'
colour_timestamp_level = f'{record.asctime} {colour_levelname}'
return msg.replace(timestamp_level, colour_timestamp_level, 1)使用时,为不同 Handler 分配不同 Formatter 即可实现完全隔离:
# 控制台使用着色 Formatter
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(ColourFormatter(
fmt='%(asctime)s %(levelname)s => %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# 文件使用纯文本 Formatter(无颜色)
file_handler = logging.FileHandler('app.log', mode='a')
file_handler.setFormatter(logging.Formatter(
fmt='%(asctime)s %(levelname)s => %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))⚠️ 注意事项:
- 永远不要在 Filter 中修改 record 的核心字段(如 levelname, msg, message)——这违反了 Filter 的设计契约(应仅做“是否通过”判断);
- Formatter.format() 是安全的着色入口,因它作用于最终字符串,且每个 Handler 独立调用;
- 替换逻辑建议优先匹配 asctime + levelname 组合,而非单独 levelname,可彻底规避日志消息中偶然出现相同单词导致的误着色;
- 若需兼容 Windows 终端(旧版 cmd),可在程序启动时调用 colorama.init() 或检查 sys.stdout.isatty() 再启用颜色。
总结:日志着色的本质是输出层定制,而非记录层干预。通过分离关注点(Filter 做路由,Formatter 做渲染),你既能获得直观的终端体验,又能保证文件日志的机器可读性与跨平台兼容性。










