python 多進(jìn)程日志 logging

python的logging模塊提供了靈活的標(biāo)準(zhǔn)模塊,使得任何Python程序都可以使用這個(gè)第三方模塊來(lái)實(shí)現(xiàn)日志記錄。
  但是 python 中l(wèi)ogging 并不支持多進(jìn)程,所以會(huì)遇到不少麻煩。
  以 TimedRotatingFileHandler 這個(gè)類的問(wèn)題作為例子。這個(gè)Handler本來(lái)的作用是:按天切割日志文件。(當(dāng)天的文件是xxxx.log 昨天的文件是xxxx.log.2016-06-01)。這樣的好處是,一來(lái)可以按天來(lái)查找日志,二來(lái)可以讓日志文件不至于非常大, 過(guò)期日志也可以按天刪除。
  但是問(wèn)題來(lái)了,如果是用多進(jìn)程來(lái)輸出日志,則只有一個(gè)進(jìn)程會(huì)切換,其他進(jìn)程會(huì)在原來(lái)的文件中繼續(xù)打,還有可能某些進(jìn)程切換的時(shí)候早就有別的進(jìn)程在新的日志文件里打入東西了,那么他會(huì)無(wú)情刪掉之,再建立新的日志文件。反正將會(huì)很亂很亂,完全沒(méi)法開心的玩耍。還會(huì)有一些其他莫名其妙的麻煩比如: os.rename(self.baseFilename, dfn)WindowsError: [Error 32] 錯(cuò)誤 (進(jìn)程無(wú)法訪問(wèn)文件,因?yàn)榱硪粋€(gè)程序正在使用此文件 是文件已經(jīng)打開的錯(cuò)誤,改名前沒(méi)有關(guān)閉文件。就是一個(gè)進(jìn)程在使用此文件,另一個(gè)進(jìn)程想要修改文件名)
  so 我們需要改寫一個(gè) logging中的 handler 以使logging支持多進(jìn)程
  重寫FileHandler類(這個(gè)類是所有寫入文件的Handler都需要繼承的TimedRotatingFileHandler 就是繼承的這個(gè)類;我們?cè)黾右恍┖?jiǎn)單的判斷和操作就可以。
  我們的邏輯是這樣的:
?。? 判斷當(dāng)前時(shí)間戳是否與指向的文件名是同一個(gè)時(shí)間
 2. 如果不是,則切換 指向的文件即可
?。? 結(jié)束,是不是很簡(jiǎn)單的邏輯。
以下代碼參考messud4312的博客 感謝這位大哥

#multiprocessloghandler.py
import os
import re
import datetime
import logging

try:
    import codecs
except ImportError:
    codecs = None
