目錄
- 一、引言:日志模塊在項目開發中的重要性
- 二、從 Django 日志配置看 Logging 模塊的核心組成
- 三、logging模塊核心組件詳解
- 3.1 記錄器Logger
- 3.2 級別Level
- 3.3 根記錄器使用
- 3.4 處理器Handler
- 3.5 格式化器Formatter
- 3.6 日志流
- 3.7 日志示例
- 四、日志模塊總結
一、引言:日志模塊在項目開發中的重要性
在日常的 Python 項目開發中,日志系統往往是一個容易被初學者忽視,卻對中大型項目至關重要的基礎設施。很多初學者在調試程序時習慣使用 print() 語句輸出變量和程序執行狀態,但這種方式有明顯的局限性:
- 信息不可控: print 輸出會混雜在一起,無法區分嚴重程度;
- 維護成本高: 上線前還需要手動刪除或注釋掉調試語句;
- 缺乏上下文: 無法記錄時間、代碼位置、線程等信息;
- 不適用于線上環境: 一旦部署,無法查看標準輸出,定位問題困難。
而 Python 標準庫提供的 logging 模塊,正是為了解決這些問題而設計的。在實際項目中,日志的作用包括但不限于:
- 調試和排查問題
- 當用戶反饋出現 bug 時,通過日志文件可以還原出錯時的系統狀態和調用鏈;
- 比如:接口返回 500,日志能顯示是數據庫連接失敗還是第三方服務超時。
- 監控系統運行狀態
- 結合日志采集系統(如 ELK、Fluentd、Sentry 等)可以實時監控錯誤、異常和性能瓶頸;
- 比如:一個接口響應超過 1 秒,可以通過日志告警定位慢查詢。
- 記錄用戶行為與業務日志
- 日志不僅是系統的
"體溫計"
,也可以作為業務分析的數據源; - 比如:記錄用戶注冊、登錄、下單、支付等關鍵操作,幫助后續做運營分析。
- 日志不僅是系統的
- 滿足合規與審計要求
- 某些金融、政務類項目要求保留操作日志,確保可追溯性和安全合規;
- 比如:記錄某個管理員什么時候對用戶數據進行了修改。
- 多模塊協作與團隊開發
- 在多人協作項目中,統一的日志規范和日志格式有助于快速定位代碼問題,提升協作效率;
- 通過 logger 名稱還能追蹤是哪一個模塊或組件產生日志,便于歸類分析。
正因為日志在調試、監控、安全、分析等方面都扮演著重要角色,一個成熟的項目往往都離不開一套合理的日志體系。接下來,我們就一起系統掌握 Python 的標準日志模塊 —— logging
的使用方法與實戰技巧。
二、從 Django 日志配置看 Logging 模塊的核心組成
如果你接觸過 Django 項目中的日志配置,你可能見過類似這樣的設置(位于 settings.py 中):
LOGGING = {"version": 1,"disable_existing_loggers": False,"formatters": {"verbose": {"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}","style": "{",},"simple": {"format": "{levelname} {message}","style": "{",},},"filters": {"special": {"()": "project.logging.SpecialFilter","foo": "bar",},"require_debug_true": {"()": "django.utils.log.RequireDebugTrue",},},"handlers": {"console": {"level": "INFO","filters": ["require_debug_true"],"class": "logging.StreamHandler","formatter": "simple",},"mail_admins": {"level": "ERROR","class": "django.utils.log.AdminEmailHandler","filters": ["special"],},},"loggers": {"django": {"handlers": ["console"],"propagate": True,},"django.request": {"handlers": ["mail_admins"],"level": "ERROR","propagate": False,},"myproject.custom": {"handlers": ["console", "mail_admins"],"level": "INFO","filters": ["special"],},},
}
參考鏈接:https://docs.djangoproject.com/zh-hans/5.0/topics/logging/#logging-explanation
這一配置可能讓人望而生畏,但它其實正好體現了 Python logging 模塊的 核心組成結構。學習 logging 模塊時,我們其實只需要掌握下面幾個關鍵概念,就能完全理解這段配置的意義。
日志系統的四大核心組件
- Logger(日志記錄器)
- 每個 logger 負責產生日志消息。
- 你可以為每個模塊或子系統創建不同的 logger(如:django, myapp.api)。
- 常用方法如:logger.info(),logger.error() 等。
- Handler(日志處理器),日志的實際處理者。有眾多處理器子類
- 日志記錄器本身不負責輸出日志,而是將日志交給一個或多個 Handler 來處理。
- 例如:StreamHandler 控制臺輸出,FileHandler 寫入文件,SMTPHandler 發送郵件等。
- 一個 logger 可以綁定多個 handler,實現
"一個日志,多個出口"
。
- Formatter(格式化器,日志輸出格式控制)
- 定義日志消息的輸出格式,如是否包含時間、級別、模塊名等。
- 不同的 handler 可以使用不同的格式器。
- Filter(過濾器,可選)
- 用于更精細地控制哪些日志記錄可以通過,通常不作為初學重點。
- 示例用途:只記錄某個模塊或某種業務類型的日志。
為什么要掌握這些組件?
- 在 Django 項目中,LOGGING 配置就是對這四大組件的組合使用;
- 如果你自己寫 Python 腳本或服務,也完全可以手動用代碼構建出同樣的日志體系;
- 理解這四個組件的關系,是靈活使用 logging 模塊的關鍵。
接下來分別對各個組件進行詳細講解。
三、logging模塊核心組件詳解
3.1 記錄器Logger
日志記錄器都是 Logger 類的實例,可以通過它實例化得到。但是 logging 模塊也提供了工廠方法。 Logger 實例的構建,使用 Logger 類也行,但推薦 getLogger 方法。
# 我目前使用的是python3.12版本,源碼中約2015行,為Logger類注入一個manager類屬性
Logger.manager = Manager(Logger.root)# 用工廠方法返回一個Logger實例
def getLogger(name=None):"""Return a logger with the specified name, creating it if necessary.If no name is specified, return the root logger."""if not name or isinstance(name, str) and name == root.name:return rootreturn Logger.manager.getLogger(name)
根記錄器: logging 模塊為了使用簡單,提供了一些快捷方法,這些方法本質上都用到了記錄器實例,即根記錄器實例。
# 源碼約1861行
class RootLogger(Logger):"""A root logger is not that different to any other logger, except thatit must have a logging level and there is only one instance of it inthe hierarchy."""def __init__(self, level):"""Initialize the logger with the name "root"."""Logger.__init__(self, "root", level)def __reduce__(self):return getLogger, ()# 根記錄器默認是警告
root = RootLogger(WARNING)
Logger.root = root
Logger.manager = Manager(Logger.root)
可以跟進一下 WARNING:
# 看到日志的級別總共5種
CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING # WARN不常用了,被WARNING所替代
INFO = 20
DEBUG = 10
NOTSET = 0_levelToName = {CRITICAL: 'CRITICAL',ERROR: 'ERROR',WARNING: 'WARNING',INFO: 'INFO',DEBUG: 'DEBUG',NOTSET: 'NOTSET',
}
_nameToLevel = {'CRITICAL': CRITICAL,'FATAL': FATAL,'ERROR': ERROR,'WARN': WARNING,'WARNING': WARNING,'INFO': INFO,'DEBUG': DEBUG,'NOTSET': NOTSET,
}
也就是說,logging 模塊一旦加載,就立即創建了一個 root 對象,它是 Logger 子類 RootLogger 的實例,日志記錄必須使用 Logger 實例。
實例和名稱: 每一個 Logger 實例都有自己的名稱,使用 getLogger 獲取記錄器實例時,必須指定名稱。在管理器內部維護一個名稱和 Logger 實例的字典,根記錄器的名稱就是 "root"
,未指定名稱,getLogger 返回根記錄器對象。示例代碼:
# -*- coding: utf-8 -*-
# @Time : 2025-05-14 10:53
# @Author : AmoXiang
# @File : logging_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680import logging# 不同的方式取根記錄器
root = logging.root
'''
<class 'logging.RootLogger'> <RootLogger root (WARNING)>
<RootLogger root (WARNING)>
True
True
'''
print(type(root), root)
print(logging.getLogger(None))
print(logging.getLogger(None) is root)
print(logging.Logger.root is root)# 通過Logger類實例化
l1 = logging.Logger('m1')
l2 = logging.Logger('m1')
'''
1611588981568 1611567646000 False
'''
print(id(l1), id(l2), l1 is l2)# 通過工廠方法獲取記錄器實例
m1 = logging.getLogger('m1')
print(type(m1), m1)
m2 = logging.getLogger('m2')
print(type(m2), m2)
m3 = logging.getLogger('m1')
'''
<class 'logging.Logger'> <Logger m1 (WARNING)>
<class 'logging.Logger'> <Logger m2 (WARNING)>
<class 'logging.Logger'> <Logger m1 (WARNING)>
1611561051312 1611588981616 1611561051312 True
m1 m2 m1
'''
print(type(m3), m3)
print(id(m1), id(m2), id(m3), m1 is m3)
print(m1.name, m2.name, m3.name)
層次結構: 記錄器的名稱另一個作用就是表示 Logger 實例的層次關系。Logger 是有層次結構的,使用 .
點號分割,如 'a'
、'a.b'
或 'a.b.c.d'
,a 是 a.b 的 父 parent,a.b 是 a 的子 child。對于 foo 來說,名字為 foo.bar、foo.bar.baz、foo.bam 都是 foo 的后代。
import logging# 父子 層次關系
# 根logger
root = logging.getLogger()
'''
1 root <class 'logging.RootLogger'> None
2 a <class 'logging.Logger'> root True
3 a.b <class 'logging.Logger'> a True
'''
print(1, root.name, type(root), root.parent) # 根logger沒有父
parent = logging.getLogger('a')
print(2, parent.name, type(parent), parent.parent.name, parent.parent is root)
child = logging.getLogger('a.b')
print(3, child.name, type(child), child.parent.name, child.parent is parent)
3.2 級別Level
CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0
級別可以是一個整數。0表示未設置,有特殊意義。級別可以用來表示日志消息級別、記錄器級別、處理器級別。
消息級別: 每一條日志消息被封裝成一個 LogRecord 實例,該實例包含消息本身、消息級別、記錄器的 name 等信息。消息級別只能說明消息的重要等級,但不一定能輸出。
記錄器級別: 日志輸出必須依靠記錄器,記錄器設定自己的級別,它決定著消息是否能夠通過該日志記錄器輸出。如果日志記錄器未設置自己的級別,默認級別值為0。
記錄器有效級別: 如果日志記錄器未設置自己的級別,默認級別值為0,等效級別就繼承自己的父記錄器的非0級別,如果設置了自己的級別且不為0,那么等效級別就是自己設置的級別。如果所有記錄器都沒有設置級別,最終根記錄器一定有級別,且默認設置為 WARNING。 只有日志級別高于產生日志的記錄器有效級別才有資格輸出,涉及源碼如下:
def getEffectiveLevel(self):"""Get the effective level for this logger.Loop through this logger and its parents in the logger hierarchy,looking for a non-zero logging level. Return the first one found."""logger = selfwhile logger:if logger.level:return logger.levellogger = logger.parentreturn NOTSET
處理器級別: 每一個 Logger 實例其中真正處理日志的是處理器 Handler,每一個處理器也有級別。它控制日志消息是否能通過該處理器 Handler 輸出。
3.3 根記錄器使用
產生日志: logging 模塊提供了 debug、info、warning、error、critical 等快捷方法,可以快速產生相應級別消息。本質上這些方法使用的都是根記錄器對象。舉個例子:
import logginglogging.warning('test~')
運行結果如下圖所示:
跟進 warning 方法,如下:
def warning(msg, *args, **kwargs):"""Log a message with severity 'WARNING' on the root logger. If the logger hasno handlers, call basicConfig() to add a console handler with a pre-definedformat."""# 1.可以看到操作的是根記錄器 即都是使用的root# 2.由于我們沒有給根記錄器設置handler,先會走這里,root.handlers類型是一個列表,handlers該屬性繼承自Logger類# self.handlers = []if len(root.handlers) == 0:basicConfig()root.warning(msg, *args, **kwargs)class Logger(Filterer):def __init__(self, name, level=NOTSET):"""Initialize the logger with a name and an optional level."""Filterer.__init__(self)self.name = nameself.level = _checkLevel(level)self.parent = Noneself.propagate = Trueself.handlers = []self.disabled = Falseself._cache = {}class RootLogger(Logger):def __init__(self, level):"""Initialize the logger with the name "root"."""Logger.__init__(self, "root", level)def __reduce__(self):return getLogger, ()
接著我們跟進一下 basicConfig() 方法,看它又在干啥(看源碼的時候,我們不一定要求每行都看懂,能知道大致邏輯即可),源碼如下所示:
def basicConfig(**kwargs):"""Do basic configuration for the logging system.This function does nothing if the root logger already has handlersconfigured, unless the keyword argument *force* is set to ``True``.It is a convenience method intended for use by simple scriptsto do one-shot configuration of the logging package.The default behaviour is to create a StreamHandler which writes tosys.stderr, set a formatter using the BASIC_FORMAT format string, andadd the handler to the root logger.A number of optional keyword arguments may be specified, which can alterthe default behaviour.filename Specifies that a FileHandler be created, using the specifiedfilename, rather than a StreamHandler.filemode Specifies the mode to open the file, if filename is specified(if filemode is unspecified, it defaults to 'a').format Use the specified format string for the handler.datefmt Use the specified date/time format.style If a format string is specified, use this to specify thetype of format string (possible values '%', '{', '$', for%-formatting, :meth:`str.format` and :class:`string.Template`- defaults to '%').level Set the root logger level to the specified level.stream Use the specified stream to initialize the StreamHandler. Notethat this argument is incompatible with 'filename' - if bothare present, 'stream' is ignored.handlers If specified, this should be an iterable of already createdhandlers, which will be added to the root logger. Any handlerin the list which does not have a formatter assigned will beassigned the formatter created in this function.force If this keyword is specified as true, any existing handlersattached to the root logger are removed and closed, beforecarrying out the configuration as specified by the otherarguments.encoding If specified together with a filename, this encoding is passed tothe created FileHandler, causing it to be used when the file isopened.errors If specified together with a filename, this value is passed to thecreated FileHandler, causing it to be used when the file isopened in text mode. If not specified, the default value is`backslashreplace`.Note that you could specify a stream created using open(filename, mode)rather than passing the filename and mode in. However, it should beremembered that StreamHandler does not close its stream (since it may beusing sys.stdout or sys.stderr), whereas FileHandler closes its streamwhen the handler is closed... versionchanged:: 3.2Added the ``style`` parameter... versionchanged:: 3.3Added the ``handlers`` parameter. A ``ValueError`` is now thrown forincompatible arguments (e.g. ``handlers`` specified together with``filename``/``filemode``, or ``filename``/``filemode`` specifiedtogether with ``stream``, or ``handlers`` specified together with``stream``... versionchanged:: 3.8Added the ``force`` parameter... versionchanged:: 3.9Added the ``encoding`` and ``errors`` parameters."""# Add thread safety in case someone mistakenly calls# basicConfig() from multiple threads_acquireLock()try:# 這里我們沒有傳遞參數,所以 kwargs 一定是 {}# pop()方法--刪除字典中指定鍵對應的鍵值對并返回被刪除的值# key不存在,返回設置的default值force = kwargs.pop('force', False) # Falseencoding = kwargs.pop('encoding', None) # Noneerrors = kwargs.pop('errors', 'backslashreplace') # backslashreplace# force為False不會進入該判斷語句中執行其對應邏輯if force:for h in root.handlers[:]:root.removeHandler(h)h.close()# 條件成立,走這里面的邏輯處理if len(root.handlers) == 0:handlers = kwargs.pop("handlers", None)# 排他if handlers is None:if "stream" in kwargs and "filename" in kwargs:raise ValueError("'stream' and 'filename' should not be ""specified together")else:if "stream" in kwargs or "filename" in kwargs:raise ValueError("'stream' or 'filename' should not be ""specified together with 'handlers'")# 走到這里 if handlers is None:# Nonefilename = kwargs.pop("filename", None)# 'a'mode = kwargs.pop("filemode", 'a')# 由于filename為None,所以會走else邏輯if filename:if 'b' in mode:errors = Noneelse:encoding = io.text_encoding(encoding)h = FileHandler(filename, mode,encoding=encoding, errors=errors)else:stream = kwargs.pop("stream", None)# 得到一個StreamHandler實例# self.stream = stream# 身上掛了一個屬性: stream = sys.stderr stderr屬性——標準錯誤對象h = StreamHandler(stream)# 將得到的StreamHandler實例放入列表中,并賦值給handlershandlers = [h]dfs = kwargs.pop("datefmt", None) # Nonestyle = kwargs.pop("style", '%') # '%'# _STYLES是一個字典,你可以ctrl進去看,%是keyif style not in _STYLES:raise ValueError('Style must be one of: %s' % ','.join(_STYLES.keys()))# '%': (PercentStyle, BASIC_FORMAT),# 取值 _STYLES['%'] ? (PercentStyle, BASIC_FORMAT)[1] ? BASIC_FORMAT# BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s" # 從之前的輸出結果來看,與BASIC_FORMAT設置的一模一樣# levelname: WARNING,name: root,message: test~# WARNING:root:test~fs = kwargs.pop("format", _STYLES[style][1])# 格式化器Formatter,得到實例fmt = Formatter(fs, dfs, style)for h in handlers:if h.formatter is None:# 為handler設置輸出格式h.setFormatter(fmt)# 將handler添加到日志處理器中,干活root.addHandler(h)# Nonelevel = kwargs.pop("level", None)if level is not None:root.setLevel(level)if kwargs:keys = ', '.join(kwargs.keys())raise ValueError('Unrecognised argument(s): %s' % keys)finally:_releaseLock()
至此 basicConfig() 方法整個邏輯執行完畢,接下來走:
root.warning(msg, *args, **kwargs)def warning(self, msg, *args, **kwargs):"""Log 'msg % args' with severity 'WARNING'.To pass exception information, use the keyword argument exc_info witha true value, e.g.logger.warning("Houston, we have a %s", "bit of a problem", exc_info=True)"""if self.isEnabledFor(WARNING):# 這里的源碼有興趣自己去看吧,太多了self._log(WARNING, msg, args, **kwargs)def isEnabledFor(self, level):"""Is this logger enabled for level 'level'?"""if self.disabled:return Falsetry:return self._cache[level]except KeyError:_acquireLock()try:if self.manager.disable >= level:is_enabled = self._cache[level] = Falseelse:is_enabled = self._cache[level] = (# 核心邏輯,判斷消息級別是否大于等于記錄器Logger的有效級別 # warning ? 30 self.getEffectiveLevel() ? 30 故返回True# 即self.isEnabledFor(WARNING): 為True,則會繼續向下執行邏輯 # self._log(WARNING, msg, args, **kwargs) 所以最后能在控制臺輸出level >= self.getEffectiveLevel())finally:_releaseLock()return is_enabled
以上大致分析了 logging.warning() 函數的一個執行邏輯,其他函數類似一個道理,講解到這里,你也應該知道,在我們沒有進行任何配置的情況下, logging.info() 函數為啥不能在控制臺輸出 msg 了,本質是達不到有效級別。
在分析源碼的過程中,我們看到了 BASIC_FORMAT = "%(levelname)s:%(name)s:%(message)s"
,這里羅列一下我們常會使用到的格式字符串:
占位符 | 含義描述 |
---|---|
%(asctime)s | 日志記錄時間,默認格式為 YYYY-MM-DD HH:MM:SS,mmm (毫秒) |
%(created)f | 日志事件的時間戳(UNIX 時間戳,float 類型) |
%(relativeCreated)d | 自 logging 模塊加載以來的毫秒數(相對時間) |
%(msecs)d | 日志時間中的毫秒部分 |
%(levelname)s | 日志級別名稱,如 DEBUG , INFO |
%(levelno)s | 日志級別的數值,如 10 , 20 |
%(name)s | Logger 的名稱 |
%(message)s | 日志消息內容,由 logger.debug()/info()/error() 等方法傳入的內容。當調用Formatter.format()時設置 |
%(pathname)s | 當前執行代碼的完整路徑 |
%(filename)s | 當前執行代碼的文件名 |
%(module)s | 模塊名(即去掉擴展名后的 filename ) |
%(funcName)s | 調用日志函數的函數名 |
%(lineno)d | 調用日志函數的源代碼行號 |
%(thread)d | 當前線程的 ID |
%(threadName)s | 當前線程名稱 |
%(process)d | 當前進程的 ID |
%(processName)s | 當前進程名稱 |
%(stack_info)s | 堆棧信息(如果提供了 stack_info=True ) |
示例 format 格式模板:
# 1.簡潔風格:
"%(asctime)s - %(levelname)s - %(message)s"
# 2.包含模塊和行號,適合調試用:
"%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"
# 3.適合生產環境的詳細日志格式:
"%(asctime)s | %(levelname)s | %(name)s | %(process)d | %(threadName)s | %(message)s"
# 4.和 Django 默認格式類似:
"%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"
基本配置: 從源碼中我們可以看到 logging 模塊提供 basicConfig() 函數,本質上是對根記錄器做最基本配置。示例:
# -*- coding: utf-8 -*-
# @Time : 2025-05-14 10:53
# @Author : AmoXiang
# @File : logging_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680import loggingformatter = "%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"# 根logger
logging.basicConfig(level=logging.INFO, format=formatter) # 設置輸出消息的格式
# 注意basicConfig只能調用一次
logging.basicConfig(level=logging.INFO) # 設置級別,默認WARNING
logging.basicConfig(filename="/tmp/test.log", filemode='w', encoding='utf-8') # 輸出到文件
logging.info('info msg~') # info函數第一個參數就是格式字符串中的%(message)s
logging.debug('debug msg~') # 日志消息級別不夠# 控制臺輸出結果為:
# 2025-05-14 14:01:39,587 [INFO] logging_demo.py:17 - info msg~
basicConfig() 函數執行完后,就會為 root 提供一個處理器,那么 basicConfig() 函數就不會再被調用了。
3.4 處理器Handler
日志記錄器需要處理器來處理消息,處理器決定著日志消息輸出的設備。Handler 控制日志信息的輸出目的地,可以是控制臺、文件。
可以單獨設置level
可以單獨設置格式
可以設置過濾器
Handler 類層次:
- Handler
- StreamHandler # 不指定使用 sys.stderr
- FileHandler # 文件
- _StderrHandler # 標準輸出NullHandler # 什么都不做
- StreamHandler # 不指定使用 sys.stderr
日志輸出其實是 Handler 做的,也就是真正干活的是 Handler。basicConfig() 函數執行后,默認會生成一個 StreamHandler 實例,如果設置了 filename,則只會生成一個 FileHandler 實例。每一個記錄器實例可以設置多個 Handler 實例。
# 定義處理器
handler = logging.FileHandler('o:/test.log', 'w', 'utf-8')
handler.setLevel(logging.WARNING) # 設置處理器級別
3.5 格式化器Formatter
每一個記錄器可以按照一定格式輸出日志,實際上是按照記錄器上的處理器上的設置的格式化器的格式字符串輸出日志信息。如果處理器上沒有設置格式化器,會調用缺省 _defaultFormatter,而缺省的格式符為:
class PercentStyle(object):default_format = '%(message)s'# 定義格式化器
formatter = logging.Formatter('#%(asctime)s <%(message)s>#')
# 為處理器設置格式化器
handler.setFormatter(formatter)
3.6 日志流
下圖是官方日志流轉圖:
繼承關系及信息傳遞:
- 每一個 Logger 實例的 level 如同入口,讓水流進來,如果這個門檻太高,信息就進不來。例如
log3.warning('log3')
,如果 log3 定義的級別高,就不會有信息通過 log3 - 如果 level 沒有設置,就用父 logger 的,如果父 logger 的 level 沒有設置,繼續找父的父的,最終可以找到 root 上,如果 root 設置了就用它的,如果 root 沒有設置,root 的默認值是 WARNING
- 消息傳遞流程
- 如果消息在某一個 logger 對象上產生,這個 logger 就是當前 logger,首先消息 level 要和當前 logger 的 EffectiveLevel 比較,如果低于當前 logger 的 EffectiveLevel,則流程結束;否則生成 log 記錄
- 日志記錄會交給當前 logger 的所有 handler 處理,記錄還要和每一個 handler 的級別分別比較,低的不處理,否則按照 handler 輸出日志記錄
- 當前 logger 的所有 handler 處理完后,就要看自己的 propagate 屬性,如果是 True 表示向父 logger 傳遞這個日志記錄,否則到此流程結束
- 如果日志記錄傳遞到了父 logger,不需要和父 logger 的 level 比較,而是直接交給父的所有 handler,父 logger 成為當前 logger。重復2、3步驟,直到當前 logger 的父 logger 是 None 退出,也就是說當前 logger 最后一般是 root logger(是否能到 root logger 要看中間的 logger 是否允許 propagate)
- logger 實例初始的 propagate 屬性為 True,即允許向父 logger 傳遞消息
- logging.basicConfig() 函數,如果 root 沒有 handler,就默認創建一個 StreamHandler,如果設置了 filename,就創建一個 FileHandler。如果設置了 format 參數,就會用它生成一個 Formatter 對象,否則會生成缺省 Formatter,并把這個 formatter 加入到剛才創建的 handler 上,然后把這些 handler 加入到 root.handlers 列表上。level 是設置給 root logger 的。如果 root.handlers 列表不為空,logging.basicConfig 的調用什么都不做。
3.7 日志示例
import logging# 根logger # 設置輸出消息的格式
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(threadName)s [%(message)s]")
print(logging.root.handlers)
mylogger = logging.getLogger(__name__) # level為0
mylogger.info('my info ~~~') # 實際上是傳播給了root輸出的
print('=' * 30)
# 定義處理器
handler = logging.FileHandler('./test.log', 'w', 'utf-8')
handler.setLevel(logging.WARNING) # 設置處理器級別
# 定義格式化器
formatter = logging.Formatter('#%(asctime)s <%(message)s>#')
# 為處理器設置格式化器
handler.setFormatter(formatter)
# 為日志記錄器增加處理器
mylogger.addHandler(handler)
mylogger.propagate = False # 阻斷向父logger的傳播
mylogger.info('my info2 ~~~~')
mylogger.warning('my warning info ---')
mylogger.propagate = True
mylogger.warning('my warning info2 +++')
結合日志輪轉 使用 RotatingFileHandler 或 TimedRotatingFileHandler,避免日志文件無限增長。示例:
import logging
from logging.handlers import TimedRotatingFileHandler
import time# 根logger # 設置輸出消息的格式
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(threadName)s [%(message)s]")
print(logging.root.handlers)
mylogger = logging.getLogger(__name__) # level為0
# 定義處理器
handler = TimedRotatingFileHandler('./test.log', 's', 30)
handler.setLevel(logging.INFO) # 設置處理器級別
# 定義格式化器
formatter = logging.Formatter('#%(asctime)s <%(message)s>#') # 為處理器設置格式化器
handler.setFormatter(formatter)
# 為日志記錄器增加處理器
mylogger.addHandler(handler)
# mylogger.propagate = True # 默認傳播到父
for i in range(20):time.sleep(3)mylogger.info('my message {:03} +++'.format(i))
'''
#2025-05-14 14:29:09,325 <my message 000 +++>#
#2025-05-14 14:29:12,325 <my message 001 +++>#
#2025-05-14 14:29:15,326 <my message 002 +++>#
#2025-05-14 14:29:18,327 <my message 003 +++>#
#2025-05-14 14:29:21,327 <my message 004 +++>#
#2025-05-14 14:29:24,328 <my message 005 +++>#
#2025-05-14 14:29:27,329 <my message 006 +++>#
#2025-05-14 14:29:30,329 <my message 007 +++>#
'''
四、日志模塊總結
在深入學習并閱讀了 logging 模塊的源碼之后,我們會發現:整個日志系統的設計其實非常清晰 —— 模塊化的組件組合(Logger、Handler、Formatter)加上可配置化的等級和輸出方式,邏輯非常清楚,上手也并不復雜。但在真實項目中,日志系統真正的挑戰不在于 "如何使用 logging"
,而在于 "日志應該寫在哪里,寫多少,寫什么"
。
這部分并沒有標準答案,它是依賴于經驗、項目規模、團隊協作模式和后期分析工具的。以下是一些實際工作中常見的思考與經驗總結:
場景 | 應該寫日志的位置 |
---|---|
關鍵業務流程 | 例如:用戶下單、支付、扣庫存、發貨等,建議打 INFO 日志記錄業務鏈路狀態 |
異常捕獲 | 在 try...except 中用 logger.exception() 記錄異常棧 |
性能瓶頸點 | 比如:數據庫慢查詢、接口超時、調用外部 API 的耗時,建議使用 WARNING 或 INFO 并記錄耗時數據 |
調試分支 | 某些重要但不常觸發的代碼分支,用 DEBUG 打印關鍵變量值 |
用戶輸入與驗證失敗 | 用戶輸入數據異常、驗證失敗、權限拒絕等,可用 WARNING 等級記錄 |
第三方服務調用失敗 | 例如:請求微信支付、發短信失敗等,要及時打日志,方便運維排查 |
如何寫出 "對未來有用"
的日志?
- 上下文清晰:日志中要包含發生了什么,在哪兒發生的,哪些參數,結果如何;
- 結構化內容:即便不使用 JSON,日志內容也要方便后續正則匹配、搜索;
- 避免日志泛濫:不要什么都打印,會掩蓋重點(特別是在循環、頻繁調用中);
- 區分等級與模塊:合理使用
DEBUG/INFO/WARNING/ERROR/CRITICAL
,并為每個模塊設置不同 logger,有助于日志隔離; - 提前考慮分析方式:日志最終可能用于搜索、告警、監控、審計,所以寫日志時可以站在
"未來使用者"
的角度思考。
日志模塊在爬蟲項目中的典型用途:
-
記錄請求與響應狀態。 在爬蟲中,請求網頁的每一個步驟都可能出現問題。我們通常會記錄如下內容:請求的 URL、響應狀態碼(200、403、404 等)、是否觸發反爬機制(驗證碼、跳轉)、頁面解析是否成功
logger.info(f"正在請求頁面: {url}") response = requests.get(url, headers=headers) if response.status_code != 200:logger.warning(f"請求失敗,狀態碼: {response.status_code},URL: {url}")
-
捕捉異常與失敗信息。 爬蟲運行過程中常見如連接超時、JSON 解析失敗、數據字段缺失、頁面結構變化等問題。
try:data = response.json() except Exception as e:logger.exception(f"解析 JSON 失敗,url={url}")
-
記錄數據抓取情況。 你可以用日志記錄:
-
每個頁面成功抓取的數據量;
-
每條數據是否完整;
-
抓取成功/失敗總計(可用于后期統計);
logger.info(f"成功抓取 {len(items)} 條數據 from {url}")
-
-
調試與優化爬蟲邏輯。 通過調試級別的日志輸出字段、分頁參數、選擇器內容、cookie 狀態等,有助于在開發階段排查問題。上線前可以關閉 DEBUG 級別日志,避免輸出過多無關信息。
logger.debug(f"當前請求參數: page={page}, keyword={keyword}")
-
應對反爬機制。 一些反爬機制會導致某些請求被封鎖或重定向,你可以通過日志及時發現:UA 被識別、IP 被封、驗證碼頁面、頁面結構突變
if "請輸入驗證碼" in response.text:logger.warning(f"觸發驗證碼,已停止爬取: {url}")
-
分模塊記錄日志。 對于較大的爬蟲系統(如 Scrapy、分布式爬蟲),可以對不同模塊(抓取、解析、存儲、調度等)使用不同的 logger 進行分類管理。這樣你可以只查看某一類日志,如只分析解析失敗的日志。
fetch_logger = logging.getLogger("fetcher") parse_logger = logging.getLogger("parser") save_logger = logging.getLogger("saver")
推薦一個第三方好用的日志庫:https://github.com/Delgan/loguru 優點:
- 開箱即用,幾乎無需配置
- 自動格式化、美化輸出(支持顏色)
- 內置異常捕捉
- 支持日志文件自動輪轉、壓縮、保留策略
- 支持 enqueue=True 異步寫入
簡單示例:
from loguru import loggerlogger.add("logfile.log", rotation="10 MB", retention="7 days", compression="zip")logger.info("抓取成功:{}", "http://example.com")
logger.warning("觸發驗證碼:{}", "http://example.com/captcha")
logger.exception("解析異常")
運行結果如下所示:
適用場景: 適合中小型項目、快速開發、爬蟲項目、自動化腳本等,極度推薦用于替代 logging 的簡潔封裝。
總結一句話:logging 模塊的語法可以一天掌握,但寫出對將來有價值的日志,需要很多天,很多項目,很多線上問題的積累。