拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python logging 不打印/重复打印/中文乱码?9 个高频坑症状→原因→修法对照表

Python logging 不打印/重复打印/中文乱码?9 个高频坑症状→原因→修法对照表

先给结论

logging出问题的九成原因是搞错了这三件事:① logger 和 handler 是两级过滤,都要过;②basicConfig()只在 root 没配置过时生效;③ handler 会重复叠加,而 logger 默认还会向上传到 root。本文把最常见的 9 个坑做成症状 → 原因 → 修法对照表,每一条都配可复现的最小代码。—## 一、先记住这张对照表(排错时按症状查)| 症状 | 真正的原因 | 修法 ||—|—|—||info()一点都不输出 | root logger 默认级别是WARNING|logging.basicConfig(level=logging.INFO)||basicConfig设了还是不输出 |之前已经有人配置过 root(导库时触发) | 显式给自己的 logger 加 handler || 一条日志打印2 次| 既给 logger 加了 handler,又传播到 root|logger.propagate = False|| 一条日志打印3 次以上| handler 被重复添加(模块级/函数级各加一次) | 加之前先判断if not logger.handlers|| 文件里中文是乱码|FileHandler默认用系统编码(Windows 是 GBK) | 显式encoding='utf-8'|| 文件里啥都没有 | 没flush,进程被强杀 |handler.flush()或用RotatingFileHandler|| 时间字段是UTC|asctime用time.localtime的时区 | 自定义 formatter 的converter|| 多进程写日志丢行/串行错乱| 多个进程写同一个文件句柄 | 用ConcurrentRotatingFileHandler或队列 || 在 uvicorn/gunicorn 里失效| 框架接管了 root logger | 用dictConfig或框架提供的配置 |—## 二、坑 1:info()不输出(级别问题)pythonimport logginglogging.info("看不到我") # 什么都不打印logging.warning("看得到我") # 只有这个会打印原因:root logger 的默认级别是WARNING,INFO/DEBUG直接被拦掉。
pythonimport logginglogging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")logging.info("这次能看到了")关键点:级别判断发生在产生日志的第一关(logger 自身级别),随后还有 handler 的级别再过滤一次。—## 三、坑 2:basicConfig设了却没生效pythonimport loggingimport requests # 很多库在 import 时就碰过 logginglogging.basicConfig(level=logging.INFO) # 无效,root 已有 handler原因:basicConfig()内部有一句“如果 root 已经有 handler,就直接 return”。第三方库只要import时调过一次,你的basicConfig就成了空操作。正确做法:别依赖 basicConfig,显式建自己的 logger。pythonimport logginglogger = logging.getLogger("myapp") # 用模块名/应用名,不要用 rootlogger.setLevel(logging.INFO)if not logger.handlers: # 关键:防止重复添加 h = logging.StreamHandler() h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) logger.addHandler(h)—## 四、坑 3:一条日志打印两次(propagate)pythonlogger = logging.getLogger("myapp")logger.addHandler(logging.StreamHandler())logging.basicConfig(level=logging.INFO) # root 也有 handlerlogger.info("hello")原因:logger 的propagate默认为True,日志传给自己的 handler 后,还会向上传给 root,root 的 handler 再打一遍。
pythonlogger.propagate = False # 一刀切:不再往上传判断标准:如果你的 logger 自己配了 handler,就把propagate关掉。—## 五、坑 4:打印 N 次(handler 重复叠加)pythondef get_logger(): lg = logging.getLogger("myapp") lg.addHandler(logging.StreamHandler()) # 每次调用都加一个 return lgget_logger(); get_logger(); get_logger().info("hi") # 打印 3 次原因:getLogger返回的是同一个单例,但addHandler会无脑追加。修法:加之前先判空,或用 dictConfig 一次性配置。pythonif not logger.handlers: logger.addHandler(h)—## 六、坑 5:中文乱码(编码问题,Windows 高发)pythonh = logging.FileHandler("app.log") # ❌ Windows 下默认 GBKh = logging.FileHandler("app.log", encoding="utf-8") # ✅原因:FileHandler不指定encoding时,用locale.getpreferredencoding(),中文 Windows 是GBK/CP936。写 UTF-8 文本进去就乱码或抛UnicodeEncodeError。顺带:读取时用open("app.log", encoding="utf-8"),两边编码一致才不会看花。—## 七、坑 6:日志时间不对(UTC / 时区)pythonimport timeformatter = logging.Formatter("%(asctime)s %(message)s")formatter.converter = time.localtime # 改成本地时区````asctime` 默认走 `time.localtime`(其实是本地时间),但容器里 `TZ` 没设置时**容器默认是 UTC**,看起来就"差 8 小时"。 **修法**:容器启动时带环境变量 `TZ=Asia/Shanghai`,或者在代码里指定 `converter`。---## 八、坑 7:多进程写同一个文件(丢日志)pythonh = logging.handlers.RotatingFileHandler(“app.log”, maxBytes=10_000_000, backupCount=5)**原因**:多个进程各自持有文件句柄和偏移量,追加写会互相踩。**方案 A(推荐)**:用 `concurrent-log-handler` 的 `ConcurrentRotatingFileHandler`(文件锁)。pythonfrom concurrent_log_handler import ConcurrentRotatingFileHandlerh = ConcurrentRotatingFileHandler(“app.log”, maxBytes=10_000_000, backupCount=5)**方案 B**:每个进程写自己的文件(`app.{pid}.log`),采集时再合并。---## 九、坑 8:f-string 的性能与聚合问题pythonlogger.debug(f"user={user} cost={cost}“) # ❌ 即使不输出也会先格式化logger.debug(“user=%s cost=%s”, user, cost) # ✅ 惰性格式化**两个理由**:① 惰性写法在日志被过滤掉时**不做字符串拼接**,省 CPU;② 接入 Sentry / ELK 这类系统时,模板相同才能**聚合成一条**,f-string 会让每个用户都变成独立事件。---## 十、坑 9:`logger.exception` 只能在 except 里用pythontry: risky()except ValueError: logger.exception(“处理失败”) # ✅ 自动带 traceback````logger.exception()等价于logger.error(…, exc_info=True)。在except块外调用,Python 3 会提示没有活动异常,打印NoneType: None。**另外**:捕获后要重新抛出时,用logger.error(”…", exc_info=True)+raise,别重复打两遍。---## 十一、可直接抄的配置(dictConfig 版) ```pythonimport logging.configLOGGING = { "version": 1, "disable_existing_loggers": False, # 别让第三方库的 logger 失效 "formatters": { "std": {"format": "%(asctime)s %(levelname)s [%(name)s] %(message)s"}, }, "handlers": { "console": {"class": "logging.StreamHandler", "level": "INFO", "formatter": "std"}, "file": {"class": "logging.FileHandler", "level": "DEBUG", "formatter": "std", "filename": "app.log", "encoding": "utf-8"}, # ← 编码一定要写 }, "loggers": { "myapp": {"level": "DEBUG", "handlers": ["console", "file"], "propagate": False}, # ← 防止打两遍 },}logging.config.dictConfig(LOGGING)logger = logging.getLogger("myapp")```**三个必须注意的参数**:disable_existing_loggers: False、encoding: “utf-8”、propagate: False。---## 十二、5 分钟排查清单1.logger.level是多少(0 表示继承父级)?2.logger.handlers里有几个(**>1 就是重复打印的元凶**)?3.logger.propagate是 True 吗(自己配了 handler 就该关掉)?4. 写文件时传了encoding=“utf-8”吗?5. 多进程场景用了带锁的 handler 吗?---## 小结logging的坑不在 API 难,而在于它是**"logger + handler + formatter + propagate" 四层结构**,任何一层配错都会表现成"日志不见了"或者"日志打两遍"。 **三条硬记住**:① 级别要过两道(logger 和 handler);②basicConfig只对"干净的 root"生效;③ 自己加了 handler 就把propagate` 关掉、且加之前判空。—你最常被哪种症状折磨?评论区留个"重复打印/不打印/乱码"三选一,我把出现频率最高的坑再单独展开一篇。

返回列表