【Python】—日志模块logging使用详解1

本文深入解析Python的logging模块,涵盖日志级别、流程、关键类(Logger、Handler、Filter、Formatter)的使用与配置,包括示例代码和最佳实践。


开发部署项目时,不可能将所有的信息都输出到控制台中,因此我们将这些信息记录到日志文件中,不仅方便查看程序运行的情况,也可以在项目出现故障时根据该运行时产生的日志快速定位问题。

Python中提供了日志模块logging,可以方便的记录日志信息。

本文主要分析了logging模块的相关知识及具体使用。
当前介绍该日志模块的文章有很多,之所以还要进行记录,一方面是因为没有看到系统的介绍,二是通过梳理此部分内容,做到不止会用,还明白内部函数调用。

1、日志级别

Python标准库logging用做记录日志,默认分为五种日志级别:

  • DEBUG(10):详细信息,调试问题时所需
  • INFO(20):证明事情可按预期工作
  • WARNING(30):有意外,将来可能发生问题,但依然可用
  • ERROR(40):严重问题,程序不能执行一些功能
  • CRITICAL(50):严重错误

我们自定义日志级别时注意不要和默认的日志级别数值型相同(这是什么意思呢),logging执行时输出大于等于设置的值日级别的日志信息,如设置级别为INFO,则INFO, WARNING, ERROR, CRITICAL级别的日志都会输出。
5种等级分别对应5种日志打印方法:debug, info, warning, error, critical
默认的是WARNING,即当等级大于等于WARNING才被跟踪。

2、logging流程

官方的logging模块工作流程图如下:
logging流程图

logging流程图

从上图中可以看到这几种python类型,Logger, LogRecord, Filter, Handler, Formatter
类型说明

  • Logger日志,暴露函数给应用程序,基于日志记录器和过滤器级别决定哪些日志有效。
  • LogRecord日志记录器,将日志传到相应的处理器处理。
  • Handler处理器,将(日志记录器产生的)日志记录发送至合适的目的地。
  • Filter过滤器,提供了更好的粒度控制,它可以决定输出哪些日志记录。
  • Formatter格式化器,指明了最终输出中日志记录的布局。

基本流程:

  • 判断Logger对象对于设置的级别是否可用,如果可用,则往下执行,否则,流程结束。
  • 创建LogRecord对象,如果注册到Logger对象中的Filter对象过滤后返回False, 则不记录日志,流程结束,否则,向下执行。
  • LogRecord对象将Handler对象传入当前的Logger对象,(图中子流程)如果Handler对象的日志级别大于设置的日记级别,再判断注册到Handler对象中的Filter对象过滤后是否返回True而放行输出日志信息,否则不放行,流程结束。
  • 如果传入的Handler大于Logger中设置的级别,也即Handler有效,则往下执行,否则,流程结束。
  • 判断这个Logger对象是否还有父Logger对象,如果没有(代表当前Logger对象是最顶层的Logger对象root Logger),流程结束。否则将Logger对象设置为它的父Logger对象,重复上面3,4两步,输出父类Logger对象中的日志输出,直到是root Logger对象。

3、几个重要的类

前面讲logging流程的时候,提到几个概念:LoggerHandlerFilterFormatter

3.1 Logger类

Logger对象有3个任务要做:

  • 向应用程序代码暴露info, debug等方法,使应用程序可以在运行时记录日志消息
  • 基于日志级别(默认的过滤设施)或Filter对象来决定要对哪些日志进行后续处理
  • 将日志消息传送给所有感兴趣的日志handlers

一个系统只有一个根Logger对象,并且该对象不能被直接实例化

通过调用logging.getLogger(name)函数创建Logger类对象。

Logger对象最常用的方法有两类:(1)配置方法,(2)消息发送方法

Logger对象可以设置多个Handler对象和Filter对象,Handler对象可以设置Formatter对象。
Formatter对象用来设置消息的具体输出格式。

