美文网首页
Python精点02

Python精点02

作者: MoShengLive | 来源:发表于2025-03-19 14:18 被阅读0次

正则

11.1.3 匹配多个字符串

在前面的例子中,只是通过 search 方法搜索一个字符串,要想搜索多个字符串,如搜索 bike、car 、truck,最简单的方法是在文本模式字符串中使用择一匹配符号(|)。择一匹配符号和逻辑或类似,只要满足任何一个,就算匹配成功。

s='bike l car l truck" #定义使用择一匹配符号的文本模式字符串
m= re.match(s, "bike') #bike满足要求,匹配成功
print (m.group()) #运行结果:bike
m= re.match(s, 'truck') #truck 满足要求,匹配成功
print (m.group()) #运行结果:truck

11.1.4 匹配任何单个字符

在前面给出的文本模式字符串都是精确匹配,不过这种精确匹配的作用不大,在正则表达式中最常用的是匹配一类字符串,而不是一个。所以就需要使用一些特殊符号表示一类字符串。本节将介绍第1个可以匹配一类字符串的特殊符号:点(.)。这个符号可以匹配任意一个单个字符。
m = re.match('.ind','bind')#匹配成功
在上面的代码中,文本模式字符串是“.ind”,第1个字符是点(.),表示可以匹配任意一个字符串。也就是说,待匹配的字符串,只要以“.ind”开头,都会匹配成功,其中“”可以表示任意一字符,例如,“bind”“xind”“5ind”都可以和文本模式字符串“.ind” 成功匹配。
使用点(.)符号会带来一个问题,如果要匹配真正的点(.)字符,应该如何做呢?要解决这问题,需要使用转义符()。
m = re.match('.ind','bind')# 匹配失败
在上面的代码中,由于使用了转义符修饰点(.)符号,所以这个点就变成了真正的字符点(.)所以匹配“bind”很显然就失败了,应该匹配“.ind”才会成功。

11.15 使用字符集

如果待匹配的字符串中,某些字符可以有多个选择,就需要使用字符集([]),也就是一对中括号来的字符串。例如,[abe]表示 a、b、c三个字符可以取其中任何一个,相当于“alble”,所以对单字符使用或关系时,字符集和择一匹配符的效果是一样的。
m= re.match('[abcd]', 'a')#使用字符集,匹配成功
print(m.group()) #运行结果:a
ms re.match('alblcld', 'a') #使用择一匹配符,匹配成功
print (m.group()) #运行结果:a
对长度大于1的字符串使用或关系时,字符集就无能为力了,这时只能使用择一匹配符。因为字符集会将中括号括起来的字符串拆成单个的字符,然后再使用“或”关系。

使用字符集,匹配成功

m = re.match('[abcd]",'ab')

运行结果:< sre,SRE Match object; span=(0,1), match='a'>

print(m)

使用择一匹配符,匹配成功

m = re.match('ablcd', "ab')

运行结果:< sre.SRE Match object; span=(0,2), match='ab'>

print(m)
如果将多个字符集写在一起,相当于字符串的连接。

相当于匹配以第1个字母是a或b,第2个字母是c或d开头的字符串,如ac、acx等

m = re.match('[ab][cd]",'ac')

运行结果:< sre.SRE Match object; span=(0,2),match='ac'>

print(m)

11.1.6 重复、可选和特殊字符

正则表达式中最常见的就是匹配一些重复的字符申,例如,匹配3个连续出现的a(aaa 符合要求),或匹配至少出现一个0的字符串(0、00、000都符合要求)、要对这种重复模式进行匹配,需要使用两个字符:””和“+”。其中,“”表示字符串出现0到n次,“+” 表示字符中出现1到n次。
s = 'a' #使用“”修饰 a
strList = ['', 'a', "aa','baa']
for value in strList:
m= re.match(s,value)
print(m)

image.png
如果对“a”使用“+”符号,就意味着“a”至少要出现1次,所以空串自然就无法匹配成功,这就是"和"baa"都无法匹配成功的原因。
前面的例子都是重复一个字符,如果要想将多个字符作为一组重复,需要用一对圆括号将这个字
将串括起来。
s='(abc)+' #匹配 abe 至少出现1 次的字符串
print(re.match(s, 'abcabcabc')) #匹配成功

除了“*"和“+”外,还有另外一个常用的符号“?”,表示可选符号。例如,“a?”表示或者有或没有a,即a可有可无。下面的代码利用“?”符号指定了匹配字符串的前缀和后缀,前缀可以是一个任意的字母或数字,而后缀可以是至少一个数字,也可以不是数字,中间必须是“wow”。在这里要引入两个特殊符号:“\w”和“\d”。其中“\w”表示任意一个字母或数字,“\d”表示任意一个数字。
s='w?wow(\d?)+' #使用了“?”“+”和“\w”“\d”的模式字符串
m = re.search(s, 'awow') #匹配成功
print(m)
m = re.search(s, 'awow12') #匹配成功
print(m)
m = re.search(s, 'wow12') #匹配成功
print(m)
m = re.search(s,'ow12') #匹配失败,因为中间不是“wow”
print(m)


image.png

import re

匹配'a'、"b'、"c"三个字母按顺序从左到右排列,而且这3个字母都必须至少有1个

abc、aabc、abbbccc 都可以匹配成功

s ='a+b+c+'
strList =['abc','aabc',"bbabc' "aabbbcccxyz']

只有"bbabc"无法匹配成功,因为开头没有'a’

for value in strList:
m = re.match(s, value)
if m is not None:
print (m.group())
else:
print('{}不匹配{}'.format (value,s))
print('--------------!

匹配任意3个数字-任意3个小写字母

123-abc、433-xyz 都可以成功

下面采用了两种设置模式字符串的方式

image.png
image.png
在本例中还用了一些特殊标识符,例如,[a-z]、[A-Z]、[0-9]是字母或关系的简写形式,分别表示26 个小写字母(az)中的任何一个,26个大写字母(AZ)中的任何一个,10个数字(0~9)中的任何一个。{N}形式表示前面修饰的部分重复N次,例如“(abc){3}”表示字符串“abc”重复3次相当于“abcabcabc”。还有就是,如果要修饰多于一个字母的字符串,要用圆括号将字符串括起来否则只会修饰前面的一个字符,例如,“abc{3}”表示字母“c”重复3次,而不是“abc”重复3次相当于“abccc”
11.1.7 分组

如果一个模式字符串中有用一对圆括号括起来的部分,那么这部分就会作为一组,可以通过 group方法的参数获取指定的组匹配的字符串。当然,如果模式字符串中没有任何用圆括号括起来的部分,那么就不会对待匹配的字符串进行分组。
m =re.match('(\d\d\d)-(\d\d)',"123-45')
在上面的代码中,模式字符串可以匹配以三个数字开头,后面跟着一个连字符(-),最后跟着两个数字的字符串。由于“\d\dd”和“\dd”都在圆括号中,所以这个模式字符串会将匹配成功的字符串分成两组,第1组的值是“123”,第2组的值是“45”,m.group(1)会获取第1个分组值,m.group(2会获取第2个分组值。如果模式字符串改成下面的形式,虽然可以匹配“123-45”,但“123-45”并没有被分组。
m = re.match('\d\d\d-\d\d','123-45')
使用分组要了解如下几点:
口 只有圆括号括起来的部分才算一组,如果模式字符串中既有圆括号括起来的部分,也有没有被圆括号括起来的部分,如“\d{3}-\d{4}-([a-z]{2})”,那么只会将被圆括号括起来的部分算作一组,其他的部分忽略。
口用group方法获取指定组的值时,组从1开始。也就是说,group(1)获取第1组的值,group(2)
获取第2组的值,以此类推。
口 groups 方法用于获取所有组的值,以元组形式返回。所以除了使用 group(1)获取第1组的值外,还可以使用 groups()[0]获取第1组的值。获取第2组以及其他组的值的方式类似。

#11.1.8 匹配字符串的起始和结尾以及单词边界

“^”符号用于表示匹配字符串的开始,“$”符号用于表示匹配字符串的结束,“\b”符号用于表示单词的边界。这里的边界是指单词两侧是空格或标点符号。例如,“abc?”可以认为 abc 两侧都有边界,左侧是空格,右侧是问号(?),但" abcx”就不能认为 abc 右侧有边界,因为“x”和“abc”都可以认为是单词。
import re

匹配成功

m = re.search("^The', 'The end.')
print(m)
if m is not None:
print (m.group()) #运行结果:The

The在匹配字符串的最后,不匹配

m = re,search('^The', 'end. The')
print(m)
if m is not None:
print(m.group())

匹配成功

m = re.search('The', 'end. The') print(m) if m is not None: print (m.group()) #运行结果:The m = re.search('The','The end.')
print(m)
if m is not None:
print(m.group())

this的左侧必须有边界,成功匹配,this左侧是空格

m = re.search(r'\bthis', "What's this?")
print(m)
if m is not None:
print(m.group()) #运行结果:this

不匹配,因为 this 左侧是“s”,没有边界

字符串前面的r表示该字符串中的特殊字符(如“\b”)不进行转义

m = re.search(r'\bthis', "What'sthis?")
print(m)
if m is not None:
print(m.group())

this 的左右两侧都有边界,成功匹配,因为 this左侧是空格,右侧是问号(?)

m re.search(r'\bthis\b', "What's this?")
print(m)
if m is not None:
print(m.group()) #运行结果:this

不匹配,因为 this 右侧是 a,a也是单词,不是边界

m = re.search(r'\bthis\b', "what's thisa")
print (m)
if m is not None:
print (m.group())


image.png

对于单词边界问题,要认清什么是边界。例如,“\bthis\b”要求 this 两侧都有边界,如果匹配“What'sthis?”,是可以匹配成功的,因为空格和“?”都可以认为是 this 的边界,这里可以将“?”换成其他字符,如“*”。但不能换成数字,如“What's thisa”“What's this4”都无法匹配成功。

11.1.9 使用 findall 和 finditer 查找每一次出现的位置

findall 函数用于查询字符串中某个正则表达式模式全部的非重复出现情况,这一点与 search 函数在执行字符串搜索时类似,但与 match 函数和 search 函数不同之处在于,findall 函数总是返回一个包含搜索结果的列表。如果 findall 函数没有找到匹配的部分,就会返回一个空列表,如果匹配成功,列表将包含所有成功的匹配部分(从左向右按匹配顺序排列)。
result = re.findall('bike', 'bike')

运行结果:['bike']

print(result)
result = re.findall('bike','My bike')

运行结果:['bike']

print(result)

运行结果:['bike','bike']

result = re.findall('bike', 'This is a bike, This is my bike.')
print(result)
finditer 函数在功能上与 findall 函数类似,只是更节省内存。这两个函数的区别是 findall 函数会将所有匹配的结果一起通过列表返回,而 finditer 函数会返回一个迭代器,只有对 finditer 函数返回结果进行迭代,才会对字符串中某个正则表达式模式进行匹配。findall 函数与 finditer 函数相当于读取XML 文档的两种技术:DOM 和 SAX。前者更灵活,但也更耗内存资源;后者顺序读取 XML 文档的
内容,不能随机读取 XML 文档中的内容,但更节省内存资源。
import re

待匹配的字符串

s ='12-a-abc54-a-xyz---78-A-ytr

匹配以2个数字开头,结尾是3个小写字母,中间用“-a”分隔的字符串,对大小写敏感'

下面的代码都使用了同样的模式字符串

result = re.findall(r'\d\d-a-[a-z]{3}',s)

运行结果:['12-a-abc','54-a-xyz']

print(result)

将模式字符串加了两个分组(用圆括号括起来的部分),findall方法也会以分组形式返回

result = re.findall(r'(\d\d)-a-([a-z]{3})',s)

运行结果:[('12',"abc'),('54','xyz')]

print(result)

忽略大小写(最后一个参数值:re.I)

result = re.findall(r'\d\d-a-[a-z]{3}',s,re.I)

运行结果:['12-a-abc','54-a-xyz','78-A-ytr']

print(result)

忽略大小写,并且为模式字符串加了2个分组

result = re.findall(r'(\d\d)-a-([a-z]{3})''s,re.I)

运行结果:[('12','abc'),('54','xyz'),('78','ytr')]

print(result)

使用 finditer 函数匹配模式字符串,并返回匹配迭代器

it = re.finditer(r'(\d\d)-a-([a-z]{3})''s re.I)

对迭代器进行迭代

for result in it:
print(result.group(),end='<')
#获取每一个迭代结果中组的所有的值
groups = result.groups()
#对分组进行迭代
for i in groups:
print(i,end ='')
print('>')
程序运行结果如图 11-12 所示。


image.png

不管是 findall 函数,还是 finditer 函数,都可以通过第3个参数指定 re.l,将匹配方式设为大小写不敏感。如果为模式字符串加上分组,那么 findall 函数就会返回元组形式的结果(列表的每一个元素是一个分组)。

11.1.10 用 sub 和 subn 搜索与替换

import re

sub 函数第1个参数是模式字符串,第2个参数是要替换的字符串,第3个参数是被替换的字符串

匹配'Bill is my son'中的'Bill',并用'Mike'替换'Bill'

result = re.sub('Bill', 'Mike', 'Bill is my son')

运行结果:Mike is my son

print(result)

返回替换结果和替换总数

result = re.subn('Bill', 'Mike', 'Bill is my son, I like Bill')

运行结果:('Mike is my son,I like Mike',2)

print(result)

运行结果:Mike is my son,I like Mike

print(result[0])

运行结果:替换总数=2

print('替换总数','=',result[1])

使用“\N”形式引用匹配字符串中的分组

result = re.sub('([0-9])([a-z]+)',r'产品编码(\1-\2)','01-labc,02-2xyz,03-9hgf')

运行结果:01-产品编码(1-abc),02-产品编码(2-xyz),03-产品编码(9-hgf)

print(result)

该函数返回要替换的字符串

def fun():
return r'产品编码(\1-\2)'
result = re.subn('([0-9])([a-z]+)', fun(),'01-labe,02-2xyz,03-9hgf')

运行结果:('01-产品编码(1-abc),02-产品编码(2-xyz),03-产品编码(9-hgf)',3)

print(result)

运行结果:01-产品编码(1-abc),02-产品编码(2-xyz),03-产品编码(9-hgf)

print(result[0])

运行结果:替换总数 =3

print('替换总数''=',result[1])


image.png
11.1.11 使用 spit 分隔字符串

split 函数用于根据正则表达式分隔字符串,也就是说,将字符串中与模式匹配的子字符串都作为
分隔符来分隔这个字符串。split的数返回一个列表形式的分隔结果,每一个列表元素都是分隔的子符串。split函数的第1个参数是模式字符串,第2个参数是待分隔的字符串,如果待分隔的字符串非常大,可能并不希望对这个字符串永远使用模式字符串分隔下去,那么可以使用 maxsplit 关键字参指定最大分隔次数。如果将 split 想象成用菜刀来切香肠,那么 maxsplit 的值就是最多切几刀。
import re
result = re.split(';",'Bill;Mike;John')

运行结果:['Bi11',"Mike','John']

print(result)

至少1个逗号(,),分号(;),点(.)和空白符(\s)分隔字符串

result = re.split('[,;.\s]+','a,b,,d,d;x c;d. e')

运行结果:['a’, 'b’, 'd’, 'd','x','c','d','e’]

print(result)、用以3个小写字母开头,紧接着一个连字符(-),并以2个数字结尾的字符串作为分隔符对字符串进行分隔
result = re.split('[a-z]{3}-0-9','testabc-4312productxyz-43abill')

运行结果:['test',"12product','abill']

print(result)

使用 maxsplit 参数限定分隔的次数,这里限定为 1,也就是只分隔一次

result = re.split('[a-z]{3}-[0-9]{2}','testabc-4312productxyz-43abi11' maxsplit=1)

运行结果:['test',"12productxyz-43abill']

print(result)

image.png
import re
result = re.split(';",'Bill;Mike;John')
手运行结果:['Bi11',"Mike','John']print(result)手用至少1个逗号(,),分号(;),点(.)和空白符(\s)分隔字符串result = re.split('[,;.\s]+','a,b,,d,d;x c;d. e')善 运行结果:['a’, 'b’, 'd’, 'd','x','c','d','e’]print(result)、用以3个小写字母开头,紧接着一个连字符(-),并以2个数字结尾的字符串作为分隔符对字符串进行分隔result = re.split('[a-z]{3}-0-9','testabc-4312productxyz-43abill')
主运行结果:['test',"12product','abill']
print(result):使用 maxsplit 参数限定分隔的次数,这里限定为 1,也就是只分隔一次result = re.split('[a-z]13}-[0-9j(2)','testabc-4312productxyz-43abi11' maxsplit=1)#运行结果:['test',"12productxyz-43abill']print(result)
11.2 一些常用的正则表达式

本节给出了几个常用的正则表达式,这些正则表达式如下
口 Email:"[0-9a-zA-Z]+@[0-9a-zA-Z]+.[a-zA-Z]{2,3}
口IP地址(IPv4): '\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}'
口 Web 地址:"https?:/{2}\w.+'
需要说明的是,根据具体要求不同,相应的正则表达式也可能不同。例如,匹配 Email 的正则表达式就有很多种,这要看具体的要求是什么,例如,本节给出的匹配 Email 的正则表达式就相对简单只要保证字符串含有“@”字符,并且“@”字符前面至少有一个数字或字母组成的字符串,以及“@"后面是域名的形式即可(geekori.comgeekori.org 等)。

12.1sys 模块

image.png
image.png
image.png

12.2 os 模块

os 模块以及子模块 path 中包含了大量获取各种系统信息,以及对系统进行设置的函数,本节讲解这两个模块中的一些常用函数的使用方法。

12.2.1 获取与改变工作目录

getcwd 函数用于获取当前的工作目录,如果指定文件或目录,则在不指定任何路径的情况下,系统会认为该文件或目录在当前的工作目录中。通过 chdir 函数可以改变当前的工作目录。


image.png
12.2.2 文件与目录操作

在os模块中提供了一些操作目录和文件的函数。这些函数的功能描述如下:
口 mkdir(dirname, permissions):创建目录,dirame 表示目录名,如果 dirname 指定的目录名存在,则抛出 OSError 异常。permissions 表示目录的权限。在Linux 或 Mac OSX上可以设置目录读(r)、写(w)和执行(x)权限。permissions 参数值一般是一个八进制的数值。如0o777表示最高的权限,7表示同时具有rwx权限。关于 Linux/Mac OS X权限的细节,请参阅相关的文档,这里不再详细讲解。
口 makedirs(dirname, permissions,exist_ok):与 mkdir 函数类似,用于建立目录,但 dirname 指定的目录可以是多级的,而且上一级不存在,也会建立上一级的目录。例如,dirname 参数的值是x/y/z。也就是说,需要在当前目录建立三级目录。如果使用mkdir 函数,并且x或y目录不存在,那么程序会直接抛出 OSEror 错误,但使用 makedirs 函数建立目录,会连同x和y
一起建立。makedirs 函数最后一个参数的值为 False,当目录存在时,会抛出 OSError 异常,否则即使目录存在,也不会抛出异常。
口 rmdir(dirname):删除 dirname 参数指定的目录,如果目录不为空,则抛出一个 OSError 异常。
口 removedirs(dirname):删除 dirname 参数指定的目录,dirname参数可以指定多级目录。如x/y/z,该函数会同时删除x、y和z目录。不过如果某一级目录不为空,那么该目录以及所有的父目录都不会被删除。例如,如果在y目录中除了有一个z 子目录外,还有一个 test.fxt 文件,那么在删除 x/y/z目录时,只会删除y目录中的z子目录,x和y目录都不会被删除。
remove(filename):删除 filename 参数指定的文件。
口 rename(src, dst):将 src 参数指定的文件(目录)名改成dst 参数指定的文件名。
口 renames(src,dst):与 rename函数的功能类似,只是 src 和 dst 可以是多级目录(最后一级可以是文件名)。该函数会将每一级的目录都改成对应的目录名,如可以将 x/y/z改成a/b/c。该函数会将x改成 a,y 改成b,z改成c。

12.2.3 软链接与硬链接

软链接和硬链接是Linux 和 Mac OSX的概念。软链接就像 Windows中的快捷方式,只是保存了源文件或目录的引用,而且只有固定尺寸。硬链接只能针对文件建立,这是因为硬链接是将整个文件复制一份,相当于一个副本,所以在建立硬链接时会进行限制。软链接和硬链接都是同步的。也就是说,只要修改软链接文件或硬链接文件,源文件的内容就会变,反之亦然。
不管是 Linux 还是 Mac Os X,建立软链接和硬链接的命令都是 ln,在建立软链接时,需要加“-s命令行参数。假设有一个 test.txt 文件,现在要对该文件建立一个软链接文件 slink.txt 和一个硬链接文件 link.txt,则 test.txt 文件的内容如下:
Hello World
世界你好
建立软链接和硬链接的命令如下:
In -s test.txt slink.txt
In test.txt link.txt

12.3.1 集合

从Python2.3开始,集合就通过set类型成为语言的一部分,因此,在Python3 中可以直接使用集合类型(set),而不需要再引用sets模块(这个模块已经被去除了)。Python 语言中的集合和数字集合的概念非常类似。在数学中集合有如下三个特性。
口 无序性:集合中每个元素的值都是平等的,元素之间是无序的。
口 互异性:集合中任意两个元素都是不同的,即每个元素只能出现一次。
口 确定性:集合中每个元素都是确定的,对于一个值来说,要么属于该集合,要么不属于该集合
所以集合、列表和字典都不能作为集合的元素值,因为它们都是可变的。
Python 语言中的集合也同样满足这三个特性,而且同样支持集合的标准操作,如创建集合、合并集合、集合相交、求集合的差等。在 Python 语言中有很多操作同时提供了运算符和方法,例如,集合的并操作,可以使用其中一个集合的 union 方法,也可以使用按位或运算符“|”。
在创建集合类 set 的实例时,需要为 set类的构造方法提供一个列表或元组类型的值,用于建立集合的数据源。也就是说,set类可以将列表或元组转换为集合,在转换的过程中,会去除重复的值、并且列表或元组中元素的顺序可能被打乱,因为集合中的元素是无序的。

创建一个有 10个元素值的集合

set1 = set(range(10))

输出 set1的类型,运行结果:<class'set'>

print(type(set1))

运行结果:(0,1,2,3,4,5,6,7,8,9}

print(set1)

将字符串中的每一个字符作为元素添加进集合,因为字符串可看作字符的列表

会去除重复的字符,而且顺序会打乱

set2 = set('hello')

运算结果:{'h','o’,'1','e')

print(set2)

利用字符串列表建立集合,会去除重复的字符串,字符串的顺序会打乱

set3 = set(['Bill','John','Mike','John'])

运行结果:{'John','Mike','Bi11')

print(set3)

利用元组建立一个集合

a = set((1,2,3))

利用列表建立一个集合

b = set([3,5,1,7])

使用 union方法合并a集合和b集合,运行结果:{1,2,3,5,7))

print (a.union(b))

使用“|”运算符合并a集合和b集合,运行结果:(1,2,3,5,7)

print(a | b)

使用 intersection方法求a集合和b集合的交集,运行结果:(1,3)

print (a.intersection(b))

使用“&”运算符求a集合和b集合的交集,运行结果:(1,3)

print (a & b)

使用列表创建一个集合

c = set([2,3])

判断c集合是否为a集合的子集,运行结果:True

print(c.issubset(a)):

判断a集合是否为c集合的子集,运行结果:False

print(a.issubset(c))

判断c集合是否为a集合的超集,运行结果:False

print(c.issuperset(a))

判断a集合是否为c集合的超集,运行结果:True

print(a.issuperset(c))

使用列表创建一个集合

d = set([1,2,3])

判断a集合和d集合是否相等,运行结果:True

print(a == d)

使用 difference 方法计算a集合与b集合的差,a和b的差值就是在a中删除在b中存在的元素

运行结果:{2}

print(a.difference(b))

使用“-”运算符计算a集合与b集合的差,运行结果:{2)

print(a - b)

使用 symmetric_difference 方法计算a集合与b集合的对称差,运行结果:{2,5,7}

print(a.symmetric difference(b))

使用“^”运算符计算a集合与b集合的对称差,运行结果:{2,5,7}

print (a ^ b)

对称差相当于a-b与b-a的并集,运行结果:{2,5,7}

Print((a-b)|(b- a))

使用 copy 方法将a复制一份,并将该副本赋给变量x

x= a.copy()

判断x与a是否相同,运行结果:False

print(x is a)

向集合x中添加一个新的元素

x.add(30)

运行结果:{1,2,3,30}

print(x)

运行结果:{1,2,3}

print(a)

运行结果:(1,2,3}

print (d):

判断1是否属于集合 d,运行结果:True

print(l in d)

判断10是否属于集合 d,运行结果:False

print (10 in d)


image.png

由于集合是可变的,所以不能作为元素值添加到集合中,也不能作为字典的 key。不过可以利用
frozenset 类型将集合变成只读的,这样就可以作为集合元素和字典的 key。

12.3.2堆

堆也是一种众所周知的数据结构,它是优先队列中的一种。使用优先队列能以任意顺序增加元素,并能快速找到最小(大)的元素值,或前n个最小或最大的元素值,这要比用于列表的 min 函数和max 函数高效得多。
与集合不同,在 Python3 中并没有独立的堆类型,只有一个包含一些堆操作函数的模块,该模块名为heapq(q是 queue 的缩写,即队列)。heapq模块中常用的函数如表 12-2 所示。


image.png
12.3.3 双端队列

双端队列不同于普通的队列。对于普通的队列来说,只能操作队列的头,而不能操作队列的尾,也就是先进先出操作。双端队列是普通队列的扩展,在队列的头和尾都可以进行队列的操作,所以对
一组值的两头进行操作,使用双端队列是非常方便的。
使用双端队列需要导入 collections 模块中的 deque 类。该类中提供了若千个方法用于操作双端队列,例如,append 方法可以将值添加到队列的尾部,而 appendleft 方法可以将值添加到队列的头部。pop方法可以弹出队列尾部的最后一个值,并返回这个值。popleft 方法可以弹出队列头部的第1个值并返回这个值。
from collections import deque

创建一个包含 10 个数字的双端队列

g= deque (range(10))

运行结果:deque([0,1,2,3,4,5,6,7,8,9])

print(g)

将 100 追加到双端队列g的队尾

g.append(100)

将-100 追加到双端队列q的队尾

g.append(-100)

运行结果:degue([0,1,2,3,4,5,6,7,8,9,100,-100])

print(g)

将 20 追加到双端队列g的队首g.appendleft(20)

运行结果:deque([20,0,1,2,3,4,5,6,7,8,9,100,-100])

print(g)

弹出队尾的值,运行结果:-100

print(g.pop())

运行结果:degue([20,0,1,2,3,4,5,6,7,8,9,100])

print(g)

弹出队首的值,运行结果:20

print(g.popleft())

运行结果:deque([0,1,2,3,4,5,6,7,8,9,100])

print(g)

将双端队列中的元素向左循环移动两个位置,也就是队首的元素会移动到队尾

g.rotate(-2)

运行结果:degue([2,3,4,5,6,7,8,9,100,0,1])

print(g)
!将双端队列中的元素向右循环移动两个位置,也就是队尾的元素会移动到队首
g.rotate(4)

运行结果:deque([9,100,0,1,2,3,4,5,6,7,8])

print(g)

创建一个双端队列 q1

g1 = deque (['a', 'b'])

将 q1 追加到q的后面

g.extend (gl)

运行结果:degue([9,100,0,1,2,3,4,5,6,7,8,'a','b'])

print (g)

将 q1 追加到q的前面,这时 g1 会倒序排列

g.extendleft(gl)

运行结果: degue(['b', 'a', 9, 100,0,1,2,3,4,5, 6, 7,8,'a','b'])

print(g)

12.4 时间、日期与日历(time 模块)

Python 程序能用很多方式处理日期和时间,转换日期格式是一个常见的功能。Python 提供的time、datetime 和 calendar 模块可以用于格式化日期和时间。时间间隔是以秒为单位的浮点数。每个间戳使用从 1970年1月1日午夜(历元)到现在经过的时间来表示。
Python 语言的 time 模块下有很多函数可以转换常见日期格式。例如,函数 time 用于获取当前时间戳,代码如下:
import time
ticks = time,time()

运行结果:当前时间戳为:1511675087.7990139

print("当前时间戳为:",ticks)
从前面代码的运行结果可以看出,ticks变量的值是一个浮点数,这个浮点数就是从运行程序那一刻的时间点到 1970 年1月1日午夜之间的秒数。当然,Python 语言还可以使用更多的函数来操作间和日期。本节深入介绍与时间、日期和日历相关函数的用法。

12.4.1 时间元组

在 Python 语言中,时间用一个元组表示。表示时间的元组有9个元素,这9个元素都有其对应的属性,所以时间元组中的每一个元素值既可以通过属性获得,也可以通过索引获得。时间元组中的元景仅仅要求显示4位的年。这就要求在输出日期和时间之前要先进行格式化。
格式化日期和时同需要使用 strftime 函数,该函数的第1个参数是格式化字符串,第2个参数是时间元组。Python语言支持多个日期和时间格式化符号,这些符号如表 12-4所示。


image.png

Python 语言中所有用于格式化日期和时间的符号都以百分号(%)开头,如“%Y-%m-%d”会将日期格式化为如“2017-11-12”的形式。
要注意的是,如果格式化字符串中包含中文,必须用locate模块中的 setlocale 函数将日期与时间格式设置的格式设为中文的 UTF-8 格式,否则无法成功对日期和时间格式化。如果不知道当前系统有哪些Locate,则可以使用 locate -a 命令査询,或使用 locate a|grep UTF-8 只显示 UTF-8的Locate。这些会令仅仅针对 Linux 和 Mac Os X

12.4.3 时间戳的增量

通过 time 模块的 time 函数可以获取当前时间的时间戮(浮点数格式),时间戳可以加上或减去一个整数(n),这就是时间戳的增量,单位是秒。也就是说,当前时间加 10表示未来 10s,减10多示过去10s。如果想得到未来 1h 的时间戳,需要将 time 函数的返回值加 3600。

12.4.4 计算日期和时间的差值

datetime 模块中的 datetime 类允许计算两个日期的差值,可以得到任意两个日期之间的天数以及剩余的秒数。通过 datetime 模块的 timedelta 函数还可以得到一个时间增量,如 timedetla(hours=2)可省到往后延两个小时的时间增量。

12.5 随机数(random 模块

在 random 模块中封装了多个函数用于产生各种类型的随机数,这些函数有的可以产生单值随机数,有的可以产生一组随机数,还有的可以打乱列表原来的顺序,类似于洗牌。random 模块中常用函数及描述如下:
□ randint(m,n):产生 m~n 的随机整数,包括 m 和 n。
口 random():产生 0~1的随机浮点数,包括 0,但不包括1。
口 uniform(m,n):产生 m~n的随机浮点数,m 和n可以是浮点数,包括 m和 n。
口 randrange(m,n.step):在一个递增的序列中随机选择一个整数。其中,step 是步长,如
randrange(1,6,2),该函数就会在列表[1.3.5]中随机选择一个整数。
口 choice(seq):从 seq 指定的序列中随机选择一个元素值。seq 指定的列表元素可以是任意类型的值。
口 sample(seq,k):从 seq 指定的序列中随机选取k个元素,然后生成一个新的序列
口 shuffle(seq):把 seq 指定的序列中元素的顺序打乱,该函数直接修改原有的序列
import random

产生 1~100 的随机整数

print(random.randint(1,100))

产生 0~1 的随机数

print(random.random())

从[1,4,7,10,13,16,19]随机选一个数

print(random.randrange(1, 20,3))

产生一个从 1~100.5的随机浮点数

Print(random.uniform(1, 100.5))
intlist = [1,2,3,4,5,6,7,8 9,'a’,'b','c','d']

从 intzist 列表中随机选一个元素值

print(random.choice(intList))

从 intList 列表中随机选3个元素值,并生成一个新的序列 …

newList = random.sample(intList, 3)
print(newList)

随机排列 intList 中的元素值,该函数直接改变了 intList 列表

random.shuffle(intList)
print(intList)


image.png
12.6 数学(math 模块)

在 math 模块中封装了很多与数学有关的函数和变量,如取整、计算幂值、平方根、三角函数等。
本节介绍 math 模块中比较常用的数学函数。
import math
print('圆周率','=', math.pi)
print('自然常数’,'=’,math.e)

取绝对值

print (math.fabs(-1.0))

向上取整,运行结果:2

print(math.ceil(1.3))

向下取整,运行结果:1

print(math.floor(1.7))

计算2的 10次方,运行结果:1024.0

print(math.pow(2,10))

计算8的平方根,运行结果:2.8284271247461903

print(math.sqrt(8))

计算π/2.的正弦,运行结果:1.0

print(math.sin(math.pi /2))

计算"的余弦,运行结果:-1.0

print (math.cos (math.pi))

计算x/4的正切,运行结果:0.9999999999999999

print(math.tan(math.pi /4))


image.png

文件与流

13.1 打开文件

open 函数用于打开文件,通过该函数的第1个参数指定要打开的文件名(可以是相对路径,也可以是绝对路径)。
f= open('test.txt')
f = open('./files/test.txt')
如果使用 open 函数成功打开文件,那么该函数会返回一个 TextIOWrapper 对象,该对象中的方法可用来操作这个被打开的文件。如果要打开的文件不存在,则抛出如图 13-1所示的 FileNotFoundError异常


image.png

open 函数的第2个参数用于指定文件模式(用一个字符串表示)。这里的文件模式是指操作文件的方式,如只读、写入、追加等。表 13-1 描述了 Python3 支持的常用文件模式。
可以看到,在表13-1所示的文件模式中,主要涉及对文件的读写和文件格式(文本和二进制)的问题。使用 open 函数打开文件时默认是读模式,如果要想向文件中写数据,需要通过 open 函数的第
2个参数指定文件模式。
f = open('./files/test.txt','w') #以写模式打开文件
f = open('./files/test.txt', 'a') #以追加模式打开文件


image.png

以写模式打开文件

写模式和追加模式的区别是如果文件存在,写模式会覆盖原来的文件,而追加模式会在原文容的基础上添加新的内容。
在文件模式中,有一些文件模式需要和其他文件模式放到一起使用,如 open函数不指定第2个数时默认以读模式打开文本文件,也就是'rt'模式。如果要以写模式打开文本文件,需要使用'wt'模击对于文本文件来说,用文本模式(t)打开文件和用二进制模式(b)打开文件的区别不大,都是以字节为单位读写文件,只是在读写行结束符时有一定的区别。
如果使用文本模式打开纯文本文件,在读模式下,系统会将"\n'作为行结束符,对于 UNIX、MacOSX这样的系统来说,会将"\n作为行结束符,而对于 Windows 来说,会将"r\n'作为行结束符,还有的系统会将"\r"作为行结束符。对于“\r\n"和"\r"这样的行结束符,在文本读模式下,会自动转换为"\n',而在二进制读模式下,会按原样读取,不会做任何转换。在文本写模式下,系统会将行结束符转换为 0S应的行结束符,如 Windows 平台会自动用"\r\n'作为行结束符。可以使用 os 模块中的 linesep 变量来获得当前 OS 对应的行结束符。
在表 13-1 最后一项是'+’文件模式,表示读写模式,必须与其他文件模式一起使用,如'r+'、'w+
'a+'。这三个组合文件模式都可以对文件进行读写操作,它们之间的区别如下。
口 r+:文件可读写,如果文件不存在,会抛出异常;如果文件存在,会从当前位置开始写入新内容,通过 seek 函数可以改变当前的位置,也就是文件指针。
口 w+:文件可读写,如果文件不存在,会创建一个新文件;如果文件存在,会清空整个文件
并写入新内容。
口 a+文件可读写,如果文件不存在,会创建一个新文件;如果文件存在,会将要写入的内容加到原文件的最后,也就是说,使用'a+模式打开文件,文件指针会直接跳到文件的尾部,果要使用 read 方法读取文件内容,需要使用 seek 方法改变文件指针,如果调用 seek(0)会直接将文件指针移到文件开始的位置。

13.2操作文件的基本方法
13.2.1 读文件和写文件

使用 open 函数成功打开文件后,会返回一个 TextIOWwrapper 对象,然后就可以调用该对象中的方法对文件进行操作。TextIOWwrapper 对象有如下4个非常常用的方法。
口 write(string):向文件写入内容,该方法返回写入文件的字节数。
口read([n]):读取文件的内容,n 是一个整数,表示从文件指针指定的位置开始读取的n字节。如果不指定n,该方法就会读取从当前位置往后的所有的字节。该方法返回读取的数据。
口 seek(n):重新设置文件指针,也就是改变文件的当前位置。使用 write 方法向文件写入内容后
需要调用 seek(0)才能读取刚才写入的内容。
口 close0):关闭文件,对文件进行读写操作后,关闭文件是一个好习惯。
【例 13.1】 本例分别使用?、'w、r+、'w+等文件模式打开文件,并读写文件的内容,可以从中学习到不同文件模式操作文件的差别。

以写模式打开 test1.txt 文件

f= open('./files/testl.txt','w')

向test1.txt 文件写入“I love ”,运行结果:7

print(f.write('I love '))

向test1.txt 文件写入“python”,运行结果:6

print(f.write('python'))

关闭 test1.txt 文件

f.close()

以读模式打开 test1.txt 文件

f = open('./files/testl.txt','r')

从 test1.txt 文件中读取7字节的数据,运行结果:I 1ove

print(f.read(7))

从 test1.txt 文件的当前位置开始读取6字节的数据,运行结果:python

print(f.read(6))

关闭 test1.txt 文件

f.close()
try:
#如果 test2.txt 文件不存在,会抛出异常
f = open('./files/test2.txt','r+')
except Exception as e:
print(e)

用追加可读写模式打开 test2.txt 文件

f = open('./files/test2.txt', 'a+')

向 test2.txt 文件写入“hello”

print(f.write('hello'))

关闭 test2.txt 文件

f.close()

用追加可读写模式打开 test2.txt 文件

f = open('./files/test2.txt', 'a+')

读取 test2.txt 文件的内容,由于目前文件指针已经在文件的结尾,所以什么都不会读出来

print(f.read())

将文件指针设置到文件开始的位置

f.seek(0)

读取文件的全部内容,运行结果:hello

print (f.read())#关闭 test2.txt 文件
f.close()

用写入可读写的方式打开 test2.txt 文件,该文件的内容会清空

try:
f = open('./files/test2.txt','w+')

读取文件的全部内容,什么都没读出来

print(f.read())

向文件写入“How are you?”

f.write('How are you?')
#重置文件指针到文件的开始位置
f.seek(0)
#读取文件的全部内容,运行结果:How are you?
 print(f.read())

finally:

关闭 test2.txt 文件,建议在 finally 中关闭文件

f.close()


image.png

print (f.read())#关闭 test2.txt 文件
f.close()

用写入可读写的方式打开 test2.txt 文件,该文件的内容会清空

try:
f = open('./files/test2.txt','w+')

读取文件的全部内容,什么都没读出来

print(f.read())

向文件写入“How are you?”

f.write('How are you?')

重置文件指针到文件的开始位置

f.seek(0)

读取文件的全部内容,运行结果:How are you?

print(f.read())
finally:

关闭 test2.txt 文件,建议在 finally 中关闭文件

f.close()

尽管一个文件对象在退出程序后(也可能在退出前)会自动关闭,尽管是否关闭不重要,但在文件完成相关操作后关闭文件也没什么坏处,而且还可以避免浪费操作系统中打开文件的配额。因建议在对文件进行读写操作后,使用 close 方法关闭文件,而且最好在 finally 子句中关闭文件,这做可以保证文件一定会被关闭

13.2.2 管道输出

在 Linux、UNIX、Mac Osx等系统的 Shell 中,可以在一个命令后面写另外一个命令,前一个命令的执行结果将作为后一个命令的输入数据,这种命令书写方式被称为管道,多个命令之间要使用"|"符号分隔。下面是一个使用管道命令的例子。
ps aux l grep mysql
在上面的管道命令中先后执行了两个命令,首先执行 ps aux命令查看当前系统的进程以及相关信息,然后将查询到的数据作为数据源提供给 grep 命令,grep mysql命令表示查询进程信息中所有包含mysql 字样的进程。图 13-3 所示是一种可能的输出结果。


image.png

在 Python 程序中,可以通过标准输入来读取从管道传进来的数据,所以 python 命令也可以使用在管道命令中。


image.png
13.2.3 读行和写行

读写一整行是纯文本文件最常用的操作,尽管可以使用read 和 write 方法加上行结束符来读写文件中的整行,但比较麻烦。因此,如果要读写一行或多行文本,建议使用 readline 方法、readlines方法.
法和 writelines 方法。注意,并没有 writelime 方法,写一行文本需要直接使用 write 方法。
readline 方法用于从文件指针当前位置读取一整行文本,也就是说,遇到行结束符停止读取文,但读取的内容包括了行结束符。readines方法从文件指针当前的位置读取后面所有的数据,并将这些数据按行结束符分隔后,放到列表中返回。writelines 方法需要通过参数指定一个字符串类型的列表。该方法会将列表中的每一个元素值作为单独的一行写入文件。
【例 13.3】本例通过 readlime 方法、readlines 方法和 writelines 方法对 urls.txt 文件进行读写行作,并将读文件后的结果输出到控制台。

实例位置:PythonSampleslsrechapter13\demo13.02.py
import os
#以读写模式打开 urls.txt 文件
f = open('./files/urls.txt','r+')
#保存当前读上来的文本
url = ‘’
while True:
     #从 urls.txt 文件读一行文本
      url = f.readline()
      #将最后的行结束符去掉
       url = url.rstrip()
       #当读上来的是空串,结束循环
       if url == '':
           break;
        else:
            #输出读上来的行文本
            print (url)
print('-----------')
#将文件指针重新设为 0
f.seek(0)
#读 urls.txt 文件中的所有行
print(f.readlines())
#向 urls.txt 文件中添加一个新行
f.write('https://jiketiku.com' + os.linesep)
#关闭文件
f.close()
#使用'a+'模式再次打开 urls.txt 文件
f = open('./files/urls.txt','a+')
#定义一个要写入 urls.txt 文件的列表
urllist=['https://geekori.com'+ os.linesep, "https://www.google.com’ + os.linesep]
#将 urlList 写入 urls.txt 文件
f.writelines(urllist)
f.close()

在运行上面的程序之前,先要在当前目录中建立一个 fles子目录,并在该目录下建立一个 urls.txt文件,并输入下面3行内容。
files/urls.txt
https://geekori.com
https://geekori.com/que.php
http://edu.geekori.com

13.3使用 Filelnput 对象读取文件

如果需要读取一个非常大的文件,使用 readlines 函数会占用太多内存,因为该函数会一次性将文
件所有的内容都读到列表中,列表中的数据都需要放到内存中,所以非常占内存。为了解决这个问题可以使用 for 循环和 readline 方法逐行读取,也可以使用 fleinput 模块中的 input 函数读取指定的文件。
input方法返回一个 Filelnput对象,通过FileInput对象的相应方法可以对指定文件进行读取,FileInput
对象使用的缓存机制,并不会一次性读取文件的所有内容,所以比 readlines 函数更节省内存资源。

import fileinput
#使用 input 方法打开 urls.txt 文件
fileobj = fileinput,input(',/files/urls.txt')
#输出 fileobj 的类型
print(type(fileobj))
#读取 urls.txt 文件第1行
print(fileobj.readline() .rstrip())
# 通过 for 循环输出 urls.txt 文件的其他行
for line in fileobj:
     line = line.rstrip()
      #如果 file 不等于空串,输出当前行号和内容
     if line != ' ':
         print (fileobj.lineno (),':' line)
       else:
             #输出当前正在操作的文件名
               print (fileobj.filename()) #必须在第1行读取后再调用,否则返回 None
image.png

要注意的是,flename 方法必须在第1次读取文件内容后调用,否则返回 None。

14.2 处理 JSON 格式的数据

JSON格式的数据同样被广泛使用在各种应用中,JSON 格式要比 XML 格式更轻量,所以现在很多数据都选择使用 JSON 格式保存,尤其是需要通过网络传输数据时,这对于移动应用更有优势,因为保存同样的数据,使用 JSON 格式要比使用 XML 格式的数据尺寸更小,所以传输速度更快,也更节省流量,因此,在移动 App 中通过网络传输的数据,几乎都采用了 JSON 格式。
JSON格式的数据可以保存数组和对象,JSON 数组用一对中括号将数据括起来,JSON 对象用一对大括号将数据括起来。下面就是一个典型的 JSON 格式的字符串。在这个 JSON 格式字符串中定义了一有两个元素的数组,每一个元素的类型都是一个对象。对象的 key 和 value 之间要用冒号(:)分隔,key-value 对之间用逗号(,)分隔。注意,key 和字符串类型的值要用双引号括起来,不能使用单引号。
[
{ "itemi":"valuel","item2": 30,"item3":10},
{"item1":"value2", "item2": 30, "item3":20}
]

14.2.1 JSON 字符串与字典互相转换

将字典转换为 JSON 字符串需要使用 json 模块的 dumps 函数,该函数需要将字典通过参数传入,后返回与字典对应的 JSON 字符串。将 JSON 字符串转换为字典可以使用下面两种方法。
(1)使用 json 模块的 loads 函数,该函数通过参数传入 JSON 字符串,然后返回与该 JSON 字符
对应的字典。
(2)使用 eval 函数将 JSON 格式字符串当作普通的 Python 代码执行,eval 函数会直接返回与JSON格式字符串对应的字典。
尽管eval 函数与 loads 函数都可以将 JSON 字符串转换为字典,但建议使用 loads 函数进行转换,8为eval函数可以执行任何 Python 代码,如果 JSON 字符串中包含了有害的 python 代码,执行 JSON符串可能会带来风险。

14.2.2 将 JSON 字符串转换为类实例

loads 函数不仅可以将 JSON 字符串转换为字典,还可以将 JSON 字符串转换为类实例。转换原理
是通过loads 函数的 object_hook 关键字参数指定一个类或一个回调函数。具体处理方式如下:
口指定类:loads 函数会自动创建指定类的实例,并将由 JSON 字符串转换成的字典通过类的构造方法传入类实例,也就是说,指定的类必须有一个可以接收字典的构造方法。
口 指定回调函数:loads 函数会调用回调函数返回类实例,并将由 JSON 字符串转换成的字典传入回调函数,也就是说,回调函数也必须有一个参数可以接收字典。
从前面的描述可以看出,不管指定的是类还是回调函数,都会由 loads 函数传入由 JSON 字符串成的字典,也就是说,loads 函数将 JSON 字符串转换为类实例的本质是先将 JSON 字符串转换为字典,然后再将字典转换为对象。区别是指定类时,创建类实例的任务由 loads 函数完成,而指定回调函数时,创建类实例的任务需要在回调函数中完成,前者更方便,后者更灵活。


image.png
14.2.3 将类实例转换为 JSON 字符串

dumps 函数不仅可以将字典转换为 JSON 字符串,还可以将类实例转换为 JSON 字符串。dumps函数需要通过 default 关键字参数指定一个回调函数,在转换的过程中,dumps 函数会向这个回调函传入类实例(通过 dumps 函数第1个参数传入),而回调函数的任务是将传入的对象转换为字典,然后 dumps 函数再将由回调函数返回的字典转换为JSON 字符串。也就是说,dumps 函数的本质还是将字典转换为 JSON 字符串,只是如果将类实例也转换为 JSON 字符串,需要先将类实例转换为字典,然后再将字典转换为JSON 字符申,而将类实例转換为字典的任务就是通过 default 关键字参数指定回调函数完成的。


image.png
14.2.4 类实例列表与 JSON 字符串互相转换

前面讲的类实例和 JSON 字符串直接地互相转换,转换的只是单个对象,如果 JSON 字符串是一个类实例数组或一个类实例的列表,也可以互相转换。

14.4.2 用 Python 操作 SQLite 数据库

通过 sqlite3 模块中提供的函数可以操作 SQLite 数据库,sqlite3 模块是 Python 语言内置的,不需要安装,直接导入即可。
sqlite3 模块中提供了丰富的函数可以对 SQLite 数据库进行各种操作,但是,在对数据进行增、删、改、查以及其他操作之前,要先使用 connect 函数打开 SQLite 数据库,通过该函数的参数指定 SOLite数据库的文件名即可。打开数据库后,通过 cursor 方法获取 sqlite3.Cursor 对象,然后通过 sqlite3.Cursor时象的 execute 方法执行各种 SQL 语句,如创建表、创建视图、删除记录、插入记录、查询记录等。如果执行的是查询 SOL 语句(SELECT 语句),那么 execute 方法会返回 sqlite3.Cursor 对象,需要对该对象进行迭代,才能获取查询结果的值。


image.png
image.png

14.5 MySQL 数据库

MySQL 是非常常用的关系型数据库,现在很多互联网应用都使用了 MySQL 数据库,在Python语言中需要使用 pymysql 模块来操作 MySQL 数据库。如果使用的是 Anaconda 的 Python 环境,则需要使用下面的命令安装 pymysql模块:
conda install pymysql
如果使用的是标准的 Python 环境,需要使用 pip 命令安装 pymysq1模块:
pip install pymysql
pymysql模块中提供的 AP与 sqlite3 模块中提供的 API类似,因为它们都遵循 Python DB API 2.0标准。下面的页面是该标准的完整描述:
https://www.python.org/dev/peps/pep-0249
其实也不必详细研究 Python DB API规范,只需要记住几个函数和方法,绝大多数的数据库的操作就可以搞定。
口 connect 函数:连接数据库,根据连接的数据库类型不同,该函数的参数也不同。connect函数
返回 Connection 对象。
口 cursor 方法:获取操作数据库的 Cursor 对象。cursor 方法属于 Connection 对象。
□ execute 方法:用于执行 SOL语句,该方法属于 Cursor 对象。
口 commit 方法:在修改数据库后,需要调用该方法提交对数据库的修改,commit 方法属于 Cursor
对象。
口 rollback 方法:如果修改数据库失败,一般需要调用该方法进行数据库回滚,也就是将数据恢复成修改之前的样子。

from pymysql import *
import json
#打开MySQL数据库,其中127.0.0.1是MySQL服务器的IP,root 是用户名,12345678 是密码
#test是数据库名
def connectDB():
    db=connect ("127.0.0.1","root","12345678","test",charset='utf8')
     return db
db = connectDB()
#创建persons 表
def createTable(db):
    #获取 Cursor 对象
    cursor=db.cursor()
    sql='''CREATE TABLE persons
        (id INT PRIMARY KEY       NOT ULL,
          name   TEXT     NOT NUL,
            age INT      NOT NULL,
          address   CHAR(50),
          salary  REAL);'''
try:
#执行创建表的 SQL 语句
image.png
image.png
image.png

从前面的代码和输出结果可以看出,操作 MySOL 和 SQLite 的 API基本是一样的,只是有如下点区别:
口 用 Cursor.execute 方法査询 SOLite 数据库时会直接返回查询结果,而使用该方法查询 MySQL数据库时返回了 None,需要调用 Cursor.fetchall 方法才能返回查询结果。
口 Cursor.execute 方法返回的查询结果和 Cursor.fetchall 方法返回的查询结果的样式是不同的,这一点从输出结果就可以看出来。如果想让 MySOL 的查询结果与 SOLite 的查询结果相同,需要使用 zip 函数和 dict 函数进行转换。关于这两个函数的用法,请参阅 6.2节的内容。

14.6 ORM

在前面讲的用 Python 语言操作 SQLite 和 MySQL 的过程中,使用的都是 SOL 语句,尽管 SQL语句比较方便,但缺点是必须要求程序员了解 SQL 语句,而且不同数据库在实现同样功能的 SQL语句时可能有差异,因此,直接在程序中嵌入 SQL 语句,会增加程序对数据库的耦合度,如果要更换数据库,可能还需要修改程序中的 SQL 语句。为了解决这个问题,出现了 ORM(Object Relational Mapping对象关系映射)技术,可以将纯 SQL 抽象化,或者说将 SQL 语句直接映射成 Python 对象,程序员只需要使用 Python 对象,就可以操作各种类型的数据库,ORM 系统会根据不同类型的数据库,以及相应的操作,自动生成 SQL 语句。因此,使用 ORM编写的应用在更换数据库(如 MySQL 换成 SQL Server,Oracle)时,不需要修改程序源代码。
绝大多数编程语言都支持ORM,有的是直接内置的,有的是通过第三方实现的。在 Python 语部中使用 ORM 有多种选择,都是通过模块支持的。比较著名的有 SQLAlchemy 和 SQLObject,

14.7 非关系型数据库
14.7.2 MongoDB 数据库

MongoDB 是非常著名的文档数据库,所有的数据以文档形式存储

14.7.3 pymongo 模块

在 python 语言中使用 MongoDB 数据库需要先导入 pymongo 模块,如果使用了 Anaconda Python开发环境,pymongo 模块已经被集成到 Anaconda:如果使用的是标准的 Python 开发环境,需要使下面的命令安装 pymongo 模块:
pip install pymongo

14.8 小结

本章讲解了 Python 语言中大多数场景需要使用到的各种数据存储技术,主要包括文本格式文件(XML和 JSON)、关系型数据库(SOLite 和 MySQL)和非关系型数据库(MongoDB),以及操作关系型数据库的两个 ORM 模块(SOLAlchemy 和 SQLObject)。可能很多读者会有这样的疑问,讲了这么多数据存储方案,那么在实际的应用中,应该使用哪种数据存储方案呢?其实在大多数应用中,存储方案都是多元化的,因为任何一种数据存储方案都不能适用于所有的场景。例如,存储配置信息一般会使用 XML 或 JSON,在网络上传输数据会使用JSON,在本地存储较多的数据,而且希望可以快速检索,可以考虑使用 SOLite 数据库,对于互联网应用,可以考虑使用 MySQL 数据库,但对于数据关系比较复杂的情况,可以采用 MySOL 和 MongoDB 混合的方式。总之,适合的才是最好的。

15.1 套接字

套接字(Socket)是用于网络通信的数据结构。在任何类型的通信开始之前,都必须创建 Socket可以将它们比作电话插孔,没有它就无法进行通信。
Socket 主要分为面向连接的 Socket 和无连接 Socket。面向连接的 Socket 使用的主要协议是传输控制协议,也就是常说的 TCP,TCP的 Socket 名称是 SOCK_STREAM。无连接 Socket 的主要协议是用户数据报协议,也就是常说的 UDP,UDP Socket 的名字是 SOCK_DGRAM。本节详细介绍如何使用socket模块进行面向连接的通信(TCP)以及无连接的通信(UDP)。
15.1.1 建立 TCP 服务端
Socket 分为客户端和服务端。客户端 Socket 用于建立与服务端 Socket 的连接,服务端 Socket 用
等待客户端 Socket 的连接。因此,在使用客户端 Socket 建立连接之前,必须建立服务端 Socket。
服务端 Socket 除了要指定网络类型(IPv4 和 IPv6)和通信协议(TCP 和 UDP)外,还必须要持个端口号。所有建立在 TCP/UDP 之上的通信协议都有默认的端口号。例如,HTTP 协议的默认端口号是 80,HTTPS 协议的默认端口号是 443,FTP 协议的默认端口号是21。这些都是应用层协议,建在TCP 协议之上,这些内容会在本章稍后的部分讲解。在Python 语言中创建 Socket 服务端程序,需要使用 socket 模块中的 socket类。创建 Socket 服务端程序的步骤如下:
(1)创建 Socket对象。
(2)绑定端口号。
(3)监听端口号。
(4)等待客户端 Socket 的连接。
(5)读取从客户端发送过来的数据。
(6)向客户端发送数据。
(7)关闭客户端 Socket 连接。
(8)关闭服务端 Socket连接。
上面的某些步骤可能会执行多次,例如,第4步等待客户端 Socket连接,可以放在一个循环山
当处理完一个客户端请求后,再继续等待另外一个客户端的请求。这些步骤的伪代码描述如下、

#创建 Socket 对象
tcpServerSocket = socket(...)
#绑定 Socket 服务端端口号
tcpServerSocket.bind(..)
#监听端口号
tcpServerSocket.listen(..)
#等待客户端的连接
tcpClientSocket = tcpServerSocket.accept()
#读取服务端发送过来的数据
data = tcpClientSocket.recv (..)
#向客户端发送数据
tcpClientSocket.send(...)
#关闭客户端 Socket 连接
tcpClientSocket.close()
#关闭服务端 Socket 连接
tcpServerSocket.close()
【例 15.1】 本例使用 socket 模块中的相关 API建立一个 Socket 服务端,端口号是 9876,可以使用浏览器、telnet 等客户端软件测试这个 Socket 服务。
实例位置:PythonSamples\src\chapter15\demo15.01.py
#导入 socket 模块中的所有 API
from socket import *
#定义一个空的主机名,在建立服务端 Socket 时一般不需要使用 host
host=' '
#用于接收客户端数据时的缓冲区尺寸,也就是每次接收的最大数据量(单位:字节)
bufferSize=1024
#服务端 Socket 的端口号
port = 9876
#将 host 和 port 封装成一个元组
addr =(host,port)
#创建 Socket 对象,AF INET表示 IPV4,AF INET6 表示 IPv6,SOCK_STREAM 表示 TCP
tcpServerSocket = socket(AF_INET, SOCK_STREAM)
#使用 bind 方法绑定端口号
tcpServerSocket.bind(addr)
#监听端口号
tcpServerSocket.listen()
print('server port:9876,)
print('正在等待客户端连接)
#等待客户端号Socket的连接、这里程序会被阻塞,直到接收到客户端的连接请求,才会往下执行
#接收到客户端请求后,同时返回了客户端 Socket 和客户端端口号

tcpClientsocket,addr = tcpServerSocket.accept()
print('客户端已经连接','addr’', '=', addr)
#开始读取客户端发送过来的数据,每次最多会接收不超过buffersize字节的数据
#如果客户端发送过来的数据量大于 bufferSize 所指定的字节数,那么recv万法只会返回buffersize 个
字节,剩下的数据会等待recv 方法的下一次读取
data = tcpclientSocket.recv(buffersize)
# recv方法返回了字节形式的数据,如果要使用字符串,需要将其进行解码,本例使用 utf8 格式解码
print (data.decode ('utfg'))
#向客户端以 utf-8 格式发送数据
tcpClientSocket.send("你好,I love you.\n'.encode (encoding='utf 8'))
#关闭客户端 Socket
tcpClientSocket.close()
#关闭服务端 Socket
tcpServerSocket.close()
image.png
尽管从表面上看,只有服务端 Socket 需要绑定端口号,其实客户端 Socket 在与服务端 Socket连接时也需要一个端口号,这个客户端 Socket 的端口号一般是自动产生和绑定的,这个端口号由 Socke对象的 accept方法返回。图 15-4 中的 53051 就是客户端 Socket 的端口号,每一个客户端 Socket的口号一般都是不同的。
除了使用 telnet 测试 Socket 服务端程序外,也可以使用浏览器进行测试,本例选择了 Google 的Chrome 浏览器。首先启动Socket 服务端程序,然后在 Chrome 浏览器地址栏中输入如下的 Url:http://localhost:9876/geekori
其中,“/geekori”是 Url 的路径(Path),由于 Socket 服务端程序并不是 HTTP 服务器,所以这个路径可以任意指定,浏览器只会使用 localhost 和后面的端口号(9876)连接 Socket 服务端。
输入上面的 Ul,并按 Enter 键后,在浏览器中会显示“该网页无法正常运作”或类似的信息,这个无关紧要,因为 Socket 服务端程序并没有返回 HTTP 响应头®和相关信息。出现这个信息也说明Chrome 浏览器成功连接到了 Socket 服务端程序。
在服务端,会在 Console 中输出如图 15-5 所示的信息。
在 Console 中显示的都是 Chrome 浏览器发送给 Socket 服务端程序的 HTTP 请求头信息,如果服务端是 HTTP 服务器,那么应该给浏览器返回 HTTP 响应头信息,而目前服务端程序只给浏览器返回了“你好,Ilove python.”,所以浏览器会认为返回的信息错误,即没有正常显示返回内容。
image.png
服务端的 Console 之所以能成功显示 Chrome 浏览器发送过来的所有 HTTP 请求头信息,是因为bufferSize 设置得足够大(1024 字节),所以一次就可以获得浏览器发送给服务端的所有数据,如果bufferSize 设置得比较小,那么就只会获得客户端发送过来的一部分数据。例如,如果将 bufferSize 设为10,那么获取的 HTTP 请求头信息如图 15-6所示。
image.png
很明显,在 Console 中只输出了 HTTP 请求头的前 10字节的内容(GET/geeko),要想获取所有客户端发送过来的数据,需要使用循环不断调用 recv 方法,最多每次获取 10 字节的数据,详细的实现过程见 15.1.2 节。
15.1.2 服务端接收数据的缓冲区

如果客户端传给服务端的数据过多,则需要分多次读取,每次最多读取缓冲区尺寸的数据,也就是 15.1.1 节例子中设置的 bufferSize 变量的值。如果要分多次读取,则根据当前读取的字节数是否小于缓冲区的尺寸来判断是否后面还有其他未读的数据,如果没有,则终止循环。


image.png
image.png
15.1.3 服务端的请求队列

通常服务端程序不会只为一个客户端服务,当 accept方法在接收到一个客户端请求后,除非再次调用 accept 方法,否则将不会再等待下一个客户端请求。当然,可以在 accept方法接收到一个客户端请求后启动一个线程(见 15.1.4 节)来处理当前客户端的请求,从而让 accept方法尽可能快地再次被调用(一般会将调用 accept 方法的代码放在一个循环中,见 15.1.4 节的例子),但就算 accept 方法很快被下一次调用,也是有时间间隔的(如两次调用 accept 方法的时间间隔是 100ms),如果在这期间又有客户端请求,该如何处理呢?
在服务端 Socket 中有一个请求队列。如果服务端暂时无法处理客户端请求,会先将客户端请求放到这个队列中,而每次调用 accept方法,都会从这个队列中取一个客户端请求进行处理。不过这个请求队列也不能无限制地存储客户端请求,请求队列的存储上限与当前操作系统有关,例如,有的 Linux系统的请求队列存储上限是 128 个。请求队列的存储上限也可以进行设置,只要通过 listen 方法监听端口号时指定请求队列上限即可(这个值也被称为 backlog),如果这个指定的上限超过了操作系统限
制的最大值(如 128),那么会直接使用这个最大值。


image.png
image.png
5.1.4 TCP时间戳服务端

本节会利用 Socket 实现一个可以将时间返回给客户端的时间戳服务端。当客户端向服务端发送数据后,服务端会将这些数据原样返回,并附带上服务端当前的时间

15.1.5 用 Socket 实现 HTTP 服务器

本节会使用 Socket 实现一个 HTTP 服务器。如果要实现 HTTP 服务器,服务端在接收数据,以及
向客户端发送数据时,必须遵循 HTTP 协议。也就是说,服务端在接收数据时,要解析 HTTP 请求头向客户端发送数据时,要在发送的数据前面加上 HTTP请求头。关于 HTTP 的详细信息请访问如下的URL:
https://tools.ietf.org/html/rfc2616
其实读者也不需要深入了解 HTTP 协议,因为 HTTP 协议是 Web 中经常使用的协议,已经有很多实现了,通常并不需要自己去实现完整的HTTP 协议。本节之所以要自己实现 HTTP 协议,只是为了演示 Socket 的功能,而且并没有实现完整的 HTTP 协议,只是通过 HTTP 协议实现了一个最基本的HTTP 服务器。
下面解释一下 HTTP 服务器的实现原理。首先应该了解一下 HTTP 请求头的格式。HTTP 请求头是纯文本形式,除了第1行,其他行都是 key-value 形式。例如,下面是一个典型的 HTTP 请求头。
GET /main/index.html HTTP/1.1
Host:geekori.com
Accept:/
Pragma: no-cache
Cache-Control:no-cache
Referer:http://download.microtool.de/
User-Agent:Mozilla/4.04en
Range:bytes=554554-
大家不必理会上面的大多数内容,因为浏览器会自动发送这些内容,服务端也会自动处理。但必须了解第1行,因为在第1行中包含了请求路径。第1行分为如下三个部分,中间用空格分隔。
(1)方法(GET、POST等)。
(2)请求路径,需要将其映射成服务端对应的本地文件路径
(3)HTTP 版本,目前一般是 1.1。由于本节要实现的是一个基本的 HTTP 服务器,所以只考虑第1行。HTTP 响应头与 HTTP 请求头类似,下面是一个 HTTP 响应头的例子。
HTTP/1.1 200 OK
Date:Mon,31Dec2012 04:25:57GMT
Server:Apache/1.5(UNIX)
Content-type:text/html
Content-length:1234
HTTP 响应头只有两行是必须指定的,即第1行和 Content-length 字段。第1行描述了 HTTP 版本。
返回状态码等信息,其中 200和 OK 描述了访问成功。如果要描述页面没找到,可以返回 404。不过本节的例子不管是否找到服务端的页面,都返回了 200,只是在没找到页面时固定返回了“File Not Found”信息。读者可以自己修改这个例子,做更有趣的实验。
还有一点要注意,HTTP 响应头在返回时,一定与后面的要返回的内容之间有一个空行,浏览器会依赖这个空行区分 HTTP 响应头到哪里结束。

image.png
image.png
现在运行程序,然后在当前目录建立一个 static 子目录,并在该目录中建立两个文件:test.txt 和index.html。分别输入如下的内容:
test.txt
hello world
index.html
<h1>Main Page</h1>
接下来在当前路径建立一个 response_headers.txt 文件,并输入如下内容。在该文件中使用了“%d’作为格式化符号,在读取该文件时需要将“%d”格式化为发送到客户端数据的长度,单位是字节。
HTTP/1.1 OK
Server:custom
Content-type:text/html
Content-length:号d
现在打开浏览器,在浏览器地址栏中输入如下的 URL:
http://localhost:9876
会在浏览器中显示如图 15-13 所示的内容。
image.png
如果在浏览器地址栏中输入 http://ocalhost:9876/test.txt,那么会输出如图 15-14 所示的内容
image.png
15.1.6 客户端 Socket

在前面的部分一直使用 telnet 和浏览器作为客户端测试 Socket 服务端,其实 socket 类同样可以作为客户端连接服务器。socket 类连接服务端的方式与创建 Socket 服务端类似,只是这时 host (IP 或域名)就有用了,因为客户端 Socket 在连接服务端时,必须指定服务器的IP 或命名。当然,端口号也是必需的。在浏览器中使用 http/https 访问 Web 页面时,之所以没有指定端口号,是因为使用了默认的端口号,http 的默认端口号是 80,https 默认的端口号是 443。
客户端 Socket 成功连接服务端后,可以使用 send 方法向服务端发送数据,也可以使用 recv 方法接收从服务端返回的数据,使用方法与服务端 Socket 相同。

from socket import *
#服务器的名称(可以是 IP 或域名)
host ='localhost'
#服务器的端口号
port= 9876
#客户端 Socket 接收数据的缓冲区
buffersize=1024
addr =(host,port)
tcpClientSocket = socket(AF_INET, SOCK_STREAM)
#开始连接时间戳服务端
tcpClientSocket.connect (addr)
while True:
  #从终端采集用户输入信息
   data = input('>')
   #如果什么都未输入,退出循环
    if not data:.
        break
     #将用户输入的字符串按 utf-8 格式编码成字节序列
     data = data.encode('utf-8')
     #向服务端发送字节形式的数据
      tcpClientsocket.send(data)
      #从服务端接收数据
      data = tcpClientSocket.recv(buffersize)
      #输出从服务端接收到的数据
      print(data.decode('utf-8'))
#关闭客户端 Socket
tcpClientSocket.close()
image.png
15.1.7 UDP 时间戳服务端

UDP 与TCP 的一个显著差异就是前者不是面向连接的,也就是说,UDPSocket 是无连接的,TCESocket是有连接的。那么什么是无连接?什么是有连接呢?有连接的网络传输协议(如 TCP)是指在网络数据传输的过程中客户端与服务端的网络连接会一直存在,而且面向连接的网络传输协议会通过某些机制保证数据传输的可达性,如果用比较科幻的说法就是通过面向连接的网络协议在客户端和服务端建立一个稳定的虫洞,可以放心大胆地在虫洞中传递数据。面向无连接的网络协议(如 UDP)相当于将一東光射向远方,对于发射光源的一方只负责开启光源,至于射出的这束光能不能到达目的地那就不管了。当然,这束光有可能会到达目的地,也有可能发生意外,如碰到某个障碍物或被散射。因此,通过像 UDP 这类无连接的网络协议传输的数据不能保证 100%到达目的地,但操作更简单,没有像 TCP 这类有连接的网络协议需要那么多设置。
本节会利用 UDP 服务实现一个与 15.1.4 节实现的时间戳服务端功能完全相同的服务端程序。当
然,一般都是在本地测试(使用 localhost 或 127.0.0.1),所以几乎不会出现传输的数据不会到达目的地的情况,但在复杂的网络中运行本节的例子,就有可能会出现数据无法传输到目的地的情况。

实例位置:PythonSamples\src\chapter15\demo15.07.py
from socket import *
from time import ctime
host= ' '
port = 9876
buffersize = 1024
addr =(host, port)
# SOCK DGRAM 表示 UDP
udpServerSocket = socket(AF_INET, SOCK_DGRAM)
udpServerSocket.bind(addr)
while True:
     print('正在等待消息.......')
     #接收从客户端发过来的数据
      data, addr = udpServerSocket.recvfrom(buffersize)
       #向客户端发送服务端时间和客户端发送过来的字符串
       udpServerSocket.sendto(ctime().encode (encoding='utf-8') + b' '+ data,addr)
       print('客户端地址:',addr)
udpserverSocket.close()

要注意的是,使用 UDP Socket发送和接收数据的方法与 TCP Soeke 不同。UDp Socket接收数据的方法是recvfrom,发送数据的方法是 sendto.

15.2 socketserver 模块

socketserver 是标准库中的一个高级模块,该模块的目的是让 Socket 编程更简单。在 socketserver模块中提供了很多样板代码,这些样板代码是创建网络客户端和服务端所必需的代码。本节会利用socketserver 模块中的 API重新实现时间戳客户端和服务端,从中会看到,使用 socketserver 模块实现的时间戳服务端的代码更简洁,也更容易维护。

15.2.1 实现 socketserver TCP 时间戳服务端

socketserver 模块中提供了一个 TCPServer 类,用于实现 TCP 服务端。TCPServer 类的构造方法有两个参数:第1个参数需要传入 host 和 port(元组形式);第2个参数需要传入一个回调类,该类必须是 StreamRequestHandler 类的子类。在 StreamRequestHandler 类中需要实现一个 handle 方法,如果接收到客户端的响应,那么系统就会调用 handle 方法进行处理,通过 handle 方法的 self参数中的响应API 可以与客户端进行交互。

#将 TCPServer 类重命名为 TCP,将 streamRequestHandler 类重命名为 SRH
from socketserver import (TCPServer as TCP,StreamRequestHandler as SRH)
from time import ctime
host = ' '
port = 9876
addr =(host,port)
#定义回调类,该类必须从 streamRequestHandler 类(已经重命名为 SRH)继承
class MyRequestHandler(SRH):
     #处理客户端请求的方法
     def handle (self):
     #获取并输出客户端 IP和端口号
     print('客户端已经连接,地址:"self.client address)
     #向客户端发送服务端的时间,以及按原样返回客户端发过来的字符串
     self.wfile.write(ctime().encode(encoding='utf-8')+ b' '+ self.rfile.readline())
#创建 TCPServer 类(已经重命名为 TCP)的实例
tcpServer = TCP(addr, MyRequestHandler)
image.png

从上面的代码可以看出,在 handle 方法中通过 self.rfle.readline 方法从客户端读取数据,通过self.wfle.write 方法向客户端发送数据。由于读取客户端数据使用了 readline 方法,该方法读取客户端发送过来的数据的第 1行,所以客户端发送过来的数据至少要有一个行结束符(“\r\n”或“\n"),否则服务端在读取客户端发送过来的数据时会一直处于阻塞状态,直到超时才结束读取。

15.2.2实现 socketserver TCP 时间戳客户端

socketserver TCP 客户端与前面实现的 TCP Socket 客户端没什么区别,只是在向 socketserver TCP时间戳服务端发送文本数据时要加一个行结束符。


image.png
image.png

相关文章

网友评论

      本文标题:Python精点02

      本文链接:https://www.haomeiwen.com/subject/apozmjtx.html