Python学习21:Logging日志管理和封装

笔者: 出处:https://blog.csdn.net/JackMengJin 笔者原创,文章转载需注明,如果喜欢请点赞+关注,感谢支持!

导读:本文带你用logging模块来实现日志管理功能。模块三是重点内容,建议用较长的时间阅读。

 

目录

logging模块日志管理

一、日志介绍

二、基本用法

2.1 日志等级

2.2.logging提供的模块级别的函数

三、进阶用法——四大组件

3.1 记录器

3.2 处理程序介绍

3.3 处理程序的具体用法

3.4 过滤器

3.5 格式化程序

四、树形结构

五、logging模块封装

六、配置文件使用logging模块


 

Logging模块日志管理

logging官方中文攻略:https://docs.python.org/zh-cn/3.7/howto/logging.html#logging-advanced-tutorial

日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述(比如:每个事件发生时的数据都是不同的)。开发者还会区分事件的重要性,重要性也被称为 等级 或 严重性。

 

一、日志介绍

日志的定义:运行时会出现或记录的打印信息。

日志的作用:一个好的日志记录不仅可以追踪某些软件运行时所发生事件,还可以记录并定位到一些问题,比如跑自动化用例。

通过log的分析,可以方便用户了解系统或软件、应用的运行情况;如果你的应用log足够丰富,也可以分析以往用户的操作行为、类型喜好、地域分布或其他更多信息;如果一个应用的log同时也分了多个级别,那么可以很轻易地分析得到该应用的健康状况,及时发现问题并快速定位、解决问题,补救损失。

简单来讲就是,我们通过记录和分析日志可以了解一个系统或软件程序运行情况是否正常,也可以在应用程序出现故障时快速定位问题。

比如运维:在接收到报警或各种问题反馈后,进行问题排查时通常都会先去看各种日志,大部分问题都可以在日志中找到答案。

比如开发:可以通过IDE控制台上输出的各种日志进行程序调试。

比如测试:在做自动化测试时可以通过日志记录bug,并将bug原因填写在日志里记录,等等。

不仅仅是python,C、JAVA等其他常用的语言中都有日志功能,而文本主要是讲解python中logging模块实现的日志功能。

 

二、基本用法

同于JAVA,Python自身也提供了一个用于记录日志的标准库模块—— logging模块。

官方介绍:

该模块定义的基础类和函数都列在下面。

  • 记录器暴露了应用程序代码直接使用的接口。

  • 处理程序将日志记录(由记录器创建)发送到适当的目标。

  • 过滤器提供了更精细的附加功能,用于确定要输出的日志记录。

  • 格式化程序指定最终输出中日志记录的样式。

2.1 日志等级

logging模块默认定义了以下几个日志等级,它允许开发人员自定义其他日志级别,但是这是不被推荐的,了解即可。

python的日志等级记录的数值如下:

CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0

级别

数值

CRITICAL

50

ERROR 

40

WARNING

30

INFO

20

DEBUG 

10

NOTSET

0

你想要执行的任务

此任务最好的工具

对于命令行或程序的应用,结果显示在控制台。

print()

在对程序的普通操作发生时提交事件报告(比如:状态监控和错误调查)

logging.info() 函数(当有诊断目的需要详细输出信息时使用 logging.debug() 函数)

提出一个警告信息基于一个特殊的运行时事件

warnings.warn() 位于代码库中,该事件是可以避免的,需要修改客户端应用以消除告警

logging.warning() 不需要修改客户端应用,但是该事件还是需要引起关注

对一个特殊的运行时事件报告错误

引发异常

报告错误而不引发异常(如在长时间运行中的服务端进程的错误处理)

logging.error()logging.exception() 或 logging.critical() 分别适用于特定的错误及应用领域

CRITICAL

在系统产生严重的错误,表明程序本身可能无法继续运行的时候使用,也就是导致系统崩溃的级别!一般出现CRITICAL级别的错误就代表要扣绩效了。

ERROR 

程序发生了错误,软件没能执行一些功能,但还可以继续执行的时候用ERROR级别,而之前异常和捕获(try expect)就是error级别。在这个级别上就用ERROR等级。

WARNING 

程序一些意想不到的事情发生,比如:警告内存空间不足,此时程序还能按预期工作,但在不久的将来会出现问题。

