Fork me on GitHub

常用模块二

re模块

1 正则表达式

介绍

​ 正则就是用一些具有特殊含义的符号组合到一起(称为正则表达式)来描述字符或者字符串的方法。或者说:正则就是用来描述一类事物的规则。(在python中)它内嵌在Python中,并通过re模块实现。正则表达式模式被编译成一系列的字节码,然后用C编写的匹配引擎执行。

字符组

字符组 : [字符组]
在同一个位置可能出现的各种字符组成了一个字符组,在正则表达式中用[]表示
字符分为很多类,比如数字、字母、标点等等。
假如你现在要求一个位置"只能出现一个数字",那么这个位置上的字符只能是0、1、2...9这10个数之一。
正则 待匹配字符 匹配结果 说明
[0123456789] 8 True 在一个字符组里枚举合法的所有字符,字符组里的任意一个字符和"待匹配字符"相同都视为可以匹配
[0123456789] a False 由于字符组中没有"a"字符,所以不能匹配
[0-9] 7 True 也可以用-表示范围,[0-9]就和[0123456789]是一个意思
[a-z] s True 同样的如果要匹配所有的小写字母,直接用[a-z]就可以表示
[A-Z] B True [A-Z]就表示所有的大写字母

字符:

元字符 匹配内容
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线
\s 匹配任意的空白符
\d 匹配数字
\n 匹配一个换行符
\t 匹配一个制表符
\b 匹配一个单词的结尾
^ 匹配字符串的开始
$ 匹配字符串的结尾
\W 匹配非字母或数字或下划线
\D 匹配非数字
\S 匹配非空白符
a|b 匹配字符a或字符b
() 匹配括号内的表达式,也表示一个组
[...] 匹配字符组中的字符
[^...] 匹配除了字符组中字符的所有字符

3 .^$

正则 待匹配字符 匹配 结果 说明
海. 海燕海娇海东 海燕海娇海东 匹配所有"海."的字符
^海. 海燕海娇海东 海燕 只从开头匹配"海."
海.$ 海燕海娇海东 海东 只匹配结尾的"海.$"

4 *+?{}

正则 待匹配字符 匹配 结果 说明
李.? 李杰和李莲英和李二棍子 李杰 李莲 李二 ?表示重复零次或一次,即只匹配"李"后面一个任意字符
李.* 李杰和李莲英和李二棍子 李杰和李莲英和李二棍子 *表示重复零次或多次,即匹配"李"后面0或多个任意字符
李.+ 李杰和李莲英和李二棍子 李杰和李莲英和李二棍子 +表示重复一次或多次,即只匹配"李"后面1个或多个任意字符
李. 李杰和李莲英和李二棍子 李杰和 李莲英 李二棍 {1,2}匹配1到2次任意字符

注意:前面的*,+,?等都是贪婪匹配,也就是尽可能匹配,后面加?号使其变成惰性匹配

正则 待匹配字符 匹配 结果 说明
李.*? 李杰和李莲英和李二棍子 李 李 李 惰性匹配

4 字符集 [] [^]

正则 待匹配字符 匹配 结果 说明
李[杰莲英二棍子]* 李杰和李莲英和李二棍子 李杰 李莲英 李二棍子 表示匹配"李"字后面[杰莲英二棍子]的字符任意次
李[^和]* 李杰和李莲英和李二棍子 李杰 李莲英 李二棍子 表示匹配一个不是"和"的字符任意次
[\d] 456bdha3 4 5 6 3 表示匹配任意一个数字,匹配到4个结果
[\d]+ 456bdha3 456 3 表示匹配任意多个数字,匹配到2个结果

5 分组 ()与 或 |[^]

身份证号码是一个长度为15或18个字符的字符串,如果是15位则全部🈶️数字组成,首位不能为0;如果是18位,则前17位全部是数字,末位可能是数字或x,下面我们尝试用正则来表示:

正则 待匹配字符 匹配 结果 说明
[1]\d{13,16}[0-9x]$ 110101198001017032 110101198001017032 表示可以匹配一个正确的身份证号
[2]\d{13,16}[0-9x]$ 1101011980010170 1101011980010170 表示也可以匹配这串数字,但这并不是一个正确的身份证号码,它是一个16位的数字
[3]\d{14}(\d{2}[0-9x])?$ 1101011980010170 False 现在不会匹配错误的身份证号了()表示分组,将\d{2}[0-9x]分成一组,就可以整体约束他们出现的次数为0-1次
^([1-9]\d{16}[0-9x]|[1-9]\d{14})$ 110105199812067023 110105199812067023 表示先匹配[1-9]\d{16}[0-9x]如果没有匹配上就匹配[1-9]\d{14}

