Python logging 同时输出到文件和控制台

Python 的 logging.basicConfig 有一个常见坑:一旦指定了 filename,默认只写文件,不往控制台打印

问题复现

logging.basicConfig(
    filename="app.log",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

print("print 能看到")
logging.info("logger 看不到")  # 只写文件,控制台不显示

同时输出到文件和控制台

需要显式添加 StreamHandler

import logging
import sys
from datetime import datetime

def init_logger(log_file="app.log"):
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")

    # 文件输出
    fh = logging.FileHandler(log_file, encoding="utf-8")
    fh.setFormatter(fmt)
    logger.addHandler(fh)

    # 控制台输出
    ch = logging.StreamHandler(sys.stdout)
    ch.setFormatter(fmt)
    logger.addHandler(ch)

    return logger

或者用 basicConfig + 手动追加:

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    filename="app.log",
    encoding="utf-8"
)

# 追加控制台 Handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logging.getLogger().addHandler(console)

带结构化参数的 log 方法

class MyService:
    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)

    def log(self, *args, **kwargs):
        msg = " ".join(str(a) for a in args)
        if kwargs:
            extra = " ".join(f"{k}={v!r}" for k, v in kwargs.items())
            msg = f"{msg} | {extra}"
        self.logger.info(msg)

# 使用
svc = MyService()
svc.log("任务完成", task_id=42, duration=1.5)
# 输出: 任务完成 | task_id=42 duration=1.5

PyInstaller —noconsole 下看不到日志

--noconsole 打包的 exe 绑定到 Windows subsystem,stdout / stderr 不存在,StreamHandler 无输出目标。

解决方案

1. 只写文件,用 tail 等工具实时查看

# PowerShell(Windows 自带,等价于 tail -f)
Get-Content app.log -Tail 50 -Wait

2. 自定义 GUI 日志窗口(Tkinter)

import tkinter as tk
from tkinter.scrolledtext import ScrolledText

class TextHandler(logging.Handler):
    def __init__(self, widget):
        super().__init__()
        self.widget = widget

    def emit(self, record):
        msg = self.format(record)
        self.widget.insert(tk.END, msg + "\n")
        self.widget.see(tk.END)

root = tk.Tk()
text = ScrolledText(root, width=80, height=20)
text.pack()

handler = TextHandler(text)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logging.getLogger().addHandler(handler)

logging.info("程序启动")
root.mainloop()

3. 调试 / 发布双版本

:: build.bat
pyinstaller --onefile --console --name=控制器_debug scripts.py
pyinstaller --onefile --noconsole --name=控制器 scripts.py

开发用 debug 版,用户用 release 版。