Python模块

  • Post author:
  • Post category:python




Python模块



模块(module)

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式.在Python中,一个.py文件就称之为一个模块(Module)

而在Python中的module分为三种:

  • Python标准库
  • 第三方模块
  • 应用自定义模块



引用方法

import modname
##引用本地目录的module

from modname import name1[, name2[, ... nameN]]
from filename.filename… import modname
##引用那个指定目录下的module

from modname import *
##引用该module里面所有的函数
>>> import sys
>>> print(sys.path)
['', 'C:\\Users\\DELL\\AppData\\Local\\Programs\\Python\\Python36\\Lib\\idlelib', 'C:\\Users\\DELL\\AppData\\Local\\Programs\\Python\\Python36\\python36.zip', 'C:\\Users\\DELL\\AppData\\Local\\Programs\\Python\\Python36\\DLLs', 'C:\\Users\\DELL\\AppData\\Local\\Programs\\Python\\Python36\\lib', 'C:\\Users\\DELL\\AppData\\Local\\Programs\\Python\\Python36', 'C:\\Users\\DELL\\AppData\\Roaming\\Python\\Python36\\site-packages', 'C:\\Users\\DELL\\AppData\\Local\\Programs\\Python\\Python36\\lib\\site-packages']
##import的运行本质的在sys.path里面的路径找module



包(Package)

每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录(文件夹),而不是一个包.要避免与同级目录的Package重名.

print(__file__)##该结果是当前的文件

import sys,os
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
##os.path.dirname() 返回上一级, os.path.abspath() 绝对路径
sys.path.append(BASE_DIR) ##把该路径暂时放到系统path里面,就能找到了
import hello
hello.hello1()



防止程序被引用

##写在这下面的程序,可以防止被其他调用运行,也可以用于被调用函数的测试
if __name__ == "main":
    program                  



—-time模块

import time
 
# 1 time() :返回当前时间的时间戳
time.time()  #1473525444.037215
 
#----------------------------------------------------------
 
# 2 localtime([secs])
# 将一个时间戳转换为当前时区的struct_time。secs参数未提供,则以当前时间为准。
time.localtime() #time.struct_time(tm_year=2016, tm_mon=9, tm_mday=11, tm_hour=0,
# tm_min=38, tm_sec=39, tm_wday=6, tm_yday=255, tm_isdst=0)
time.localtime(1473525444.037215)
 
#----------------------------------------------------------
 
# 3 gmtime([secs]) 和localtime()方法类似,gmtime()方法是将一个时间戳转换为UTC时区(0时区)的struct_time。
 
#----------------------------------------------------------
 
# 4 mktime(t) : 将一个struct_time转化为时间戳。
print(time.mktime(time.localtime()))#1473525749.0
 
#----------------------------------------------------------
 
# 5 asctime([t]) : 把一个表示时间的元组或者struct_time表示为这种形式:'Sun Jun 20 23:21:05 1993'。
# 如果没有参数,将会将time.localtime()作为参数传入。
print(time.asctime())#Sun Sep 11 00:43:43 2016
 
#----------------------------------------------------------
 
# 6 ctime([secs]) : 把一个时间戳(按秒计算的浮点数)转化为time.asctime()的形式。如果参数未给或者为
# None的时候,将会默认time.time()为参数。它的作用相当于time.asctime(time.localtime(secs))。
print(time.ctime())  # Sun Sep 11 00:46:38 2016
 
print(time.ctime(time.time()))  # Sun Sep 11 00:46:38 2016
 
# 7 strftime(format[, t]) : 把一个代表时间的元组或者struct_time(如由time.localtime()和
# time.gmtime()返回)转化为格式化的时间字符串。如果t未指定,将传入time.localtime()。如果元组中任何一个
# 元素越界,ValueError的错误将会被抛出。
print(time.strftime("%Y-%m-%d %X", time.localtime()))#2016-09-11 00:49:56
 
# 8 time.strptime(string[, format])
# 把一个格式化时间字符串转化为struct_time。实际上它和strftime()是逆操作。
print(time.strptime('2011-05-05 16:37:06', '%Y-%m-%d %X'))
 
#time.struct_time(tm_year=2011, tm_mon=5, tm_mday=5, tm_hour=16, tm_min=37, tm_sec=6,
#  tm_wday=3, tm_yday=125, tm_isdst=-1)
 
#在这个函数中,format默认为:"%a %b %d %H:%M:%S %Y"。
 
 
# 9 sleep(secs)
# 线程推迟指定的时间运行,单位为秒。
 
