week4 day3/4 常用模块

week4 day3/4 常用模块

    • 一. 正则模块 import re(字符串匹配)
    • 1.1 基本操作和工作原理
    • 1.2 应用
    • 二. 序列化模块 import json/pickle(存档和跨平台共享数据)
    • 2.1 json模块
    • 2.2 pickle模块
    • 2.3 ujson模块(猴子补丁)
    • 三. 随机模块 import random(验证码)
    • 四. 哈希算法模块 import hashlib(检验文件传输完整性和密码加盐)
    • 五. 子进程模块 import subprocess
    • 六. os/sys模块 import os/sys(重要!)
    • 七. 配置模块 import configparser(重要!)
    • 八. 时间模块 import time/datetime(重要!)

书接上回,上次我们讲到了日志模块。对于很多项目,记录用户/应用程序的日常操作和用户信息的变动都是非常必要的。因此日志模块在python日常使用中频率还是比较高的。今天,我们继续来介绍几个常见的模块,来提高我们日常编程效率和编程宽度。

一. 正则模块 import re(字符串匹配)

正则模块主要是针对python中的字符串数据格式的一个模块。可以将字符串中的某些我们想要取出的字符取出来。

1.1 基本操作和工作原理

import re
res=re.findall('abc','abcxxxabcyyyabc')
print(res)----->['abc','abc','abc']

通过这个小例子我们先来了解一些正则模块使用的语法规范。通过调用re模块下面的findall功能来进行下一步操作。而使用findall的语法标准是re.findall(搜索的字符串,被搜索的字符串)。而具体的工作机制是这样的,拿着搜索的字符串'abc'去和被搜索的字符串'abcxxxabcyyyabc'从头开始一个一个作比较,匹配到一样的字符继续匹配,直到完全匹配所有搜索的字符'abc'。如果匹配到了'ab'但是没有匹配到'c',则从'a'匹配到的那个字符的下一个字符重新开始匹配。

# \w代表匹配数字字母下划线
import re
res=re.findall('\w','hello123_+-=') # 单个字符匹配
print(res)-----># ['h','e','l','l','o','1','2','3','_']
res1=re.findall('a\wb','a1b aab ab a_ba=b') # 多个字符匹配
print(res1)-----># ['a1b', 'aab', 'a_b'

# \W代表匹配除了数字字母下划线以外的其他字符
res2=re.findall('\w','hello123_+-=') # 单个字符匹配
print(res2)-----># ['+','-','=']
res3=re.findall('a\Wf','a fabfa3fa*f') # 多个字符匹配
print(res3)-----># ['a f', 'a*f']
# \s代表空白字符,不止有空格,还有\n\t
import re
res=re.findall('\s','a b\n\tj')
print(res)-----># [' ', '\n', '\t']
res1=re.findall('a\sb','a ba\nba\tb')
print(res1)-----># ['a b', 'a\nb', 'a\tb']

# \S代表非空白字符,非空格,非\n\t
res2=re.findall('\S','a b   \n\tc123')
print(res2)-----># ['a', 'b', 'c', '1', '2', '3']
# \d代表数字
import re
res=re.findall('\d','a1b26\n\tj')
print(res)-----># ['1', '2', '6']
res1=re.findall('a\db','a ba44ba5b')
print(res1)-----># ['a5b']

# \D代表非数字
res2=re.findall('\D','a b123')
print(res2)-----># ['a', 'b', ' ']
res3=re.findall('a\Dd','a da3d')
print(res3)-----># ['a d']
# ^代表只从开头找
import re
res=re.findall('^ab','aab26\n\tj')
print(res)-----># []

# $代表只从结尾找
res1=re.findall('ab$','abab')
print(res1)-----># ['ab']

# 当^和$连用时,只能找到匹配字符和被匹配字符完全相同的字符
res2=re.findall('^egon$','egon') # ['egon'] 只找开头是e结尾是n,中间是go的字符,如果被匹配字符串还有其他内容,什么也匹配不出来
print(res2)-----># ['egon']
res3=re.findall('^egon$','egonegon')
print(res3)-----># []
# 当使用^和$连用,可以搜索多行字符
print(re.findall('^egon$','''
egon
egon123
egon
''',re.M))-----># ['egon','egon'] # 寻找每一行以e开头n结尾,go在中间的字符
# .代表除了换行符以外的任意字符
import re
res=re.findall('.','a1b26\n\tj')
print(res)-----># ['a', '1', '2','6','\t','j']
print(re.findall('a.c','a1c a2c aAc a+c a\nc aaaac'))-----># ['a1c', 'a2c', 'aAc', 'a+c', 'aac']
print(re.findall('a.c','a1c a2c aAc a+c a\nc aaaaccccccc',re.DOTALL))-----># ['a1c', 'a2c', 'aAc', 'a+c', 'a\nc', 'aac'] 
# re.DOTALL相当于让.可以匹配换行符,也可以匹配多字符里面的符合要求的字符串
print(re.findall('a[1+]c','a1caca2c aAc a+c a\nc aaaac'))-----># ['a1c', 'a+c']
print(re.findall('a[0-9]c','a1c a2c aAc a+c a\nc aaaac a10c',re.DOTALL))-----># ['a1c', 'a2c']

