python实例26[将log同时写入文件和显示到UI的控件中]

本文介绍了一种基于singleton模式的全局日志管理系统GlobalLogging,该系统能够将日志记录到文件和控制台,并支持自定义日志处理函数。通过简单的配置即可启用不同级别的日志输出。


特点:

1) 此GlobalLogging 采用了singlten模式,全局唯一,必须通过getInstance方法来获得GlobalLogging的实例;

2) 此GlobalLogging可以采用logging提供的log handlers(例如log到console,file。。。),同时支持提供自定义函数来处理log的接口,可以自己定义如何处理log;

 

 

GlobalLogging的使用如下: (将系统的所有的函数的log,同时写到log文件和UI中的text控件中)

 

from GlobalLogging import GlobalLogging

class A:
  
def __init__(self):
    GlobalLogging.getInstance().setLoggingToHanlder(self.getLog)
    GlobalLogging.getInstance().setLoggingToFile(
'logfile.txt')
    GlobalLogging.getInstance().setLoggingLevel(logging.INFO)

  
def getLog(self, s):
    self.outputText.append(s)

  
def FA(self):    
    GlobalLogging.getInstance().info(
'XXX')
    GlobalLogging.getInstance().error(
'XXX')

 

 

GlobalLogging代码:

 

import logging

class NullHandler(logging.Handler):
  
def emit(self, record): pass

class GlobalLogging:

  log 
= None
  
  @staticmethod
  
def getInstance():
    
if GlobalLogging.log == None: 
      GlobalLogging.log 
= GlobalLogging()
    
return GlobalLogging.log 

  
def __init__(self):
    self.logger 
= None
    self.handler 
= None
    self.level 
= logging.INFO
    self.logger 
= logging.getLogger("GlobalLogging")
    self.formatter 
= logging.Formatter("%(levelname)s - %(message)s")
    h 
= NullHandler()
    self.logger.addHandler(h)

def setLoggingToFile(self,file):     
fh 
= logging.FileHandler(file)
      fh.setFormatter(self.formatter)
      self.logger.addHandler(ch)
      
  
def setLoggingToConsole(self) : 
      ch 
= logging.StreamHandler()
      ch.setFormatter(self.formatter)
      self.logger.addHandler(fh)
      
  
def setLoggingToHanlder(self,handler): 
      self.handler 
= handler
      
  
def setLoggingLevel(self,level):
    self.level 
= level
    self.logger.setLevel(level)
    
  
def debug(self,s):
      self.logger.debug(s)
      
if not self.handler == None and self.level <= logging.DEBUG :
        
print logging.DEBUG
        
print self.level
        self.handler(
'debug:' + s)
  
def info(self,s):
      self.logger.info(s)
      
if not self.handler == None and self.level <= logging.INFO:
        self.handler(
'info:' + s)
  
def warn(self,s):
      self.logger.warn(s)
      
if not self.handler == None and self.level <= logging.WARNING:
        self.handler(
'warn:' + s)
  
def error(self,s):
      self.logger.error(s)
      
if not self.handler == None and self.level <= logging.ERROR:
        self.handler(
'error:' + s)
  
def critical(self,s):
      self.logger.critical(s)
      
if not self.handler == None and self.level <= logging.CRITICAL:
        self.handler(
'critical:' + s)

 

 

 

 

完!