# 10 clock()
# 这个需要注意,在不同的系统上含义不同。在UNIX系统上,它返回的是“进程时间”,它是用秒表示的浮点数(时间戳)。
# 而在WINDOWS中,第一次调用,返回的是进程运行的实际时间。而第二次之后的调用是自第一次调用以后到现在的运行
# 时间,即两次时间差。



—-random模块

import random
 
print(random.random())
##返回[0,1)之间的任意浮点数
 
print(random.randint(1,10))
##返回[1,10]之间的任意整数
 
print(random.randrange(1,100,2)) 
##从指定范围内,按指定基数递增的集合中获取一个随机数
 
print(random.choice([1,'23',[4,5]]))
##在sequence里面选择一个
 
print(random.sample([1,'23',[4,5]],2))
##从指定序列中随机获取指定长度的片段,sample函数不会修改原有序列
 
print(random.uniform(1,3))
##用于生成一个指定范围内的随机浮点数
 
item=[1,3,5,7,9]
random.shuffle(item)
print(item)
##用于打乱序列



—-os模块

import os

os.getcwd() #获取当前工作目录,即当前python脚本工作的目录路径
os.chdir("dirname")  #改变当前脚本工作目录;相当于shell下cd
os.curdir  #返回当前目录: ('.')
os.pardir  #获取当前目录的父目录字符串名:('..')
os.makedirs('dirname1/dirname2')    #可生成多层递归目录
os.removedirs('dirname1')    #若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推
os.mkdir('dirname')    #生成单级目录;相当于shell中mkdir dirname
os.rmdir('dirname')    #删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdir dirname
os.listdir('dirname')    #列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印
os.remove()  #删除一个文件
os.rename("oldname","newname")  #重命名文件/目录
os.stat('path/filename')  #获取文件/目录信息
os.sep    #输出操作系统特定的路径分隔符,win下为"\\",Linux下为"/"
os.linesep    #输出当前平台使用的行终止符,win下为"\t\n",Linux下为"\n"
os.pathsep    #输出用于分割文件路径的字符串 win下为;,Linux下为:
os.name    #输出字符串指示当前使用平台。win->'nt'; Linux->'posix'
os.system("bash command")  #运行shell命令,直接显示
os.environ  #获取系统环境变量
os.path.abspath(path)  #返回path规范化的绝对路径
os.path.split(path) #将path分割成目录和文件名二元组返回
os.path.dirname(path)  #返回path的目录。其实就是os.path.split(path)的第一个元素
os.path.basename(path)  #返回path最后的文件名。如何path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素
os.path.exists(path)  #如果path存在,返回True;如果path不存在,返回False
os.path.isabs(path)  #如果path是绝对路径,返回True
os.path.isfile(path)  #如果path是一个存在的文件,返回True。否则返回False
os.path.isdir(path)  #如果path是一个存在的目录,则返回True。否则返回False
os.path.join(path1[, path2[, ...]])  #将多个路径组合后返回,第一个绝对路径之前的参数将被忽略
os.path.getatime(path)  #返回path所指向的文件或者目录的最后存取时间
os.path.getmtime(path)  #返回path所指向的文件或者目录的最后修改时间



—-sys模块

import sys
sys.argv           #命令行参数List,第一个元素是程序本身路径
sys.exit(n)        #退出程序,正常退出时exit(0)
sys.version        #获取Python解释程序的版本信息
sys.maxint         #最大的Int值
sys.path           #返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值
sys.platform       #返回操作系统平台名称



—-json模块

我们把对象(变量)从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling;序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上;反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling

import json

dic = {"name":"Hermaeus",
	   "age":19,
	   "hometown":"MeiShan",
	   "hobby":"Coding"}

j_dic = json.dumps(dic)   #===> json.dump(dic, f)
f = open("test.json","w")
f.write(j_dic)

f.close()

import json

f = open("test.json","r")
date = json.loads(f.read()) #===> json.load(f)
print(date)

输入结果为:

{'name': 'Hermaeus', 'age': 19, 'hometown': 'MeiShan', 'hobby': 'Coding'}



—-pickle模块

import pickle

#----------------------序列化------------------------
dic = {"name":"Hermaeus",
	   "age":19,
	   "hometown":"MeiShan",
	   "hobby":"Coding"}

j_dic = pickle.dumps(dic)
f = open("test.json","wb")  #必须输入二进制
f.write(j_dic)              #===>pickle.dump(dic, f)

f.close()

