Python 常用模塊(八):logging模塊

目錄

  • 一、引言:日志模塊在項目開發中的重要性
  • 二、從 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 模塊,正是為了解決這些問題而設計的。在實際項目中,日志的作用包括但不限于:

  1. 調試和排查問題
    • 當用戶反饋出現 bug 時,通過日志文件可以還原出錯時的系統狀態和調用鏈;
    • 比如:接口返回 500,日志能顯示是數據庫連接失敗還是第三方服務超時。
  2. 監控系統運行狀態
    • 結合日志采集系統(如 ELK、Fluentd、Sentry 等)可以實時監控錯誤、異常和性能瓶頸;
    • 比如:一個接口響應超過 1 秒,可以通過日志告警定位慢查詢。
  3. 記錄用戶行為與業務日志
    • 日志不僅是系統的 "體溫計",也可以作為業務分析的數據源;
    • 比如:記錄用戶注冊、登錄、下單、支付等關鍵操作,幫助后續做運營分析。
  4. 滿足合規與審計要求
    • 某些金融、政務類項目要求保留操作日志,確保可追溯性和安全合規;
    • 比如:記錄某個管理員什么時候對用戶數據進行了修改。
  5. 多模塊協作與團隊開發
    • 在多人協作項目中,統一的日志規范和日志格式有助于快速定位代碼問題,提升協作效率;
    • 通過 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 模塊時,我們其實只需要掌握下面幾個關鍵概念,就能完全理解這段配置的意義。

日志系統的四大核心組件

  1. Logger(日志記錄器)
    • 每個 logger 負責產生日志消息。
    • 你可以為每個模塊或子系統創建不同的 logger(如:django, myapp.api)。
    • 常用方法如:logger.info(),logger.error() 等。
  2. Handler(日志處理器),日志的實際處理者。有眾多處理器子類
    • 日志記錄器本身不負責輸出日志,而是將日志交給一個或多個 Handler 來處理。
    • 例如:StreamHandler 控制臺輸出,FileHandler 寫入文件,SMTPHandler 發送郵件等。
    • 一個 logger 可以綁定多個 handler,實現 "一個日志,多個出口"
  3. Formatter(格式化器,日志輸出格式控制)
    • 定義日志消息的輸出格式,如是否包含時間、級別、模塊名等。
    • 不同的 handler 可以使用不同的格式器。
  4. Filter(過濾器,可選)
    • 用于更精細地控制哪些日志記錄可以通過,通常不作為初學重點。
    • 示例用途:只記錄某個模塊或某種業務類型的日志。

為什么要掌握這些組件?

  1. 在 Django 項目中,LOGGING 配置就是對這四大組件的組合使用;
  2. 如果你自己寫 Python 腳本或服務,也完全可以手動用代碼構建出同樣的日志體系;
  3. 理解這四個組件的關系,是靈活使用 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)sLogger 的名稱
%(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 # 什么都不做

日志輸出其實是 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 日志流

下圖是官方日志流轉圖:
在這里插入圖片描述
繼承關系及信息傳遞:

  1. 每一個 Logger 實例的 level 如同入口,讓水流進來,如果這個門檻太高,信息就進不來。例如 log3.warning('log3'),如果 log3 定義的級別高,就不會有信息通過 log3
  2. 如果 level 沒有設置,就用父 logger 的,如果父 logger 的 level 沒有設置,繼續找父的父的,最終可以找到 root 上,如果 root 設置了就用它的,如果 root 沒有設置,root 的默認值是 WARNING
  3. 消息傳遞流程
    • 如果消息在某一個 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)
  4. logger 實例初始的 propagate 屬性為 True,即允許向父 logger 傳遞消息
  5. 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 的耗時,建議使用 WARNINGINFO 并記錄耗時數據
調試分支某些重要但不常觸發的代碼分支,用 DEBUG 打印關鍵變量值
用戶輸入與驗證失敗用戶輸入數據異常、驗證失敗、權限拒絕等,可用 WARNING 等級記錄
第三方服務調用失敗例如:請求微信支付、發短信失敗等,要及時打日志,方便運維排查

如何寫出 "對未來有用" 的日志?

  1. 上下文清晰:日志中要包含發生了什么,在哪兒發生的,哪些參數,結果如何;
  2. 結構化內容:即便不使用 JSON,日志內容也要方便后續正則匹配、搜索;
  3. 避免日志泛濫:不要什么都打印,會掩蓋重點(特別是在循環、頻繁調用中);
  4. 區分等級與模塊:合理使用 DEBUG/INFO/WARNING/ERROR/CRITICAL,并為每個模塊設置不同 logger,有助于日志隔離;
  5. 提前考慮分析方式:日志最終可能用于搜索、告警、監控、審計,所以寫日志時可以站在 "未來使用者" 的角度思考。