print(re.findall('a[-+%*]c','a1c a2c aAc a+c a\nc aaaac a10c',re.DOTALL)) # 只能匹配中间a和c之间是加减乘除符号的字符。但注意,-左右两边不能同时有东西

print(re.findall('a[^-+%*]c','a1c a2c aAc a+c a\nc aaaac a10c',re.DOTALL)) # ^ 在中括号中代表取反的意思,除了中括号内的字符,都能匹配
# 接下来是贪婪匹配 ? * + {}
import re
# ?左边那个字符出现0次或者1次
res=re.findall('da?','daa jhueadda')
print(res)-----># ['da', 'd', 'da']
print(re.findall('a\w?',' a abbbbbbbbbbbb ab'))-----># ['a','ab','ab']
print(re.findall('ab{0,1}',' a abbbbbbbbbbbb ab'))-----># ['a', 'ab', 'ab']

# *左边那个字符出现0次或无数次
print(re.findall('ab*',' a abbbbbbbbbbbb ab'))-----># ['a', 'abbbbbbbbbbbb', 'ab']
print(re.findall('ab{0,}',' a abbbbbbbbbbbb ab'))-----># ['a', 'abbbbbbbbbbbb', 'ab']

# +左边那个字符出现1次或无数次
print(re.findall('ab+',' a abbbbbbbbbbbb ab'))-----># ['abbbbbbbbbbbb', 'ab']
print(re.findall('ab{1,}',' a abbbbbbbbbbbb ab'))-----># ['abbbbbbbbbbbb', 'ab']
print(re.findall('a.*?b','a123b ab'))-----># ['a123b', 'ab']
print(re.findall('a.+?b','a123b ab'))-----># ['a123b']
# {n,m}:左边那一个字符出现n次或m次
print(re.findall('ab{2,4}',' a abbbbbbbbbbbb ab'))-----># ['abbbb']
print(re.findall('ab{2,4}',' a abb abbb abbbbbbbbbbbb ab'))-----># ['abb', 'abbb', 'abbbb']

# 出现在贪婪匹配后面的 ? 可以消除贪婪匹配的效果,见例
print(re.findall('a.*c','ayirfyg=-csfhoiushc'))-----># ['ayirfyg=-csfhoiushc']
print(re.findall('a.*?c','ayirfyg=-csfhoiushc'))-----># ['ayirfyg=-c']
# ()小括号不改变匹配规则,改变产出结果
print(re.findall("href='(.+?)'","< a href='https://www.baidu.com.cn'>'我特么是百度啊'< a href='https://www.sina.com.cn'>'我特么是新浪啊'"))-----># ['https://www.baidu.com.cn', 'https://www.sina.com.cn']
# 而如果想保留小括号分组查询功能,但是去除只保留分组结果的效果,可以在小括号内加?:
print(re.findall("href='(?:.+?)'","< a href='https://www.baidu.com.cn'>'我特么是百度啊'< a href='https://www.sina.com.cn'>'我特么是新浪啊'"))-----># ["href='https://www.baidu.com.cn'", "href='https://www.sina.com.cn'"]

print(re.findall('compan(?:y|ies)','Too many companies have gone bankrupt, and the next one is my company'))
print(re.findall('company|companies','Too many companies have gone bankrupt, and the next one is my company'))-----># ['companies', 'company']

1.2 应用

取出字符串'abc11usdfn33.4jhbjfhs11uf3.4'中的整数和小数。

# 个人尝试版本:只能取出小数
import re
res=re.findall('\d+\.\d','abc11usdfn33.4jhbjfhs11uf3.4')
print(res)-----># ['33.4','3.4']

二. 序列化模块 import json/pickle(存档和跨平台共享数据)

序列化就是把某种数据格式转变成字符串格式保存在文件里面,保存在硬盘里面。
反序列化就是把文件里面的字符串转化成原来的数据类型方便使用。
(我们之前学过的字符编码只针对了字符串数据类型,序列化针对几乎所有数据类型。)