#---------------------反序列化------------------------
f = open("test.json","rb")  #输入的也一定是二进制
date = pickle.loads(f.read()) #===>pickle.load(f)
print(date)



—-shelve模块

##类字典操作
import shelve

f = shelve.open(r"shelve.txt") #就一个open函数

f["info_1"] = {"name":"Hermaeus", "age":19}
f["info_2"] = {"name":"YuanMing", "age":20}

# f.close()
print(f["info_1"])
print(f["info_2"]["name"])



—-xml模块

xml的格式如下,就是通过<>节点来区别数据结构的:

<?xml version="1.0"?>
<data>
    <country name="Liechtenstein">
        <rank updated="yes">2</rank>
        <year>2008</year>
        <gdppc>141100</gdppc>
        <neighbor name="Austria" direction="E"/>
        <neighbor name="Switzerland" direction="W"/>
    </country>
    <country name="Singapore">
        <rank updated="yes">5</rank>
        <year>2011</year>
        <gdppc>59900</gdppc>
        <neighbor name="Malaysia" direction="N"/>
    </country>
    <country name="Panama">
        <rank updated="yes">69</rank>
        <year>2011</year>
        <gdppc>13600</gdppc>
        <neighbor name="Costa Rica" direction="W"/>
        <neighbor name="Colombia" direction="E"/>
    </country>
</data>

操作如下:

import xml.etree.ElementTree as ET
 
tree = ET.parse("xmltest.xml")
root = tree.getroot()
print(root.tag)
 
#遍历xml文档
for child in root:
    print(child.tag, child.attrib)
    for i in child:
        print(i.tag,i.text)
 
#只遍历year 节点
for node in root.iter('year'):
    print(node.tag,node.text)
#---------------------------------------

import xml.etree.ElementTree as ET
 
tree = ET.parse("xmltest.xml")
root = tree.getroot()
 
#修改
for node in root.iter('year'):
    new_year = int(node.text) + 1
    node.text = str(new_year)
    node.set("updated","yes")
 
tree.write("xmltest.xml")
 
 
#删除node
for country in root.findall('country'):
   rank = int(country.find('rank').text)
   if rank > 50:
     root.remove(country)
 
tree.write('output.xml')

创建一个xml文档:

import xml.etree.ElementTree as ET
 
new_xml = ET.Element("namelist")
name = ET.SubElement(new_xml,"name",attrib={"enrolled":"yes"})
age = ET.SubElement(name,"age",attrib={"checked":"no"})
sex = ET.SubElement(name,"sex")
sex.text = '33'
name2 = ET.SubElement(new_xml,"name",attrib={"enrolled":"no"})
age = ET.SubElement(name2,"age")
age.text = '19'
 
et = ET.ElementTree(new_xml) #生成文档对象
et.write("test.xml", encoding="utf-8",xml_declaration=True)
 
ET.dump(new_xml) #打印生成的格式

结果如下:

<?xml version='1.0' encoding='utf-8'?>
<namelist>
	<name enrolled="yes">
		<age checked="no" />
		<sex>33</sex>
	</name>
	<name enrolled="no">
			<age>19</age>
	</name>
</namelist>



configparser模块

##类字典操作
import configparser

config = configparser.ConfigParser()
config["DEFAULT"] = {'ServerAliveInterval': '45',
					 'Compression': 'yes',
					 'CompressionLevel': '9'
					 }
config["bitbucket"] = {}
config["bitbucket"]["user"] = "hg"
config["topsecret.server.com"] = {}
topsecret = config["topsecret.server.com"]
topsecret["Host Port"] = '32204'
topsecret["Fowardx11"] = "no"
config["DEFAULT"]["Fowardx11"] = "yes"
with open("example.ini", "w") as configfile:
	config.write(configfile)

得到结果:

[DEFAULT]
serveraliveinterval = 45
compression = yes
compressionlevel = 9
fowardx11 = yes

[bitbucket]
user = hg

[topsecret.server.com]
host port = 32204
fowardx11 = no

其他操作

import configparser

config = configparser.ConfigParser()
config.read("example.ini")
print(config.sections())  #sections()是获得非DEFAULT的值

['bitbucket', 'topsecret.server.com']

print("bytebong.com" in config)

False

print(config["bitbucket"]["user"])

hg

print(config["DEFAULT"]["Compression"])

yes

print(config["topsecret.server.com"]["Fowardx11"])

no

for key in config["bitbucket"]:   #每一次打印都会把DEFAULT里面的值打印下来
	print(key)

