Python 标准库之 shutil 高阶文件操作『详细』

在这里插入图片描述

Python标准库之 shutil 高阶文件操作『详细』


一、Python shutil介绍🤪

shutil 模块提供了一系列对文件和文件集合的高阶操作,特别是提供了一些支持文件拷贝和删除的函数主要功能有对文件、文件夹的 复制、删除、移动操作,以及压缩包的处理模块

本文章内容较多,如果需要查找某个特定的方法或属性,建议使用浏览器的 查找 ctrl + f 功能


二、导入 shutil 库

在看下列内容并实际操作前,别忘记导入shutil 标准库

import shutil

三、对文件的操作👺

1)、shutil.copyfileobj(fsrc, fdst[, length]):

将文件类对象 fsrc 的内容拷贝到文件类对象 fdst 中,整数值 length 如果给出则设为缓冲区大小,特别的,length为负值事表示拷贝数据时不对源数据进行分块循环处理;默认情况下会分块读取数据以避免不受控制的内存消耗。

演示代码:👇

# 复制文件A中的全部内容到文件B
with open(file_A, "r", encoding="utf-8") as f_r, open(file_B, "w", encoding="utf-8") as f_w:
    shutil.copyfileobj(f_r, f_w)

如果 fsrc 对象的当前指针位置不为0,则会当前文件指针位置到文件末尾的内容会被拷贝

with open(file_A, "r", encoding="utf-8") as f_r, open(file_B, "w", encoding="utf-8") as f_w:
    # 将文件A中光标指向3,即第二个字符为开头
    # 注意对于UTF-8编码,每三间隔为一个字符,不为三的倍数将会抛出异常
    f_r.seek(3)
    shutil.copyfileobj(f_r, f_w)

2)、shutil.copyfile(src, dst, *, follow_symlinks=True):「常用」

将名为 src 的文件的内容(不包括元数据)拷贝到名为 dst 的文件并以尽可能高效的方式返回 dst(即目标文件路径)

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件
  • dst: 路径对象或以字符串形式给出的路径名目标文件
  • follow_symlinks:follow(遵循),symlinks(符号链接),如果 follow_symlinks 为假值且 src 为符号链接,则将创建一个新的符号链接而不是拷贝 src 所指向的文件

如果 src 和 dst 指定了同一个文件,则将引发 SameFileError
目标位置必须是可写的,否则将引发 OSError 异常

演示代码:👇

不同于shutil.copyfileobj,此方法无需目标文件存在,会自动创建生成一个新的文件如果目标文件已经存在,它将被替换

shutil.copyfile(file_A, file_B)

3)、shutil.copymode(src, dst, *, follow_symlinks=True):

从 src 拷贝权限位到 dst,文件的内容、所有者和分组将不受影响

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件
  • dst: 路径对象或以字符串形式给出的路径名目标文件
  • follow_symlinks:follow(遵循),symlinks(符号链接),如果 follow_symlinks 为假值,并且 src 和 dst 均为符号链接,copymode() 将尝试修改 dst 本身的模式(而非它所指向的文件), 此功能并不是在所有平台上均可用

演示代码:👇

shutil.copymode(file_A, file_B)

4)、shutil.copystat(src, dst, *, follow_symlinks=True):

从 src 拷贝状态信息,文件的内容、所有者和分组将不受影响,如:权限位(mode)、最近访问时间(atime)、最近修改时间(mtime)以及旗标(flags)到 dst。 在 Linux上,copystat() 还会在可能的情况下拷贝“扩展属性”。

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件
  • dst: 路径对象或以字符串形式给出的路径名目标文件
  • follow_symlinks:follow(遵循),symlinks(符号链接),如果 follow_symlinks 为假值,并且 src 和 dst 均指向符号链接,copystat() 将作用于符号链接本身而非该符号链接所指向的文件 — 从 src 符号链接读取信息,并将信息写入 dst 符号链接。

演示代码:👇

shutil.copystat(file_A, file_B)

5)、shutil.copy(src, dst, *, follow_symlinks=True):「常用」

