python多进程打印输出_python 多进程日志 logging
python的logging模塊提供了靈活的標準模塊,使得任何Python程序都可以使用這個第三方模塊來實現日志記錄。
但是 python 中logging 并不支持多進程,所以會遇到不少麻煩。
以 TimedRotatingFileHandler 這個類的問題作為例子。這個Handler本來的作用是:按天切割日志文件。(當天的文件是xxxx.log 昨天的文件是xxxx.log.2016-06-01)。這樣的好處是,一來可以按天來查找日志,二來可以讓日志文件不至于非常大, 過期日志也可以按天刪除。
但是問題來了,如果是用多進程來輸出日志,則只有一個進程會切換,其他進程會在原來的文件中繼續打,還有可能某些進程切換的時候早就有別的進程在新的日志文件里打入東西了,那么他會無情刪掉之,再建立新的日志文件。反正將會很亂很亂,完全沒法開心的玩耍。還會有一些其他莫名其妙的麻煩比如: os.rename(self.baseFilename, dfn)WindowsError: [Error 32] 錯誤 (進程無法訪問文件,因為另一個程序正在使用此文件 是文件已經打開的錯誤,改名前沒有關閉文件。就是一個進程在使用此文件,另一個進程想要修改文件名)
so 我們需要改寫一個 logging中的 handler 以使logging支持多進程
重寫FileHandler類(這個類是所有寫入文件的Handler都需要繼承的TimedRotatingFileHandler 就是繼承的這個類;我們增加一些簡單的判斷和操作就可以。
我們的邏輯是這樣的:
1. 判斷當前時間戳是否與指向的文件名是同一個時間
2. 如果不是,則切換 指向的文件即可
3. 結束,是不是很簡單的邏輯。
以下代碼參考messud4312的博客感謝這位大哥
#multiprocessloghandler.py
import os
import re
import datetime
import logging
try:
import codecs
except ImportError:
codecs = None
class MultiprocessHandler(logging.FileHandler):
"""支持多進程的TimedRotatingFileHandler"""
def __init__(self,filename,when='D',backupCount=0,encoding=None,delay=False):
"""filename 日志文件名,when 時間間隔的單位,backupCount 保留文件個數
delay 是否開啟 OutSteam緩存
True 表示開啟緩存,OutStream輸出到緩存,待緩存區滿后,刷新緩存區,并輸出緩存數據到文件。
False表示不緩存,OutStrea直接輸出到文件"""
self.prefix = filename
self.backupCount = backupCount
self.when = when.upper()
#正則匹配 年-月-日
#正則寫到這里就對了
self.extMath = r"^\d{4}-\d{2}-\d{2}"
# S 每秒建立一個新文件
# M 每分鐘建立一個新文件
# H 每天建立一個新文件
# D 每天建立一個新文件
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寫在這里
#這個正則匹配不應該寫到這里,不然非D模式下 會造成 self.extMath屬性不存在的問題
#不管是什么模式都是按照這個正則來搜索日志文件的。
# if self.when == 'D':
# 正則匹配 年-月-日
# self.extMath = r"^\d{4}-\d{2}-\d{2}"
if not self.suffix:
raise ValueError(u"指定的日期間隔單位無效: %s" % self.when)
#拼接文件路徑 格式化字符串
self.filefmt = os.path.join("logs","%s.%s" % (self.prefix,self.suffix))
#使用當前時間,格式化文件格式化字符串
self.filePath = datetime.datetime.now().strftime(self.filefmt)
#獲得文件夾路徑
_dir = os.path.dirname(self.filefmt)
try:
#如果日志文件夾不存在,則創建文件夾
if not os.path.exists(_dir):
os.makedirs(_dir)
except Exception:
print u"創建文件夾失敗"
print u"文件夾路徑:" + self.filePath
pass
if codecs is None:
encoding = None
#調用FileHandler
logging.FileHandler.__init__(self,self.filePath,'a+',encoding,delay)
def shouldChangeFileToWrite(self):
"""更改日志寫入目的寫入文件
return True 表示已更改,False 表示未更改"""
#以當前時間獲得新日志文件路徑
_filePath = datetime.datetime.now().strftime(self.filefmt)
#新日志文件日期 不等于 舊日志文件日期,則表示 已經到了日志切分的時候
# 更換日志寫入目的為新日志文件。
#例如 按 天 (D)來切分日志
# 當前新日志日期等于舊日志日期,則表示在同一天內,還不到日志切分的時候
# 當前新日志日期不等于舊日志日期,則表示不在
#同一天內,進行日志切分,將日志內容寫入新日志內。
if _filePath != self.filePath:
self.filePath = _filePath
return True
return False
def doChangeFile(self):
"""輸出信息到日志文件,并刪除多于保留個數的所有日志文件"""
#日志文件的絕對路徑
self.baseFilename = os.path.abspath(self.filePath)
#stream == OutStream
#stream is not None 表示 OutStream中還有未輸出完的緩存數據
if self.stream:
self.stream.flush()
self.stream.close()
#delay 為False 表示 不OutStream不緩存數據 直接輸出
# 所有,只需要關閉OutStream即可
if not self.delay:
self.stream.close()
#刪除多于保留個數的所有日志文件
if self.backupCount > 0:
for s in self.getFilesToDelete():
#print s
os.remove(s)
def getFilesToDelete(self):
"""獲得過期需要刪除的日志文件"""
#分離出日志文件夾絕對路徑
#split返回一個元組(absFilePath,fileName)
#例如:split('I:\ScripPython\char4\mybook\util\logs\mylog.2017-03-19)
#返回(I:\ScripPython\char4\mybook\util\logs, mylog.2017-03-19)
# _ 表示占位符,沒什么實際意義,
dirName,_ = os.path.split(self.baseFilename)
fileNames = os.listdir(dirName)
result = []
#self.prefix 為日志文件名 列如:mylog.2017-03-19 中的 mylog
#加上 點號 . 方便獲取點號后面的日期
prefix = self.prefix + '.'
plen = len(prefix)
for fileName in fileNames:
if fileName[:plen] == prefix:
#日期后綴 mylog.2017-03-19 中的 2017-03-19
suffix = fileName[plen:]
#匹配符合規則的日志文件,添加到result列表中
if re.compile(self.extMath).match(suffix):
result.append(os.path.join(dirName,fileName))
result.sort()
#返回 待刪除的日志文件
# 多于 保留文件個數 backupCount的所有前面的日志文件。
if len(result) < self.backupCount:
result = []
else:
result = result[:len(result) - self.backupCount]
return result
def emit(self, record):
"""發送一個日志記錄
覆蓋FileHandler中的emit方法,logging會自動調用此方法"""
try:
if self.shouldChangeFileToWrite():
self.doChangeFile()
logging.FileHandler.emit(self,record)
except (KeyboardInterrupt,SystemExit):
raise
except:
self.handleError(record)
messud4312的博客大哥的源代碼是這個樣子的,但是 經過我測試發現在使用中會造成一些I/O錯誤
下面我們來測試一下:
import sys
import time
import multiprocessing
from multiprocessloghandler import MultiprocessHandler
# 定義日志輸出格式
formattler = '%(levelname)s - %(name)s - %(asctime)s - %(message)s'
fmt = logging.Formatter(formattler)
# 獲得logger,默認獲得root logger對象
# 設置logger級別 debug
# root logger默認的級別是warning級別。
# 不設置的話 只能發送 >= warning級別的日志
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 設置handleer日志處理器,日志具體怎么處理都在日志處理器里面定義
# SteamHandler 流處理器,輸出到控制臺,輸出方式為stdout
# StreamHandler默認輸出到sys.stderr
# 設置handler所處理的日志級別。
# 只能處理 >= 所設置handler級別的日志
# 設置日志輸出格式
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(logging.DEBUG)
stream_handler.setFormatter(fmt)
# 使用我們寫的多進程版Handler理器,定義日志輸出到mylog.log文件內
# 文件打開方式默認為 a
# 按分鐘進行日志切割
file_handler = MultiprocessHandler('mylog', when='M')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(fmt)
# 對logger增加handler日志處理器
logger.addHandler(stream_handler)
logger.addHandler(file_handler)
# 發送debug級別日志消息
def test(num):
time.sleep(3)
logger.debug('日志測試' + 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 '完畢'
測試結果如下:
image.png
image.png
這樣則能正常的使用
下面重點來了:
def test(num):
time.sleep(10)
logger.debug('日志測試' + 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 '完畢'
運行結果如下:
image.png
在休眠時間過長的情況下 會造成 對已關閉文件進行I/0操作的錯誤,也不是每次都出現。導致日志無法正確寫入日志文件內。
為什么會造成這個原因呢?
在方法 doChangeFile中,我們每次輸出完self.stream中的信息后,都把stream關閉了 self.stream.close():
def doChangeFile(self):
"""輸出信息到日志文件,并刪除多于保留個數的所有日志文件"""
#日志文件的絕對路徑
self.baseFilename = os.path.abspath(self.filePath)
#stream == OutStream
#stream is not None 表示 OutStream中還有未輸出完的緩存數據
if self.stream:
self.stream.flush()
self.stream.close()
#delay 為False 表示 不OutStream不緩存數據 直接輸出
# 所有,只需要關閉OutStream即可
if not self.delay:
self.stream.close()
logging調用我們覆蓋的emit方法
doChangeFile關閉了stream,
當 logging.FileHandler.emit(self,record)
的時候 stream其實已經關閉了。
def emit(self, record):
"""發送一個日志記錄
覆蓋FileHandler中的emit方法,logging會自動調用此方法"""
try:
if self.shouldChangeFileToWrite():
self.doChangeFile()
#此時 stram已經關閉
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中 檢查 當stream為 None的情況下 重新打開 steam
然而我們在doChangeFile中僅僅關閉了stream stram.close()但是并沒有設置stream為 None。關閉的stream仍然還是 標準流對象,并不會成為None
#coding=utf-8
import sys
#stream 就是標準輸出流,或者標準錯誤流,logging源碼中默認的是標準錯誤流
#我們來看一下stream是什么東西
stream = sys.stdout
#可以看到是一個file對象
print type(stream)
#寫入文件,刷新緩沖區(如果沒有設置緩沖區,則可以不刷新)關閉流
stream.write('abc\n')
stream.flush()
stream.close()
#流關閉后,還會是file對象么
#是的 關閉后仍然是file對象
print type(stream)
#可以看到 報錯信息為 對已經關閉的文件對象file進行io操作,說明sream關閉后仍然是file對象。
#所以說我們需要 將已經關閉的stream設置為None,srteam = None
# 避免對已關閉的文件對象進行i0操作。
```

找到問題所在 那么久好辦咯:
在doChangeFile中將關閉后的stream 重新設置為 None即可
```
if self.stream:
#flush close 都會刷新緩沖區,flush不會關閉stream,close則關閉stream
#self.stream.flush()
self.stream.close()
#關閉stream后必須重新設置stream為None,否則會造成對已關閉文件進行IO操作。
self.stream = None
#delay 為False 表示 不OutStream不緩存數據 直接輸出
# 所有,只需要關閉OutStream即可
if not self.delay:
#這個地方如果關閉colse那么就會造成進程往已關閉的文件中寫數據,從而造成IO錯誤
#delay == False 表示的就是 不緩存直接寫入磁盤
#我們需要重新在打開一次stream
#self.stream.close()
self.stream = self._open()
```
if not self.delay中為甚要打開stream內

在這里我們可以看到
delay為False的時候 需要打開stream
FileHandler_init我們在 我們寫的多進程版Handler_init中已經提前初始化了。多進程后面使用中可能會造成stream關閉。所以再打開一次。
這樣就好了
改正后完整的代碼如下:
```
#coding=utf-8
import os
import re
import datetime
import logging
try:
import codecs
except ImportError:
codecs = None
class MultiprocessHandler(logging.FileHandler):
"""支持多進程的TimedRotatingFileHandler"""
def __init__(self,filename,when='D',backupCount=0,encoding=None,delay=False):
"""filename 日志文件名,when 時間間隔的單位,backupCount 保留文件個數
delay 是否開啟 OutSteam緩存
True 表示開啟緩存,OutStream輸出到緩存,待緩存區滿后,刷新緩存區,并輸出緩存數據到文件。
False表示不緩存,OutStrea直接輸出到文件"""
self.prefix = filename
self.backupCount = backupCount
self.when = when.upper()
# 正則匹配 年-月-日
self.extMath = r"^\d{4}-\d{2}-\d{2}"
# S 每秒建立一個新文件
# M 每分鐘建立一個新文件
# H 每天建立一個新文件
# D 每天建立一個新文件
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"指定的日期間隔單位無效: %s" % self.when)
#拼接文件路徑 格式化字符串
self.filefmt = os.path.join("logs","%s.%s" % (self.prefix,self.suffix))
#使用當前時間,格式化文件格式化字符串
self.filePath = datetime.datetime.now().strftime(self.filefmt)
#獲得文件夾路徑
_dir = os.path.dirname(self.filefmt)
try:
#如果日志文件夾不存在,則創建文件夾
if not os.path.exists(_dir):
os.makedirs(_dir)
except Exception:
print u"創建文件夾失敗"
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 表示已更改,False 表示未更改"""
#以當前時間獲得新日志文件路徑
_filePath = datetime.datetime.now().strftime(self.filefmt)
#新日志文件日期 不等于 舊日志文件日期,則表示 已經到了日志切分的時候
# 更換日志寫入目的為新日志文件。
#例如 按 天 (D)來切分日志
# 當前新日志日期等于舊日志日期,則表示在同一天內,還不到日志切分的時候
# 當前新日志日期不等于舊日志日期,則表示不在
#同一天內,進行日志切分,將日志內容寫入新日志內。
if _filePath != self.filePath:
self.filePath = _filePath
return True
return False
def doChangeFile(self):
"""輸出信息到日志文件,并刪除多于保留個數的所有日志文件"""
#日志文件的絕對路徑
self.baseFilename = os.path.abspath(self.filePath)
#stream == OutStream
#stream is not None 表示 OutStream中還有未輸出完的緩存數據
if self.stream:
#flush close 都會刷新緩沖區,flush不會關閉stream,close則關閉stream
#self.stream.flush()
self.stream.close()
#關閉stream后必須重新設置stream為None,否則會造成對已關閉文件進行IO操作。
self.stream = None
#delay 為False 表示 不OutStream不緩存數據 直接輸出
# 所有,只需要關閉OutStream即可
if not self.delay:
#這個地方如果關閉colse那么就會造成進程往已關閉的文件中寫數據,從而造成IO錯誤
#delay == False 表示的就是 不緩存直接寫入磁盤
#我們需要重新在打開一次stream
#self.stream.close()
self.stream = self._open()
#刪除多于保留個數的所有日志文件
if self.backupCount > 0:
print '刪除日志'
for s in self.getFilesToDelete():
print s
os.remove(s)
def getFilesToDelete(self):
"""獲得過期需要刪除的日志文件"""
#分離出日志文件夾絕對路徑
#split返回一個元組(absFilePath,fileName)
#例如:split('I:\ScripPython\char4\mybook\util\logs\mylog.2017-03-19)
#返回(I:\ScripPython\char4\mybook\util\logs, mylog.2017-03-19)
# _ 表示占位符,沒什么實際意義,
dirName,_ = os.path.split(self.baseFilename)
fileNames = os.listdir(dirName)
result = []
#self.prefix 為日志文件名 列如:mylog.2017-03-19 中的 mylog
#加上 點號 . 方便獲取點號后面的日期
prefix = self.prefix + '.'
plen = len(prefix)
for fileName in fileNames:
if fileName[:plen] == prefix:
#日期后綴 mylog.2017-03-19 中的 2017-03-19
suffix = fileName[plen:]
#匹配符合規則的日志文件,添加到result列表中
if re.compile(self.extMath).match(suffix):
result.append(os.path.join(dirName,fileName))
result.sort()
#返回 待刪除的日志文件
# 多于 保留文件個數 backupCount的所有前面的日志文件。
if len(result) < self.backupCount:
result = []
else:
result = result[:len(result) - self.backupCount]
return result
def emit(self, record):
"""發送一個日志記錄
覆蓋FileHandler中的emit方法,logging會自動調用此方法"""
try:
if self.shouldChangeFileToWrite():
self.doChangeFile()
logging.FileHandler.emit(self,record)
except (KeyboardInterrupt,SystemExit):
raise
except:
self.handleError(record)
```
總結
以上是生活随笔為你收集整理的python多进程打印输出_python 多进程日志 logging的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: picpick尺子像素大小精度不够准确_
- 下一篇: python使用scrapy_pytho