1) 最常用的配置方法
方法描述
Logger.setLevel设置日志器将会处理的日志消息的最低严重级别
Logger.addHandler()为该Logger对象添加一个handler对象
Logger.removeHandler为该Logger对象移除一个handler对象
Logger.addFilter()和Logger.removeFilter()为该Logger对象添加 和 移除一个filter对象

logger对象配置完成后,可使用下面的方法来创建日志记录:

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), Logger.critical():创建一个与他们的方法名对应等级的日志记录
  • Logger.exception():创建一个类似于Logger.error()的日志消息
  • Logger.log():需要获取一个明确的日志level参数来创建一个日志记录

1. 获取Logger对象
通常是通过--logging.getLogger()的方法。该方法有一个可选参数name,该参数表示将要返回的日志器的名称标识,如果不提供该参数,则其值为root
若以相同的name参数值多次调用getLogger()方法,将会返回指向同一个Logger对象的引用。

2. logger的层级结构与有效等级

  • logger的名称是一个'.'分割的层级结构,每个'.'后面的logger都是’.'前面的logger的children,例如,有一个名称为 foologger,其它名称分别为foo.bar, foo.bar.bazfoo.bam都是 foo 的后代。

  • logger有一个"有效等级(effective level"的概念。如果一个logger上没有被明确设置一个level,那么该logger就是使用它parent的level;如果它的parent也没有明确设置level则继续向上查找parentparent的有效level,依次类推,直到找到个一个明确设置了level的祖先为止。需要说明的是,root logger总是会有一个明确的level设置(默认为 WARNING。当决定是否去处理一个已发生的事件时,logger的有效等级将会被用来决定是否将该事件传递给该loggerhandlers进行处理。

  • child loggers在完成对日志消息的处理后,默认会将日志消息传递给与它们的祖先loggers相关的handlers。因此,我们不必为一个应用程序中所使用的所有loggers定义和配置handlers只需要为一个顶层的logger配置handlers,然后按照需要创建child loggers就可足够了。我们也可以通过将一个logger的propagate属性设置为False来关闭这种传递机制。

2) 创建Logger对象logging.getLogger([name])

日志记录的工作主要由Logger对象来完成。前面讲到一个系统只有一个根Logger对象,并且该对象不能被直接实例化

需要通过logging.getLogger([name])来获取Logger对象。
在调用getLogger时要提供Logger的名称(多次使用相同名称来调用getLogger,返回的是同一个对象的引用。),Logger实例之间有层次关系,这些关系通过Logger名称来体现。

p = logging.getLogger(“root”)
c1 = logging.getLogger(“root.c1”)
c2 = logging.getLogger(“root.c2”)

例子中,p父logger, c1,c2分别是p的子loggerc1, c2将继承p的设置。如果省略了name参数, getLogger将返回日志对象层次关系中的根Logger

每个Logger对象都可以设置一个名字,如果设置logger = logging.getLogger(__name__)__name__Python中的一个特殊内置变量,他代表当前模块的名称(默认为__main__)。

该函数内部根据是否指定名称返回对应的Logger对象

root = RootLogger(WARNING)
Logger.root = root
Logger.manager = Manager(Logger.root)

def getLogger(name=None):
    """
    Return a logger with the specified name, creating it if necessary.
    If no name is specified, return the root logger.
    """
    if name:
        return Logger.manager.getLogger(name)
    else:
        return root

示例:

import logging
logger = logging.getLogger('test')
logger.warning('this is a warning info')
# 输出
WARNING:test:this is a warning info

3.2 Handler

Handler对象的作用是(基于日志消息的等级)将消息分发到handler指定的位置(文件、网络、邮件等)。Logger对象可以通过addHandler()方法为自己添加handler对象。

应用程序代码不应该直接实例化和使用Handler实例。因为Handler是一个基类,只定义了所有handlers都应该有的接口,同时提供了一些子类可以直接使用或覆盖的默认行为。

常用接口有:

  • setLevel()
  • setFormatter()
  • set_name()

常用的Handler