6 转义符\

在正则表达式中,有很多有特殊意义的是元字符,比如\n和\s等,如果要在正则中匹配正常的"\n"而不是"换行符"就需要对""进行转义,变成'\'。

在python中,无论是正则表达式,还是待匹配的内容,都是以字符串的形式出现的,在字符串中\也有特殊的含义,本身还需要转义。所以如果匹配一次"\n",字符串中要写成'\n',那么正则里就要写成"\\n",这样就太麻烦了。这个时候我们就用到了r'\n'这个概念,此时的正则是r'\n'就可以了。

正则 待匹配字符 匹配 结果 说明
\n \n False 因为在正则表达式中\是有特殊意义的字符,所以要匹配\n本身,用表达式\n无法匹配
\n \n True 转义\之后变成\\,即可匹配
"\\n" '\n' True 如果在python中,字符串中的'\'也需要转义,所以每一个字符串'\'又需要转义一次
r'\n' r'\n' True 在字符串之前加r,让整个字符串不转义

7 贪婪匹配

贪婪匹配:在满足匹配时,匹配尽可能长的字符串,默认情况下,采用贪婪匹配

几个常用的非贪婪匹配Pattern
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

8 常用方法

import re

msg = re.findall('a', 'abc')
print(msg)
# 返回所有满足匹配条件的结果,放在列表里
msg = re.search('a', 'abc')
print(msg)
# 返回<re.Match object; span=(0, 1), match='a'>,但如果没有匹配可以返回None
msg = re.search('a', 'abc').group()
print(msg)
# 只能返回匹配到的值,没有则报错

msg = re.match('a', 'abc').group()
print(msg)
# 同search,不过仅在字符串开始处进行匹配
msg = re.split('[ab]', 'abcd')
# 先按'a'分割得到''和'bcd',在对''和'bcd'分别按'b'分割
print(msg)
# ['', '', 'cd']

obj = re.compile('\d{3}')
# 将正则表达式编译成为一个 正则表达式对象,规则要匹配的是3个数字
msg = obj.search('aaaa123dddd')
# 正则表达式对象调用search,参数为待匹配的字符串
print(msg.group())
# 结果 : 123

示范

# 1、匹配密码,密码必须是由6位数字与字母组成,并且不能是纯数字也不能是纯字母
# 1.1 知识点:# ?!pattern,表示在没有配到pattern的字符串的前提下,再进行后续的正则表达式匹配,后续匹配仍然从被匹配字符串的头开始

# 1.2 答案:
print(re.search("(?![0-9]+$)(?!^[a-zA-Z]+$)^[0-9A-Za-z]{6}$", "1211k1"))  # 123asf
#           如果不是全都是字母或是数字所以需要^和$去固定 排除了特殊符号

#1.3 解释:
#上述正则的意思为:在匹配(?!^[0-9]+$)以及(?!^[a-zA-Z]+$)过后,如果字符串成功后在从头去匹配(?!^[a-#zA-Z]+$),最终匹配完。
while True:
    pwd=input('please input your password:')
    pwd_pattern=re.compile('(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#%&])^([a-zA-Z0-9!@#%&]){6,}$')
    if pwd_pattern.search(pwd) is None:
        print('密码强度不够,重新输入')
    else:
        print('强度够')

# 遇到了?=断言成功会回到起始断言的点,
# 只要有一个就进行下一个判断,所以有.*为前缀,去找最远的需要的字符
# re.compile:在模式匹配发生之前,正则表达式模式必须编译成正则表达式对象,
# 由于正则表达式在执行过程中将进行多次比较操作,因此强烈建议使用预编译,
# 既然正则表达式的编译是必须得,那么使用预编译来提升性能无疑是明智之举

# 2、匹配密码,密码强度:强,必须包含大写,小写和数字,和特殊字符(!,@,#,%,&),且大于6位
# 2.1 知识点:# ?=pattern,表示在配到pattern的字符串的前提下,再进行后续的正则表达式匹配,后续匹配仍然从被匹配字符串的头开始

