深度剖析 Python 日志重复打印问题

news2025/2/27 12:20:30

python 日志处理流程

  • 使用 python 做日志输出时,首先我们需要一个创建一个 Logger 对象:import logging; logger = logging.getLogger()

    然后就可以用 logger.info/debug/error(msg) 来输出日志

  • 如果只是单纯地打印日志,这样做和 print 没有任何区别。我们期望 log 能有一定的格式,这时就会用到 logging.Formatter

    我们还希望日志不仅在 console 中输出,还要向文件输出

    这样需要给 logger 添加 handler,一个 handler 指向标准输出流,一个 handler 指向文件 handler

    logging.handlers 提供了一些这些常用的 handler

  • 针对不同的输出流进行精准的控制

    比如:在 console 中只输出某些高级别的日志,而在文件日志中输出所有日志

    console 中,使用一种输出 formatter,在文件输出中使用另一种 formatter

    不满足于 python 提供的 DEBUG/INFO/WARNING/ERROR/CRITICAL 的控制粒度,想要更精细地控制日志就需要理解日志是如何流转、继承

如下是python官方提供的 log 工作流:

在这里插入图片描述

详细资料请查阅: https://docs.python.org/3/howto/logging.html

python logger 重复打印问题

情景介绍

log 重复打印存在两种情况:

  • 未定义 loggerlogger = logging.getLogger() ),默认使用了 RootLogger,一个 python 程序内全局唯一的,所有 Logger 对象的祖先

    每次实例化返回的都是 RootLogger 对象

  • 自定义的 log 函数,每次调用都实例化,传入相同的 logger name,经验证 logging.getLogger(“name”) 使用了单例模型

    也就是说每次实例化返回的是同一个 logger 对象,然后每次都调用 logger.addHandler(handler) 方法添加日志处理器

    导致 handlers 列表添加了相同的 handler(注意:日志的打印由 handler 控制)

    以此类推,调用几次就会有几个 handler,然后前面打印的 log 就会影响后面定义的 log

总结:

  • 日志重复打印的问题源于 handlers 列表添加了相同的 handler

    附上源码截图
    在这里插入图片描述
    在这里插入图片描述

注意:

验证 logging.getLogger(“name”) 使用了单例模型,最简单的就是打印 logger id

在这里插入图片描述

示例验证

打印日志重复次数会与实例化 log 函数的次数相同,每次实例化都打印出 self.handlers 会发现 handlers 列表累加一个 handler 元素

在这里插入图片描述

示例代码

import logging


def log(msg):
    logger = logging.getLogger()
    handler = logging.FileHandler('test.log')

    logger.setLevel(logging.INFO)  # 设置日志等级
    # 日志输出格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    # 输入到控制台
    console = logging.StreamHandler()
    console.setLevel(logging.INFO)
    logger.addHandler(handler)
    logger.addHandler(console)

    print(logger.handlers)

    logger.info(msg)

if __name__ == '__main__':
    log("a")
    log("bb")
    log("ccc")

输出结果

'''
a
bb
bb
ccc
ccc
ccc
[<FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>]
[<FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>, <FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>]
[<FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>, <FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>, <FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>]
'''

解决方法

方法一

  • 打印日志后,通过 removeHandler() 将处理器移除掉

    # 示例
    import logging
    
    
    def log(msg):
        logger = logging.getLogger()
    	.....
        logger.info(msg)
        
        #移除处理器
        logger.removeHandler(handler)
        logger.removeHandler(console)
    
    if __name__ == '__main__':
        log("a")
        log("bb")
        log("ccc")
        
    # 输出
    '''
    a
    bb
    ccc
    [<FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>]
    [<FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>]
    [<FileHandler C:\Users\EDY\PycharmProjects\pythonProject1\test.log (NOTSET)>, <StreamHandler <stderr> (INFO)>]
    '''
    

    相同的 handler 没有被添加到 handlers,日志也就不会重复打印了

