Python 內建的 logging 模組可取代 print() 的除錯功能, 開發完成後只要用 logging.disable(50) 指令取消日誌輸出功能即可, 不需刪除日誌指令. 此外, 日誌除了輸出到螢幕顯示外, 還可以輸出到檔案保存, 這是 print() 做不到的. 總之, 使用 logging 功能可以輕易地在程式中輸出自訂訊息, 若執行時部分訊息沒有輸出的話, 表示相關的部分程式碼段被跳過沒有執行.
以下測試參考了下列書籍 :
- Python 3 技術手冊 (碁峰, 黃書逸) - 第 11-28 節
- Python 自動化的樂趣 (碁峰, AL Sweigart) - 第 10 章
- Python 網路爬蟲實戰 (松崗, 胡松濤) -第 4-2 節
- Python 3 程式庫參考手冊 (碁峰, 陳建勳) - 第 9-10 節
- Python 入門邁向高手之路-王者歸來 (深石, 洪錦魁) - 第 15-7 節
# Python 學習筆記 : 安裝執行環境與 IDLE 基本操作
# Python 學習筆記 : 檔案處理
logging 模組是一個非常有彈性的日誌系統, 可將程式執行中發生的事件, 錯誤, 或警告依據安全等級予以過濾後輸出至螢幕 (標準輸出) 或檔案中保存. logging 模組為內建, 使用前只要 import 即可.
D:\Python\test>python
Python 3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:54:40) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import logging
說明文件參考 :
# https://docs.python.org/3/library/logging.html
logging 模組預先定義了如下 6 種安全等級常數以及對應之日誌輸出函數 (但 logging.NOTSET 無對應之輸出函數), 其中 DEBUG 等級最低也最不重要, 用來輸出除錯訊息; INFO 用來記錄一般事件; WARNING 用來輸出目前無礙程式執行, 但未可能使程式無法執行之潛在警訊; ERROR 用來記錄已經發生之程式錯誤; CRITICAL 等級最高, 用來記錄會使程式停止或當掉的致命性錯誤 :
安全等級 | 數值 | 說明 | 輸出函數 |
logging.NOTSET | 0 | 未設定 | x |
logging.DEBUG | 10 | 除錯等級 | logging.debug() |
logging.INFO | 20 | 訊息等級 | logging.info() |
logging.WARNING | 30 | 警告等級 | logging.warning() |
logging.ERROR | 40 | 錯誤等級 | logging.error() |
logging.CRITICAL | 50 | 嚴重錯誤等級 | logging.critical() |
查詢 logging 之安全等級如下, 傳回值為代表等級高低之數值 :
>>> logging.NOTSET
0
>>> logging.DEBUG
10
>>> logging.INFO
20
>>> logging.WARNING
30
>>> logging.ERROR
40
>>> logging.CRITICAL
50
這 6 種安全等級是 logging 模組預設的等級, 基本上足敷一般使用需求. 如果要自訂等級也可以呼叫 logging.addLevelName(level, levelName) 來新增自己的安全等級, 其中 level 為與嚴重性成正比之正整數, 而 levelName 則為其對應之等級名稱. 呼叫 logging.getLevelName(level) 則會傳回等級值 level 之等級名稱, 例如 :
>>> logging.addLevelName(35, 'MINOR') #新增自訂之等級名稱 'MINOR'
>>> logging.getLevelName(35) #傳回等級名稱
'MINOR'
>>> logging.getLevelName(0)
'NOTSET'
>>> logging.getLevelName(10)
'DEBUG'
>>> logging.getLevelName(20)
'INFO'
>>> logging.getLevelName(30)
'WARNING'
>>> logging.getLevelName(40)
'ERROR'
>>> logging.getLevelName(50)
'CRITICAL'
呼叫日誌輸出函數 (須傳入自訂訊息) 會依據日誌之安全等級設定來自動決定是否要紀錄或輸出該訊息, logging 模組預設安全等級為 WARNING, 大於或等於 WARNING 等級之訊息才會被記錄, 例如 :
>>> logging.debug("debug message") #小於安全層級 : 不輸出
>>> logging.info("info message") #小於安全層級 : 不輸出
>>> logging.warning("warning message") #大於等於安全層級 : 輸出
WARNING:root:warning message
>>> logging.error("error message") #大於等於安全層級 : 輸出
ERROR:root:error message
>>> logging.critical("critical message") #大於等於安全層級 : 輸出
CRITICAL:root:critical message
可見預設情況下, 只有等級大於或等於 WARNING 的才會被記錄.
其中安全等級為 DEBUG, INFO, ... CRITICAL 等, 使用者為電腦之使用者帳號, 最後才是自訂訊息. 利用 logging 模組的 basicConfig() 函數可以設定日誌之安全層級, 只要將安全等級以 level 參數傳進去即可 (亦可傳入數值), 例如 :
>>> logging.basicConfig(level=logging.DEBUG) #設定日誌安全等級為 DEBUG
>>> logging.debug("debug message")
DEBUG:root:debug message
>>> logging.info("info message")
INFO:root:info message
>>> logging.warning("warning message")
WARNING:root:warning message
>>> logging.error("error message")
ERROR:root:error message
>>> logging.critical("critical message")
CRITICAL:root:critical message
可見將日誌安全等級降到最低的 DEBUG 後, 所有比 DEBUG 等級還要高的訊息都會被記錄. 如果只對 ERROR 層級以上的訊息有興趣, 則只要將 level 設為 40 (logging.ERROR) 即可, 但一個訊息之重要性屬於哪一個安全等級還是需要寫程式的人自行判定要呼叫哪一個輸出函數.
logging.basicConfig() 有 8 個參數如下表所示 :
參數 | 說明 |
level | 日誌之安全等級 (0, 10, 20, 40, 50) |
format | 控制輸出訊息的格式化字串 |
filename | 用來儲存輸出訊息的日誌檔案名稱 |
filemode | 開啟日誌檔案之模式, 如 'a' (預設), 'w' 等 |
datefmt | 輸出日期時間 asctime 之格式字串, 與 time.strftime() |
style | 格式化字串的標示字元, 有三種 : % (預設), {, 或 $ |
handlers | 加入至根日誌之處理器, 不可與 stream, filename 同時存在 |
stream | 標準輸出之串流 |
其中 filename 參數是當要將日誌紀錄於日誌檔案時用來指定檔案名稱的. 而 datefmt 則是用來設定 format 參數的格式化字串中 asctime 之日期時間格式, 與 time.strftime() 函數一樣, 常用格式如下 :
參數 | 說明 |
%Y | 長年份格式, 例如 2018 |
%y | 短年份格式, 例如 18 |
%m | 月份 01~12 |
%d | 日期 01~31 |
%H | 小時 00~23 |
%w | 星期 0~6 (0=星期日) |
%M | 分鐘 00~59 |
%S | 秒 00~59 |
關於 basicConfig() 參考 :
# https://docs.python.org/3/library/logging.html#logging.basicConfig
格式化字串 | 說明 |
%(asctime)s | 日期時間, 格式為 YYYY-MM-DD HH:mm:SS,ms (毫秒) |
%(message)s | 使用者自訂訊息 |
%(levelname)s | 日誌安全等級 |
%(levelno)s | 日誌安全等級之數值 |
%(name)s | 使用者名稱 (帳號) |
%(lineno)d | 日誌輸出函數呼叫於程式中所在之列數 |
%(filename)s | 日誌輸出函數之模組的檔名 |
%(module)s | 日誌輸出函數之模組名稱 |
%(pathname)s | 日誌輸出函數之模組之完整路徑 |
%(funcName)s | 日誌輸出函數之名稱 |
%(threrad)d | 執行緒 ID |
%(threradName)s | 執行緒名稱 |
%(process)d | 程序 ID |
%(created)f | 以 UNIX 標準表示之現在時間 (浮點數) |
如果沒有傳入 format 參數, 則預設之輸出訊息格式為 :
安全等級:使用者:自訂訊息
即格式化字串預設為 format='%(levelname)s:%(name)s:%(message)s'
例如 :
>>> import logging
>>> logging.warning("Warning message") #預設安全層級 : WARNING
WARNING:root:Warning message
>>> logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(levelname)s : %(message)s')
>>> logging.debug('Debug message')
2018-04-24 18:18:19,848 - DEBUG : Debug message
>>> logging.Warning('Warning message')
2018-04-24 18:22:24,796 - WARNING : Warning message
>>> logging.info('Info message')
2018-04-24 18:27:01,729 - INFO : Info message
>>> logging.error('Error message')
2018-04-24 18:27:31,980 - ERROR : Error message
>>> logging.critical('Critical message')
2018-04-24 18:28:02,600 - CRITICAL : Critical message
注意, asctime 的預設格式為 '%Y-%m-%d %H:%M:%S,', 毫秒部分另外用 '%s,%03d' 格式 (參考 formatTime). 這可用 datefmt 參數來自訂格式, 例如 :
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG,
... format='%(asctime)s - %(levelname)s : %(message)s',
... datefmt='%Y%m%dT%H%M%S')
>>> logging.info('Info message')
20180424T193715 - INFO : Info message
可見時間格式已經改為自訂格式了. 我在 Windows 下測試發現, 連續用 logging.basicConfig() 更改設定有時候並無效果, 這時只要用 exit() 跳出 IDLE 重新啟動再執行 logging.basicConfig() 即可.
如果要將日誌輸出到檔案, 則須傳入 filename 參數, 傳入值為可含路徑之檔案名稱字串, 例如 :
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG,
... format='%(asctime)s - %(levelname)s : %(message)s',
... filename='mylog.txt')
>>> logging.critical("critical message")
>>> logging.debug("debug message")
>>> logging.info("info message")
>>> logging.warning("warning message")
>>> logging.error("error message")
>>> logging.critical("critical message")
此處是將訊息輸出到目前目錄下的 mylog.txt 日誌檔裡. 可見使用 filename 參數將訊息輸出至日誌檔案後, 就不會輸出至標準輸出 (螢幕) 了. 開啟 mylog.txt 果然訊息都記錄在輸出檔 :
日誌檔的預設寫入模式為 'a' (append), 亦即新的輸出訊息會被記錄在日誌檔尾端. 如果要改為其他寫入模式例如 'w', 則須傳入參數 filemode='w'.
Python 的 logging 模組可以同時紀錄多個日誌, 上面的範例中直接使用 logging 模組輸出日誌訊息使用的是預設之根日誌 (Root Logger), 因此預設之輸出訊息中的 %(name) 顯示使用者為 'root'. 不傳入參數直接呼叫 logging.getLogger() 會傳回根日誌 RootLogger 物件, 它是 Logger 物件的一個特例, 提供如下幾個常用方法 :
Logger 物件方法 | 說明 |
setLevel(level) | 設定日誌安全等級 |
getEffectiveLevel() | 傳回目前設定之安全等級 |
isEnabledFor(level) | 檢查傳入之 level 是否會被處理 (依據目前等級), 傳回 True/False |
debug(msg) | 輸出 debug 等級之訊息 |
info(msg) | 輸出 info 等級之訊息 |
warning(msg) | 輸出 warning 等級之訊息 |
error(msg) | 輸出 error 等級之訊息 |
critical(msg) | 輸出 critical 等級之訊息 |
log(level, msg) | 輸出指定等級 level 之訊息 |
addHandler(handler) | 加入處理器 handler |
說明文件參考 :
# https://docs.python.org/3/library/logging.html#logging.Logger
# https://docs.python.org/2.4/lib/minimal-example.html
例如 :
>>> import logging
>>> rootlogger=logging.getLogger() #傳回根日誌 RootLogger 物件
>>> type(rootlogger) #型態為 RootLogger
<class 'logging.RootLogger'>
>>> rootlogger.getEffectiveLevel() #傳回目前根日誌安全等級=WARNING
30
>>> rootlogger.setLevel(logging.ERROR) #設定根日誌安全等級=ERROR
>>> rootlogger.getEffectiveLevel() #傳回目前根日誌安全等級=ERROR
40
>>> rootlogger.isEnabledFor(logging.DEBUG) #DEBUG 低於 ERROR
False
>>> rootlogger.isEnabledFor(logging.WARNING) #WARNING 低於 ERROR
False
>>> rootlogger.isEnabledFor(logging.ERROR) #等於 ERROR : True
True
>>> rootlogger.isEnabledFor(logging.CRITICAL) #大於 ERROR : False
True
>>> rootlogger.debug("debug message") #低於 ERROR : 不輸出
>>> rootlogger.warning("warning message") #低於 ERROR : 不輸出
>>> rootlogger.error("error message") #等於 ERROR : 輸出
error message
>>> rootlogger.critical("critical message") #大於 ERROR : 輸出
critical message
>>> rootlogger.log(logging.CRITICAL,"critical message") #通用之日誌輸出函數
critical message
如果要使用自訂的日誌而非根日誌, 可在呼叫 logging.getLogger() 函數時傳入日誌名稱字串, 傳回值是一個 Logger 物件, 呼叫 Logger 物件的 debug(), info(), warning(), error(), critical() 或 log() 方法即可將訊息輸出至該自訂日誌而非根日誌, 例如 :
>>> import logging
>>> tonylogger=logging.getLogger('Tony') #設定名為 Tony 之自訂日誌
>>> type(tonylogger) #傳回 Logger 物件
<class 'logging.Logger'>
>>> tonylogger.getEffectiveLevel() #傳回 Tony 日誌安全等級=WARNIMG
30
>>> tonylogger.setLevel(40) #設定 Tony 日誌安全等級=ERROR
>>> tonylogger.getEffectiveLevel() #傳回 Tony 日誌安全等級=ERROR
40
>>> tonylogger.debug("debug message") #低於 ERROR : 不輸出
>>> tonylogger.warning("warning message") #低於 ERROR : 不輸出
>>> tonylogger.error("error message") #等於 ERROR : 輸出
error message
>>> tonylogger.critical("critical message") #大於 ERROR : 輸出
critical message
可見不論使用 Logger 或 RootLogger 物件輸出日誌訊息, 預設格式只有訊息本身, 如果要同時記錄使用者, 安全等級與日期時間等訊息, 須依下列順序設定 :
- 呼叫 logging.Formatter() 建立 Formatter 格式化物件
- 呼叫 logging.StreamHandler() 建立 StreamHandler 輸出串流處理物件
- 呼叫 Handler 物件之 setFormatter() 方法設定處理器之輸出格式
- 呼叫 Logger 物件之 addHandler() 加入處理器物件
>>> formatter=logging.Formatter('%(levelname)s:%(name)s:%(message)s') #格式化
>>> type(formatter) #傳回 Formatter 物件
<class 'logging.Formatter'>
>>> streamhandler=logging.StreamHandler() #傳回 StreamHandler 物件
>>> type(streamhandler)
<class 'logging.StreamHandler'>
>>> streamhandler.setFormatter(formatter) #為處理器指定格式化物件
>>> tonylogger.addHandler(streamhandler) #為自訂日誌添加處理器
>>> tonylogger.log(logging.WARNING,"warning message") #低於 ERROR : 不輸出
>>> tonylogger.log(logging.ERROR,"error message")
ERROR:Tony:error message
>>> tonylogger.log(logging.CRITICAL,"critical message")
CRITICAL:Tony:critical message
>>> format='%(asctime)s:%(levelname)s:%(name)s:%(message)s'
>>> formatter=logging.Formatter(format) #傳回 Formatter 物件
>>> type(formatter)
<class 'logging.Formatter'>
>>> logfilehandler=logging.FileHandler('./tonylog.txt') #傳回 FileHandler 物件
>>> type(logfilehandler)
<class 'logging.FileHandler'>
>>> filehandler.setFormatter(formatter) #為處理器指定格式化物件
>>> tonylogger.addHandler(filehandler) #為自訂日誌添加處理器
>>> tonylogger.log(logging.WARNING,"warning message") #低於 ERROR : 不輸出
>>> tonylogger.log(logging.ERROR,"error message")
ERROR:Tony:error message
>>> tonylogger.log(logging.CRITICAL,"critical message")
CRITICAL:Tony:critical message
開啟日誌檔案 tonylog.txt 內容如下 :
2018-04-25 14:59:56,069:ERROR:Tony:error message
2018-04-25 15:00:04,043:CRITICAL:Tony:critical message
可見與上面使用根日誌時不同之處在於, 日誌訊息同時輸出到螢幕與日誌檔, 這是因為我們同時在自訂日誌 tonylogger 中開啟了 StreamHandler (給螢幕) 與 FileHandler (給檔案) 之故. 使用自訂日誌雖然較麻煩, 但彈性比較大.
上面我們是武斷地為自訂日誌取名 ('Tony'), 事實上若以目前登入者帳號為名可能較實用, 這可以用 Python 內建 getpass 模組之 getuser() 來取得使用者名稱, 例如 :
>>> import getpass
>>> user=getpass.getuser() #傳回登入者帳號 (字串)
>>> user
'user'
>>> type(user) #getuser() 傳回值為字串
<class 'str'>
>>> import logging
>>> logger=logging.getLogger(user) #建立以使用者為名之日誌物件
>>> type(logger)
<class 'logging.Logger'>
>>> logger.getEffectiveLevel() #查詢目前安全等級之值
30 #30=logging.WARNING
為了使用日誌方便, 可將所需日誌格式寫成一個類別, 在 "Python 網路爬蟲實戰" 的 4-2-2 節有介紹一個自訂日誌類別寫法, 我將其改寫如下 :
#myLog.py
import logging
import getpass
class MyLog(object):
def __init__(self):
user=getpass.getuser()
self.logger=logging.getLogger(user)
self.logger.setLevel(logging.DEBUG)
format='%(asctime)s - %(levelname)s -%(name)s : %(message)s'
formatter=logging.Formatter(format)
streamhandler=logging.StreamHandler()
streamhandler.setFormatter(formatter)
self.logger.addHandler(streamhandler)
logfile='./' + user + '.log'
filehandler=logging.FileHandler(logfile)
filehandler.setFormatter(formatter)
self.logger.addHandler(filehandler)
def debug(self, msg):
self.logger.debug(msg)
def info(self, msg):
self.logger.info(msg)
def warning(self, msg):
self.logger.warning(msg)
def error(self, msg):
self.logger.error(msg)
def critical(self, msg):
self.logger.critical(msg)
def log(self, level, msg):
self.logger.log(level, msg)
def setLevel(self, level):
self.logger.setLevel(level)
def disable(self):
logging.disable(50)
將此類別存成 myLog.py 檔案, 用 from myLog import MyLog 匯入 MyLog 類別後呼叫 MyLog() 建構式建立物件, 然後就可以呼叫此物件之 debug(), info(), warning(), error(), critical(), 以及 log() 等方法來記錄日誌訊息於螢幕與日誌檔 "使用者名稱.log" 了, 例如 :
>>> from myLog import MyLog
>>> mylog=MyLog()
>>> mylog.error("error message") #預設安全等級=DEBUG
2018-04-25 18:17:07,364 - ERROR -Tony : error message
>>> mylog.debug("debug message")
2018-04-25 18:25:03,276 - DEBUG -Tony : debug message
>>> mylog.info("info message")
2018-04-25 18:30:28,747 - INFO -Tony : info message
>>> mylog.warning("warning message")
2018-04-25 18:30:41,814 - WARNING -Tony : warning message
>>> mylog.error("error message")
2018-04-25 18:31:08,937 - ERROR -Tony : error message
>>> mylog.critical("critical message")
2018-04-25 18:31:27,330 - CRITICAL -Tony : critical message
>>> mylog.log(40, "error message")
2018-04-25 18:40:15,587 - ERROR -Tony : error message
可見使用這個 MyLog 類別讓紀錄日誌簡單多了. 此類別預設安全等級為 DEBUG (根日誌預設是 WARNING), 因此每一種等級之資訊都會輸出. 日誌同時會記錄到以使用者為名之 .log 檔案, 此處使用者為 Tony, 因此開啟目前目錄下之 Tony.log 會發現與螢幕輸出完全一樣之內容 :
2018-04-25 18:25:03,276 - DEBUG -Tony : debug message
2018-04-25 18:30:28,747 - INFO -Tony : info message
2018-04-25 18:30:41,814 - WARNING -Tony : warning message
2018-04-25 18:31:08,937 - ERROR -Tony : error message
2018-04-25 18:31:27,330 - CRITICAL -Tony : critical message
2018-04-25 18:40:15,587 - ERROR -Tony : error message
此類別預設安全等級為 10 (DEBUD), 因此每一種訊息都會輸出紀錄, 呼叫 setLevel() 方法傳入 10, 20, 30, 40, 50 可以更改安全等級設定 (不要用 logging.DEBUG 等, 會出現錯誤) :
>>> mylog.setLevel(30) #安全等級由預設之 DEBUG 改為 WARNING
>>> mylog.info("info message") #低於 WARNING : 不輸出
>>> mylog.warning("warning message") #等於 WARNING : 輸出
2018-04-25 18:44:20,290 - WARNING -Tony : warning message
此處將安全等級改為 30 (WARNING) 後, INFO 等級以下的就不會輸出了, 要等於或大於 WARNING 的才會輸出.
最後, 程式開發除錯結束後, 在根日誌可以呼叫 logging.disable(50) 或 logging.disable(logging.CRITICAL) 來關閉日誌輸出功能, 不需要將程式中所有日誌輸出指令刪除, 例如 :
>>> logging.disable(50) #關閉日誌輸出功能
>>> logging.debug("debug message") #無輸出
>>> logging.critical("critical message") #無輸出
可見關閉後即使是 CRITICAL 的訊息也不會輸出了.
使用上面的自訂日誌類別的話, 只要呼叫 mylog.disable() 就可以了 :
>>> from myLog import MyLog
>>> mylog=MyLog()
>>> mylog.warning("warning message")
2018-04-25 19:16:43,426 - WARNING -Tony : warning message
>>> mylog.disable() #關閉日誌功能
>>> mylog.warning("warning message")
>>> mylog.critical("critical message")
>>>
可見日誌功能被關閉後, 全部日誌都不會被記錄.
參考 :
# 6.29.2 Basic example
# https://docs.python.org/3/howto/logging-cookbook.html
# [Python] logging 教學
1 則留言 :
謝謝!
這篇寫得超好的呀~
清楚易懂!
張貼留言