Handler描述
logging.StreamHandler将日志消息发送到输出到Stream,如std.out, std.err或任何file-like对象。
logging.FileHandler将日志消息发送到磁盘文件,默认情况下文件大小会无限增长
logging.handlers.RotatingFileHandler将日志消息发送到磁盘文件,并支持日志文件按大小切割
logging.hanlders.TimedRotatingFileHandler将日志消息发送到磁盘文件,并支持日志文件按时间切割
logging.handlers.HTTPHandler将日志消息以GETPOST的方式发送给一个HTTP服务器
logging.handlers.SMTPHandler将日志消息发送给一个指定的email地址
logging.NullHandlerHandler实例会忽略error messages,通常被想使用logginglibrary开发者使用来避免'No handlers could be found for logger XXX'信息的出现。

类之间的继承关系如下:
在这里插入图片描述

示例:

import logging

logger = logging.getLogging() # 获取Logger对象
handler = logging.FileHandler('output.log', mode='a', encoding=None, delay=False)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S',
                              style='%')
handler.setFormatter(formatter) # handler对象设置格式
handler.setLevel(logging.DEBUG)# handler设置日志级别

logger.addHandler(handler) # 添加handler对象

3.3 Filter

Filter实例用于执行日志记录的任意筛选。
LoggersHandlers可以根据需要使用筛选器实例筛选记录.
logging标准库只提供了一个base filter,其构造函数__init__接收name,默认的行为是名为namelogger及其子logger的消息能通过过滤器,其余的被过滤掉。

class Filter(object):
    """
    Filter instances are used to perform arbitrary filtering of LogRecords.
    """
    def __init__(self, name=''):
        """
        Initialize a filter.

        Initialize with the name of the logger which, together with its
        children, will have its events allowed through the filter. If no
        name is specified, allow every event.
        """
        self.name = name
        self.nlen = len(name)

3.4 Formatter

Formatter对象用于配置日志信息的最终顺序、结构和内容。与logging.Handler基类不同的是,应用代码可以直接实例化Formatter类。

Formatter类的构造方法定义如下:
logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

该方法可接收3个可选参数:

  • fmt:指定消息格式化字符串,如果不指定该参数则默认使用message的原始值
  • datefmt:指定日期格式字符串,如果不指定,则默认使用"%Y-%m-%d %H:%M:%S"
  • style:可取值为'%', '{'和'$',如果不指定,则默认使用'%'
格式含义
%(levelno)s打印日志级别的数值
%(levelname)s打印日志级别名称
%(pathname)s打印当前执行程序的路径
%(filename)s打印当前执行程序名
%(funcName)s打印日志的当前函数
%(lineno)d打印日志的当前行号
%(asctime)s打印日志的时间
%(thread)d打印线程ID
%(message)s打印日志信息
%(process)s当前进程,进程ID
%(module)s调用日志输出函数的模块名,filename的名称部分,不包含后缀
%(msecs)d日志事件发生事件的毫秒部分。logging.basicConfig()中用参数datefmt将会去掉asctime中产生的毫秒部分,可以用这个加上。

一般直接使用logging.Formatter(fmt, datefmt)

import logging
fmt = "%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s: %(message)s"
datefmt="%Y-%m-%d %H:%M:%S"
logFormatter = logging.Formatter(fmt, datefmt)

4. 使用示例

该部分主要展示如何使用logging模块记录日志信息的使用方法。

默认的输出格式如下:
在这里插入图片描述
示例:
在这里插入图片描述
logging模块如何使用呢?下面介绍几种常用的使用方式:

一是使用logging的对外函数接口;二是使用basicConfig()方法,该方法能够满足基本的使用需求;三是通过创建Logger对象,可以进行更加复杂的处理。

4.1 直接调用logging接口

最简单的使用方法,就是直接调用logging标准库的接口,如logging.debug()logging.info()等函数。
默认的日志级别是WARNING,当等级大于等于WARNING才被跟踪。

内部默认调用的是root.log(level, mas)函数