方法二

  • 每个 Logger 都使用不同的命名

    自定义 logger

    #!/usr/bin/python
    # -*- coding:utf-8 -*-
     
    import logging
    import time
    import os
     
     
    class Log(object):
        '''
    	封装后的logging
        '''
     
        def __init__(self, logger=None, log_cate='search'):
            '''
                指定保存日志的文件路径,日志级别,以及调用文件
                将日志存入到指定的文件中
            '''
     
            # 创建一个logger
            self.logger = logging.getLogger(logger)
            self.logger.setLevel(logging.DEBUG)
            # 创建一个handler,用于写入日志文件
            self.log_time = time.strftime("%Y_%m_%d")
            file_dir = os.getcwd() + '/../log'
            if not os.path.exists(file_dir):
                os.mkdir(file_dir)
            self.log_path = file_dir
            self.log_name = self.log_path + "/" + log_cate + "." + self.log_time + '.log'
            # print(self.log_name)
     
            fh = logging.FileHandler(self.log_name, 'a')  # 追加模式  这个是python2的
            # fh = logging.FileHandler(self.log_name, 'a', encoding='utf-8')  # 这个是python3的
            fh.setLevel(logging.INFO)
     
            # 再创建一个handler,用于输出到控制台
            ch = logging.StreamHandler()
            ch.setLevel(logging.INFO)
     
            # 定义handler的输出格式
            formatter = logging.Formatter(
                '[%(asctime)s] %(filename)s->%(funcName)s line:%(lineno)d [%(levelname)s]%(message)s')
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
     
            # 给logger添加handler
            self.logger.addHandler(fh)
            self.logger.addHandler(ch)
     
            # 关闭打开的文件
            fh.close()
            ch.close()
     
        def getlog(self):
            return self.logger
    

    测试示例

    # a.py
    from common.log import Log
    log = Log(__name__).getlog()
    log.info("I am a.py")
    
    # b.py
    from common.log import Log
    log = Log(__name__).getlog()
    log.info("I am b.py")
    
    # c.py
    import b
    import a
     
    from common.log import Log
     
    log = Log(__name__).getlog()
    log.info("I am c.py")
    
    # 执行 c.py 脚本输出结果
    [2019-01-14 16:24:12,008] b.py-><module> line:6 [INFO]I am b.py
    [2019-01-14 16:24:12,009] a.py-><module> line:6 [INFO]I am a.py
    [2019-01-14 16:24:12,009] c.py-><module> line:10 [INFO]I am c.py
    

方法三

  • log 方法里做判断,如果这个 logger 已有 handler,则不再添加 handler

    import logging
    
    
    def log(message):
        logger = logging.getLogger('testlog')
    
        #  这里进行判断,如果logger.handlers列表为空,则添加,否则,直接去写日志
        if not logger.handlers:
            streamhandler = logging.StreamHandler()
            streamhandler.setLevel(logging.ERROR)
            formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
            streamhandler.setFormatter(formatter)
            logger.addHandler(streamhandler)
    
        logger.error(message)
    
    
    if __name__ == '__main__':
        log('hi')
        log('hi too')
        log('hi three')
    