class MultiprocessHandler(logging.FileHandler):
    """支持多進(jìn)程的TimedRotatingFileHandler"""
    def __init__(self,filename,when='D',backupCount=0,encoding=None,delay=False):
        """filename 日志文件名,when 時(shí)間間隔的單位,backupCount 保留文件個(gè)數(shù)
        delay 是否開啟 OutSteam緩存
            True 表示開啟緩存,OutStream輸出到緩存,待緩存區(qū)滿后,刷新緩存區(qū),并輸出緩存數(shù)據(jù)到文件。
            False表示不緩存,OutStrea直接輸出到文件"""
        self.prefix = filename
        self.backupCount = backupCount
        self.when = when.upper()
     #正則匹配 年-月-日
        #正則寫到這里就對(duì)了
    self.extMath = r"^\d{4}-\d{2}-\d{2}"

        # S 每秒建立一個(gè)新文件
        # M 每分鐘建立一個(gè)新文件
        # H 每天建立一個(gè)新文件
        # D 每天建立一個(gè)新文件
        self.when_dict = {
            'S':"%Y-%m-%d-%H-%M-%S",
            'M':"%Y-%m-%d-%H-%M",
            'H':"%Y-%m-%d-%H",
            'D':"%Y-%m-%d"
        }
        #日志文件日期后綴
        self.suffix = self.when_dict.get(when)
        #源碼中self.extMath寫在這里
        #這個(gè)正則匹配不應(yīng)該寫到這里,不然非D模式下 會(huì)造成 self.extMath屬性不存在的問(wèn)題
        #不管是什么模式都是按照這個(gè)正則來(lái)搜索日志文件的。
        # if self.when == 'D':
        #    正則匹配 年-月-日
        #    self.extMath = r"^\d{4}-\d{2}-\d{2}"
        if not self.suffix:
            raise ValueError(u"指定的日期間隔單位無(wú)效: %s" % self.when)
        #拼接文件路徑 格式化字符串
        self.filefmt = os.path.join("logs","%s.%s" % (self.prefix,self.suffix))
        #使用當(dāng)前時(shí)間,格式化文件格式化字符串
        self.filePath = datetime.datetime.now().strftime(self.filefmt)
        #獲得文件夾路徑
        _dir = os.path.dirname(self.filefmt)
        try:
            #如果日志文件夾不存在,則創(chuàng)建文件夾
            if not os.path.exists(_dir):
                os.makedirs(_dir)
        except Exception:
            print u"創(chuàng)建文件夾失敗"
            print u"文件夾路徑:" + self.filePath
            pass
        if codecs is None:
            encoding = None
    #調(diào)用FileHandler
        logging.FileHandler.__init__(self,self.filePath,'a+',encoding,delay)

  def shouldChangeFileToWrite(self):
        """更改日志寫入目的寫入文件
        return True 表示已更改,F(xiàn)alse 表示未更改"""
        #以當(dāng)前時(shí)間獲得新日志文件路徑
        _filePath = datetime.datetime.now().strftime(self.filefmt)
        #新日志文件日期 不等于 舊日志文件日期,則表示 已經(jīng)到了日志切分的時(shí)候
        #   更換日志寫入目的為新日志文件。
        #例如 按 天 (D)來(lái)切分日志
        #   當(dāng)前新日志日期等于舊日志日期,則表示在同一天內(nèi),還不到日志切分的時(shí)候
        #   當(dāng)前新日志日期不等于舊日志日期,則表示不在
        #同一天內(nèi),進(jìn)行日志切分,將日志內(nèi)容寫入新日志內(nèi)。
        if _filePath != self.filePath:
            self.filePath = _filePath
            return True
        return False

    def doChangeFile(self):
        """輸出信息到日志文件,并刪除多于保留個(gè)數(shù)的所有日志文件"""
        #日志文件的絕對(duì)路徑
        self.baseFilename = os.path.abspath(self.filePath)
        #stream == OutStream
        #stream is not None 表示 OutStream中還有未輸出完的緩存數(shù)據(jù)
        if self.stream:
            self.stream.flush()
            self.stream.close()
        #delay 為False 表示 不OutStream不緩存數(shù)據(jù) 直接輸出
        #   所有,只需要關(guān)閉OutStream即可
        if not self.delay:
            self.stream.close()
           
        #刪除多于保留個(gè)數(shù)的所有日志文件
        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                #print s
                os.remove(s)
  def getFilesToDelete(self):
        """獲得過(guò)期需要?jiǎng)h除的日志文件"""
        #分離出日志文件夾絕對(duì)路徑
        #split返回一個(gè)元組(absFilePath,fileName)
        #例如:split('I:\ScripPython\char4\mybook\util\logs\mylog.2017-03-19)
        #返回(I:\ScripPython\char4\mybook\util\logs, mylog.2017-03-19)
        # _ 表示占位符,沒(méi)什么實(shí)際意義,
        dirName,_ = os.path.split(self.baseFilename)
        fileNames = os.listdir(dirName)
        result = []
        #self.prefix 為日志文件名 列如:mylog.2017-03-19 中的 mylog
        #加上 點(diǎn)號(hào) . 方便獲取點(diǎn)號(hào)后面的日期
        prefix = self.prefix + '.'
        plen = len(prefix)
        for fileName in fileNames:
            if fileName[:plen] == prefix:
                #日期后綴 mylog.2017-03-19 中的 2017-03-19
                suffix = fileName[plen:]
                #匹配符合規(guī)則的日志文件,添加到result列表中
                if re.compile(self.extMath).match(suffix):
                    result.append(os.path.join(dirName,fileName))
        result.sort()

        #返回  待刪除的日志文件
        #   多于 保留文件個(gè)數(shù) backupCount的所有前面的日志文件。
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def emit(self, record):
        """發(fā)送一個(gè)日志記錄
        覆蓋FileHandler中的emit方法,logging會(huì)自動(dòng)調(diào)用此方法"""
        try:
            if self.shouldChangeFileToWrite():
                self.doChangeFile()
            logging.FileHandler.emit(self,record)
        except (KeyboardInterrupt,SystemExit):
            raise
        except:
            self.handleError(record)