将文件 src 内容和权限 拷贝到文件或目录 dst,并返回目标文件所对应的路径。

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件
  • dst: 路径对象或以字符串形式给出的路径名目标文件
  • follow_symlinks:follow(遵循),symlinks(符号链接),如果 follow_symlinks 为假值且 src 为符号链接,则 dst 也将被创建为符号链接。如果 follow_symlinks 为真值且 src 为符号链接,dst 将成为 src 所指向的文件的一个副本。

源码:

源码其实很简单,其实就是同时调用了shutil.copyfilecopymode,可以把shutil.copy 理解成这两个shutil.copyfilecopymode的结合

def copy(src, dst, *, follow_symlinks=True):
    """Copy data and mode bits ("cp src dst"). Return the file's destination.

    The destination may be a directory.

    If follow_symlinks is false, symlinks won't be followed. This
    resembles GNU's "cp -P src dst".

    If source and destination are the same file, a SameFileError will be
    raised.

    """
    if os.path.isdir(dst):
        dst = os.path.join(dst, os.path.basename(src))
    copyfile(src, dst, follow_symlinks=follow_symlinks)
    copymode(src, dst, follow_symlinks=follow_symlinks)
    return dst

演示代码:👇

shutil.copy(file_A, file_B)

6)、shutil.copy2(src, dst, *, follow_symlinks=True):「常用」

类似于 shutil.copy,区别在于 shutil.copy2拷贝文件内容时还会拷贝文件状态的信息

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件
  • dst: 路径对象或以字符串形式给出的路径名目标文件
  • follow_symlinks:follow(遵循),symlinks(符号链接),当 follow_symlinks 为假值且 src 为符号链接时,copy2() 会尝试将来自 src 符号链接的所有元数据拷贝到新创建的 dst 符号链接。

源码:

shutil.copy是一个套路,就是同时调用了shutil.copyfileshutil.copystat

def copy2(src, dst, *, follow_symlinks=True):
    """Copy data and metadata. Return the file's destination.

    Metadata is copied with copystat(). Please see the copystat function
    for more information.

    The destination may be a directory.

    If follow_symlinks is false, symlinks won't be followed. This
    resembles GNU's "cp -P src dst".
    """
    if os.path.isdir(dst):
        dst = os.path.join(dst, os.path.basename(src))
    copyfile(src, dst, follow_symlinks=follow_symlinks)
    copystat(src, dst, follow_symlinks=follow_symlinks)
    return dst

演示代码:👇

shutil.copy2(file_A, file_B)

四、对目录的操作🐔

1)、shutil.ignore_patterns(*patterns):

这个函数为shutil.copytree辅助函数,作用于 shutil.copytreeignore 可调用对象参数,以 glob 风格忽略 shutil.copytree 中的文件和目录,具体使用请移步到 2)、shutil.copytree

glob介绍:

glob 类似于正则表达式,但与正则表达式又不完全相同,可以简单的将其理解成一个简化的正则,glob非常强大的用途在于路径匹配,另外每个平台和开发语言所支持glob路径匹配可能不完全一样

  • *: 匹配零个或多个字符,不匹配以 . 开头,如 .abcd
  • ?: 匹配一个字符
  • [ ]: 匹配一系列的字符,这与正则一样,如[abcd]可以匹配abcd中的任意一个字符
  • [^ ] 和 [! ]: 匹配除这一系列之外的字符,这与正则一样,如[^abcd]可以匹配除了abcd之外的任意一个字符
  • {a, b}: 匹配a或b,可以与其他通配符组合使用
  • !: 排除,如!test.py 可以排除掉 test.py 文件

演示代码:👇

# 比较常用的是忽略掉以.pyc结尾以及tmp开头的文件
shutil.ignore_patterns('*.pyc', 'tmp*')
# 忽略掉以.log和.tmp结尾的文件
shutil.ignore_patterns('*.log', '*.tmp')

2)、shutil.copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False, dirs_exist_ok=False):「常用」

