八、python的模块和包

模块

概念

Python 模块(Module),其本质是一个 Python 文件,以 .py 结尾,包含了 Python 对象定义和Python语句。

模块的作用

模块能定义函数,类和变量,模块里也能包含可执行的代码。在其他python程序中可以导入模块,使用模块中的函数或类等,避免代码的重复编写,也加强代码的逻辑结构.

导入整个模块

工作流程
1、先从sys.modules()中寻找模块是否已经导入;如果没有就在sys.path中寻找模块;
注意

import sys
sys.path.append(os.path.dirname(os.getcmd()))
#将本程序目录添加为sys.path,便于后面导入自己编写的模块文件

2、找到模块,创建模块的专属内存空间,将模块中的代码读到专属内存空间(与程序的内存空间独立);
3、在新创建的命名空间中执行模块中包含的代码,模块中的代码只在第一次遇到导入import语句时才执行.

导入格式

  • import 文件名字(不含.py)
  • import 文件名字(不含.py) as 别名
    将模块重命名,方便调用较长的模块名

调用模块中函数(变量)
模块名.函数名(变量名)
time.time()
函数名(变量名)不会和本身程序文件的相同函数名(变量名)冲突,属于两个独立内存空间

导入顺序
1、内置模块 2、扩展模块 3、自定义模块

一个模块只会被导入一次,不管你执行了多少次import。这样可以防止导入模块被一遍又一遍地执行。
多个模块的import 建议分多行

导入模块中的函数(变量)

格式
from 模块名 import 目标名字
from 模块名 import *

对比import 模块名,会将模块文件的名称空间带到当前名称空间中,使用时必须是模块名.名字的方式.而from 语句相当于import,也会创建新的名称空间,但是将模块中的名字直接导入到当前的名称空间中,在当前名称空间中,直接使用名字就可以了

在模块中__all__是一个列表,如果没有该列表,则import *时会导入模块的所有变量和函数,有则导入列表__all__中指定的。

扫描二维码关注公众号,回复: 9118675 查看本文章

调用模块中函数
可直接函数名 无需先加上模块名.
此种方式导入指定的名字,相对的更加节省内存空间,但导入的变量名或函数名会和本文件的冲突

特殊变量__name__

在py文件中有一个特殊变量__name__,当直接执行该py时,name__的值为__main;当该py文件被做模块引用时,__name__变的值为模块名(即py文件名)。

在.py文件中有部分代码A只有在文件自己执行而使用,被作为模块import时不执行这部分代码。那么就可以利用上面的特性__name__来实现;

if  __name__=__main__:
	部分代码A

常用模块

re模块

re模块是让python也具备正则表达式功能,先简单的介绍下正则表达式

字符组
字符可以是数字、字母、标点符号等。用[]将字符组成一个组,匹配组内的任一字符。
[0-9]:匹配任一个数字
[a-z]:匹配任一个小写字母
[0-9a-z]:匹配任一数字和小写字母

字符匹配
. 匹配除换行符外的任一单个字符
\w 匹配单个字母或数字或下划线
\s 匹配任意个数的空白符 包括tab
\d 匹配单个数字
\n 匹配一个换行符
\t 匹配一个制表符 tab

位置匹配
通常紧跟在匹配内容的后面,用于说明匹配内容出现的位置
\b 表示前面匹配项出现在单词结尾 g\b
^ 表示后面的匹配项出现在行首 ^a
$ 表示前面的出现在行尾 c$

数量匹配
通常紧跟在匹配内容的后面,用于说明匹配内容出现的次数
* 前面的匹配零次或更多次
+ 前面的匹配项至少出现一次
? 前面的匹配项出现0或1次
{n} 前面的匹配项出现n次
{n,} 前面的匹配项至少出现n次
{,n} 前面的匹配项至多出现n次

*,+,?等数量匹配使用时都是贪婪匹配(也就是尽可能多匹配后面内容),但如果在他们后面再加个?,就成了惰性匹配
举例

name='pksa1231b32b'
/a.*b/  贪婪匹配结果a1231b32b
/a.*?b/ 惰性匹配结果a1231b