INFO

确认一切按预期运行,一般用于输出重要运行情况,比如%多少运行,可以用于观察,是否达到了预期。

DEBUG

主要是用来输出详细的运行情况,用于调试居多,不太重要。

总结:

  • CRITICAL  当发生严重错误,导致应用程序不能继续运行时记录的信息。
  • ERROR  由于一个更严重的问题导致某些功能不能正常运行时记录的信息。
  • WARNING 当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的。
  • INFO 信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作。
  • DEBUG 最详细的日志信息,典型应用场景是问题诊断。

善用日志等级,可以在项目中发挥200%的效果。

 

2.2.logging提供的模块级别的函数

logging提供的模块级别的函数可以便捷的使用日志。

导入模块

import logging

不同级别的日志语句

logging.debug("这是debug级别的log!")
logging.info("这是info级别的log!")
logging.error("这是error级别的log!")
logging.warning("这是warning级别的log!")
logging.critical("这是critical级别的log!")

控制台打印

可以看到在python中默认不打印debug、info级别的日志。

日志记录到文件

import logging

logging.basicConfig(filename='demo.log', level=logging.DEBUG)
logging.debug("This is a debug log!")
logging.info("This is an info log!")
logging.error("This is an error log!")
logging.warning("This is a warning log!")
logging.critical("This is a critical log!")

Python学习21:Logging日志管理和封装_第1张图片

需要注意这里用到的logging.basicConfig实际上用到的是Logging日志系统的四大组件,只不过用的默认值。

Python学习21:Logging日志管理和封装_第2张图片

还可以设置日志格式,过滤器等操作,而学会利用logging模块四大组件才是学好logging模块的核心。

 

三、进阶用法——四大组件

四大组件分别是:LoggerHandlerFilterFormatter

日志库采用模块化方法,并提供几类组件:记录器、处理程序、过滤器和格式化程序。

  • 记录器暴露了应用程序代码直接使用的接口。

  • 处理程序将日志记录(由记录器创建)发送到适当的目标。

  • 过滤器提供了更精细的附加功能,用于确定要输出的日志记录。

  • 格式化程序指定最终输出中日志记录的样式。

默认情况下,没有为任何日志记录消息设置目标, 你可以使用logging.basicConfig指定目标(例如控制台或文件),也就是本文模块2.3中举例用的方法。

3.1 记录器

Logger:记录器又名收集器,用来收集log,负责记录日志。最广泛的作用就是配置和消息发送。Logger对象提供应用程序可直接使用的接口。

最常见的配置方法:

  • logger.setLevel() 指定记录器将处理的最低严重性日志消息,其中 debug 是最低内置严重性级别, critical 是最高内置严重性级别。 例如,如果严重性级别为 INFO ,则记录器将仅处理 INFO 、 WARNING 、 ERROR 和 CRITICAL 消息,并将忽略 DEBUG 消息。
  • logger.addHandler() 从记录器对象中添加处理程序(处理器)对象。
  • logger.addFilter()可以添加记录器对象中的过滤器。

记录器和处理程序中的日志事件信息流程如下图所示,具体流程在本文第四模块详细讲解:

Python学习21:Logging日志管理和封装_第3张图片

Logger是一个树形层级结构(在本文第四模块中会详细说明),在使用接口debug,info,warn,error,critical之前必须创建Logger实例,即创建一个记录器。如果没有显式的进行创建,则默认创建一个root logger,并应用默认的日志级别(WARN),处理器Handler(StreamHandler,即将日志信息打印输出在标准输出上),和格式化器Formatter(默认的格式即为第一个简单使用程序中输出的格式)。

  • 创建记录器(收集器),不传参默认为root,传参标识自定义:
logger = logging.getLogger("demo")

设置记录器的收集等级为debug等级:

logger.setLevel("DEBUG")

记录器(收集器)要和处理程序(输出处理器)绑定使用。

 

3.2 处理程序介绍

Handler:处理程序又名输出处理器,广泛使用的分为控制台输出处理器 StreamHandler,和文件输出处理器 FileHandler。主要用来决定要输出的日志。Handler 发送日志到适当的目的地。