将src整个目录树拷贝到dst目录,会自动创建dst目录文件夹并返回目标目录的路径,目录的权限和时间会通过 shutil.copystat 来拷贝,单个文件则会使用 shutil.copy2 来拷贝

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件
  • dst: 路径对象或以字符串形式给出的路径名目标文件
  • symlinks:如果 symlinks 为真值,源目录树中的符号链接会在新目录树中表示为符号链接,并且原链接的元数据在平台允许的情况下也会被拷贝;如果为假值或省略,则会将被链接文件的内容和元数据拷贝到新目录树。当 symlinks 为假值时,如果符号链接所指向的文件不存在,则会在拷贝进程的末尾将一个异常添加到 Error 异常中的错误列表。 如果你希望屏蔽此异常那就将可选的 ignore_dangling_symlinks 旗标设为真值。 请注意此选项在不支持 os.symlink() 的平台上将不起作用。
  • ignore: 必须是一个可调用对象,配合 shutil.ignore_patterns 创建这种基于 glob 风格模式来忽略特定名称的可调用对象
  • copy_function: copy_function 参数以允许提供定制的拷贝函数,必须是一个将被用来拷贝每个文件的可调用对象,默认情况下使用shutil.copy2,即拷贝文件内容时还会拷贝文件状态的信息
  • ignore_dangling_symlinks:在symlinks 为假值时是否屏蔽符号链接错误
  • dirs_exist_ok:是否要在 dst 或任何丢失的父目录已存在的情况下引发异常

目标目录不能存在,否则会抛出FileExistsError异常

演示代码:👇

比较常用的调用方法

# 将文件夹A拷贝到文件夹B
shutil.copytree(folder_A, folder_B)
# 将文件夹A拷贝到文件夹B, 在此过程中忽略掉以.log和.tmp结尾的文件
shutil.copytree(folder_A, folder_B, ignore=shutil.ignore_patterns('*.log', '*.tmp'))

再结合copy_function参数,将原来的 shutil.copy2(拷贝内容和文件状态的信息) 更改为 shutil.copy(拷贝内容和权限)

# 将文件夹A拷贝到文件夹B, 拷贝文件的内容和权限
shutil.copytree(folder_A, folder_B, copy_function=shutil.copy)

在上诉的参数说明中提到过,copy_function 参数以允许提供定制的拷贝函数,没错我们可以使用自己自定义的函数对每个文件进行拷贝操作,如下方代码所示,结合了 shutil.copyfileshutil.copystatshutil.copymode 对文件进行拷贝

def copy_mode(*args, **kwargs):
    shutil.copyfile(*args, **kwargs)
    shutil.copystat(*args, **kwargs)
    shutil.copymode(*args, **kwargs)


# 将文件夹A拷贝到文件夹B, 并以自己自定义的拷贝文件方式拷贝
shutil.copytree(folder_A, folder_B, copy_function=copy_mode)

注意在使用copy_function参数时,应传入函数本身而不是加上括号调用函数

3)、shutil.rmtree(path, ignore_errors=False, οnerrοr=None):「常用」

删除一个完整的目录树,此方法会经常用到

需要注意的是,文件夹分两种,一种是空文件夹,另外一种则是文件夹中有内容的非空文件夹,对于空文件夹来说,可以使用os标准模块下的os.rmdir将其删除,但如果对非空文件夹删除会出现 OSError 错误,对于删除非空文件夹,一种方法是使用os模块下的os.walk将文件与文件夹分别遍历删除,另外一种方法就是使用现所讲的shutil.rmtree

详情请移步到我所写的文章 《Python 以优雅的姿势 操作文件》 下的删除文件夹

参数如下:

  • path: 必须指向一个目录,但不能是一个目录的符号链接
  • ignore_errors:如果 ignore_errors 为True,删除失败导致的错误将被忽略;默认为False,此类错误将通过调用由 onerror 所指定的处理程序来处理,或者如果此参数不传入则将引发一个异常。
  • onerror:只接受三个形参的可调用对象: function, path 和 excinfo
    • a. 第一个形参 function 是引发异常的函数,它依赖于具体的平台和实现
    • b. 第二个形参 path 将是传递给 function 的路径名
    • c. 第三个形参 excinfo 将是由 sys.exc_info() 所返回的异常信息,由 onerror 所引发的异常将不会被捕获

演示代码:👇

可以配合os标准库模块下的os.path.isdir判断是否为文件夹再进行删除