序列化的作用:

  1. 存档和读档,保存数据状态
  2. 跨平台合并数据
# 补充知识点:eval可以运行字符串里面的代码
eval('print(111)')-----># 111

2.1 json模块

优点:跨平台性强(java,python,c都可以读)
缺点:并非所有数据类型都能覆盖(python的tuple无法存储)

import json
# json序列化 dump/dumps
dic={
     "name":123,"xxx":True,'yyy':None,'zzz':1.3}
dic_json=json.dumps
print(dic_json,type(dic_json))-----># {"name": 123, "xxx": true, "yyy": null, "zzz": 1.3} 
with open('a.json',mode='wt',encoding='utf-8') as f:
    f.write(dic_json)
# 上面的json序列化和写入文件两步操作可以合二为一,也就是dump操作
# dump语法:json.dump(被序列化内容,存储路径)
json.dump(dic,open('a.json',mode='wt',encoding='utf-8'))

# json反序列化 load/loads
res=json.loads(dict_json)
print(res,type(res))-----># {'name': 123, 'xxx': True, 'yyy': None, 'zzz': 1.3} 
with open('a.json',mode='rt',encoding='utf-8') as f:
    res=f.read()
    res_json=json.loads(res)
# 上面的json反序列化和从文件读出两步操作可以合二为一,也就是load操作
# load语法:json.load(打开文件的方式)
resdic=json.load(open('a.json',mode='rt',encoding='utf-8'))
print(resdic)----->被序列化的内容

2.2 pickle模块

缺点:跨平台性差(python所有数据类型都覆盖,导致有些python独有数据类型无法被别的语言识别)
优点:数据类型广,语言独有性强

2.3 ujson模块(猴子补丁)

ujson模块在序列化和反序列化的效率都比json模块要稍高一些,因此可以在导入json模块前可以先打上猴子补丁,具体操作见代码:

import ujson
def monkey_patch_json():
    json.dumps=ujson.dumps
    json.dump=ujson.dump
    json.loads=ujson.loads
    json.load=ujson.load
monkey_patch_json()

三. 随机模块 import random(验证码)

import random
print(random.random())#(0,1)----float    大于0且小于1之间的小数
print(random.randint(1,3))  #[1,3]    大于等于1且小于等于3之间的整数
print(random.randrange(1,3)) #[1,3)    大于等于1且小于3之间的整数
print(random.choice([1,'23',[4,5]]))#1或者23或者[4,5]
print(random.sample([1,'23',[4,5]],2))#列表元素任意2个组合
print(random.uniform(1,3))#大于1小于3的小数,如1.927109612082716

item=[1,3,5,7,9]
random.shuffle(item) # 打乱item的顺序,相当于‘洗牌’
# 小练习:做验证码
# 补充知识点:ord(),chr()
print(ord('a'))---->97
print(ord('z'))---->122
print(ord('A'))---->65
print(ord('Z'))---->90
# 验证码一般为6位,由数字,大写字母和小写字母组成的字符串
import random
def make_verify_code(max_size=6):
    str1=''
    for i in range(max_size):
        numpart=random.randint(0,9)
        upper_char=chr(random.randint(65,90))
        lower_char=chr(random.randint(97,122))
        str1+=random.choice([str(numpart),upper_char,lower_char])
    return str1
res=make_verify_code()
print(res)

四. 哈希算法模块 import hashlib(检验文件传输完整性和密码加盐)

hash算法:传入一段内容会得到一段hash值。

hash值有三大特点:

  1. 如果传入的内容与采用的算法一样,无论hash次数,得到的hash值都一样。
  2. 只要采用的算法是固定的,无论传入的值有多大,hash值的长度就是固定的,不会随着内容的增多而变长。
  3. hash值不可逆,即不能通过hash值反解出内容是什么

----------------------------------------------特点1+特点3可以校验文件的完整性。----------------------------------------------
当我们从网上下载任务完成时,我们通常需要等待一段时间。这段时间就是在检查下载完毕文件的完整性。这就要求我们从服务端下载时,除了被下载的具体内容外,还需要下载对应的hash值。下载完毕后,需要在用户端用同样的hash算法对下载内容hash,与下载文件的hash值做比对,如果两个hash值相同,说明文件下载完整。
----------------------------------------------特点1+特点2可以给密码加密。----------------------------------------------
服务端在用户注册时,只需要明确hash算法,保存用户的用户名和密码的hash值即可。每次用户登陆时,会先在用户端把密码使用服务端规定的hash算法拿到hash值,再将用户名和密码的hash值打包发送给服务端。服务端拿到用户输入的账号和密码的hash值,与数据库信息作比较。如果匹配,则用户登陆成功。