messud4312的博客 大哥的源代碼是這個(gè)樣子的,但是 經(jīng)過(guò)我測(cè)試發(fā)現(xiàn)在使用中會(huì)造成一些I/O錯(cuò)誤
下面我們來(lái)測(cè)試一下:

import sys
import time
import multiprocessing
from multiprocessloghandler import MultiprocessHandler

# 定義日志輸出格式
formattler = '%(levelname)s - %(name)s - %(asctime)s - %(message)s'
fmt = logging.Formatter(formattler)

# 獲得logger,默認(rèn)獲得root logger對(duì)象
# 設(shè)置logger級(jí)別 debug
# root logger默認(rèn)的級(jí)別是warning級(jí)別。
# 不設(shè)置的話 只能發(fā)送 >= warning級(jí)別的日志
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 設(shè)置handleer日志處理器,日志具體怎么處理都在日志處理器里面定義
# SteamHandler 流處理器,輸出到控制臺(tái),輸出方式為stdout
#   StreamHandler默認(rèn)輸出到sys.stderr
# 設(shè)置handler所處理的日志級(jí)別。
#   只能處理 >= 所設(shè)置handler級(jí)別的日志
# 設(shè)置日志輸出格式
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(logging.DEBUG)
stream_handler.setFormatter(fmt)

# 使用我們寫的多進(jìn)程版Handler理器,定義日志輸出到mylog.log文件內(nèi)
#   文件打開方式默認(rèn)為 a
#   按分鐘進(jìn)行日志切割
file_handler = MultiprocessHandler('mylog', when='M')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(fmt)

# 對(duì)logger增加handler日志處理器
logger.addHandler(stream_handler)
logger.addHandler(file_handler)


# 發(fā)送debug級(jí)別日志消息
def test(num):
    time.sleep(3)
    logger.debug('日志測(cè)試' + str(num))

if __name__ == '__main__':

    pool = multiprocessing.Pool(processes=10)

    for i in range(10):
        pool.apply_async(func=test, args=(i,))
    pool.close()
    pool.join()
    print '完畢'

測(cè)試結(jié)果如下:


image.png

image.png

這樣則能正常的使用
下面重點(diǎn)來(lái)了:

def test(num):
    time.sleep(10)
    logger.debug('日志測(cè)試' + str(num))

if __name__ == '__main__':

    pool = multiprocessing.Pool(processes=3)

    for i in range(10):
        pool.apply_async(func=test, args=(i,))
    pool.close()
    pool.join()
    print '完畢'

運(yùn)行結(jié)果如下:


image.png

  在休眠時(shí)間過(guò)長(zhǎng)的情況下 會(huì)造成 對(duì)已關(guān)閉文件進(jìn)行I/0操作的錯(cuò)誤,也不是每次都出現(xiàn)。導(dǎo)致日志無(wú)法正確寫入日志文件內(nèi)。
  為什么會(huì)造成這個(gè)原因呢?
  在方法 doChangeFile中,我們每次輸出完self.stream中的信息后,都把stream關(guān)閉了 self.stream.close():

 def doChangeFile(self):
        """輸出信息到日志文件,并刪除多于保留個(gè)數(shù)的所有日志文件"""
        #日志文件的絕對(duì)路徑
        self.baseFilename = os.path.abspath(self.filePath)
        #stream == OutStream
        #stream is not None 表示 OutStream中還有未輸出完的緩存數(shù)據(jù)
        if self.stream:
            self.stream.flush()
            self.stream.close()
        #delay 為False 表示 不OutStream不緩存數(shù)據(jù) 直接輸出
        #   所有,只需要關(guān)閉OutStream即可
        if not self.delay:
            self.stream.close()