user
serveraliveinterval
compression
compressionlevel
fowardx11

print(config.options("bitbucket"))  #返回键

['user', 'serveraliveinterval', 'compression', 'compressionlevel', 'fowardx11']

print(config.items("bitbucket"))   #把键值对以元组方式输出

[('serveraliveinterval', '45'), ('compression', 'yes'), ('compressionlevel', '9'), ('fowardx11', 'yes'), ('user', 'hg')]

print(config.get("bitbucket", "compression"))  #返回bitbucket里面是否有compression

yes

config.add_section("answer") #建立一个section
config.remove_section("topsecret.server.com") #删除一个section
config.remove_option("bitbucket", "user")  #删除一个键
config.set("bitbucket", "k1", "111")  #增添一个键值对
config.write(open("i.cfg", "w"))  #写入磁盘



—-hashlib

用于加密相关的操作,主要提供 SHA1, SHA224, SHA256, SHA384, SHA512 ,MD5 算法

import hashlib

m = hashlib.md5()
m.update("hello".encode("utf-8"))
print(m.hexdigest())

5d41402abc4b2a76b9719d911017c592

m.update("hello".encode("utf-8"))   
m.update("world!".encode("utf-8"))  ##等同于"helloworld!",这样是把它们两个拼接在一起
print(m.hexdigest())

420e57b017066b44e05ea1577f6e2e12

m.update("helloworld!".encode("utf-8"))
print(m.hexdigest())

420e57b017066b44e05ea1577f6e2e12

然后这种加密,可以通过撞库来反解,但是可以在加密算法里添加自定义key再来加密

import hashlib

m = hashlib.sha256("ok".encode("utf-8"))
m.update("hello".encode("utf-8"))
print(m.hexdigest())

ce9eba905e14b4f0dceb89f329280c10bb0403375d2a613c4bfb8aa726145c73



—-logging模块

import logging

logging.debug("debug message")
logging.info("info message")
logging.warning("waring message")
logging.error("error message")
logging.critical("critical message")

WARNING:root:waring message
ERROR:root:error message
CRITICAL:root:critical message
##在默认情况下,输入显示的是大于等于warning级别的(critical>error>warning>info>debug)
import logging
logging.basicConfig(level = logging.DEBUG,
					format = "%(asctime)s, %(filename)s[line:%(lineno)d] %(levelname)s %(message)s",
					datefmt = "%a %d %b %Y %H:%M:%S",
					filename = "test.log",
					filemode = "w")

logging.debug("debug message")
logging.info("info message")
logging.warning("waring message")
logging.error("error message")
logging.critical("critical message")

输入结果如下,

Thu 16 Aug 2018 12:48:50, experiment.py[line:1762] DEBUG debug message
Thu 16 Aug 2018 12:48:50, experiment.py[line:1763] INFO info message
Thu 16 Aug 2018 12:48:50, experiment.py[line:1764] WARNING waring message
Thu 16 Aug 2018 12:48:50, experiment.py[line:1765] ERROR error message
Thu 16 Aug 2018 12:48:50, experiment.py[line:1766] CRITICAL critical message

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

filename:用指定的文件名创建FiledHandler(后边会具体讲解handler的概念),这样日志会被存储在指定的文件中。

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对象
import logging

logger = logging.getLogger()
# 创建一个handler,用于写入日志文件
fh = logging.FileHandler('test.log')

# 再创建一个handler,用于输出到控制台
ch = logging.StreamHandler()

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

fh.setFormatter(formatter)
ch.setFormatter(formatter)

# 定义一个filter
filter = logging.Filter('mylogger')
fh.addFilter(filter)
ch.addFilter(filter)

# logger.addFilter(filter)
logger.addHandler(fh)
logger.addHandler(ch)

logger.setLevel(logging.DEBUG)

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')

logger1 = logging.getLogger('mylogger')
logger1.setLevel(logging.DEBUG)

logger2 = logging.getLogger('mylogger')
logger2.setLevel(logging.INFO)

logger1.addHandler(fh)
logger1.addHandler(ch)

logger2.addHandler(fh)
logger2.addHandler(ch)

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

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



—-re模块



re的元字符
import re

ret = re.findall("e..a", "Hermaeus") #"."是模糊表示一个字符
print(ret)

['erma']

ret = re.findall("^H..m", "Hermaeus") #"^"表示开头必须是某个字符
print(ret)

['Herm']

ret = re.findall("a..s$", "Hermaeus") #"$"表示结尾必须是1某个字符
print(ret)