特殊匹配
[……] 匹配中括号内的单个字符
[^……] 匹配非中括号内的单个字符

分组
(正则表达式)
将匹配中的内容列为分组供后面的表达式调用,\1表示第一个括号中的匹配内容

(\d)abc\1 1abc1符合此表达式 1abc2不符合表达式

特别注意
正则表达式使用 \ 对特殊字符进行转义,比如,为了匹配字符串 ‘python.org’,我们需要使用正则表达式 ‘python.org’,而 Python 的字符串本身也用 \ 转义,所以上面的正则表达式在 Python 中应该写成 ‘python\.org’,这会很容易陷入 \ 的困扰中,因此,我们建议使用 Python 的原始字符串,只需加一个 r 前缀,上面的正则表达式可以写成:r’python.org’

正则表达式-re模块

re 模块的一般使用步骤如下:

  1. 使用 compile 函数将正则表达式的字符串形式编译为一个 Pattern 对象
  2. 通过 Pattern对象提供的一系列方法对文本进行匹配查找,获得匹配结果(一个 Match 对象)
  3. 最后使用 Match对象提供的属性和方法获得信息,根据需要进行其他的操作

compile 函数用于编译正则表达式,生成一个 Pattern 对象,使用形式如下

re.compile(pattern[, flag])

举例

import re

# 将正则表达式编译成 Pattern 对象 
pattern1 = re.compile(r'\d+')

Pattern 对象的一些常用方法主要有:

  • match 方法
  • search 方法
  • findall 方法
  • finditer 方法
  • split 方法
  • sub 方法
  • subn 方法
    以上方法使用方式与re模块中的函数基本相近。

match方法
从指定的位置范围进行匹配,指定的开头必须能被匹配否则就是匹配失败,匹配到第一个结果就返回Match对象,使用格式如下

match(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,pos和endpos分别默认是0和len(string)

import re

pattern = re.compile(r'\d+')
m = pattern.match('aone123twothree34four') #从0位置开始,0位置是字符a匹配\d+失败
print(m)  #输出None
a= pattern.match('aone123twothree34four', 4, 10) #从4位置开始,4位置是数字1匹配成功,向后贪婪匹配得获得Match对象
print(a) #输出<re.Match object; span=(3, 6), match='123'>
print(a.group()) #group方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group() 或 group(0)。group(1)对应正则表达中第一个()

补充例子说明:

import re
pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I)   # re.I 表示忽略大小写
m = pattern.match('Hello World Wide Web')
print (m)     # 匹配成功,返回一个 Match 对象
<_sre.SRE_Match object at 0x10bea83e8>

>>> m.group(0)    # 返回匹配成功的整个子串
'Hello World'
>>> m.span(0)     # 返回匹配成功的整个子串的在匹配目标中的索引
(0, 11)
>>> m.group(1)    # 返回第一个分组匹配成功的子串
'Hello'
>>> m.span(1)     # 返回第一个分组匹配成功的子串的索引
(0, 5)
>>> m.group(2)    # 返回第二个分组匹配成功的子串
'World'
>>> m.span(2)     # 返回第二个分组匹配成功的子串
(6, 11)
>>> m.groups()    # 等价于 (m.group(1), m.group(2), ...)
('Hello', 'World')
>>> m.group(3)    # 不存在 输出失败

search方法
search 方法用于在指定位置内查找字符串的位置,它也是一次匹配,只要找到了第一个匹配的结果就返回为Match对象。匹配失败则返回None,使用格式如下