logging調(diào)用我們覆蓋的emit方法
doChangeFile關(guān)閉了stream,
當(dāng) logging.FileHandler.emit(self,record)
的時(shí)候 stream其實(shí)已經(jīng)關(guān)閉了。

    def emit(self, record):
        """發(fā)送一個(gè)日志記錄
        覆蓋FileHandler中的emit方法,logging會(huì)自動(dòng)調(diào)用此方法"""
        try:
            if self.shouldChangeFileToWrite():
                self.doChangeFile()
            #此時(shí) stram已經(jīng)關(guān)閉
            logging.FileHandler.emit(self,record)
        except (KeyboardInterrupt,SystemExit):
            raise
        except:
            self.handleError(record)

我們看一下 logging.FileHandler.emit的源碼:

    def emit(self, record):
        """
        Emit a record.

        If the stream was not opened because 'delay' was specified in the
        constructor, open it before calling the superclass's emit.
        """
        if self.stream is None:
            #打開stream
            self.stream = self._open()
        StreamHandler.emit(self, record)

logging.FileHandler.emit中 檢查 當(dāng)stream為 None的情況下 重新打開 steam
然而我們?cè)赿oChangeFile中僅僅關(guān)閉了stream stram.close()但是并沒(méi)有設(shè)置stream為 None。關(guān)閉的stream仍然還是 標(biāo)準(zhǔn)流對(duì)象,并不會(huì)成為None

#coding=utf-8
import sys
#stream 就是標(biāo)準(zhǔn)輸出流,或者標(biāo)準(zhǔn)錯(cuò)誤流,logging源碼中默認(rèn)的是標(biāo)準(zhǔn)錯(cuò)誤流
#我們來(lái)看一下stream是什么東西
stream = sys.stdout
#可以看到是一個(gè)file對(duì)象
print type(stream)
#寫入文件,刷新緩沖區(qū)(如果沒(méi)有設(shè)置緩沖區(qū),則可以不刷新)關(guān)閉流
stream.write('abc\n')
stream.flush()
stream.close()
#流關(guān)閉后,還會(huì)是file對(duì)象么
#是的 關(guān)閉后仍然是file對(duì)象
print type(stream)
#可以看到 報(bào)錯(cuò)信息為 對(duì)已經(jīng)關(guān)閉的文件對(duì)象file進(jìn)行io操作,說(shuō)明sream關(guān)閉后仍然是file對(duì)象。
#所以說(shuō)我們需要 將已經(jīng)關(guān)閉的stream設(shè)置為None,srteam = None
# 避免對(duì)已關(guān)閉的文件對(duì)象進(jìn)行i0操作。
```
![image.png](http://upload-images.jianshu.io/upload_images/4131789-0ff3830bc4ce6dba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
找到問(wèn)題所在 那么久好辦咯:
在doChangeFile中將關(guān)閉后的stream 重新設(shè)置為 None即可
```
        if self.stream:
            #flush close 都會(huì)刷新緩沖區(qū),flush不會(huì)關(guān)閉stream,close則關(guān)閉stream
            #self.stream.flush()
            self.stream.close()
            #關(guān)閉stream后必須重新設(shè)置stream為None,否則會(huì)造成對(duì)已關(guān)閉文件進(jìn)行IO操作。
            self.stream = None
        #delay 為False 表示 不OutStream不緩存數(shù)據(jù) 直接輸出
        #   所有,只需要關(guān)閉OutStream即可
        if not self.delay:
            #這個(gè)地方如果關(guān)閉colse那么就會(huì)造成進(jìn)程往已關(guān)閉的文件中寫數(shù)據(jù),從而造成IO錯(cuò)誤
            #delay == False 表示的就是 不緩存直接寫入磁盤
            #我們需要重新在打開一次stream
            #self.stream.close()
            self.stream = self._open()