['aeus']

ret = re.findall("abc*", "abccccc")  #贪婪匹配[0,+∞]
print(ret)

['abccccc']

ret = re.findall("abc+", "abccccc")  #贪婪匹配[1,+∞]
print(ret)

['abccccc']

ret = re.findall("abc?", "abccccc")  #惰性匹配[0,1]
print(ret)

['abc']


ret = re.findall("abc{1,3}", "abccccc")   #贪婪匹配[1,3]
print(ret)

['abccc']

ret = re.findall("abc*?", "abccccc")  #这样会让其变成惰性匹配
print(ret)

['ab']


字符集===>“[ ]”

即在字符集中任意匹配一个

import re

ret = re.findall("a[bc]", "abc")
print(ret)

['ab']

ret = re.findall("[a-z]", "ahfao")
print(ret)

['a', 'h', 'f', 'a', 'o']

ret = re.findall("[%$]", "ada%jf$")  #在字符集里面除了:- ^ \以外都是普通字符
print(ret)

['%', '$']

##" - " 取在这之间的
ret = re.findall("[1-9]", "1jkah1k2k3j")
print(ret)

['1', '1', '2', '3']

##" ^ " 取反,即在这字符集里面没有的
ret = re.findall("[^ab]", "7ajkf2bfjab")
print(ret)

['7', 'j', 'k', 'f', '2', 'f', 'j']

##" \ " 转义符
ret = re.findall("[\d]", "45dhaf2")
print(ret)

['4', '5', '2']


转义符
反斜杠后边跟元字符去除特殊功能,比如\.
反斜杠后边跟普通字符实现特殊功能,比如\d

\d  匹配任何十进制数;它相当于类 [0-9]。
\D 匹配任何非数字字符;它相当于类 [^0-9]。
\s  匹配任何空白字符;它相当于类 [ \t\n\r\f\v]。
\S 匹配任何非空白字符;它相当于类 [^ \t\n\r\f\v]。
\w 匹配任何字母数字字符;它相当于类 [a-zA-Z0-9_]。
\W 匹配任何非字母数字字符;它相当于类 [^a-zA-Z0-9_]
\b  匹配一个特殊字符边界,比如空格 ,&,#等
import re
ret = re.findall(r"I\b", "I am LIST") ##添加"r",表示把需要匹配的字符集直接交给模块处理
print(ret)


分组 “( )”
import re
ret = re.search("(?P<id>\d{2})/(?P<name>\w{3})", "23/com") #"?P<id> 取名
print(ret)

<_sre.SRE_Match object; span=(0, 6), match='23/com'> #返回一个对象

print(ret.group())

23/com

print(ret.group("id"))

23



“|”符号

import re

ret = re.search("(ab)|\d", "rabjk4fk") #二选一
print(ret)
print(ret.group())


re下的常用方法
import re
#1
re.findall('a','alvin yuan')    #返回所有满足匹配条件的结果,放在列表里
#2
re.search('a','alvin yuan').group()  #函数会在字符串内查找模式匹配,只到找到第一个匹配然后返回一个包含匹配信息的对象,该对象可以;通过调用group()方法得到匹配的字符串,如果字符串没有匹配,则返回None。
 
#3
re.match('a','abc').group()     #同search,不过尽在字符串开始处进行匹配
 
#4
ret=re.split('[ab]','abcd')     #先按'a'分割得到''和'bcd',在对''和'bcd'分别按'b'分割
print(ret)#['', '', 'cd']
 
#5
ret=re.sub('\d','abc','alvin5yuan6',1)
print(ret)#alvinabcyuan6     #替代
ret=re.subn('\d','abc','alvin5yuan6')
print(ret)#('alvinabcyuanabc', 2)  #替代,且可以选择次数
 
#6
obj=re.compile('\d{3}')  #对象赋予
ret=obj.search('abc123eeee')
print(ret.group())#123

#7
ret=re.finditer('\d','ds3sy4784a')
print(ret)        #<callable_iterator object at 0x10195f940>
 	              #返回迭代器
print(next(ret).group())
print(next(ret).group())

注意:

import re
 
ret=re.findall('www.(baidu|oldboy).com','www.oldboy.com')
print(ret)['oldboy']     #这是因为findall会优先把匹配结果组里内容返回,如果想要匹配结果,取消权限即可
 
['oldboy']

ret=re.findall('www.(?:baidu|oldboy).com','www.oldboy.com')
print(ret)

['www.oldboy.com']



版权声明:本文为qq_41700104原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。