Handler对象负责将适当的日志消息(基于日志消息的严重性)分派给处理程序的指定目标。logger对象可以使用 addHandler()方法向自己添加零个或多个处理程序对象。

setLevel() 方法 —— 就像在记录器对象中一样,指定将被分派到适当目标的最低严重性。

为什么有两个 setLevel() 方法?

记录器(收集器)中设置的级别确定将传递给其处理程序的消息的严重性。

处理程序(输出处理器)中设置的级别确定处理程序将发送哪些消息。

如何理解?

在收集器里设置的日志级别是记录在收集器中的,而不代表要发送这个级别。而处理器中设置的级别才是最后决定要发送的哪些级别的日志消息。只有日志等级大于或等于设置的日志级别的日志才会被输出。

setLevel(logging.WARN) # 指定日志级别,低于WARN级别的日志将被忽略

setFormatter() 方法—— 选择一个该处理程序使用的 Formatter 对象。

setFormatter(formatter_name) # 设置一个格式化器formatter

addFilter()、removeFilter() 方法—— 新增或删除一个filter对象。

addFilter(filter_name) # 增加一个过滤器,可以增加多个

 

3.3 处理程序的具体用法

初始化控制台输出处理器

stream_handler = logging.StreamHandler()

初始化文件输出处理器

file_handler = logging.FileHandler("log.txt", mode="a", encoding="utf-8")

这里用a模式也就是追加模式,记录在文件的日志不会被覆盖。

编码格式用utf-8格式,这样如果日志里有中文就不会出现乱码。

未设置utf-8:

file_handler = logging.FileHandler("log.txt", mode="a")

设置之后:

file_handler = logging.FileHandler("log.txt", mode="a", encoding="utf-8")

设置输出处理器等级

file_handler.setLevel("INFO")
stream_handler.setLevel("INFO")

输出处理器添加到收集器

logger.addHandler(stream_handler)
logger.addHandler(file_handler)

设置输出处理器的日志格式

# 日志格式
fmt='%(asctime)s--%(filename)s--line:%(lineno)d]--%(levelname)s:%(message)s'

# 设置输出处理器日志格式
file_handle.setFormatter(fmt)

设置输出处理器的过滤器(日志格式和过滤器后面将)

filter = logging.Filter()
stream_handler.addFilter(filter)

需要注意,logger收集器可以添加多个handler处理器,而下面的补充资料供大家了解,用到的时候查看下即可。


作为补充资料,了解即可:

作为 Handler 基类的补充,提供了很多有用的子类:

StreamHandler 实例发送消息到流(类似文件对象)。

FileHandler 实例将消息发送到硬盘文件。

BaseRotatingHandler 是轮换日志文件的处理程序的基类。它并不应该直接实例化。而应该使用 RotatingFileHandlerTimedRotatingFileHandler代替它。

RotatingFileHandler 实例将消息发送到硬盘文件,支持最大日志文件大小和日志文件轮换。

TimedRotatingFileHandler 实例将消息发送到硬盘文件,以特定的时间间隔轮换日志文件。

SocketHandler 实例将消息发送到 TCP/IP 套接字。从 3.4 开始,也支持 Unix 域套接字。

DatagramHandler 实例将消息发送到 UDP 套接字。从 3.4 开始,也支持 Unix 域套接字。

SMTPHandler 实例将消息发送到指定的电子邮件地址。

SysLogHandler 实例将消息发送到 Unix syslog 守护程序,可能在远程计算机上。

NTEventLogHandler 实例将消息发送到 Windows NT/2000/XP 事件日志。

MemoryHandler 实例将消息发送到内存中的缓冲区,只要满足特定条件,缓冲区就会刷新。

HTTPHandler 实例使用 GET 或 POST 方法将消息发送到 HTTP 服务器。

WatchedFileHandler 实例会监视他们要写入日志的文件。如果文件发生更改,则会关闭该文件并使用文件名重新打开。此处理程序仅在类 Unix 系统上有用; Windows 不支持依赖的基础机制。

QueueHandler 实例将消息发送到队列,例如在 queue 或 multiprocessing 模块中实现的队列。

NullHandler 实例对错误消息不执行任何操作。它们由想要使用日志记录的库开发人员使用,但是想要避免如果库用户没有配置日志记录,则显示 "无法找到记录器XXX的消息处理器" 消息的情况。

 