接口函数如下:

  • debug()
  • info()
  • warning()
  • error()
  • critical()

也可以直接使用同一接口logging.log(),输入级别及具体信息。

  • log(level, msg)level表示日志级别,输入日志级别对应的整数或代码,如下:
    CRITICAL = 50
    FATAL = CRITICAL
    ERROR = 40
    WARNING = 30
    WARN = WARNING
    INFO = 20
    DEBUG = 10
    NOTSET = 0
    

这些接口函数内部实际调用的都是Logger类的对应函数。如果logger没有handler时,可默认调用basicConfig()添加控制台handler
如,logging.info()函数内部调用root.info()函数,如下

def info(msg, *args, **kwargs):
    """
    Log a message with severity 'INFO' on the root logger. If the logger has
    no handlers, call basicConfig() to add a console handler with a pre-defined
    format.
    """
    if len(root.handlers) == 0:
        basicConfig()
    root.info(msg, *args, **kwargs)

示例如下:

import logging
logging.warning('this is a warning info.')
logging.error('this is a error info.')
logging.debug('this is a debuginfo.')
# 输出
WARNING:root:this is a warning info.
ERROR:root:this is a error info.

logging.log(20, 'this is a info msg.')
logging.log(30, 'this is a warn msg.')
# 输出:
WARNING:root:this is a warn msg.

记录具体异常信息

当发生异常时,直接使用无参数的debug(), info(), warning(), error(), critical()方法并不能记录异常信息。

我们看一下各个接口的定义及参数:

# 普通接口
def warning(msg, *args, **kwargs)
# 内部调用如下函数:
self._log(WARNING, msg, args, **kwargs)

# 统一接口
def log(level, msg, *args, **kwargs)
# 内部调用如下函数:
self._log(level, msg, args, **kwargs)

# exception函数,参数中默认exc_info为True
def exception(msg, *args, exc_info=True, **kwargs):
   error(msg, *args, exc_info=exc_info, **kwargs)
# 内部调用如下函数:
self._log(ERROR, msg, args, **kwargs)

由上面代码可见,其内部都是调用函数Logger._log(level, msg, args, **kwargs)。不同的是参数,如level日志级别。
函数Logger._log()的定义如下:

def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False)

可见,其除了levelmsg以外,还有参数exc_info。该参数用来控制是否打印具体信息。

函数exception()就等于error(msg, exc_info=True)

因此:
记录异常信息的方法

  • 设置exc_info参数为True,指示是否打印执行信息
  • 或者使用exception()方法,同时记录当前的堆栈追踪(仅从异常处理程度调用此方法)
  • 还可以使用log()方法,但是要设置日志级别和exce_info参数。

logger.log(level, msg, exc_info=True)是各个接口函数的统一调用方式。

示例代码:

import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
a = 5
b = 0
try:
    c = a / b
except Exception as e:
    # 下面三种方式三选一,推荐使用第一种
    logging.exception("Exception occurred")
    logging.error("Exception occurred", exc_info=True)
    logging.log(level=logging.DEBUG, msg="Exception occurred", exc_info=True)

输出文件test.log的内容:
在这里插入图片描述

4.2 使用basicConfig()函数

使用basicConfig()方法就能够满足基本的使用需要,如果方法没有传入参数,会根据默认的配置创建Logger对象,默认的日志级别被设置为 WARNING
默认的日志输出格式:

WARNINGrootmessage
日志级别Logger实例日志信息

函数定义:
def basicConfig(**kwargs)

该函数可选的参数如下表所示:

参数名称参数描述
filename日志输出到文件的文件名
filemode文件模式,r[+]w[+]a[+]
format日志输出的格式
datefmt日志附带日期时间的格式
style格式占位符,默认为“%”“{}”
level设置日志输出级别
stream定义输出流,用来初始化 StreamHandler对象,不能和 filename参数一起使用,否则会ValueError异常
handles定义处理器,用来创建Handler对象,不能和filename,stream参数一起使用,否则也会抛出ValueError异常
  • stream输出流不与filename输出文件一起使用
  • handler处理器不与stream输出流或filename输出文件一起用
  • streamfilenamehandler互相排斥
  • 若想将日志信息同时输出到文件和控制台,则需要使用Logger处理器增加对应的处理方法。