import os import sys import time from datetime import datetime from PyQt6.QtWidgets import QTextEdit from PyQt6.QtCore import QObject, pyqtSignal from PyQt6.QtCore import Qt from PyQt6.QtGui import QTextCursor # 添加这行导入 import glob import shutil class Logger(QObject): # 定义信号用于在主线程更新UI update_log_signal = pyqtSignal(str) # 添加新信号用于清空日志 clear_log_signal = pyqtSignal() # 日志级别 DEBUG = 1 INFO = 2 WARNING = 3 ERROR = 4 CRITICAL = 5 def __init__(self, name='root', log_file=None, max_lines=1000, ui=None, max_file_size=10*1024*1024, max_backup_count=5): super().__init__() self.name = name self.log_file = log_file self.max_lines = max_lines self.ui = ui self.level = self.INFO self.log_count = 0 self.max_file_size = max_file_size # 最大文件大小,默认10MB self.max_backup_count = max_backup_count # 最大备份文件数量,默认5个 # 连接信号槽 if self.ui: self.update_log_signal.connect(self.update_ui_log) self.clear_log_signal.connect(self.clear_ui_log) # 确保日志目录存在 if self.log_file: log_dir = os.path.dirname(self.log_file) if log_dir and not os.path.exists(log_dir): os.makedirs(log_dir) # 初始化时清理旧日志文件 self._cleanup_old_logs() def set_level(self, level): """设置日志级别""" self.level = level def debug(self, message, show_in_ui=True): """输出调试日志""" if self.level <= self.DEBUG: self._log('DEBUG', message, show_in_ui) def info(self, message, show_in_ui=True): """输出信息日志""" if self.level <= self.INFO: self._log('INFO', message, show_in_ui) def warning(self, message, show_in_ui=True): """输出警告日志""" if self.level <= self.WARNING: self._log('WARNING', message, show_in_ui) def error(self, message, show_in_ui=True): """输出错误日志""" if self.level <= self.ERROR: self._log('ERROR', message, show_in_ui) def critical(self, message, show_in_ui=True): """输出严重错误日志""" if self.level <= self.CRITICAL: self._log('CRITICAL', message, show_in_ui) def _log(self, level_name, message, show_in_ui=True): """记录日志的内部方法 - 优化版""" timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] # 增强的消息编码处理 try: if not isinstance(message, str): message = str(message) # 确保消息是有效的UTF-8,并处理特殊字符 message = self._safe_encode_message(message) except Exception as e: error_msg = f"日志消息编码错误: {str(e)}" message = f"[编码错误] {error_msg}" log_message = f"[{timestamp}] [{level_name}] [{self.name}] {message}\n" log_ui_message = f"[{timestamp}] {message}\n" # 写入文件(带日志轮转) if self.log_file: self._write_to_file_with_rotation(log_message) # 更新UI if self.ui and show_in_ui: self.update_log_signal.emit(log_ui_message) def _safe_encode_message(self, message): """安全地编码消息,确保UTF-8兼容性""" try: # 如果是bytes,尝试解码为UTF-8 if isinstance(message, bytes): message = message.decode('utf-8', errors='replace') # 确保字符串是有效的UTF-8 message_bytes = message.encode('utf-8', errors='replace') message = message_bytes.decode('utf-8') # 替换可能导致问题的控制字符 message = message.replace('\x00', '\\x00') # NULL字符 message = message.replace('\ufffd', '?') # 替换字符 return message except Exception as e: # 如果所有方法都失败,返回安全的ASCII表示 try: safe_message = str(message).encode('ascii', errors='ignore').decode('ascii') return f"[编码修复] {safe_message}" except: return "[无法处理的消息编码]" def _write_to_file_with_rotation(self, log_message): """写入文件并执行日志轮转""" try: # 检查是否需要轮转 if self._should_rotate(): self._rotate_logs() # 写入当前日志文件 with open(self.log_file, 'a', encoding='utf-8', errors='replace') as f: f.write(log_message) f.flush() # 立即刷新到磁盘 except Exception as e: # 如果UTF-8写入失败,尝试其他编码 self._fallback_write(log_message, e) def _fallback_write(self, log_message, original_error): """备用写入方法,处理编码问题""" try: # 尝试使用系统默认编码 with open(self.log_file, 'a', encoding='utf-8', errors='ignore') as f: f.write(f"[编码警告] 原始错误: {str(original_error)}\n") f.write(log_message) f.flush() except Exception as e: # 最后的备用方案:写入错误日志 try: error_log = self.log_file + '.error' with open(error_log, 'a', encoding='utf-8', errors='replace') as f: f.write(f"[{datetime.now()}] 日志写入失败: {str(e)}\n") f.write(f"原始消息长度: {len(log_message)}\n") f.flush() except: pass # 静默失败,避免无限递归 def _should_rotate(self): """检查是否需要轮转日志文件""" if not os.path.exists(self.log_file): return False try: file_size = os.path.getsize(self.log_file) return file_size >= self.max_file_size except: return False def _rotate_logs(self): """执行日志文件轮转""" try: if not os.path.exists(self.log_file): return # 生成时间戳 timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') # 创建备份文件log_dir = os.path.dirname(self.log_file) log_name = os.path.basename(self.log_file) name_parts = os.path.splitext(log_name) backup_name = f"{name_parts[0]}_{timestamp}{name_parts[1]}" backup_path = os.path.join(log_dir, backup_name) # 移动当前日志文件为备份文件 shutil.move(self.log_file, backup_path) # 清理旧的备份文件 self._cleanup_old_logs() except Exception as e: # 如果轮转失败,尝试清空当前文件 try: with open(self.log_file, 'w', encoding='utf-8') as f: f.write(f"[{datetime.now()}] 日志轮转失败,文件已重置: {str(e)}\n") except: pass def _cleanup_old_logs(self): """清理旧的日志备份文件""" if not self.log_file: return try: log_dir = os.path.dirname(self.log_file) log_name = os.path.basename(self.log_file) name_parts = os.path.splitext(log_name) # 查找所有备份文件 pattern = os.path.join(log_dir, f"{name_parts[0]}_*{name_parts[1]}") backup_files = glob.glob(pattern) # 按修改时间排序(最新的在前) backup_files.sort(key=os.path.getmtime, reverse=True) # 删除超出数量限制的旧文件 for old_file in backup_files[self.max_backup_count:]: try: os.remove(old_file) except: pass # 静默忽略删除失败的情况 except Exception: pass # 静默忽略清理失败 def update_ui_log(self, message): """更新UI日志显示""" if not self.ui: return # 添加新日志 self.ui.append(message.rstrip('\n')) self.log_count += 1 # 检查是否超过最大行数 if self.log_count > self.max_lines: # 获取所有文本 text = self.ui.toPlainText() # 分割成行 lines = text.split('\n') # 保留后面的行 new_text = '\n'.join(lines[-self.max_lines:]) # 设置新文本 self.ui.setPlainText(new_text) # 重置计数 self.log_count = self.max_lines # 滚动到底部 #// 修改前: self.ui.moveCursor(self.ui.textCursor().End) #// 之前修改: self.ui.moveCursor(QTextCursor.End) self.ui.moveCursor(QTextCursor.MoveOperation.End) # 正确引用MoveOperation枚举 def clear(self): """清空日志""" if self.ui: # 通过信号触发UI线程中的清空操作 self.clear_log_signal.emit() def clear_ui_log(self): """在UI线程中清空日志的槽函数""" self.ui.clear() self.log_count = 0 def get_logger(self, name): """获取子Logger,继承父Logger的配置""" return Logger( name=f"{self.name}.{name}", log_file=self.log_file, max_lines=self.max_lines, ui=self.ui, max_file_size=self.max_file_size, max_backup_count=self.max_backup_count ) def rotate_now(self): """手动触发日志轮转""" if self.log_file and os.path.exists(self.log_file): self._rotate_logs() def get_log_info(self): """获取日志文件信息""" if not self.log_file: return {"error": "未设置日志文件"} try: info = { "log_file": self.log_file, "exists": os.path.exists(self.log_file), "size": 0, "size_mb": 0, "max_size_mb": self.max_file_size / (1024 * 1024), "backup_count": 0 } if info["exists"]: info["size"] = os.path.getsize(self.log_file) info["size_mb"] = round(info["size"] / (1024 * 1024), 2) # 统计备份文件数量 log_dir = os.path.dirname(self.log_file) log_name = os.path.basename(self.log_file) name_parts = os.path.splitext(log_name) pattern = os.path.join(log_dir, f"{name_parts[0]}_*{name_parts[1]}") backup_files = glob.glob(pattern) info["backup_count"] = len(backup_files) return info except Exception as e: return {"error": f"获取日志信息失败: {str(e)}"} def test_chinese_encoding(self): """测试中文编码是否正常""" test_messages = [ "测试中文日志记录功能", "开图任务已经创建", "清货任务已经创建", "采集任务已经创建", "进入循环处理", "窗口已成功激活", "找到匹配窗口" ] self.info("=== 开始中文编码测试 ===") for i, msg in enumerate(test_messages, 1): self.info(f"测试消息 {i}: {msg}") self.info("=== 中文编码测试完成 ===") return "中文编码测试已完成,请检查日志文件"
最新发布
11-01
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值