3.4 过滤器

Filter:限制只有满足过滤规则的日志才会输出。Filter提供了过滤日志信息的方法。

比如定义了filter = logging.Filter('a.b.c'),并将这个Filter添加到了一个Handler上,则使用该Handler的Logger中只有名字带a.b.c前缀的Logger才能输出其日志。

Python学习21:Logging日志管理和封装_第4张图片

根据自己的需求去设置相应的过滤规则,例如,过滤器用“A.B”初始化将允许由日志记录器“A.B”记录事件,“A.B.C”、“A.B.C.D”、“A.B.D”等等,但不是“A.BB”,“B.A.B”等。如果用空字符串初始化,表示传递所有事件。

以下内容了解即可,详细内容可以在python官方文档上查阅:

Filters 可被 Handlers 和 Loggers 用来实现比按层级提供更复杂的过滤操作。 基本过滤器类只允许低于日志记录器层级结构中低于特定层级的事件。 例如,一个用 'A.B' 初始化的过滤器将允许 'A.B', 'A.B.C', 'A.B.C.D', 'A.B.D' 等日志记录器所记录的事件。 但 'A.BB', 'B.A.B' 等则不允许。 如果用空字符串初始化,则所有事件都会被略过。

class logging.Filter(name='')

返回一个 Filter 类的实例。 如果指定了 name,则它将被用来为日志记录器命名,该类及其子类将通过该过滤器允许指定事件通过。 如果 name 为空字符串,则允许所有事件通过。

filter(record)

是否要记录指定的记录?返回零表示否,非零表示是。如果认为合适,则可以通过此方法就地修改记录。

请注意关联到处理程序的过滤器会在事件由处理程序发出之前被查询,而关联到日志记录器的过滤器则会在有事件被记录的的任何时候(使用 debug()info() 等等)在将事件发送给处理程序之前被查询。 这意味着由后代日志记录器生成的事件将不会被日志记录器的过滤器设置所过滤,除非该过滤器也已被应用于后代日志记录器。

你实际上不需要子类化 Filter: 你可以将传入任何包含 filter 方法的具有相同语义的的实例。

在 3.2 版更改: 你不需要创建专门的 Filter 类,或使用具有 filter 方法的其他类:你可以使用一个函数(或其他可调用对象)作为过滤器。 过滤逻辑将检查过滤器对象是否文化的 filter 属性:如果有,就会将它当作是 Filter 并调用它的 filter() 方法。 在其他情况下,则会将它当作是可调用对象并附带记录作为单一形参进行调用。 返回值应当与 filter() 的返回值相一致。

 

3.5 格式化程序

Formatter:设置日志信息最后的规则、结构和内容,Formatter指定日志显示格式。常用的日志格式:

%(asctime)s--%(filename)s--line:%(lineno)d--%(levelname)s:%(message)s

格式化程序对象配置日志消息的最终顺序、结构和内容。

与 logging.Handler 类不同,应用程序代码可以实例化格式化程序类,但如果应用程序需要特殊行为,则可能会对格式化程序进行子类化。构造函数有三个可选参数 —— 消息格式字符串日期格式字符串样式指示符

设置日志格式

fmt = logging.Formatter('%(asctime)s--%(filename)s--line:%(lineno)d--%(levelname)s:%(message)s')
file_handler.setFormatter(fmt)

python官网上常用的format格式,用到的时候再查阅资料。

属性名称

格式

描述

args

不需要格式化。

合并到 msg 以产生 message 的包含参数的元组,或是其中的值将被用于合并的字典(当只有一个参数且其类型为字典时)。

asctime

%(asctime)s

表示 LogRecord 何时被创建的供人查看时间值。 默认形式为 '2003-07-08 16:49:45,896' (逗号之后的数字为时间的毫秒部分)。

created

%(created)f

LogRecord 被创建的时间(即 time.time() 的返回值)。

exc_info

不需要格式化。

异常元组 (例如 sys.exc_info) 或者如未发生异常则为 None

filename

%(filename)s

pathname 的文件名部分。

funcName

%(funcName)s

函数名包括调用日志记录.

levelname

%(levelname)s

消息文本记录级别 ('DEBUG''INFO''WARNING''ERROR''CRITICAL').