# hashilib模块使用语法:
import hashlib
m=hashlib.md5()
m.upadate('你好'.encode('utf-8'))
print(m.hexdigit())-----># 拿到你好在md5算法下的hash值
# 必须要先指定hash算法,然后利用update传入需要被hash的数据。最后hexdigit吐出hash值
# 校验文件完整性的具体操作:
mm=hashlib.md5()
with open(文件路径,mode='rb') as f:
	for line in f:
		mm.update(line)
	print(mm.hexdigit())
# 但是这种方法存在一个问题,如果文件很大,则需要把所有内容都在用户端拿到hash值与从服务端下载的hash值作比较,很耗费时间
# 因此,我们可以通过f.seek()移动指针,再通过f.read(字符)把取出一定数量字符,分段比较,可以大大减少检验文件完整性的时间,来代码实现:
mm=hashlib.md5()
with open(文件路径,mode='rb') as f:
    res=f.read(10)
    mm.update(res)
    while f.seek(10,1) <= f.seek(0,2):
        res1=f.read(10)
        mm.update(res1)
    print(mm.hexdigest())
# 文件加密操作:
# 如果我们的密码只是简单设置很可能被不法分子暴力破解,因此,我们可以通过在密码加盐的操作让不法分子提高破解密码的时间
mm=hashlib.md5()
mm.update('宝塔镇河妖')
mm.update('123wth')
mm.update('天王盖地虎')
print(mm.hexdigit())
# 我们原本的密码前后被添加了其他字符,哪怕破解后也不好确定哪部分是真正的密码
但其实密码加盐也不是那么安全。黑客可以截获到的hash值不破译。而直接再写一个跳过密码hash算法的程序,直接把账号和hash完的密码发送给服务端。这种黑客方法的破解方案在后面会着重介绍。

五. 子进程模块 import subprocess

我们在刚开始学习python的时候接触过进程的概念。进程是正在运行的程序。进程在内存中是相互隔离的,彼此不影响。

我们可以在Windows中使用cmd,输入tasklist来查询当前的所有进程。我们能不能在pycharm中通过同样的命令查询进程的数量呢?

import subprocess
import time
obj=subprocess.Popen('tasklist',
					shell=True)
time.sleep(1)

我们在运行这段代码时,产生了一个这段代码所在文件的进程。这段代码的功能是产生一个子进程。但是如果代码只写到这里,运行完这段代码后还未来得及把子进程的结果显示出来,文件的进程就结束了。所以需要导一下时间模块,让大进程原地等待子进程显示出结果。但是这种方法会让大进程停顿过长时间。我们需要用到subprocess里面又一个很骚的功能。在此之前,我们需要理清楚一个事情:两个进程是相互隔离的,所以子进程的结果无法直接被大进程获得。我们需要一个“管道”,让子进程把它产生的结果扔进去,大进程再从“管道”中把子进程的结果取出来。对代码作出修改:

import subprocess
obj=subprocess.Popen('tasklist',
					shell=True,
					stdout=subprocess.PIPE,
					stderr=subprocess.PIPE)
obj_stdout=obj.stdout.read() # 这个命令就在原地等,等子进程运行产生结果就拿到结果,如果没有结果,就在原地等。这个等待时间恰到好处
obj_stderr=obj.stderr.read() # 只有当命令产生错误结果的时候,子进程才会把错误新信息写到这个管道里面,才可以继续读到错误信息

print(obj_stdut) # 打印出来的结果是bytes类型。因为tasklist是windows的命令,windows默认字符编码格式是gbk
print(obj_stdout.decode('gbk')) # 输出结果正常
print(obj_stderr) # 如果产生错误信息会放在这里面

六. os/sys模块 import os/sys(重要!)

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命令,直接显示

print(os.environ)  # 获取系统环境变量 # ---------------------登录用户后在其他文件其他位置都可以用的到

# os.path.abspath(path)  返回path规范化的绝对路径

print(os.path.abspath('a/b/c'))

# os.path.split(path)  将path分割成目录和文件名二元组返回
# os.path.dirname(path)  返回path的目录。其实就是os.path.split(path)的第一个元素

print(os.path.dirname(__file__)) # 目录名 E:/pycharm/notes and homework/w4 1.4/day2 04日志模块

print(os.path.basename(__file__)) # 文件名 day4 03 os和sys模块.py