方法四

  • 还有一个推荐方法,适用所有不同模块调用

    自定义的 logger,封装好以后,直接在 log conf 文件底部实例化 logger ,其他模块调用时都引用实例化好的 logger 对象,节省资源空间

    #!/usr/bin/python
    # -*- coding:utf-8 -*-
     
    import logging
    import time
    import os
     
     
    class Log(object):
        '''
    封装后的logging
        '''
     
        def __init__(self, logger=None, log_cate='search'):
            '''
                指定保存日志的文件路径,日志级别,以及调用文件
                将日志存入到指定的文件中
            '''
     
            # 创建一个logger
            self.logger = logging.getLogger(logger)
            self.logger.setLevel(logging.DEBUG)
            # 创建一个handler,用于写入日志文件
            self.log_time = time.strftime("%Y_%m_%d")
            file_dir = os.getcwd() + '/../log'
            if not os.path.exists(file_dir):
                os.mkdir(file_dir)
            self.log_path = file_dir
            self.log_name = self.log_path + "/" + log_cate + "." + self.log_time + '.log'
            # print(self.log_name)
     
            fh = logging.FileHandler(self.log_name, 'a')  # 追加模式  这个是python2的
            # fh = logging.FileHandler(self.log_name, 'a', encoding='utf-8')  # 这个是python3的
            fh.setLevel(logging.INFO)
     
            # 再创建一个handler,用于输出到控制台
            ch = logging.StreamHandler()
            ch.setLevel(logging.INFO)
     
            # 定义handler的输出格式
            formatter = logging.Formatter(
                '[%(asctime)s] %(filename)s->%(funcName)s line:%(lineno)d [%(levelname)s]%(message)s')
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
     
            # 给logger添加handler
            self.logger.addHandler(fh)
            self.logger.addHandler(ch)
    
            # 关闭打开的文件
            fh.close()
            ch.close()
     
        def getlog(self):
            return self.logger
    
    # 实例化 log
    concurrent_logger = Log()
    

    测试示例

    # a.py
    from log_conf import concurrent_logger
    
    
    log = concurrent_logger.getlog()
    log.info(f"01: I am a.py")
    
    # b.py
    from log_conf import concurrent_logger
    
    
    log = concurrent_logger.getlog()
    log.info(f"01: I am b.py")
    
    # c.py
    import a
    import b
    from log_conf import concurrent_logger
    
    
    log = concurrent_logger.getlog()
    log.info(f"01: I am c.py")
    
    # 执行 c.py 脚本输出结果
    [2022-11-16 17:10:35,034] a.py-><module> line:7 [INFO]01: I am a.py
    [2022-11-16 17:10:35,035] b.py-><module> line:7 [INFO]01: I am b.py
    [2022-11-16 17:10:35,035] c.py-><module> line:9 [INFO]01: I am c.py
    

方法五

  • 不同模块调用日志,需要将日志存储到对应的模块文件下,方便定位问题

    自定义 logger

    #!/usr/bin/python
    # -*- coding:utf-8 -*-
    
    import logging
    import time
    import os
    
    
    class Log(object):
        '''
        封装后的logging
        '''
    
        def __init__(self, logger=None, log_cate='search'):
            '''
                指定保存日志的文件路径,日志级别,以及调用文件
                将日志存入到指定的文件中
            '''
    
            # 创建一个logger
            self.logger = logging.getLogger(logger)
            self.logger.setLevel(logging.DEBUG)
            # 创建一个handler,用于写入日志文件
            self.log_time = time.strftime("%Y_%m_%d")
            if not os.path.exists(logger):
                os.mkdir(logger)
            self.log_path = logger
            self.log_name = self.log_path + "/" + logger + "." + self.log_time + '.log'
    
            fh = logging.FileHandler(self.log_name, 'a')  # 追加模式  这个是python2的
            # fh = logging.FileHandler(self.log_name, 'a', encoding='utf-8')  # 这个是python3的
            fh.setLevel(logging.INFO)
    
            # 再创建一个handler,用于输出到控制台
            ch = logging.StreamHandler()
            ch.setLevel(logging.INFO)
    
            # 定义handler的输出格式
            formatter = logging.Formatter(
                    '[%(asctime)s] %(filename)s->%(funcName)s line:%(lineno)d [%(levelname)s]%(message)s')
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
    
            # 给logger添加handler
            self.logger.addHandler(fh)
            self.logger.addHandler(ch)
    
            # 关闭打开的文件
            fh.close()
            ch.close()
    
        def getlog(self):
            return self.logger
    

    测试示例

    # a.py
    from log_conf import Log
    
    
    log = Log("aaa").getlog()
    log.info(f"01: I am a.py")
    
    # b.py
    from log_conf import Log
    
    
    log = Log("bbb").getlog()
    log.info(f"01: I am b.py")
    
    # c.py
    import a
    import b
    from log_conf import Log
    
    
    log = Log("ccc").getlog()
    log.info(f"01: I am c.py")
    

    执行 c.py 脚本输出结果
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/9524.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高压功率放大器的作用(功率放大器的应用领域是什么)