```
if not self.delay中為甚要打開stream內(nèi)
![image.png](http://upload-images.jianshu.io/upload_images/4131789-5033b4310e35461b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在這里我們可以看到 
delay為False的時(shí)候 需要打開stream
FileHandler_init我們?cè)?我們寫的多進(jìn)程版Handler_init中已經(jīng)提前初始化了。多進(jìn)程后面使用中可能會(huì)造成stream關(guān)閉。所以再打開一次。

這樣就好了 
改正后完整的代碼如下:
```
#coding=utf-8
import os
import re
import datetime
import logging

try:
    import codecs
except ImportError:
    codecs = None

class MultiprocessHandler(logging.FileHandler):
    """支持多進(jìn)程的TimedRotatingFileHandler"""
    def __init__(self,filename,when='D',backupCount=0,encoding=None,delay=False):
        """filename 日志文件名,when 時(shí)間間隔的單位,backupCount 保留文件個(gè)數(shù)
        delay 是否開啟 OutSteam緩存
            True 表示開啟緩存,OutStream輸出到緩存,待緩存區(qū)滿后,刷新緩存區(qū),并輸出緩存數(shù)據(jù)到文件。
            False表示不緩存,OutStrea直接輸出到文件"""
        self.prefix = filename
        self.backupCount = backupCount
        self.when = when.upper()
        # 正則匹配 年-月-日
        self.extMath = r"^\d{4}-\d{2}-\d{2}"

        # S 每秒建立一個(gè)新文件
        # M 每分鐘建立一個(gè)新文件
        # H 每天建立一個(gè)新文件
        # D 每天建立一個(gè)新文件
        self.when_dict = {
            'S':"%Y-%m-%d-%H-%M-%S",
            'M':"%Y-%m-%d-%H-%M",
            'H':"%Y-%m-%d-%H",
            'D':"%Y-%m-%d"
        }
        #日志文件日期后綴
        self.suffix = self.when_dict.get(when)
        if not self.suffix:
            raise ValueError(u"指定的日期間隔單位無(wú)效: %s" % self.when)
        #拼接文件路徑 格式化字符串
        self.filefmt = os.path.join("logs","%s.%s" % (self.prefix,self.suffix))
        #使用當(dāng)前時(shí)間,格式化文件格式化字符串
        self.filePath = datetime.datetime.now().strftime(self.filefmt)
        #獲得文件夾路徑
        _dir = os.path.dirname(self.filefmt)
        try:
            #如果日志文件夾不存在,則創(chuàng)建文件夾
            if not os.path.exists(_dir):
                os.makedirs(_dir)
        except Exception:
            print u"創(chuàng)建文件夾失敗"
            print u"文件夾路徑:" + self.filePath
            pass

        if codecs is None:
            encoding = None

        logging.FileHandler.__init__(self,self.filePath,'a+',encoding,delay)

    def shouldChangeFileToWrite(self):
        """更改日志寫入目的寫入文件
        :return True 表示已更改,F(xiàn)alse 表示未更改"""
        #以當(dāng)前時(shí)間獲得新日志文件路徑
        _filePath = datetime.datetime.now().strftime(self.filefmt)
        #新日志文件日期 不等于 舊日志文件日期,則表示 已經(jīng)到了日志切分的時(shí)候
        #   更換日志寫入目的為新日志文件。
        #例如 按 天 (D)來(lái)切分日志
        #   當(dāng)前新日志日期等于舊日志日期,則表示在同一天內(nèi),還不到日志切分的時(shí)候
        #   當(dāng)前新日志日期不等于舊日志日期,則表示不在
        #同一天內(nèi),進(jìn)行日志切分,將日志內(nèi)容寫入新日志內(nèi)。
        if _filePath != self.filePath:
            self.filePath = _filePath
            return True
        return False

    def doChangeFile(self):
        """輸出信息到日志文件,并刪除多于保留個(gè)數(shù)的所有日志文件"""
        #日志文件的絕對(duì)路徑
        self.baseFilename = os.path.abspath(self.filePath)
        #stream == OutStream
        #stream is not None 表示 OutStream中還有未輸出完的緩存數(shù)據(jù)
        if self.stream:
            #flush close 都會(huì)刷新緩沖區(qū),flush不會(huì)關(guān)閉stream,close則關(guān)閉stream
            #self.stream.flush()
            self.stream.close()
            #關(guān)閉stream后必須重新設(shè)置stream為None,否則會(huì)造成對(duì)已關(guān)閉文件進(jìn)行IO操作。
            self.stream = None
        #delay 為False 表示 不OutStream不緩存數(shù)據(jù) 直接輸出
        #   所有,只需要關(guān)閉OutStream即可
        if not self.delay:
            #這個(gè)地方如果關(guān)閉colse那么就會(huì)造成進(jìn)程往已關(guān)閉的文件中寫數(shù)據(jù),從而造成IO錯(cuò)誤
            #delay == False 表示的就是 不緩存直接寫入磁盤
            #我們需要重新在打開一次stream
            #self.stream.close()
            self.stream = self._open()
        #刪除多于保留個(gè)數(shù)的所有日志文件
        if self.backupCount > 0:
            print '刪除日志'
            for s in self.getFilesToDelete():
                print s
                os.remove(s)

    def getFilesToDelete(self):
        """獲得過(guò)期需要?jiǎng)h除的日志文件"""
        #分離出日志文件夾絕對(duì)路徑
        #split返回一個(gè)元組(absFilePath,fileName)
        #例如:split('I:\ScripPython\char4\mybook\util\logs\mylog.2017-03-19)
        #返回(I:\ScripPython\char4\mybook\util\logs, mylog.2017-03-19)
        # _ 表示占位符,沒(méi)什么實(shí)際意義,
        dirName,_ = os.path.split(self.baseFilename)
        fileNames = os.listdir(dirName)
        result = []
        #self.prefix 為日志文件名 列如:mylog.2017-03-19 中的 mylog
        #加上 點(diǎn)號(hào) . 方便獲取點(diǎn)號(hào)后面的日期
        prefix = self.prefix + '.'
        plen = len(prefix)
        for fileName in fileNames:
            if fileName[:plen] == prefix:
                #日期后綴 mylog.2017-03-19 中的 2017-03-19
                suffix = fileName[plen:]
                #匹配符合規(guī)則的日志文件,添加到result列表中
                if re.compile(self.extMath).match(suffix):
                    result.append(os.path.join(dirName,fileName))
        result.sort()

        #返回  待刪除的日志文件
        #   多于 保留文件個(gè)數(shù) backupCount的所有前面的日志文件。
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def emit(self, record):
        """發(fā)送一個(gè)日志記錄
        覆蓋FileHandler中的emit方法,logging會(huì)自動(dòng)調(diào)用此方法"""
        try:
            if self.shouldChangeFileToWrite():
                self.doChangeFile()
            logging.FileHandler.emit(self,record)
        except (KeyboardInterrupt,SystemExit):
            raise
        except:
            self.handleError(record)
```
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本篇文章主要對(duì) python logging 的介紹加深理解。更主要是 討論在多進(jìn)程環(huán)境下如何使用logging ...
    doudou0o閱讀 41,414評(píng)論 52 42
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,554評(píng)論 19 139
  • Python logging 模塊 參考 http://blog.csdn.net/zyz511919766/ar...
    ktide閱讀 1,014評(píng)論 0 2
  • 前言 在自動(dòng)化測(cè)試實(shí)踐過(guò)程中,必不可少的就是進(jìn)行日志管理,方便調(diào)試和生產(chǎn)問(wèn)題追蹤,python提供了logg...
    苦葉子閱讀 932評(píng)論 0 0
  • 本文翻譯自logging howto 基礎(chǔ)教程 日志是跟蹤軟件運(yùn)行時(shí)發(fā)生事件的一種手段。Python開發(fā)者在代碼中...
    大蟒傳奇閱讀 4,355評(píng)論 0 17

友情鏈接更多精彩內(nèi)容