basicConfig()方法可以实现将日志信息输出到文件中或者输出到定义的输出流中。

  • 输出到文件中,可以通过filename参数,或者通过handler处理器创建相应文件对象实现
    • 内部创建FileHandle()对象
  • 打印到输出流中,使用默认参数即可
    • 内部创建StreamHandler()对象

其中,format指定输出的格式和内容,format可以输出很多有用信息,具体格式见Formatter类

basicConfig()的调用应该在 debug()info() 等的前面。因为它被设计为一次性的配置,只有第一次调用会进行操作,随后的调用不会产生有效操作。

该方法相当于显示调用basicConfig()进行参数的配置,而直接调用logging.info()接口函数,若没有handler时,是隐式调用basicConfig()的默认参数形式。

1)日志信息打印到输出流

import logging
# 直接调用basicConfig()函数,使用默认参数
logging.basicConfig()
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

# 输出:(默认级别是WARNING)
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

2)格式化输出

格式化输出时间:
通过datefmt参数可以格式化输出log的时间。

常用的输出格式如下:
format = '%(asctime)s - %(filename)s[line:%(lineno)d]- %(levelname)s: %(message)s'
该格式可以输出日志的打印时间,输出模块及行号,日志级别和输出的日志内容。

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt = '%Y-%m-%d %I:%M:%S %p')
logging.warning('is when this event was occurred.')

# 输出结果
2019-10-15 04:03:17 PM is when this event was occurred.

3) 将日志输出到指定文件
指定filenamefilemode参数,可将日志内容输出到指定文件,示例代码如下:

import logging

logging.basicConfig(filename='test.log', filemode='w', format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')

生成的日志文件test.log,内容如下:
在这里插入图片描述

4.3 一步步配置Logger

basicConfig函数提供了一种较为简便的使用日志的方式,如果想要获取更加复杂的使用,则需要自己一步步配置来实现。

配置逻辑:

  • 一个logger对象可以添加多个handler对象,通过addHandler()函数
  • 每个handler对象可以有一个Formatter对象来指定格式,通过setFormatter()函数
  • handlerlogger对象都需要设置一个日志等级,通过setLevel()函数
  • 根据需要loggerhandler对象可以添加多个Filter对象,通过addFilter()函数

示例:

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)

handler = logging.FileHandler('output.log', mode='a', encoding=None, delay=False)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s',
								datefmt='%Y-%m-%d %H:%M:%S',
								style='%')
handler.setFormatter(formatter)
handler.setLevel(logging.DEBUG)
handler.set_name('output-log')

logger.addHandler(handler)

a=0
try:
    res = 100 / a
except:
    logger.error('Divisor is equal to 0.', exc_info=True)

将日志同时输出到屏幕和文件

import logging
logger = logging.getLogger(__name__)
logger.setLevel(level = logging.DEBUG)
handler = logging.FileHandler("log.txt")
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(message)s')
handler.setFormatter(formatter)

console = logging.StreamHandler()
console.setLevel(logging.INFO)

logger.addHandler(handler)
logger.addHandler(console)

logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")

log.txt文件和控制台均可看到输出,控制台的输出,其中文件中内容(有格式)如下:
在这里插入图片描述
通过这段代码可以看出,logging有一个日志处理的主对象(logger=logging.getLogger()),其他的处理方法均是通过addHandler添加进去。

4.4 通过dictConfig配置

通过dictConfig配置方法是通过python代码构建一个dict对象,也可以通过JSONyaml文件来进行配置。
这种方式使用起来更加灵活,强大。我们可以把很多的数据转换成自字典形式,然后将他们填充到一个配置字典中。

具体如何转换可以参考logging.config.py文件。

4.4.1 dictConfig()