# 判断路径是否为文件夹
if os.path.isdir(path):
    # 删除一个完整的目录树
    shutil.rmtree(path)

利用 ignore_errors 以及 onerror 参数,将遇到删除失败时的异常变成自定义的报错

def myerr(*args, **kwargs):
    raise Exception("删除文件夹时发生错误")


shutil.rmtree("不存在的路径", ignore_errors=False, onerror=myerr)

4)、shutil.move(src, dst, copy_function=copy2):「常用」
递归地将一个文件或目录 (src) 移至另一位置 (dst) 并返回目标位置路径

参数如下:

  • src: 路径对象或以字符串形式给出的路径名原文件或文件夹
  • dst: 路径对象或以字符串形式给出的路径名目标地址
  • copy_function:copy_function 参数以允许提供定制的拷贝函数,默认为shutil.copy2,这与 shutil.copytree 中的copy_function参数类似

源码:

来看看源码,其实这个移动原理很简单,其实就是先将文件或文件夹拷贝到目标目录,再将原目录进行删除操作,注意看倒数第五、第六行

def move(src, dst, copy_function=copy2):
    """Recursively move a file or directory to another location. This is
    similar to the Unix "mv" command. Return the file or directory's
    destination.

    If the destination is a directory or a symlink to a directory, the source
    is moved inside the directory. The destination path must not already
    exist.

    If the destination already exists but is not a directory, it may be
    overwritten depending on os.rename() semantics.

    If the destination is on our current filesystem, then rename() is used.
    Otherwise, src is copied to the destination and then removed. Symlinks are
    recreated under the new name if os.rename() fails because of cross
    filesystem renames.

    The optional `copy_function` argument is a callable that will be used
    to copy the source or it will be delegated to `copytree`.
    By default, copy2() is used, but any function that supports the same
    signature (like copy()) can be used.

    A lot more could be done here...  A look at a mv.c shows a lot of
    the issues this implementation glosses over.

    """
    sys.audit("shutil.move", src, dst)
    real_dst = dst
    if os.path.isdir(dst):
        if _samefile(src, dst):
            # We might be on a case insensitive filesystem,
            # perform the rename anyway.
            os.rename(src, dst)
            return

        real_dst = os.path.join(dst, _basename(src))
        if os.path.exists(real_dst):
            raise Error("Destination path '%s' already exists" % real_dst)
    try:
        os.rename(src, real_dst)
    except OSError:
        if os.path.islink(src):
            linkto = os.readlink(src)
            os.symlink(linkto, real_dst)
            os.unlink(src)
        elif os.path.isdir(src):
            if _destinsrc(src, dst):
                raise Error("Cannot move a directory '%s' into itself"
                            " '%s'." % (src, dst))
            if (_is_immutable(src)
                    or (not os.access(src, os.W_OK) and os.listdir(src)
                        and sys.platform == 'darwin')):
                raise PermissionError("Cannot move the non-empty directory "
                                      "'%s': Lacking write permission to '%s'."
                                      % (src, src))
            copytree(src, real_dst, copy_function=copy_function,
                     symlinks=True)
            rmtree(src)
        else:
            copy_function(src, real_dst)
            os.unlink(src)
    return real_dst

演示代码:👇

# 将文件夹A移动到文件夹B目标路径
shutil.move(folder_A, folder_B)

5)、shutil.disk_usage(path)

path 可以是一个文件或是一个目录,返回一个具名元组表示给定路径的磁盘使用统计数据,其中包含 total, used 和 free 属性,分别表示总计、已使用和未使用空间的字节数

演示代码:👇

print(shutil.disk_usage(folder_A))
usage(total=1000186310656, used=874484068352, free=125702242304)

五、归档功能「压缩包」🚚

shutil标准库模块提供了用于 创建和读取压缩归档文件 的高层级工具,但其实这些功能依赖于 zipfiletarfile 模块,换句话来说就等同于在使用这两个标准库模块

1)、shutil.make_archive(base_name, format[, root_dir[, base_dir[, verbose[, dry_run[, owner[, group[, logger]]]]]]])

创建一个归档文件「压缩包」,例如 zip 或 tar,并返回其归档(压缩包)的文件路径