search(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,pos和endpos分别默认是0和len(string)

示例

import re

pattern = re.compile(r'\d+')
a = pattern.search('aone123twothree34four',7)
print(a)
print(a.group()) #输出34
print(a.span()) #输出(15,17)

findall方法
在指定位置内查找匹配结果,将所有匹配内容放入一个列表中返回。

findall(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,pos和endpos分别默认是0和len(string)

import re

pattern = re.compile(r'\d+')   # 查找数字
result1 = pattern.findall('hello 123456 789')
result2 = pattern.findall('one1two2three3four4', 0, 10)

print (result1)  #['123456', '789']
print (result2)  #['1', '2']

finditer 方法
功能和findall方法基本一样,只是匹配的获得为Match对象存放在一个迭代器内。

import re

pattern = re.compile(r'\d+')

result_iter1 = pattern.finditer('hello 123456 789')

print (type(result_iter1))
#123456和789两个Match对象组成了一个迭代器
print (result_iter1)
for m1 in result_iter1:   # m1 循环获取 Match 对象
    print ('matching string: {}, position: {}'.format(m1.group(), m1.span()))

split方法
按照能够匹配内容对字符串进行分割,放入列表中返回,使用格式

split(string[, maxsplit])

其中,maxsplit 用于指定最大分割次数,不指定将全部分割。

import re

p = re.compile(r'[\s\,\;]+') #能匹配的内容为, 多个空格  多个;
print (p.split('a,b;; c   d'))
#输出结果:['a', 'b', 'c', 'd']

sub方法
sub 方法用于替换。它的使用形式如下:

sub(repl, string[, count])

repl如果是字符串,使用 repl 去替换字符串中的匹配的子串(受count控制),并返回替换后的字符串,另外repl 还可以使用 \id 的形式来引用分组,但不能使用编号 0;
repl如果是函数,则将匹配的内容放入函数中处理。该函数必须能返回替换结果

count 用于指定最多替换次数,不指定时全部替换。

import re

p = re.compile(r'(\w+) (\w+)')
s = 'hello 123, hello 456'
#有两个匹配的内容'hello 123'和'hello 456'
print(p.sub(r'hello world', s))
# 使用 hello world' 替换 'hello 123' 和 'hello 456'
print(p.sub(r'\2 \1', s))
# 引用分组 在第一个匹配的内容 \2匹配的分组是指'123' \1匹配的分组是指'hello'
# 在第二个匹配的内容 \2匹配的分组是指'456' \1匹配的分组是指'hello'
import re

p = re.compile(r'(\w+) (\w+)')
s = 'hello 123, hello 456'
#有两个匹配的内容'hello 123'和'hello 456'
def func(m):
    return 'hi' + ' ' + m.group(2)
print (p.sub(func, s))
print (p.sub(func, s, 1)) #只处理一个匹配的内容

subn 方法
subn 方法跟 sub 方法的行为类似,也用于替换。只是返回的结果是一个元组,元组的第一个元素是替换后的字符串,第二个元素是替换的次数

时间模块time

time.time() 返回一个以秒为单位的浮点数,该浮点数为时间戳(即从1970-01-01的0点0分0秒 开始的偏移量)
time.sleep(n) 程序休眠n秒后继续执行
time.strftime() 按括号中指定输出格式,对时间进行格式化,然后输出
输出格式:
%y 两位数的年份表示(00-99)
%Y 四位数的年份表示(000-9999)
%m 月份(01-12)
%d 月内中的一天(0-31)
%H 24小时制小时数(0-23)
%I 12小时制小时数(01-12)
%M 分钟数(00=59)
%S 秒(00-59)
%X 等同于%H:%M:%S

print(time.strftime("%Y-%m-%d %X"))  输出2019-02-20 15:14:48

time.locatime() 输出结构化时间,生成对象类似命名元组

时间戳与结构化时间之间的转换

t=time.time()  #得到时间戳
print(time.localtime(t))  #结构化为本地时区的时间
print(time.gmtime(t))  #结构化为格林时区的时间
print(time.mktime(time.localtime(t)))  #结构化时间转为时间戳

格式化时间和结构化时间之间的转换
time.strptime(“时间字符串”,“格式化规则”) #将时间字符串按格式化规则转换,得到结构化时间

time.strptime("2019-12.31","%Y-%m.%d")

time.strftime(“格式化规则”,结构化时间) #将结构化时间按格式化规则转换,得到格式化时间

time.strftime("%Y/%m/%d %X",time.localtime(t)) 

随机数模块random

random.random() # 大于0且小于1之间的小数
random.uniform(1,3) #大于1小于3的小数
random.randint(1,5) # 大于等于1且小于等于5之间的整数
random.randrange(1,10,2) # 大于等于1且小于10之间的奇数
random.choice([1,‘23’,[4,5]]) #列表中的元素选一个
random.sample([1,‘23’,[4,5]],2)#列表元素任意2个组合

操作系统交互模块os

os模块是与操作系统交互的一个接口
os.getcmd() 获取当前脚本的工作目录
os.chdir() 切换工作目录
os.makedirs(‘dir1/dir2’) 在当前目录下创建多层递归目录
os.removedirs(‘dir1/dir2’) 一次性删除递归的多层空目录
os.mkdir(‘dir1’) 生成单层目录
os.rmdir(‘dir1’) 删除单级空目录,若目录不为空则无法删除
os.listdir(‘dirname’) 将指定目录下的所有文件和子目录,包括掩藏的,组成一个列表返回。
os.remove() 删除一个文件
os.rename(“oldname”,“newname”) 重命名文件/目录
os.stat(‘path/filename’) 获取文件/目录信息 将这些信息组成一个命名元组返回
os.system(“bash command”) 调用程序所在系统运行shell命令,直接打印执行结果
os.popen("bash command).read() 运行shell命令,返回执行结果

import os

os.system('pwd')
a=os.popen('pwd').read()
print(a)

os.path.abspath(path) path是相对路径,返回path规范化的绝对路径
os.path.split(path) 将path按最后一个路径分割符进行分割成二元组返回
os.path.dirname(path) 返回path的目录。其实就是os.path.split(path)的第一个元素
os.path.basename(path) 返回path最后的文件名。即os.path.split(path)的第二个元素

与解释器交互模块sys

sys模块是与python解释器交互的一个接口
sys.argv 将python解释器接受的参数组成一个列表,第一个元素基本是程序名字。

import sys
a=sys.argv
print(a)
print(a[0])
print(a[1])

##调用python解释器进行  python test.py pks 123
##输出内容:
##['test.py', 'pks', '123']
##test.py
##pks

sys.exit(n) 用于退出程序,正常退出时exit(0),错误退出sys.exit(1)
sys.version 获取Python解释程序的版本信息
sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值
sys.modules 记录程序已加载的模块

序列化模块

序列化:将其他类型的数据(如字典,列表等)转换为字符串类型的数据
反序列化:将字符串类型还原为原来的数据类型
在这里插入图片描述
数据存储和网络传输,只能操作bytes类型,字典等类型的数据需要先转换为字符串类型,才能再转成bytes类型

常用序列化模块 json、pickle

json是一个众多开发语言通用的序列化格式,只有一部分数据类型能通过json转成字符串类型(数字,列表 ,字典,元组)

序列化方法:
json.dumps() 用于操作内存中的数据
json.dump() 序列化并将结果写入某个文件句柄

反序列化:
json.loads() 用于操作内存中的数据
json.load() 先从文件句柄中读取,再完成反序列化

注意
1、在进行序列化的时候json.dump和json.dumps中有一个ensure_ascii参数默认为True,会将所有非ascii码的字符进行转换,对于需要中文显示可设置为False
2、使用json.dump()和json.load()建议一次性序列化写入,并一次性读取反序列化

import json
#(1)dumps
dic = {'k1':'值1','k2':'值2','k3':'值3'}
str_dic = json.dumps(dic)   #将字典转换成一个字符串
print(type(str_dic),str_dic)
'''结果:
<class 'str'> {"k3": "\u503c3", "k1": "\u503c1", "k2": "\u503c2"}
'''
str_dic1 = json.dumps(dic,ensure_ascii=False)   #将字典转换成一个字符串
print(type(str_dic1),str_dic1)
'''结果:
<class 'str'> {"k1": "值1", "k2": "值2", "k3": "值3"}
'''

#(2)loads
dic2 = json.loads(str_dic)  #将一个序列化转换成字典
print(type(dic2),dic2)
'''结果:
<class 'dict'> {'k3': '值3', 'k1': '值1', 'k2': '值2'}
'''

#(3)dump
f1 = open('json_file','w') 
dic = {'k1':'值1','k2':'值2','k3':'值3'}
json.dump(dic,f1)   #dump方法将dic字典信息,转换成json字符串写入文件
f1.close()

#(4)load
f = open('json_file')   
dic2 = json.load(f) #load方法将文件中的内容转换成数据类型返回
f.close()
print(type(dic2),dic2)
'''结果:
<class 'dict'> {'k3': '值3', 'k1': '值1', 'k2': '值2'}
'''

摘要算法的模块hashlib

import hashlib
md5=hashlib.md5()    ##实例化一个使用使用md5算法的摘要对象
md5.update(bytes("平时",encoding="utf-8"))  ##摘要对象的update方法只能对bytes类型数据进行摘要计算 
print(md5.hexdigest())   ##摘要结果存放在对象的独立内存空间中,通过hexdigest()方法获取

注意:一个对象若多次对数据进行摘要处理,其存放在对象独立内存空间中的摘要结果会一次次的改变。

import hashlib
md5 = hashlib.md5()
md4 = hashlib.md5()

md5.update(b'12345')
print(md5.hexdigest())  ##827ccb0eea8a706c4c34a16891f84e7b

md5.update(b'12345')
print(md5.hexdigest())  ##8cfa2282b17de0a598c010f5f0109e7d

md4.update(b'12345')
print(md4.hexdigest())  ##827ccb0eea8a706c4c34a16891f84e7b
#hashlib 还提供sha算法,但计算方法更复杂,消耗较大cpu

# 摘要算法通畅用于 密码的密文存储和文件的一致性验证

#为了更加安全的进行摘要加密,还可以在实例化摘要对象时,就进行加盐
import hashlib   # 提供摘要算法的模块

md5 = hashlib.md5()
md5.update(b'123456')
print(md5.hexdigest())#输出e10adc3949ba59abbe56e057f20f883e

md5 = hashlib.md5(bytes('盐',encoding='utf-8'))   ##盐可以是任意字符
md5.update(b'123456')
print(md5.hexdigest())#输出970d52d48082f3fb0c59d5611d25ec1e

应用举例,对文件进行摘要

import hashlib
def md5sum(filename):
        """
        用于获取文件的md5值
        参数 filename: 文件名
        return: MD5码
        """
        if not os.path.isfile(filename):  
            return
        # 先判断文件是否存在,不存在则返回none
        myhash = hashlib.md5()
        f = open(filename, 'rb')
        while True:
            b = f.read(8096)
            if not b:
                break
            myhash.update(b) 
        #循环对文件的内容进行摘要计算 
        f.close()
        return myhash.hexdigest()

配置文件的模块

常见的配置文件,分为一个或多个节(section),每个节可以有多个参数(键=值)。举例如下:

[DEFAULT]
Compression = yes
CompressionLevel = 9
ForwardX11 = yes
  
[bitbucket.org]
User = hg
  
[topsecret.server.com]
ForwardX11 = no

对于此类文件的操作python中使用configparser模块

举例一 生成此类文件

import configparser

config = configparser.ConfigParser()  
# 实例化一个配置对象

config["DEFAULT"] = { 'Compression': 'yes',
                     'CompressionLevel': '9',
                     'ForwardX11':'yes'
                     }
# 配置对象的[DEFAULT]章节设置上面三个键值对

config['bitbucket.org'] = {'User':'hg'}

config['topsecret.server.com'] = {'ForwardX11':'no'}

with open('example.ini', 'w') as configfile:

   config.write(configfile) 
   #将配置对象写入文件example.ini中

举例二 此类配置文件中获取信息

import configparser

config = configparser.ConfigParser()
#实例化一个配置对象

config.read('example.ini')
#配置对象读取文件example.ini作为对象内存

print(config.sections())
##打印配置对象中除DEFAULT节以外的节名

print('bytebong.com' in config)
# 判定配置对象是否存在名为bytebong.con的节,不存在返回False,存在返回True

print(config['bitbucket.org']["user"])
# 打印配置对象bitbucket.org节中键名为user的值

print(config.options('bitbucket.org'))
# 同for循环,找到'bitbucket.org'下所有键

print(config.items('bitbucket.org'))
#找到'bitbucket.org'下所有键值对

print(config.get('bitbucket.org','compression'))
#get方法,找到指定节名下的key对应的value

举例三 此类配置文件增删改

import configparser

config = configparser.ConfigParser()
#实例化一个配置对象

config.read('example.ini')
#配置对象读取文件example.ini作为对象内存

config.add_section('yuan')
#配置对象增加名为yuan的节

config.remove_section('bitbucket.org')
#配置对象删除名为bitbucket.org的节

config.remove_option('topsecret.server.com',"forwardx11")
#配置对象删除名为topsecret.server.com节中key名为forwardx11的键值对

config.set('topsecret.server.com','k1','11111')
#配置对象设置topsecret.server.com节中键值对k1=11111

config.write(open('new2.ini', "w"))
#将配置对象写入文件new2.ini中

日志模块

为了方便进行debug,任何程序必须有专门的日志输出.
python中logging是专门的日志模块

日志的等级由低到高如下:
CRITICAL > ERROR > WARNING > INFO > DEBUG

默认情况下Python的logging模块将日志打印到了标准输出中,且只显示了大于等于WARNING级别的日志

日志模块的使用分为基础配置和对象配置两种

== 基础配置==

import logging

file_handler = logging.FileHandler(filename='x1.log', mode='a', encoding='utf-8',)
#创建一个文件操作符
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s -%(module)s:  %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S %p',
    handlers=[file_handler,],
    # 定义日志输出的处理方式handlers
    level=logging.ERROR
)
#定义一个日志基础配置

logging.error('你好')
#使用日志基础配置输出error级别的日志信息"你好"

logging.basicConfig()函数中可通过具体参数来更改logging模块默认行为,可用参数有:

filename:用指定的文件名创建FiledHandler,这样日志会被存储在指定的文件中。
filemode:文件打开方式,在指定了filename时使用这个参数,默认值为“a”还可指定为“w”。
format:指定handler使用的日志显示格式。
datefmt:指定日期时间格式。
level:设置rootlogger(后边会讲解具体概念)的日志级别
stream:用指定的stream创建StreamHandler。可以指定输出到sys.stderr,sys.stdout或者文件(f=open(‘test.log’,’w’)),默认为sys.stderr。若同时列出了filename和stream两个参数,则stream参数会被忽略。

format参数中可能用到的格式化串:
%(name)s Logger的名字
%(levelno)s 数字形式的日志级别
%(levelname)s 文本形式的日志级别
%(pathname)s 调用日志输出函数的模块的完整路径名,可能没有
%(filename)s 调用日志输出函数的模块的文件名
%(module)s 调用日志输出函数的模块名
%(funcName)s 调用日志输出函数的函数名
%(lineno)d 调用日志输出函数的语句所在的代码行
%(created)f 当前时间,用UNIX标准的表示时间的浮 点数表示
%(relativeCreated)d 输出日志信息时的,自Logger创建以 来的毫秒数
%(asctime)s 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒
%(thread)d 线程ID。可能没有
%(threadName)s 线程名。可能没有
%(process)d 进程ID。可能没有
%(message)s用户输出的消息

日志的对象配置
logger的基础配置,缺点是不能将log信息即输出到屏幕又输出到文件,而日志的对象配置恰好能解决此缺点

import logging

logger = logging.getLogger()
# 创建一个handler,用于写入日志文件
fh = logging.FileHandler('test.log',encoding='utf-8') 
# 再创建一个handler,用于输出到控制台 

ch = logging.StreamHandler() 
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setLevel(logging.DEBUG)

fh.setFormatter(formatter) 
ch.setFormatter(formatter) 
logger.addHandler(fh) #logger对象可以添加多个fh和ch对象 
logger.addHandler(ch) 

logger.debug('logger debug message') 
logger.info('logger info message') 
logger.warning('logger warning message') 
logger.error('logger error message') 
logger.critical('logger critical message')

包是一种通过使用格式:‘.模块名’来组织python模块名称空间的方式。包的本质是一个目录,该目录下存放着格式各样的模块文件,以此来提高程序的结构性和可维护性.

包的导入

包相关的导入语句也分为import和from … import …两种,但是无论哪种,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。对于from ... import ...形式的导入,点号只能出现在from后不可以用于import

发布了40 篇原创文章 · 获赞 2 · 访问量 2063

猜你喜欢

转载自blog.csdn.net/weixin_42155272/article/details/93880142