levelno

%(levelno)s

消息数字记录级别 (DEBUGINFOWARNINGERRORCRITICAL).

lineno

%(lineno)d

发出日志记录调用所在的源行号(如果可用)。

message

%(message)s

记入日志的消息,即 msg % args 的结果。 这是在发起调用 Formatter.format() 时设置的。

module 模块

%(module)s

模块 (filename 的名称部分)。

msecs

%(msecs)d

LogRecord 被创建的时间的毫秒部分。

msg

不需要格式化。

在原始日志记录调用中传入的格式字符串。 与 args 合并以产生 message,或是一个任意对象 (参见 使用任意对象作为消息)。

名称

%(name)s

用于记录调用的日志记录器名称。

pathname

%(pathname)s

发出日志记录调用的源文件的完整路径名(如果可用)。

process

%(process)d

进程ID(如果可用)

processName

%(processName)s

进程名(如果可用)

relativeCreated

%(relativeCreated)d

以毫秒数表示的 LogRecord 被创建的时间,即相对于 logging 模块被加载时间的差值。

stack_info

不需要格式化。

当前线程中从堆栈底部起向上直到包括日志记录调用并导致创建此记录的堆栈帧的堆栈帧信息(如果可用)。

thread

%(thread)d

线程ID(如果可用)

threadName

%(threadName)s

线程名(如果可用)

添加格式前:

logger.error("Oh no! error!!!")
logger.critical("Oh my GOD! critical!!!")

添加格式后:

fmt = logging.Formatter('%(asctime)s--%(filename)s--line:%(lineno)d--%(levelname)s:%(message)s')
file_handler.setFormatter(fmt)
stream_handler.setFormatter(fmt)

logger.addHandler(stream_handler)
logger.addHandler(file_handler)

logger.error("Oh no! error!!!")
logger.critical("Oh my GOD! critical!!!")

 

四、树形结构

Logger是一个树形层级结构。

上文讲到过,Logger可以包含一个或多个HandlerFilter,即Logger与Handler或Fitler是一对多的关系。

一个Logger实例可以新增多个Handler,一个Handler可以新增多个格式化器或多个过滤器,而且日志级别将会继承。

Python学习21:Logging日志管理和封装_第5张图片

具体的流程如下:

  • 第一次导入logging模块,logging模块中的代码将被执行,过程中将产生logging日志系统的默认配置。
  • 自定义配置(可选)。logging标准模块支持三种配置方式: dictConfig,fileConfig,listen。其中,dictConfig是通过一个字典进行配置Logger,Handler,Filter,Formatter;fileConfig则是通过一个文件进行配置;而listen则监听一个网络端口,通过接收网络数据来进行配置。除了以上集体化配置外,也可以直接调用Logger,Handler等对象中的方法在代码中来显式配置。
  • 使用logging模块的全局作用域中的getLogger函数来得到一个Logger对象实例(其参数即是一个字符串,表示Logger对象实例的名字,即通过该名字来得到相应的Logger对象实例)。
  • 使用Logger对象中的debug,info,error,warn,critical等方法记录日志信息。
  • 判断日志的等级是否大于Logger对象的等级,如果大于,则往下执行,否则,流程结束。
  • 产生日志。第一步,判断是否有异常,如果有,则添加异常信息。第二步,处理日志记录方法(如debug,info等)中的占位符,即一般的字符串格式化处理。
  • 使用注册到Logger对象中的Filters进行过滤。如果有多个过滤器,则依次过滤;只要有一个过滤器返回假,则过滤结束,且该日志信息将丢弃,不再处理,而处理流程也至此结束。否则,处理流程往下执行。
  • 在当前Logger对象中查找Handlers,如果找不到任何Handler,则往上到该Logger对象的父Logger中查找;如果找到一个或多个Handler,则依次用Handler来处理日志信息。但在每个Handler处理日志信息过程中,会首先判断日志信息的等级是否大于该Handler的等级,如果大于,则往下执行(由Logger对象进入Handler对象中),否则,处理流程结束。
  • 执行Handler对象中的filter方法,该方法会依次执行注册到该Handler对象中的Filter。如果有一个Filter判断该日志信息为假,则此后的所有Filter都不再执行,而直接将该日志信息丢弃,处理流程结束。
  • 使用Formatter类格式化最终的输出结果。 注:Formatter同字符串格式化不同,它会添加额外的信息,比如日志产生的时间,产生日志的源代码所在的源文件的路径等等。
  • 真正地输出日志信息(网络,文件,终端,邮件等),至于输出到哪个目的地,由Handler的种类来决定。