日志模塊在爬蟲項目中的典型用途:

  1. 記錄請求與響應狀態。 在爬蟲中,請求網頁的每一個步驟都可能出現問題。我們通常會記錄如下內容:請求的 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}")
    
  2. 捕捉異常與失敗信息。 爬蟲運行過程中常見如連接超時、JSON 解析失敗、數據字段缺失、頁面結構變化等問題。

    try:data = response.json()
    except Exception as e:logger.exception(f"解析 JSON 失敗,url={url}")
    
  3. 記錄數據抓取情況。 你可以用日志記錄:

    • 每個頁面成功抓取的數據量;

    • 每條數據是否完整;

    • 抓取成功/失敗總計(可用于后期統計);

      logger.info(f"成功抓取 {len(items)} 條數據 from {url}")
      
  4. 調試與優化爬蟲邏輯。 通過調試級別的日志輸出字段、分頁參數、選擇器內容、cookie 狀態等,有助于在開發階段排查問題。上線前可以關閉 DEBUG 級別日志,避免輸出過多無關信息。

    logger.debug(f"當前請求參數: page={page}, keyword={keyword}")
    
  5. 應對反爬機制。 一些反爬機制會導致某些請求被封鎖或重定向,你可以通過日志及時發現:UA 被識別、IP 被封、驗證碼頁面、頁面結構突變

    if "請輸入驗證碼" in response.text:logger.warning(f"觸發驗證碼,已停止爬取: {url}")
    
  6. 分模塊記錄日志。 對于較大的爬蟲系統(如 Scrapy、分布式爬蟲),可以對不同模塊(抓取、解析、存儲、調度等)使用不同的 logger 進行分類管理。這樣你可以只查看某一類日志,如只分析解析失敗的日志。

    fetch_logger = logging.getLogger("fetcher")
    parse_logger = logging.getLogger("parser")
    save_logger = logging.getLogger("saver")
    

推薦一個第三方好用的日志庫:https://github.com/Delgan/loguru 優點:

  1. 開箱即用,幾乎無需配置
  2. 自動格式化、美化輸出(支持顏色)
  3. 內置異常捕捉
  4. 支持日志文件自動輪轉、壓縮、保留策略
  5. 支持 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 模塊的語法可以一天掌握,但寫出對將來有價值的日志,需要很多天,很多項目,很多線上問題的積累。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/81125.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/81125.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/81125.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Servlet原理

Servlet 體系結構的類層次關系 Servlet&#xff08;接口&#xff09;&#xff1a;定義了 Servlet 的核心生命周期方法&#xff08;如 init()、service()、destroy()&#xff09;&#xff0c;是所有 Servlet 的頂層規范&#xff0c;任何 Servlet 都需實現該接口。GenericServlet…

數據科學和機器學習的“看家兵器”——pandas模塊 之五

目錄 4.5 pandas 高級數據處理與分析 一、課程目標 二、對數據表格進行處理 (一)行列轉置 (二)將數據表轉換為樹形結構 三、數據表的拼接 (一)merge () 函數的運用 (二)concat () 函數的運用 (三)append () 函數的運用 四、對數據表格的同級運算 五、計算數據表格中數…

組合問題(去重)

40. 組合總和 II - 力扣&#xff08;LeetCode&#xff09; class Solution { private:vector<vector<int>>result;vector<int>path;void backtracking(vector<int>& candidates, int target,int sum,int startIndex,vector<bool>&used)…

論QT6多線程技術

前言 以前我多線程使用傳統的繼承qthread重寫run()或者繼承qrunable類把對象丟到線程池解決。經過昨天的面試讓我了解到新的技術&#xff0c;我之前看到過只不過沒有詳細的去了解movetotread技術&#xff0c;這個技術是qt5推出的&#xff0c;qt6還在延續使用 代碼結構 以下是…

VTEP是什么

VTEP&#xff08;VXLAN Tunnel Endpoint&#xff0c;VXLAN 隧道端點&#xff09;是 VXLAN&#xff08;Virtual Extensible LAN&#xff09;網絡中的關鍵組件&#xff0c;用于處理 VXLAN 流量的封裝和解封裝。以下以可讀的 Markdown 格式詳細解釋 VTEP 的定義、功能、實現方式以…

antdv3 Tabs.TabPane 右上角增加一個角標Badge

1、Tabs官方說明 Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js 2、Badge角標官方效果圖 Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js 3、Tabs.TabPane要實現的效果 4、代碼 <Tabs v-m…

淺析 Spring 啟動過程:從源碼到核心方法

淺析 Spring 啟動過程&#xff1a;從源碼到核心方法 一、Spring 注解方式啟動類 Demo二、Spring 啟動過程源碼解析?AnnotationConfigApplicationContext構造函數refresh()方法詳解 三、refresh()的核心方法/步驟obtainFreshBeanFactory() - 獲取Bean工廠prepareBeanFactory(be…

貝葉斯優化Transformer融合支持向量機多變量回歸預測,附相關性氣泡圖、散點密度圖,Matlab實現

貝葉斯優化Transformer融合支持向量機多變量回歸預測&#xff0c;附相關性氣泡圖、散點密度圖&#xff0c;Matlab實現 目錄 貝葉斯優化Transformer融合支持向量機多變量回歸預測&#xff0c;附相關性氣泡圖、散點密度圖&#xff0c;Matlab實現效果一覽基本介紹程序設計參考資料…