高压功率放大器的适用范围和应用领域是很多电子工程师所关心的&#xff0c;那么高压功率放大器的作用以及有哪些使用场景呢&#xff0c;下面就让安泰电子来为大家介绍。 高压功率放大器是电子实验室会频繁使用的测试仪器&#xff0c;是在实验中能够帮助输出信号达到最大输出功率…

【黄啊码】用PHP7性能居然是5.6的三倍?赶紧看看它有什么新特性-续

大家好&#xff0c;我是黄啊码&#xff0c;上节课的东西学完了吧&#xff1f;脑瓜子嗡嗡的吧&#xff1f;来&#xff0c;继续&#xff0c;让脑瓜子一次性嗡个够&#xff0c;压力大&#xff0c;才有动力。 目录 PHP CSPRNG PHP 7 use 语句 PHP 7 错误处理 PHP intdiv() 函…

实验28:步进电机实验

OK,我是走程序猿的道路 我的blog侧重点在讲解代码 本实验结果: 步进电机正转 步进电机反转 步进电机工作原理我就不去讨论了 重点在于代码分析和讲解 01 硬件电路设计 硬件电路总图 接口: 步进电机驱动器板和Arduino Uno板之间的接线: 步进电机驱动器 Arduino Uno…

opencv之 drawContours() 函数说明应用

drawContours 之前使用mask图还进行了连通域有无status分析&#xff0c;然后才进行的绘制。 今天发现直接使用mask图进行绘制&#xff0c;然后通过设置drawContours的参数可以进行不同层次上缺陷的绘制&#xff0c;然后通过这个事情也说明&#xff0c;有问题可以直接找opencv官…

“综合”web项目编写------手把手0基础教学(一)

我们平常看到的项目代码一般都是分段单独的功能&#xff0c;但如何将功能汇总成一个完整的项目呢&#xff0c;下面我将利用IDEA来介绍一个基础的综合web项目 目录 一.创建项目 二.为项目建包 1.了解构建项目的思路 &#xff08;1&#xff09;构建模型&#xff08;模型包括数…

集线器与交换机、虚拟局域网(3.3)

集线器与交换机 传输门&#xff1a;b站湖科大教书匠 集线器 使用集线器的以太网或者局域网其实本质还是一个总线网 工作方式 集线器只工作在物理层&#xff0c;每个接口仅仅用来转发比特&#xff0c;不进行碰撞检测&#xff08;不使用CSMA/CD协议&#xff09;&#xff0c;由…

如何自定义代码生成器(上)

1 概述 1.1 介绍 ​ 在项目开发过程中&#xff0c;有很多业务模块的代码是具有一定规律性的&#xff0c;例如controller控制器、service接口、service实现类、mapper接口、model实体类等等&#xff0c;这部分代码可以使用代码生成器生成&#xff0c;我们就可以将更多的时间放…

深度学习中激活函数的用途

深度学习中激活函数的概念 激活函数&#xff0c;即Activation Function,有时候也称作激励函数。它是为了解决线性不可分的问题引出的。但是也不是说线性可分就不能用激活函数&#xff0c;也是可以的。它的目的是为了使数据更好的展现出我们想要的效果。激活函数是一种非线性的…

SAP UI5 SmartTable 控件本地运行时进行 Excel 导出的单步调试

点击 SmartTable 控件生成的表格控件的 Export to Excel 时&#xff0c;遇到如下错误消息&#xff1a; The following error has occurred during export: Unexpected server response: SmartTable 基于的是 OData V4 的模型了&#xff1a; Excel export 操作&#xff0c;触发的…

接口(上)

&#x1f437;1.接口的概念 &#x1f431;‍&#x1f680;2.接口的语法规则 &#x1f49a;3.接口的使用 &#x1f680;4.接口的特性 &#x1f386;5.实现多个接口 &#x1f436;6.接口间的继承 &#x1f38a;7.接口使用的实例 1.什么是接口呢&#xff1f;&#xff1f;&a…

【https】lighttpd增加https支持及openssl生成CA(Certificate Authority)和使用CA来制作签名证书操作说明