# 2.2 答案:
while True:
    pwd = input("please your password: ").strip()  # 比如输入:Aa3@adf123
    pwd_pattern= re.compile("(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#%&])^([a-zA-Z0-9!@#%&]){6,}$")
    if pwd_pattern.search(pwd) is None:
        print("密码强度不够")
    else:
        break
# ?=如果是,如果是有数字、大小写字母、特殊符号(有一个即可),再进行正则则是都有
# 2.3 解释:
# 上述正则表达式的意思:
# (1)首先是(?=.*[A-Z])匹配,.*表示密码中可以包含多个字符,[A-Z]代表密码中需要包含至少一个大写字母,注意一定不要去掉.*写成(?=[A-Z]),那样表示密码只能由一个字符组成,该字符是大写字母
# (2)其次是(?=.*[a-z])匹配,同上,确保密码中必须至少有一个小写字母, .识别所有字母,*识别该字母出现的所有次数
# (3)然后是(?=.*[0-9])匹配,同上,确保密码中必须至少有一个数字
# (4)然后是(?=.*[!@#%&])匹配,同上,,确保密码中必须至少有一个特殊符号!@#%&
# (5)最后是^([a-zA-Z0-9!@#%&]){6,}$,确保密码是由[a-zA-Z0-9!@#%&]字符构成,至少有6位

# 3、匹配email

"18611323113@163.com xxx@qq.com"
print(re.findall('(?:[0-9a-z]+)@(?:[0-9a-z]+).com','18611323113@163.com xxx@qq.com'))
# (?:):()是将()里的内容独立出来,并且整个只打印()里的内容,而(?:)即保证了()的独立性,还能打印出所有信息
# []+:如果从里面任意出一个值进行匹配但只能匹配一次,通过+这个值可以匹配无限次(放在一个字符串下)

# 4、匹配身份证
while True:
    your_id=input(">>: ").strip()
    your_id_pattern=re.compile('^[1-9][0-9]{16,}([0-9]|X)$')
    if your_id_pattern.search(your_id) is None:
        print('输入错误')
    else:
        print('输入正确')
        
# 5、匹配用户名,包含字母或者数字,且8位
print(re.findall("^[0-9a-zA-Z]{8}$","egonlinh"))
print(re.findall('^[0-9a-zA-Z]{8}$','egonssss'))
        
# 6、取出字符串里的数字
print(re.findall(r'\d+(?:\.\d+)?', 'sww123kw11.333e2lkd'))
print(re.findall(r'(\d+\.?\d*)','sww123kw11.333e2lkd'))

# 7、取出所有负整数
# print(re.findall(r'-\d+', '-12,3,54,-13.11,64,-9'))  # 错误答案,小数被无视
print(re.findall(r'(?!-\d+\.\d+)-\d+', '-12,3,54,-13.11,64,-9'))
#如果不是负小数,取出负整数

# 8、所有数字
# '-12.9,3.92,54.11,64,89,-9,-45.2'
print(re.findall(r'(?:-\d+|\d+)\.?\d*','-12.9,3.92,54.11,64,89,-9,-45.2'))
#负整数或是正整数,分组出来之后可以匹配到小数

# 9、所有负数
# ', '-12.9,3.92,54.11,64,89,-9,-45.2'))
print(re.findall(r'-\d+\.?\d*','-12.9,3.92,54.11,64,89,-9,-45.2'))
#整数必须要有至少有一次,小数可有可无所以小数点零次或一次

# 10、所有的非负浮点数
print(re.findall(r'\d+\.\d+', '-12.9,3.92,54.11,64,89,-9,-45.2'))

hashlib模块

1 算法介绍

Python的hashhlib提供了常见的摘要算法,如MD5、SHA1等等。

什么是摘要算法呢?摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。

摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。

摘要算法之所以能被指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。

我们以常见的摘要算法MD5为例,计算出一个字符串的MD5值:

hash值具备以下三个特点:
    1、如果传入的内容一样,并且采用的hash算法也一样,那么得到的hash值一定是一样的
    2、hash值得长度取决于采用的算法,与传入的文本内容的大小无关
    3、hash值不可逆

我们以常见的摘要算法MD5为例,计算出一个字符串的MD5值:

m = hashlib.md5()
m.update('你好'.encode('utf-8'))
m.update('egon'.encode('utf-8'))
m.update('哈哈哈'.encode('utf-8'))
res = m.hexdigest()
print(res)
#得到的hash值:824cb1b104e36de8f98ef38fbee759a5

如果数据量很大,可以分块多次调用update(),最后计算的结果是一样的:

m1 = hashlib.md5('你'.encode('utf-8'))
m1.update('好eg'.encode('utf-8'))
m1.update('on哈哈哈'.encode('utf-8'))
aaa = m1.hexdigest()
print(aaa)
#得到的hash值:824cb1b104e36de8f98ef38fbee759a5

# 同样的内容同样的算法得到的hash值是一样的,
# 不会因为加密方式的不同而产生差别
# 如果是同样的算法但是不同内容,其hash值的
# 长度是一样的

从文件中读取文件加密

m2 = hashlib.md5()
with open('db.txt', mode='rb')as f:
    for line in f:
        m2.update(line)
    print(m2.hexdigest())
# 循环文件中每一行内容并依次编译成hash文件,避免了文件过大撑爆内存
# 但是编译内容是文件的所有内容,导致加密时间过长,可以通过指针操作
# 值读取里面某一行内容加密
m2 = hashlib.md5()
with open('db.txt', mode='rb')as f:
    f.seek(-3, 2)
    data = f.read()
    m2.update(data)
    print(m2.hexdigest())

2 摘要算法应用

如果以明文保存用户口令,如果数据库泄露,所有用户的口令就落入黑客的手里。此外,网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。正确的保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:

username | password
---------+---------------------------------
michael  | e10adc3949ba59abbe56e057f20f883e
bob      | 878ef96e86145580c38c87f0410ad153
alice    | 99b1c2188db85afee403b1536010c2c9

考虑这么个情况,很多用户喜欢用123456,888888,password这些简单的口令,于是,黑客可以事先计算出这些常用口令的MD5值,得到一个反推表:

'e10adc3949ba59abbe56e057f20f883e': '123456'
'21218cca77804d2ba1922c33e0151105': '888888'
'5f4dcc3b5aa765d61d8327deb882cf99': 'password'

这样,无需破解,只需要对比数据库的MD5,黑客就获得了使用常用口令的用户账号。

对于用户来讲,当然不要使用过于简单的口令。但是,我们能否在程序设计上对简单口令加强保护呢?

由于常用口令的MD5值很容易被计算出来,所以,要确保存储的用户口令不是那些已经被计算出来的常用口令的MD5,这一方法通过对原始口令加一个复杂字符串来实现,俗称“加盐”:

hashlib.md5("salt".encode("utf8"))

经过Salt处理的MD5口令,只要Salt不被黑客知道,即使用户输入简单口令,也很难通过MD5反推明文口令。

但是如果有两个用户都使用了相同的简单口令比如123456,在数据库中,将存储两条相同的MD5值,这说明这两个用户的口令是一样的。有没有办法让使用相同口令的用户存储不同的MD5呢?

如果假定用户无法修改登录名,就可以通过把登录名作为Salt的一部分来计算MD5,从而实现相同口令的用户也存储不同的MD5。

摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。

logging模块

1 日志级别

CRITICAL = 50 #FATAL = CRITICAL
ERROR = 40
WARNING = 30 #WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0 #不设置

2 默认级别为warning,默认打印到终端

import logging

logging.debug('调试debug')
logging.info('消息info')
logging.warning('警告warn')
logging.error('错误error')
logging.critical('严重critical')

'''
WARNING:root:警告warn
ERROR:root:错误error
CRITICAL:root:严重critical
'''

3 为logging模块指定全局配置

#======介绍
可在logging.basicConfig()函数中可通过具体参数来更改logging模块默认行为,可用参数有
filename:用指定的文件名创建FiledHandler(后边会具体讲解handler的概念),这样日志会被存储在指定的文件中。
filemode:文件打开方式,在指定了filename时使用这个参数,默认值为“a”还可指定为“w”。
format:指定handler使用的日志显示格式。
datefmt:指定日期时间格式。
level:设置rootlogger(后边会讲解具体概念)的日志级别
stream:用指定的stream创建StreamHandler。可以指定输出到sys.stderr,sys.stdout或者文件,默认为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用户输出的消息




#========使用
import logging
logging.basicConfig(filename='access.log',
                    format='%(asctime)s - %(name)s - %(levelname)s -%(module)s:  %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S %p',
                    level=10)

logging.debug('调试debug')
logging.info('消息info')
logging.warning('警告warn')
logging.error('错误error')
logging.critical('严重critical')