智慧化系統安全分析報告

智慧化系統的安全背景與現狀 一、政策法規背景 &#xff08;一&#xff09;全球主要國家/地區政策對比 地區政策名稱核心內容實施時間特點中國《生成式人工智能服務管理暫行辦法》明確服務提供者責任&#xff0c;強調數據合法、隱私保護&#xff0c;禁止生成違法內容2023年8…

【學習筆記】點云自動化聚類簡要總結

聚類是將將具有相似特征劃分為相同點集的操作。 基于空間鄰近性的方法 核心思想&#xff1a;依據點的空間距離進行分組 歐式聚類&#xff08;DBSCAN&#xff0c;KD-tree) 原理&#xff1a;基于半徑搜索和最小點數擴展簇。 優點&#xff1a;適應不規則形狀&#xff0c;無需預…

全志F10c200開發筆記——移植uboot

相關資料&#xff1a; &#xff08;二&#xff09;uboot移植--從零開始自制linux掌上電腦&#xff08;F1C200S)&#xff1c;嵌入式項目&#xff1e;-CSDN博客 F1C200S挖坑日記&#xff08;3&#xff09;——Uboot編譯篇_f1c200s uboot-CSDN博客 一、安裝編譯器 Linaro Rele…

常見WEB漏洞----暴力破解

什么是暴力破解 暴力破解 (Brue Force) 是一種攻擊方法 (窮舉法)&#xff0c;簡稱為“爆破”&#xff0c;黑客通過反復猜解和實驗&#xff0c;旨在以暴力手段登入、訪問目標主機獲取服務&#xff0c;破壞系統安全&#xff0c;其屬于 ATT&CK技術中的一種&#xff0c;常利用…

ARM A64 LDR指令

ARM A64 LDR指令 1 LDR (immediate)1.1 Post-index1.2 Pre-index1.3 Unsigned offset 2 LDR (literal)3 LDR (register)4 其他LDR指令變體4.1 LDRB (immediate)4.1.1 Post-index4.1.2 Pre-index4.1.3 Unsigned offset 4.2 LDRB (register)4.3 LDRH (immediate)4.3.1 Post-index…

2.安卓逆向2-adb指令

免責聲明&#xff1a;內容僅供學習參考&#xff0c;請合法利用知識&#xff0c;禁止進行違法犯罪活動&#xff01; 內容參考于&#xff1a;圖靈Python學院 工具下載&#xff1a; 鏈接&#xff1a;https://pan.baidu.com/s/1bb8NhJc9eTuLzQr39lF55Q?pwdzy89 提取碼&#xff1…

Obsidian Callouts標注框語法

Obsidian 從 0.14 版本開始原生支持 Callouts&#xff1a; 語法基于 Markdown 引用塊&#xff08;>&#xff09;擴展&#xff1a; 語法格式如下&#xff1a; > [!類型] 可選標題 > 內容支持 **Markdown 格式**、[[內部鏈接]] 和嵌入文件。預覽 可選類型一覽&#xf…

nt!MiAllocateWsle函數分析之設置Wsle[WorkingSetIndex]

第一部分&#xff1a; 1: kd> p nt!MiAddValidPageToWorkingSet0xa9: 80a83c13 e8da9afcff call nt!MiAllocateWsle (80a4d6f2) 1: kd> t nt!MiAllocateWsle: 80a4d6f2 55 push ebp 1: kd> dv WsInfo 0x8953a1f8 PointerPte …

docker 命令操作大全

1 Docker Hello World 簡單命令 docker run ubuntu:15.10 /bin/echo "Hello world" docker run&#xff1a;啟動一個新容器。 ubuntu:15.10&#xff1a;使用的 Docker 鏡像&#xff08;Ubuntu 15.10 版本&#xff09;。 Docker 首先從本地主機上查找鏡像是否存在&a…

【軟件工程】基于機器學習的多缺陷定位

基于機器學習的多缺陷定位&#xff08;Multi-Dault Localization, MDL&#xff09;是軟件工程和自動化測試領域的重要研究方向&#xff0c;旨在通過機器學習技術高效識別代碼中多個潛在缺陷的位置。以下從方法、挑戰、應用場景及未來方向展開分析&#xff1a; 一、核心方法 監督…

用MCP往ppt文件里插入系統架構圖

文章目錄 一、技術架構解析1. Markdown解析模塊(markdown_to_hierarchy)2. 動態布局引擎(give_hierarchy_positions)3. PPTX生成模塊(generate_pptx)二、核心技術亮點1. 自適應布局算法2. MCP服務集成三、工程實踐建議1. 性能優化方向2. 樣式擴展方案3. 部署實踐四、應用…

CS016-2-unity ecs

目錄 【23】射擊改進 【24】僵尸生成器 ?編輯【25】隨機行走 【27】射擊光效 【23】射擊改進 a. 當距離目標太遠的時候&#xff0c;要繼續移動。而當距離目標到達攻擊距離之后&#xff0c;則停止移動。 上圖中的if&#xff1a;判斷自身和目標的距離是否大于攻擊距離&#…