环境说明 ubuntu18.04.1、openssl指令需要支持 openssl生成CA&#xff08;Certificate Authority&#xff09; 生成RSA Private Key openssl genrsa -out ca.key 输出信息 $ openssl genrsa -out ca.key Generating RSA private key, 2048 bit long modulus (2 primes) ...…

Scala008--Scala中的数据结构【集合】

目录 一&#xff0c;概述 二&#xff0c;set的声明 1,不可变set集合 1)向不可变集合中添加元素 【需要新的set集合接收】 2&#xff09;对两个set集合进行合并 【需要新的set集合接收】 2&#xff0c;不可变的HashSet集合 2&#xff0c;可变HashSet集合 1&…

http-only原理与防御XSS实践

目录预备知识XSS攻击实验目的实验环境实验步骤一触发XSS漏洞实验步骤二引入Http-only实验步骤三验证http–only在防御XSS攻击时的作用预备知识 XSS攻击 http-only的设计主要是用来防御XSS攻击&#xff0c;所以学习本实验的读者应首先了解XSS攻击的相关原理内容。 跨站点脚本攻…

Vue 2 如何添加 register-service-worker 以实现缓存请求的目的

Vue 2 如何添加 register-service-worker 以实现缓存请求的目的 一、问题描述 现在 vue 3 的模板中是自带 register-service-worker 的。 用这个的好处是&#xff0c;它会自动将项目中的所有文件请求缓存到 service-worker 中&#xff0c;以实现再次打开网站的时候会非常非常…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java校园二手物品交易系统051x4

做毕业设计一定要选好题目。毕设想简单&#xff0c;其实很简单。这里给几点建议&#xff1a; 1&#xff1a;首先&#xff0c;学会收集整理&#xff0c;年年专业都一样&#xff0c;岁岁毕业人不同。很多人在做毕业设计的时候&#xff0c;都犯了一个错误&#xff0c;那就是不借鉴…

HTB-Tier1

HTB-Tier1 Appointment Task 1 What does the acronym SQL stand for? ********** ***** *******e Structured Query Language Hide Answer Task 2 What is one of the most common type of SQL vulnerabilities? *** ********n sql injection Hide Answer Task …

汉字风格迁移篇----EasyFont:一个基于风格学习的系统,可以轻松构建大规模手写字体

文章目录abstract1 INTRODUCTION2 RELATED WORK3 METHOD DESCRIPTION3.1 Selecting Input Character Set3.2 Learning Font Skeleton Manifold3.2.1 Character Matching.3.2.2 Training the GP-LVM3.3 Text Segmentation3.4 Stroke Extraction3.5 Overall Style Learning3.5.1 …

UNet - 数据加载 Dataset

目录 1. 介绍 2. 数据处理 dataset 2.1 预处理 2.2 加载数据 2.2.1 初始化 2.2.2 返回数据 2.2.3 样本数量 3. 测试一下 4. 完整代码 1. 介绍 之前介绍完了Unet网络的搭建&#xff0c;接下来说一下要解决的任务。 本章介绍的是&#xff1a;数据的加载处理 下面是整…

hadoop 3.3大数据集群搭建系列1-安装hadoop

文章目录一. 软硬件配置1.1 主机配置及规划1.2 软件配置1.3 安装常用的工具二. 安装前准备2.1 设置主机名2.2 设置hosts2.3 关闭防火墙2.4 ssh免密登陆2.5 ntpdate时间同步三. 安装3.1 安装hadoop3.1.1 下载hadoop并解压3.1.2 配置hadoop_home环境变量3.1.3 编辑etc/hadoop/had…

【毕业设计】图像识别垃圾分类系统 - python 深度学习

文章目录0 前言1 简介2 识别效果3 实现3.1 数据集3.2 实现原理和方法3.3 网络结构4 最后0 前言 &#x1f525; Hi&#xff0c;大家好&#xff0c;这里是丹成学长的毕设系列文章&#xff01; &#x1f525; 对毕设有任何疑问都可以问学长哦! 这两年开始&#xff0c;各个学校对…