4th Python常用模块
4.1 模块介绍
4.1.1 模块及其好处
随着我们代码写的越来越多,功能越来越复杂,我们发在一个文件里维护就比较麻烦。所以我们就把不同的代码放在不同的py文件里,比如我们把连接数据库和后端和Web界面代码分开放。
一个py文件就可以当做最小的模块。模块最大的好处就是提高代码的利用率,而且避免了函数名、变量名的冲突(a.py下的calc()函数和b.py下的calc()函数不会有任何问题,导入使用的时候也不会产生冲突)
问题来了,就是我们应该怎么导入,这个我们下面马上来介绍。
4.1.2 模块的种类
那在如何导入之前,我们介绍一下模块的分类。
模块分为三种:
内置模块、标准模块:安装好Python后,自带的模块,大约300多个
第三方模块:全球用Python的大佬会将自己写的模块上传到社区中,供全球使用,目前总共有18w多
自定义模块:自己写的,就是自己写的py文件,我们开头一开始说到的就是自定义模块。
4.1.3 模块的导入
# 导入
import os # 导入
print(os.path) # 调用os中的变量和函数方法
# 导入多个模块
import os, sys
# 前面的导入是导入模块中的所有东西,我们也可以导入一个模块中的单个东西
from os import rename # 导入os中的rename方法
rename('', '') # 不用os.rename()了
# 导入多个方法也是同理
# from os import rename, replace, ...
# 导入模块的东西下的东西也可以
from os.path import curdir # 导入os模块path下的curdir
# 另外,我们还可以给导入的东西重命名
from asyncio.events import get_event_loop_policy as get_event
get_event() # 这样我们就可以用新的名字用了,一般是防止名字太长,才重命名
# 导入模块下所有东西(不推荐使用)
from os import * # 和import os的不同的是,我们不用调用
rename() # 不用os.rename()
# 因为from os import *导入的所有东西不用调用,就会和其他导入的模块或者你自己写的东西冲突,致使你的代码很乱
模块的导入一般全写在开头,不要在代码中间或其他地方乱写,不方便维护
4.1.3 自定义模块
放在同目录下的两个py文件的调用
# file_1.py
def say_hi(name):
print('hi, ', name)
# file_2.py
import file_1 # pycharm可能会提示错误,不用管,我们下面解释
file_1.say_hi('zm') # 输出hi, zm
但是,我们在其他地方就导入不了file_1.py了,而且上面的代码中自定义模块的导入pycharm也会提示错误,这些问题到底是什么原因?
因为Python默认的查找路径是固定的,从开发社区下载的第三方库都放在site-packages中,标准库放在第三方库的上层目录。我们在cmd中进入python,导入os库,打印os.path,我们可以看到第一个元素是空,就代表当前目录,所以import每次先找同目录下的,然后接着找标准库、第三方库那里,都没找到就报错。
所以我们想让所有程序在任何地方都可以调用到我们自己写的模块,我们就要把那个模块的py文件放到site-packages下
4.1.4 安装第三方模块
pip或者pip3
pip install 模块名
或者
pip3 install 模块名
这个方法的前提条件是必须要有python的环境变量。
不过python的pip默认是去国外的python社区下载,可能会很慢或者链接不上,所以我们可以使用国内的python镜像网站,一般是豆瓣源或者是清华源,具体可以去CSDN找方案。
pip的卸载:
pip uninstall
显示安装过的库:
pip list
或者
pip freeze
4.2 常用模块应用
4.2.1 os & sys——系统调用
os
getcwd():获取当前py文件的目录路径;listdir():获取指定目录下的所有文件和目录名;os.path.isfile():判断指定路径是否是文件;os.path.isdir():判断是否是一个目录;os.path.isabs():判断是否为绝对路径;os.path.exists():检测地址是否真的存在;os.path.abspath():获取绝对路径;os.name:显示你正在使用的操作系统(Windows是nt,Linux/Uinux是posix);os.rename(old, new):文件重命名;os.replace(old, new):替换文件;os.makedirs():创建多级目录,eg:os.makedirs(r'\python\test'),在当前目录下创建python文件夹,其中包含子文件夹test;os.mkdir():创建单级目录,eg:os.mkdir('python'),在当前目录下创建python文件夹,但是创建多级目录就不行,会报错;os.stat(file):获取文件属性;os.path.getsize(filename):获取文件大小;
sys
argv():命令行参数,以list的形式返回,list中第一个参数是程序本身的路径,前面用到过;exit():exit()这个方法Python可以不导入就可以用,而且功能差不多,所以可以忽略;maxint():获取最大的int值;path():返回python模块的搜索路径;platform():返回程序运行的操作系统;getrecursionlimit():获取最大递归层数;getdefaultencoding():获取Python解释器的编码;getfilesystemencoding():获取内存数据存到文件里的默认编码。
模块调用需要注意
import os
# abspath(__file__)可以获得当前py文件的绝对路径
print(os.path.abspath(__file__))
print(os.path.dirname(os.path.abspath(__file__)))
4.2.2 time等——时间处理
时间模块的使用还是比较多的,我们写程序对时间的处理归为以下三种:
时间的显示:在屏幕显示、或者日志记录。
时间的转换:吧字符串格式的日期转换成Python中可以运算的日期类型
时间的运算:计算两个日期间的差值。
时间通常的表示格式
时间戳:timestamp,表示的是从1970年1月1号0点按秒开始计算到现在的秒数。1970年是第一次Unix操作系统的诞生。python中用
time.time()获取当前时间戳。格式化的字符串:比如
"2020-10-03 17:54"元组(
struct_time):一共九个元素。由于Python的time模块的实现主要调用的是C语言的库,所以各个操作系统可能会有所不同,Windows上:time.struct_time为time.struct_time(tm_year=2020, tm_mon=11, tm_mday=22, tm_hour=15, tm_min=53, tm_sec=11, tm_wday=6, tm_yday=327, tm_isdst=0)UTC时间:格林威治时间,世界标准时间,在中国为UTC+8,东8区。DST(
Daylight Saving Time)即为夏令时。
time
time.localtime([secs]):将一个时间戳转换为当前时区(中国全部都是东8区)的struct_time元组。不写参数默认是当前时间;time.gmtime([secs]):和localtime()类似,和localtime差八个小时,时区不一样。同样,不写参数也默认是当前时间;time.mktime():将struct_time元组转换成时间戳,和上面相反;time.sleep(secs):程序停几秒钟,单位为秒;time.asctime([t]):用的不多,将struct_time元组传入,返回英文格式的时间字符串;time.ctime():将时间戳传入,返回英文格式的时间字符串;time.strftime(format, [t]):格式化字符串,可以按我们想要的格式来输出时间,eg:time.strftime("%Y.%m-%d %H:%M %p", time.localtime()),输出结果为2020.11-22 16:11 PM;time.strptime():和strftime()相反,eg:time.strptime('2020/04/01 19:30', "%Y/%m/%d %H:%M"),然后就可以获得相应的时间戳。
字符串转时间格式对应表:

time中时间格式的转换图:

datetime
datetime比time更加直观、更加容易调用。time一般用来格式化时间输出或者获取时间戳;datetime更多用来计算时间。
datetime模块分为五种数据类型,分别是date、datetime、time、timedelta、tzinfo。
date:
datetime.date.today():获取当前时间,输出一般为datetime.date(2020, 11, 22)datetime.date.timetuple(date):将上面的date格式转化成struct_time元组,eg:datetime.date.timetuple(datetime.date.today()),输出:time.struct_time(tm_year=2020, tm_mon=11, tm_mday=22, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=6, tm_yday=327, tm_isdst=-1)datetime.date.ctime():和time.ctime()一样,输出英文的时间格式datetime.date.fromtimestamp():把时间戳转换成date类型
time:date输出年月日,time输出的是小时分钟秒等时间信息。
datetime:
datetime.datetime.now():获取当前时间,类型为datetime类型,eg:datetime.datetime(2020, 11, 23, 10, 57, 48, 525775)datetime.datetime.fromtimestamp():将时间戳转换成datetime类型
timedelta:
datetime.timedelta():参数格式为默认参数,可以传入的参数有天数、毫秒和秒。eg:import datetime
now_time = datetime.datetime.now() # 获取当前时间
the_time = now_time - datetime.timedelta(days=3, hours=4, minutes=5) # 将当前时间减去3天4小时5分钟 # 参数写负数也可以
时间替换:
time1.replace(year=2000, month=1, day=1),上面的date、time、datetime类型都可以用这个来进行时间的替换。
tzinfo:与时区有关的,在金融行业的工作和时区比较常使用
pytz这个模块就是与时区相关的模块,pytz.all_timezones()输出所有时区,pytz.all_timezone(地区)就可以获取时区,t1 = pytz.all_timezone('Asia/Shanghai')获取之后,然后datetime.datetime.now(tz=t1),就可以生成指定时区的时间。
4.2.3 random——随机
py文件的命名
py文件的命名别和内置模块名重复,不然会冲突。
random的方法
random.randint(1, 10):返回1到10(包括10)的一个随机数;random.randrange(1, 10):返回1到10(不包括10)的一个随机数;random.random():返回一个0到1之间的随机浮点数;random.choice(the_str):返回给定字符串中的一个随机字符,列表、元组也可以,但set和dict不可以;random.sample(the_str, num):返回num个字符串中的随机字符(列表、集合、元组都可以,字典不行),以列表形式返回;# 随机验证码
import string
import random
print(''.join(random.sample(string.digits+string.ascii_lowercase, 5)))random.shuffle(the_list):将列表数据打乱,会默认修改原来的列表数据,修改成打乱的列表数据,数据就只能是列表。
4.2.4 pickle——序列化
pickle
我们知道,字符串是可以写入文件的,因为Python自动帮我们将字符串通过编码转换写入硬盘,但是像字典、列表、集合、元组这些数据怎么存进硬盘呢?这是就涉及到序列化的知识点。
序列化即是:将字典、列表这些数据序列化转成硬盘能认识的数据类型,比如说16进制的字节。
d = {
"name": "alex",
"role": "police",
"blood": 76,
"weapon": "AK47",
}
d_dump = pickle.dumps(d) # 序列化,返回16进制的字节
print(d_dump)
print(pickle.loads(d_dump)) # 反序列化
f = open("game.pkl", "wb")
# f.write(d_dump)
pickle.dump(d, f) # 和write一样,将
f.close()
f = open("game.pkl", "rb")
# f.read()
pickle.load(f) # 先取先dump进去的
pickle.load(f) # 取第二次dump进去的
# pickle.load(f) # 没数据时就报错
# dump写入文件,load读出文件;
# dumps是生成序列化的字符串,loads将序列化的字符串反序列化解析
当然,上面的演示代码我们dump和load了好几次,在实际开发中我们建议dump一次,load一次,不然程序很大的话,你也记不住程序到底dump了几次。我们可以将需要写入文件的序列化数据存在一个列表(或其他)里,然后dump这个列表,即可实现一次dump多个需要序列化的数据了。
我们将序列化的数据写入文件,文件肯定会产生乱码,因为这是通过pickle的规则转化的bytes数据,和编码的格式肯定不同,只要我们读出来不是乱码就说明数据没有被破坏,这点需要注意一下。
json vs pickle
除了pickle,json也是用来序列化的模块。他们最大的区别就是:json各种语言都能用,但是只支持str、int、dict、set、list、tuple这些基本的数据类型,以后写东西需要跨语言,就会用到json;pickle是Python专属的,支持Python所有的数据类型,比如function、datetime、class/object全都可以。
# json
d = {
"name": "alex",
"role": "police",
"blood": 76,
"weapon": "AK47",
}
print(json.dumps(d)) # dict变成了字符串
print(type(json.dumps(d))) # <class 'str'>
with open("test.json", "w") as f:
json.dump(d, f)
with open("test.json", 'r') as f:
json.load(f)
注意:json如果dump多次,load就会报错,因为json不支持多次dump。
而且我们可以注意到json和pickle的另一个区别:
json是将字典、列表这些数据先转化成字符串,然后再通过编码存入硬盘,所以我们可以看到我们json转化的时候点开文件我们看不到乱码。另外,json这样的方式耗费的空间比pickle小。
以后我们尽量使用json,因为json用的比较多,pickle基本不怎么用,知道就好。
4.2.6 hashlib——加密
hash,也就是散列或者哈希,不同的值映射出来的可以是相同的(数据量极大的情况下会出现,又称哈希碰撞)
MD5
我们每次进Python时,对相同数据的hash值都会不一样,从而hash不能直接用来加密,所以我们需要确保hash输出的值是稳定的,所以MD5(讯息摘要演算法)诞生了。
功能:
任意长度 -> 固定的长度;
不同数据会得出不同MD5值(极小可能会重复)
特点:
压缩性;
容易计算;
抗修改性;
强抗碰撞(极小可能会重复)。
MD5算法和hash一样,是不可逆的。
用途:
防止被篡改。
防止直接看到明文。
数字签名。
用法:
import hashlib
m = hashlib.md5() # 开启md5加密(创建md5的一个对象)
m.update("Hello world".encode('utf-8')) # 传入加密数据
m.update("asd".encode('utf-8'))
# print(m.digest()) # 加密,返回byte,不常用
print(m.hexdigest()) # 加密,返回16进制(常用)
# 多个update是放一块加密的,直接拼一起
m2.hashlib.md5()
m2.update("Hello worldasd".encode('utf-8'))
print(m2.hexdigest()) # 和上面的m.hexdigest()输出一样的值
注意,如果密码是纯数字或纯小写字符组成的密码,及有可能被撞库撞出来,就是一个一个试md5密文,已达到破解md5算法的目的。
一般网站通过在md5密文后再加上一些字节(俗称加盐),这样数据库被黑客攻击,黑客拿到的md5密文也不可能反推出原来对应的明文密码。
SHA
SHA对于2^64以内的数据会产生160位的消息摘要,比md5的128多了几十位,所以安全性比md5高一些。SHA是美国国家安全局设计的,目前最流行的是SHA-256。HTTPS就是基于SHA-256的,网站的加密是SSL2(以前是SSL1)也是基于SHA-256的。SHA-256是生成256位的消息摘要,更加难以破解。
用法和md5一样
s2 = hashlib.sha256()
s2.update('zm'.encode('utf-8'))
print(s2.hexdigest())
当然,SHA产生的消息摘要位数越多,加密耗费的时间也越多,所以md5一般用于文件校验,因为比较快;SHA-256一般就用在网站的安全维护上。
4.2.7 shutil——文件操作
shutil
shutil.copyfileobj(f1, f2):传入的参数必须是打开的文件就是说必须传进两个文件的对象(f = open('filename', 'w', encoding='utf-8'),f就是一个文件对象),将f1中的内容copy到f2中,不常用;copyfile(old_filename, new_filename):传入两个文件名,新的文件如果和已有的文件重名就会被覆盖;copymode():copy文件的权限,两个文件必须存在。将第一个文件的文件权限copy到第二个文件的文件权限,两个文件的权限改变,内容什么的都不变,不常用;copystat():copy文件的状态,第二个文件必须存在,覆盖掉第二个文件的创建时间啥的文件状态,用的也不多;copy():创建一个文件,然后copy权限和文件内容copyfile()和copymode()的结合;copy2():copy文件内容和状态,copyfile()和copystat()的结合;copytree():copy目录,新目录必须不存在,另外还可以设置ignore参数,可以设置不想要的文件。import shutil
shutil.copytree("", "", ignore=shutil.ignore_patterns("", ""))
move(src, dst):如果第二个文件夹存在,移动文件src文件夹下的所有文件到dst文件夹下,如果第二个文件夹不存在,那move()本质上就是重命名;rmtree(src):删除文件src文件夹下的所有文件;make_archive(base_name, format, root_dir):base_name是压缩包的文件名,format是压缩的格式(zip、tar等),root_dir是需要压缩的文件夹;
zipfile&tarfile
shutil.make_archive()是基于zipfile和tarfile写出来的,这里我们就顺便来看看这两个模块。
import zipfile
import tarfile
# zip文件压缩
with zipfile.ZipFile('test.zip', 'w') as z:
z.write('test.txt')
z.write('web.log')
# zip文件解压
with zipfile.ZipFile('test.zip', 'r') as z:
z.extractall('new/zip/')
# tar文件压缩
with tarfile.TarFile('test.tar', 'w') as t:
t.add('test.txt')
t.add('web.log')
# tar文件解压
with tarfile.TarFile('test.tar', 'r') as t:
t.extractall('new/tar/')
4.2.8 re——正则表达式
入门
正则表达式是一个很重要的一个知识点,在我们后面的web开发(Python的Django)中大量用到。
首先我们有一个需求,就是读取文件中的所有手机号
# test.txt
朱明 12345611233
王芝伟 13265411344
# test.py
with open('test.txt', 'r') as f:
phone_list = []
for line in f:
name, phone = line.split(' ')
if phone[0] == '1':
phone_list.append(phone)
现在我们有一个更加简便的方法。
import re
with open('test.txt', 'r') as f:
phone_list = re.findall('[0-9]{11}]', f.read())
print(phone_list)
这就是正则表达式,你几行的代码瞬间可以写成一行代码。
匹配语法
那学习正则表达式的语法之前呢,我们先来学习re的匹配语法,分为以下几种:
re.match(pattern, string, flags=0):从起始位置开始根据模型去字符串中匹配指定内容,匹配开头的字符是否符合指定内容,不常用;re.search(pattern, string, flags=0):根据模型去字符串中匹配指定内容,匹配到一个符合就返回,不往下找了;re.findall(pattern, string, flags=0):match()和search()均用于匹配单值,即:只能匹配字符串中的一个,如果想要匹配到字符串中所有符合条件的元素,则需要使用findall();re.split(pattern, string, maxsplit=0, flags=0):用匹配到的值做为分割点,把值分割成列表,eg:split("[0-9]", "asd1asd2asd3zxc"),输出的是['asd', 'asd', 'asd', 'zxc'];re.sub(pattern, string, flags=0):用于替换匹配的字符串,比str.replace()功能更加强大,eg:sub("abc", "ABC", "abcdef", count=2),输出ABCdef,count是指最多寻找几次;re.fullmatch(pattern, string, flags=0):全部匹配,和findall()类似,不过返回的是match类型的数据需要group()来处理输出。
表达式规则
.:匹配任意一个字符(除了\n,其他符号都可以匹配),比如print(search('.', 'zm123'))返回的是<re.Match object; span=(0, 1), match='z'>,要输出的话必须加一个group(),print(search('.', 'zm123').group())输出的就是z;^:匹配字符的开头,比如``$:匹配字符的结尾*:从开头匹配一个字符0次或多次,0次返回的不是None,返回<re.Match object; span=(0, 1), match=''>+:匹配多次?:匹配一个字符0次或1次;{m}:匹配m次;{m, n}:匹配m到n次包括m和n;|:或,匹配左边的字符或者右边的字符;(...):分组匹配;[]:比如[a-z]匹配所有小写字符,[A-Z]匹配所有大写,[a-zA-Z0-9]匹配所有小写大写和数字;\A:只从字符开头匹配,像search('\Aabc', 'zmabc')返回的就是None,相当于match();\Z:匹配字符结尾;\d:匹配数字0-9;\D:匹配非数字;\w;匹配a-z,A-Z和0-9的字符;\W;匹配非[a-zA-Z0-9]s:匹配空白字符、\t、\n、\r;(?P<name>...):分组匹配,比如分组读取身份证号:re.search(([0-9]{3})([0-9]){3}(0-9){4}, '身份证号'),然后groups()输出分组的数据;我们还可以起名字:re.search((?P<province>[0-9]{3})(?P<city>[0-9]{3})(?P<year>(0-9){4}), '身份证号')然后用groupdict()输出字典。
当需要筛选的字符串中包含-、+、*等语法字符时,我们可以在其前面加\来表示非语法字符。
那这些match、search、findall等等几个匹配语法,都有解析正则表达式的需要,re模块的设计者也将这部分的功能独立编写了函数compile(),我们可以在需要大量解析正则表达式的时候,统一compile(),然后再用match、search这些方法,传入参数只需要所分析的数据就好了。
flags
在上面match、search等匹配语法中,都有一个叫flags的参数,Flags被称作标志符,有几个固定的值
re.I:忽略大小写re.M:多行模式,改变^和$。eg:re.search('^zm', 'mack\nzmghd')返回None,但是我们加上多行模式,就可以匹配到数据了,re.search('^zm', 'mack\nzmghd', re.M)就会输出找到的zm。re.M用的不多,知道即可;re.S:改变.的行为,原本.不能匹配出\n,但是加上这个就可以匹配了;re.X:加注释,注释语法和Python是一样的,一般写的太复杂时会用。
练习
验证手机号是否合法;
import re
phone = input('请输入字符:').strip()
# 用fullmatch是为了字符总长度需为11,用findall就不会有总长11的限制
if re.fullmatch('[0-9]{11}', phone) and re.search('^1', phone):
print('手机号码符合标准')
else:
print('输入字符不合法')验证邮箱是否合法;
import re
mail = input('请输入字符:').strip()
if re.fullmatch('\d+@(qq|chzu)\.(com|edu|cn)', mail):
print('合法')
else:
print('不合法')开发一个简单的python计算器,实现加减乘除及括号优先级解析,比如
1-2*((60-30+(-40/5)*(9-2*5/3+7/3*99/4*2998 +10*568/14))-(-4*3)/(16-3*2))。这部分暂且搁置一下(其实是暂时没思路),自己之后写写看= =
4.3 软件项目设计规范
4.3.1 项目目录设计规范
目录设计规范的好处
我们设计一个层次清晰的目录结构,就是为了达到以下两点:
可读性高
可维护性高
这里看个大概就好了,我们后面写比较大的程序时,自己亲手写几遍就会了。
好的目录组织方式
假设现在我们写一个Foo的大项目,推荐的文件结构如下:
Foo/
|-- bin/
| |-- foo
|
|-- foo/
| |-- tests/
| | |-- __init__.py
| | |-- test_main.py
| |-- core/
| |-- __init__.py
| |-- main.py
|
|-- docs/
| |-- conf.py
| |-- abc.csv
|
|-- setup.py
|-- requirements.txt
|-- README
bin/:放一些可执行的文件,当然你也可以起名script/之类的也可;foo/:存放项目的所有源代码;源代码中所有模块、包都应该放这里;
子目录
test/存放单元测试代码;程序的入口最好命名为
main.py。doc/:存放一些文档;setup.py:安装、部署、打包的脚本;requirements.txt:存放软件依赖的外部Python包列表,有一个pip命令可以快速生成电脑的python环境信息:pip freeze > requirements.txt
README.md:项目说明文件。
当然,还有更多的规范内容,比如LICENSE.txt、ChangeLog.txt等,他们是项目开源时需要用到的,开源软件的目录组织可以参考。
关于README
这个文件主要目的是让读者快速熟悉代码的主要功能和一些重要信息,需要说明以下几点:
软件定位:软件的基本功能。
运行代码的方法。安装环境、启动命令等。
简要的使用说明。
代码目录结构说明,更详细可恶意说明软件的基本原理。
常见问题说明。
关于requirements.txt
这里就写一些调用的库,包括库名和版本。
关于setup.py
setup.py是来管理代码的打包、安装、部署问题。业界标准的写法是用Python流行的打包工具来管理这些事情。不过,我们初学的时候不是让我们养成用标准化的工具来解决这些问题,而是一个项目一定要有一个安装部署的工具,能让自己的代码在一台新机器上迅速将环境装好、代码部署好,最后可以使程序正常运行起来。
配置文件
即是settings.py
末
有什么Python代码或者其他问题可以私信问我或者在下面留言,Python课程设计我也偶尔可以有偿帮做,祝大家变得更强[狗头]
剩下的就是和上一篇文章末尾一样要说的,我就当成套话了。
套话:因为小破站上的文本格式对演示代码极其不友好,而且自己平时的笔记是通过Markdown语法来记录的,在格式上和美观程度上不是很好看,如果你看的不习惯,就去下载一个Typora(或者支持markdown语法的应用),我这里给出md文件的迅雷网盘链接(百度网盘上传不了),然后用Typora打开文件看就好了。
链接:https://pan.xunlei.com/s/VMVzuPFHFx9Z9EkrEKyKbTgmA1
提取码:nnfs
咳咳,emmm,这里再加几句话吧。
到这里呢,Python基础的系列就快要结束了,接下来就是Python面向对象和网络编程,比较的难,如果你之前完全没有接触过面向对象的概念,可能会有些不适应,希望大家可以耐住性子学下去(如果真的想学的话);
网络编程会先从网络这个话题引入,然后再涉及计算机组成原理,最后再到应用层程序的逻辑解释。概念....怎么减都还是很多,不过网络编程这一章就是Python基础的最终章了。
感谢给我鼓励的大家,如果你喜欢这个系列,就给每篇文章点一个赞(收藏和投币我也不会拒绝的,不过会睡不着觉,哈哈哈),也希望大家多多评论,问问题什么的都可以哒。