该函数可以从一个字典对象中获取日志配置信息,config参数就是这个字典对象。

定义如下:

def dictConfig(config):
    """Configure logging using a dictionary."""
    dictConfigClass(config).configure()

如使用YAML格式的文件类配置(配置内容见下方示例),则使用示例:

import logging
import logging.config
import yaml

with open('logging.yml', 'r') as f_config:
	dict_cfg = yaml.load(f_config)
logging.config.dictConfig(dict_cfg)

logger = logging.getLogger('console')
logger.debug('debug msg')
4.4.2 配置规则
key名称描述
version必选项,整数值,表示配置格式版本,当前唯一可用值是1
formatters可选项,其值是字典对象,该字典对象每个元素的key为要定义的格式器名称,value为格式器的配置信息组成的dict。一般会配置format,用于指定输出字符串的格式,datefmt用于指定输出的时间字符串格式,默认为%Y-%m-%d %H:%M:%S
filters可选项,其值是字典对象,该字典对象每个元素的key为要定义的过滤器名称,value为过滤器的配置信息组成的dict
handlers可选项,其值是字典对象,该字典对象每个元素的key为要定义的处理器名称,value为处理器的配置信息组成的dict。如class(必选项), formatter, filters。其他配置信息将会传递给class所指定的处理器类的构造函数。如使用logging.handlers.RotatingFileHandler,使用maxBytes, backupCount等参数。
loggers可选项,其值是字典对象,该字典对象每个元素的key为要定义的日志器名称,value为日志器的配置信息组成的dict。如level, handler, filter
root可选项,这是root logger的配置项,其值是字典对象。除非在定义其它logger时明确指定propagate值为no,否则root logger定义的handlers都会被作用到其它logger上。
incremental可选项,默认值为False。该选项的意义在于,如果这里定义的对象已经存在,那么这里对这些对象的定义是否应用到已存在的对象上。值为False表示,已存在的对象将会被重新定义。

具体配置结构如下所示:

  • version: 必要参数,必须为1
  • incremental:是否将此配置文件解释为现有配置的增量, 默认为False
  • disable_existing_loggers:是否要禁用现有的非 root logger,默认为True
  • handlers:字典形式
    • class:必须有,所使用的handler类,如logging.handlers.RotatingFileHandler
    • level:(可选),handler的等级
    • formatter:(可选),当前handlerformatterid
    • filters:(可选),当前handlerfiltersid列表
  • loggers:字典形式
    • level:(可选),logger的等级
    • propagate:(可选):默认值为1,表示将消息传递给父loggerhandler进行处理。
    • filters:(可选)
    • handlers:(可选) ,当前loggerhandlersid列表
    • qualname:代码中使用logging.getLogger()时,读取的就是qualname这个选项。
  • root:这是root logger的配置项
    • level:日志等级
    • handlers :当前root loggerhandlersid列表
4.4.3 配置示例
version:1
root:
	level:DEBUG
	handlers:[filehandler, ]
loggers:
	console:
		level:WARNING
		handlers:[consolehandler,]
		propagate:1
handlers:
	filehandler:
		class:logging.FileHandler
		filename:logs/sys.log
		level:WARNING
		formatter:fileformatter
	consolehandler:
		class:logging.StreamHandler
		stream:ext://sys.stdout
		level:DEBUG
		formatter:consoleformatter
formatters:
	fileformatter:
		format:'%(asctime)s [%(name)s][%(levelname)s]:%(message)s'
	consoleformatter:
		format:'%(asctime)s[%(levelname)s]:%(message)s'

外部对象访问:外部对象是指不能直接进行访问,需要import导入的对象,这时候需要加一个ext://前缀以便识别,然后系统就会import这个前缀后面的内容。

参考资料:
Python日志库logging总结-可能是目前为止将logging库总结的最好的一篇文章
python基础学习十 logging模块详细使用【转载】 - louis_w - 博客园
Python之路(第十七篇)logging模块 - Nicholas- - 博客园

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值