参数如下:

  • base_name: 压缩包文件的名称,也可以是路径,在只是文件名的情况下会在当前路径下创建只需文件名即可,不需要书写扩展名,如果书写列如test.zip将会再添加一次扩展名,输出的压缩包文件名为test.zip.zip
  • format: 压缩包类型(归档格式),支持的类型有:zip,tar,gztar,bztar,xztar
  • root_dir: 要压缩的根路径,默认为当前目录
  • base_dir: base_dir 必须相对于 root_dir 给出,简单来说就是对root_dir进一步进行划分,要从root_dir的哪一个位置下进行压缩,默认为当前目录即root_dir目录
  • dry_run:如果 dry_run 为True,则不会创建归档文件,但将要被执行的操作会被记录到 logger
  • owner:在创建 tar 归档文件时被使用,指定用户,默认为当前用户
  • group:在创建 tar 归档文件时被使用,指定用户组,默认为当前组
  • logger: 用于记录日志,通常为 logging.Logger 的实例

演示代码:👇

基本用法

zip_path = shutil.make_archive(
    # 压缩包文件名, 无后缀
    "test1",
    # 使用zip格式压缩包
    "zip",
    # 被压缩的路径
    root_dir="./test/1.txt",
)

配合 base_dir 参数使用

zip_path = shutil.make_archive(
    # 压缩包文件名, 无后缀
    "test2",
    # 使用zip格式压缩包
    "zip",
    # 被压缩的路径
    root_dir="./test",
    # 在root_dir的基础上,其路径下的文件或文件夹
    base_dir="文件夹一"
)

2)、shutil.unpack_archive(filename[, extract_dir[, format]])

解包一个归档文件「压缩包」

参数如下:

  • filename: 压缩包的路径
  • extract_dir: 归档文件解包的目标目录名称,默认为当前工作目录
  • format: 归档格式,应为 zip,tar,gztar,bztar,xztar 之一,或者任何通过 shutil.register_unpack_format注册的其他格式,如未提供将使用压缩包的扩展名来检查是否注册了对应于该扩展名的解包器

如果文件名或文件夹名为中文,会出现乱码情况,估计内部使用的是ASCII编码,建议目录均以英文数字命名
在这里插入图片描述

在未找到任何解包器的情况下,将引发 ValueError

演示代码:👇

shutil.unpack_archive(
    "test.zip",
    "./目标文件夹",
    format="zip"
)

3)、shutil.register_archive_format(name, function[, extra_args[, description]])

name 格式注册一个归档器

  • function: 是将被用来解包归档文件的可调用对象。 该可调用对象将接收要创建文件的 base_name,再加上要归档内容的 base_dir (其默认值为 os.curdir)。 更多参数会被作为关键字参数传入: owner, group, dry_run 和 logger (与向 make_archive() 传入的参数一致)。
  • extra_args:如果给出了 extra_args,则其应为一个 (name, value) 对的序列,将在归档器可调用对象被使用时作为附加的关键字参数。
  • description 由 get_archive_formats() 使用,它将返回归档器的列表。 默认值为一个空字符串。

4)、shutil.register_unpack_format(name, extensions, function[, extra_args[, description]])

注册一个解包格式

  • name: 为压缩包格式名称
  • extensions: 为对应于该格式的扩展名列表,例如 Zip 文件的扩展名为 .zip。
  • function: 是将被用来解包归档文件的可调用对象。 该可调用对象将接受归档文件的路径,加上该归档文件要被解包的目标目录。
  • extra_args:如果提供了 extra_args,则其应为一个 (name, value) 元组的序列,将被作为关键字参数传递给该可调用对象。
  • description:可以提供 description 来描述该格式,它将被 get_unpack_formats() 返回

5)、shutil.unregister_archive_format(name)

从支持的格式中移除归档格式
name为格式的名称

6)、shutil.unregister_unpack_format(name)

撤销注册的一个解包格式
name 为格式的名称

7)、shutil.get_archive_formats()

返回支持的归档格式列表
所返回序列中的每个元素为一个元组(名字,描述)「name, description」