# 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所指向的文件或者目录的最后修改时间
# os.path.getsize(path) 返回path的大小

res=os.path.normpath(os.path.join(__file__,'..','..'))
print(res)

import sys
print(sys.argv) # 获取运行python程序时的参数
                # 拿到的是['E:/pycharm/notes and homework/w4 1.4/day4 03 os和sys模块.py']
# 修改复制文件的时候直接从cmd拿到运行文件名,原文件名,目标文件名
src_file=sys.argv[1]
dst_file=sys.argv[2]

七. 配置模块 import configparser(重要!)

在之前我们学习文件目录管理规范时,我们把所有的变量都放在conf文件夹下面的settings.py里面了,把settings.py文件当模块导入其他文件,但可能会出现循环导入的情况。这里,nb的python第三方库又给我们提供了一种模块来解决可能存在的循环导入的问题。

我们需要先建立一个文件,起名为config.ini或者config.cnf。把我们之前放在settings.py文件里面的数据放在这个配置文件里面。下图是配置文件中的数据:
week4 day3/4 常用模块_第1张图片
现在我们通过代码尝试把数据取出:

import configparser
config=configparser.ConfigParser()
config.read('config.ini') # 先指定要读的文件

res=config.sections() # 知道这个文件里面有几个sections
print(res)-----># ['section1', 'section2']
res1=config.options('sections') # 我们拿到了section1里面所有的’key‘
print(res1)-----># ['k1', 'k2', 'user', 'age', 'is_admin', 'salary']
res2=config.items('section1') # items可以取到对应sections的所有内容
print(res2)-----># [('k1', 'v1'), ('k2', 'v2'), ('user', 'egon'), ('age', '18'), ('is_admin', 'true'), ('salary', '31')]
res3=config.get('section1','k1') # 拿section1里面k1对应的值
print(res3,type(res3))-----># v1 

八. 时间模块 import time/datetime(重要!)

时间模块也是python提供的非常强大的模块,需要非常熟悉。

时间主要分成三种形式:

  1. 时间戳,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量。我们运行“type(time.time())”,返回的是float类型。
  2. 格式化的字符串对应time.strftime(format)
  3. 结构化的时间对应time.localtime()。struct_time元组共有9个元素共九个元素:(年,月,日,时,分,秒,一年中第几周,一年中第几天,夏令时)
import time
time.time()
time.sleep(秒数)
print(time.strftime('%Y-%m-%d %H:%M:%S'))-----># 2021-01-07 18:29:59
print(time.strftime(%T))-----># 如果你只想要时间的时分秒格式,%T即可。18:30:59
res=time.localtime()-----># time.struct_time(tm_year=2021, tm_mon=1, tm_mday=7, tm_hour=18, tm_min=31, tm_sec=58, tm_wday=3, tm_yday=7, tm_isdst=0)

上述三种时间格式是可以相互转换的。具体的转换方法参照图片:
week4 day3/4 常用模块_第2张图片
代码演示:

#--------------------------按图1转换时间
# localtime([secs])
# 将一个时间戳转换为当前时区的struct_time。secs参数未提供,则以当前时间为准。
time.localtime()
time.localtime(1473525444.037215)

# gmtime([secs]) 和localtime()方法类似,gmtime()方法是将一个时间戳转换为UTC时区(0时区)的struct_time。

# mktime(t) : 将一个struct_time转化为时间戳。
print(time.mktime(time.localtime()))#1473525749.0


# 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

# 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"。

除了time以外,还有一个时间模块也非常好用,就是datetime模块。当我们计算时间差时,可以把所有时间统一成time.time()计算差值,我们也可以通过datetime.timedelta来比较。第二种方法更为方便,我们现在来介绍这种方法:

import datetime
print(datetime.datetime.now(),type(datetime.datetime.now())) # 2021-01-06 17:49:42.948038 
res=datetime.datetime.now()+datetime.timedelta(days=3.5)
print(res) # 当前时间三天半以后的时间
print(datetime.datetime.now()>res) # 比较现在时间和拿到的时间的先后顺序
print(datetime.datetime.now()-datetime.timedelta(days=3.5)) # 当前时间三天半以前的时间

# 判断时间间隔,和时间先后!!!在把字符串的时间(记录在文件里面的时间)和现在时间对比时非常好用!!!
res=datetime.datetime.strptime('2016-09-11 00:49:56','%Y-%m-%d %H:%M:%S')
# print(res,type(res))
print(datetime.datetime.now()<res)

你可能感兴趣的:(python学习,python)