注:以上内容摘抄自第三条参考资料,总结的很好,拿来借鉴。

 

五、logging模块封装

封装为函数:

def get_logger(
        name='root',
        file=None,
        logger_level='DEBUG',
        stream_level='DEBUG',
        file_level='INFO',
        fmt='%(asctime)s--%(filename)s--line:%(lineno)d]--%(levelname)s:%(message)s'
):
    """ 日志处理函数 """

    # 初始化logger收集器
    logger = logging.getLogger(name)
    # 设置收集器等级
    logger.setLevel(logger_level)
    # 初始化控制台输出处理器
    stream_handler = logging.StreamHandler()
    # 设置输出处理器等级
    stream_handler.setLevel(stream_level)

    # 定义日志格式
    fmt = logging.Formatter(fmt)
    # 设置输出处理器日志格式
    stream_handler.setFormatter(fmt)
    # 将收集器和输出处理器绑定
    logger.addHandler(stream_handler)

    # 如果文件存在
    if file:
        # 初始化文件输出处理器
        file_handle = logging.FileHandler(file, encoding='utf8')
        # 设置文件输出处理器为绝对路径
        file_handle.name = file_handle.baseFilename
        # 设置文件输出处理器等级
        file_handle.setLevel(file_level)
        # 将收集器和文件输出处理器绑定
        logger.addHandler(file_handle)
        # 设置输出处理器日志格式
        file_handle.setFormatter(fmt)

    return logger


if __name__ == '__main__':
    pass

封装思路:

为什么选择封装函数而不是封装类:封装为函数回避封装为类调用起来会更为方便,所以这里选做了封装函数。

关于参数:参数用默认参数即可,name,file,收集器等级和用到的输出器等级logger_level、stream_level、file_level,这里需要注意在控制台输出时级别和文件输出器里的级别根据需要去设定,最后一项参数就是日志格式fmt。

判断文件是否存在:文件缺省值设为None,默认不存在,就不用去生成文件收集器;如果文件存在,则走下面的代码。

整体封装为函数的难度并不高,熟练掌握四大组件即可轻松驾驭!

调用效果如下:

if __name__ == '__main__':
    logger = get_logger(name="demo", file="demo_log.log")
    logger.critical("critical!")
    logger.error("error!")
    logger.info("info!")
    logger.warning("warning!!!") 

Python学习21:Logging日志管理和封装_第6张图片

Python学习21:Logging日志管理和封装_第7张图片

 

六、配置文件使用logging模块

常见的配置文件格式比如.yml.conf文件,文本仅简单说明下.yml配置文件中如何利用日志管理工具,在之后学习到yaml文件解析在详细的讲解配置文件的用法,这里仅仅作为了解。

log:
  name: 'python'
  file_test: 'test.log'
  logger_level: 'DEBUG'
  stream_level: 'DEBUG'
  file_level: 'INFO'
# 加载yaml配置文件
yaml = yaml_handler.read_yaml(config.YAML_PATH)    

# 加载log
__logger_file_name = yaml["log"]["file_test"]
logger = logging_handler.get_logger(
name=yaml["log"]["name"],
file=os.path.join(conf.LOG_PATH, __logger_file_name),
logger_level=yaml["log"]["logger_level"],
stream_level=yaml["log"]["stream_level"],
file_level=yaml["log"]["file_level"])
# 调用
logger.info("记录内容...")

以上代码展示非完整版,更多关于配置文件的操作,会在之后的学习中再详细讲解。

关于日志在配置文件操作更为详细的官方文档请阅览:https://docs.python.org/zh-cn/3.7/library/logging.config.html

 


以上便是《Python学习21:logging日志管理和封装》的所有内容,原创不易,如果喜欢请点赞和关注,谢谢大家的支持!

想获得免费的学习资料请添加微信公众号——

你可能感兴趣的:(Python,python,logging模块,日志管理)