默认情况下 shutil 提供以下格式:

  • zip: ZIP 文件(如果 zlib 模块可用)
  • tar: 未压缩的 tar 文件,对于新归档文件将使用 POSIX.1-2001 pax 格式
  • gztar: gzip 压缩的 tar 文件(如果 zlib 模块可用)
  • bztar: bzip2 压缩的 tar 文件(如果 bz2 模块可用)
  • xztar: xz 压缩的 tar 文件(如果 lzma 模块可用)

演示代码:👇

print(shutil.get_archive_formats())
[('bztar', "bzip2'ed tar-file"), ('gztar', "gzip'ed tar-file"), ('tar', 'uncompressed tar file'), ('xztar', "xz'ed tar-file"), ('zip', 'ZIP file')]

8)、shutil.get_unpack_formats()

返回所有已注册的解包格式列表。
所返回序列中的每个元素为一个元组(名字,扩展,描述)「name, extensions, description」

默认情况下 shutil 提供以下格式:

  • zip: ZIP 文件(只有在相应模块可用时才能解包压缩文件)。
  • tar: 未压缩的 tar 文件
  • gztar: gzip 压缩的 tar 文件(如果 zlib 模块可用)。
  • bztar: bzip2 压缩的 tar 文件(如果 bz2 模块可用)。

演示代码:👇

print(shutil.get_unpack_formats())
[('bztar', ['.tar.bz2', '.tbz2'], "bzip2'ed tar-file"), ('gztar', ['.tar.gz', '.tgz'], "gzip'ed tar-file"), ('tar', ['.tar'], 'uncompressed tar file'), ('xztar', ['.tar.xz', '.txz'], "xz'ed tar-file"), ('zip', ['.zip'], 'ZIP file')]

六、查询输出终端的尺寸

1)、shutil.get_terminal_size(fallback=(columns, lines)):

获取终端窗口的尺寸

会分别检查环境变量 columnslines 两个维度。如果定义了这些变量并且其值为正整数,则将使用这些值。默认情况下未定义 columnslines,则连接到 sys.__stdout__ 的终端将通过发起调用os.get_terminal_size() 被查询。

如果由于系统不支持查询,或是由于我们未连接到某个终端而导致查询终端尺寸不成功,则会使用在 fallback 形参中给出的值,fallback 默认为 (80, 24),这是许多终端模拟器所使用的默认尺寸。

源码:

因为本函数是涉及到os标准库模块中的功能,且实现代码比较简单,我认为还是有必要将源码放出来看看的

def get_terminal_size(fallback=(80, 24)):
    """Get the size of the terminal window.

    For each of the two dimensions, the environment variable, COLUMNS
    and LINES respectively, is checked. If the variable is defined and
    the value is a positive integer, it is used.

    When COLUMNS or LINES is not defined, which is the common case,
    the terminal connected to sys.__stdout__ is queried
    by invoking os.get_terminal_size.

    If the terminal size cannot be successfully queried, either because
    the system doesn't support querying, or because we are not
    connected to a terminal, the value given in fallback parameter
    is used. Fallback defaults to (80, 24) which is the default
    size used by many terminal emulators.

    The value returned is a named tuple of type os.terminal_size.
    """
    # columns, lines are the working values
    try:
        columns = int(os.environ['COLUMNS'])
    except (KeyError, ValueError):
        columns = 0

    try:
        lines = int(os.environ['LINES'])
    except (KeyError, ValueError):
        lines = 0

    # only query if necessary
    if columns <= 0 or lines <= 0:
        try:
            size = os.get_terminal_size(sys.__stdout__.fileno())
        except (AttributeError, ValueError, OSError):
            # stdout is None, closed, detached, or not a terminal, or
            # os.get_terminal_size() is unsupported
            size = os.terminal_size(fallback)
        if columns <= 0:
            columns = size.columns
        if lines <= 0:
            lines = size.lines

    return os.terminal_size((columns, lines))

演示代码:👇

返回的值是一个 os.terminal_size 类型的具名元组。

print(shutil.get_terminal_size())
os.terminal_size(columns=80, lines=24)

扩展:将shutil支持7z格式压缩包

待更新,敬请期待

在这里插入代码片

参考资料💌

由衷感谢💖


相关博客😏

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值