#========结果
access.log内容:
2017-07-28 20:32:17 PM - root - DEBUG -test:  调试debug
2017-07-28 20:32:17 PM - root - INFO -test:  消息info
2017-07-28 20:32:17 PM - root - WARNING -test:  警告warn
2017-07-28 20:32:17 PM - root - ERROR -test:  错误error
2017-07-28 20:32:17 PM - root - CRITICAL -test:  严重critical

part2: 可以为logging模块指定模块级的配置,即所有logger的配置

4 Formatter,Handler,Logger,Filter对象

#logger:产生日志的对象

#Filter:过滤日志的对象

#Handler:接收日志然后控制打印到不同的地方,FileHandler用来打印到文件中,StreamHandler用来打印到终端

#Formatter对象:可以定制不同的日志格式对象,然后绑定给不同的Handler对象使用,以此来控制不同的Handler的日志格式

5 应用

#


import os

# 1、定义三种日志输出格式,日志中可能用到的格式化串如下
# %(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用户输出的消息
LOG_PATH_DIR = os.path.join(os.path.dirname(__file__), 'log')
BASE_PATH_NAME = 'access.log'

if not os.path.isdir(LOG_PATH_DIR):  # 是否存在改文件
    os.mkdir(LOG_PATH_DIR)  # 不存在则创建

LOG_BASE_PATH = os.path.join(LOG_PATH_DIR, BASE_PATH_NAME)

# 2、强调:其中的%(name)s为getlogger时指定的名字
standard_format = '%(asctime)s - %(name)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s'

simple_format = '%(asctime)s  - %(levelname)s - %(message)s'

# test_format = '%(asctime)s] %(message)s'

# 3、日志配置字典
LOGGING_DIC = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'standard': {
            'format': standard_format
        },
        'simple': {
            'format': simple_format
        },
    },
    'filters': {},
    'handlers': {
        # 打印到终端的日志
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',  # 打印到屏幕
            'formatter': 'simple'
        },
        # 打印到文件的日志,收集info及以上的日志
        'file1': {
            'level': 'DEBUG',
            'class': 'logging.handlers.RotatingFileHandler',  # 保存到文件,日志轮转
            'formatter': 'standard',
            # 可以定制日志文件路径
            # BASE_DIR = os.path.dirname(os.path.dirname(__file__))  # log文件的目录
            # LOG_PATH = os.path.join(BASE_DIR,"log",'a1.log')
            'filename': LOG_BASE_PATH,  # 日志文件 指定文件路径
            'maxBytes': 100,  # 日志大小 5M
            'backupCount': 5,  # 封顶轮转生成5个文件去保存日志内容
            'encoding': 'utf-8',  # 日志文件的编码,再也不用担心中文log乱码了
        },
        'file2': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',  # 保存到文件
            'formatter': 'standard',
            'filename': 'a2.log',  # 默认文件路径 直接保存在当前执行文件的文件夹下
            'encoding': 'utf-8',
        },
    },
    'loggers': {
        # logging.getLogger(__name__)拿到的logger配置
        # '': {
        #     'handlers': ['default', 'console'],  # 这里把上面定义的两个handler都加上,即log数据既写入文件又打印到屏幕
        #     'level': 'DEBUG', # loggers(第一层日志级别关限制)--->handlers(第二层日志级别关卡限制)
        #     'propagate': False,  # 默认为True,向上(更高level的logger)传递,通常设置为False即可,否则会一份日志向上层层传递
        # },

        '': {  # 可以适用于任何的命名
            'handlers': ['file1', 'console', 'file2'],
            'level': 'DEBUG',
            'propagate': False,
        },

    },
}

使用

import settings

# !!!强调!!!
# 1、logging是一个包,需要使用其下的config、getLogger,可以如下导入
# from logging import config
# from logging import getLogger

# 2、也可以使用如下导入
import logging.config # 这样连同logging.getLogger都一起导入了,然后使用前缀logging.config.

# 3、加载配置
logging.config.dictConfig(settings.LOGGING_DIC)

# 4、输出日志
logger1=logging.getLogger('用户交易')
logger1.info('egon儿子alex转账3亿冥币')

# logger2=logging.getLogger('专门的采集') # 名字传入的必须是'专门的采集',与LOGGING_DIC中的配置唯一对应
# logger2.debug('专门采集的日志')



  1. 1-9 ↩︎

  2. 1-9 ↩︎

  3. 1-9 ↩︎

posted @ 2020-09-15 21:35  artherwan  阅读(206)  评论(0编辑  收藏  举报