小黄

黄小黄的幸福生活!


  • 首页

  • 标签

  • 分类

  • 归档

  • Java

2.高级特性

发表于 2019-04-21 | 分类于 Python

高级特性

掌握了Python的数据类型,语句和函数,基本上就可以编出很多有用的程序了,比如构造一个1,3,5,...,99的列表,可以通过循环实现:

1
2
3
4
5
6
7
8
>>> L=[]
>>> n=1
>>> while n<=99:
... L.append(n)
... n=n+2
...
>>> print(L)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]

取list的前一半元素,也可以通过循环实现。

但是Python中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。

基于这一思想,我们开始介绍Python中非常有用的高级特性,1行代码能实现的功能,绝对不用五行代码。


[TOC]


切片

取一个list或者tuple的部分元素是非常常见的操作,比如,一个list如下:

1
L=['Michael','Sarah','Tracy','Bob','Jack']

取前三个元素,应该怎么办?

笨方法:

1
2
3
Q=[L[0],L[1],L[2]]
>>> print(Q)
['Michael', 'Sarah', 'Tracy']

该方法如果扩展之后让去N个元素就没有办法了。取N个元素,也就是取序号为0-N-1的元素,可以用循环:

1
2
3
4
5
6
7
8
9
10
>>> r=[]
>>> n=3
>>> i=0
>>> while i<n:
... r.append(L[i])
... i=i+1
...
>>> print(r)
['Michael', 'Sarah', 'Tracy']
>>>

或者:

1
2
3
4
5
6
7
>>> r=[]
>>> n=3
>>> for i in range(n):
... r.append(L[i])
...
>>> r
['Michael', 'Sarah', 'Tracy']

对这种经常取指定索引范围的操作,用循环身份繁琐,因此,Python提供了切片(slice)操作符,能大大简化这种操作。

对应上面的问题,取前三个元素,用一行代码就可以搞定:

1
2
3
4
5
6
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
>>> L[1:3]
['Sarah', 'Tracy']
>>> L[:3]
['Michael', 'Sarah', 'Tracy']

L[0:3]表示,从索引0开始取,取到索引3为止,但不包括3.即索引取0,1,2,正好是三个元素。如果第一个索引是0,还可以省略,也可以从索引1开始,取出两个元素。

类似的,既然Python支持L[-1]取出倒数第一个元素,那么他同样支持倒数切片:

1
2
3
4
>>> L[-3:-1]
['Tracy', 'Bob']
>>> L[-5:]
['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']

切片操作十分有用。我们先创建一个0-99的数列:

1
>>> L=list(range(100))

可以通过切片取出某一段数列,比如前十个,后十个,11-20等:

1
2
3
4
5
6
>>> L[:10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> L[-10:]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
>>> L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

前十个数,每两个去一个:

1
2
>>> L[:10:2]
[0, 2, 4, 6, 8]

所有数,每五个取一个:

1
2
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

如果什么都不写,只写[:]就可以复制一个list。

tuple也是list的一种,唯一区别是tuple不可变。因此tuple也可以用切片操作,操作的结果仍然是tuple:

1
2
3
4
5
6
7
>>> t=tuple(range(20))
>>> t
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
>>> t[:3]
(0, 1, 2)
>>> t[-12:-3]
(8, 9, 10, 11, 12, 13, 14, 15, 16)

字符串xxx也可以看成是一种list,每一个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍然是字符串:

1
2
3
4
5
>>> h='abcdefghi'
>>> h[:4]
'abcd'
>>> h[2:8:2]
'ceg'

在许多编程语言中,针对字符串提供了很多种截取函数,如substring,其实目的就是对字符串进行切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。

小结
有了切片操作,很多地方循环就不需要了。Python的切片非常灵活,一行代码就可以实现多行循环才能完成的操作

迭代

如果给定一个list或者tuple,我们可以通过for循环来遍历这个list或者tuple,这种遍历我们称为迭代(iteration)。

在Python中,迭代是通过for...in来完成的,而很多语言如Java,迭代是通过下标完成的,比如:

1
2
3
for(i=0;i<list.length;i++){
n=list[i]
}

可以看出,Python的for循环抽象程度要高于Java的for循环,因为Python的for循环不仅可以用于list或tuple上,还可以用于其他可迭代对象上,list这种数据类型虽然有下标,但很多其他数据类型是没有下标的。只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:

1
2
3
4
5
6
7
>>> d={'a':1,'b':2,'c':3}
>>> for key in d:
... print(key)
...
c
a
b

其中因为dict的存储不是按照list的方式顺序排列,所以迭代出来的顺序很可能不一样。默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k,v in d.items()。

由于字符串也是可迭代对象,因此,也可以用作for循环:

1
2
3
4
5
6
7
8
9
>>> for ch in 'abcdnf':
... print(ch)
...
a
b
c
d
n
f

所以,当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象是list还是其他的数据类型。

如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:

1
2
3
4
5
6
7
>>> from collections import Iterable
>>> isinstance('abc',Iterable) #str是否可迭代
True
>>> isinstance([1,2,3,4],Iterable) #str是否可迭代
True
>>> isinstance(123,Iterable) #str是否可迭代
False

如果对list实现类似Java那样的下标循环怎么办?Python中内置的enumerate函数可以把一个list编程索引-元素对,这样就可以在for循环中同事迭代索引和元素本身:

1
2
3
4
5
6
7
8
>>> for i, value in enumerate(['a','b','c','d','e']):
... print(i,value)
...
0 a
1 b
2 c
3 d
4 e

上面的for循环里,同时引用了两个变量,在Python里面是很常见的:

1
2
3
4
5
6
7
>>> for x,y in [(1,2),(3,4),(5,6),(7,8)]:
... print(x,y)
...
1 2
3 4
5 6
7 8

小结
任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型,只要符合迭代条件,就可以使用for循环。


列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式,如下:

1
2
>>> list(range(1,11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但是如果要生成[1*1,2*2,3*3,...,10*10],方法一是循环:

1
2
3
4
5
6
>>> L=[]
>>> for x in range(1,11):
... L.append(x*x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

1
2
>>> [x*x for x in range(1,11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

写列表生成式时,把要生成的元素x*x放在前面,后面跟上for循环,就可以把list创建出来。

for循环后面还可以加上if判断,这样我们局可以筛选出仅偶数的平方:

1
2
>>> [x*x for x in range(1,11) if x%2==0]
[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列:

1
2
>>> [x+y for x in 'abcd' for y in 'ABCD']
['aA', 'aB', 'aC', 'aD', 'bA', 'bB', 'bC', 'bD', 'cA', 'cB', 'cC', 'cD', 'dA', 'dB', 'dC', 'dD']

三层和三层以上的循环就很少用了。

运用列表生成式可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:

1
2
3
>> import os
>>> [d for d in os.listdir('.')]
['3D Objects', 'AppData', 'Application Data', 'Contacts', 'Cookies', 'Desktop', 'Documents', 'Downloads', 'Favorites', 'IntelGraphicsProfiles', 'Links', 'Local Settings', 'Music', 'My Documents', 'NetHood', 'NTUSER.DAT', 'ntuser.dat.LOG1', 'ntuser.dat.LOG2', 'NTUSER.DAT{eccc1a56-42ca-11e6-9cd4-bf9ec156b7db}.TM.blf', 'NTUSER.DAT{eccc1a56-42ca-11e6-9cd4-bf9ec156b7db}.TMContainer00000000000000000001.regtrans-ms', 'NTUSER.DAT{eccc1a56-42ca-11e6-9cd4-bf9ec156b7db}.TMContainer00000000000000000002.regtrans-ms', 'ntuser.ini', 'OneDrive', 'Pictures', 'PrintHood', 'Recent', 'Saved Games', 'Searches', 'SendTo', 'Templates', 'Videos', '「开始」菜单']

for循环其实可以同时使用两个甚至多个变量,如dict的items()可以同时迭代key和value:

1
2
3
4
5
6
>>> for k,v in d.items():
... print(k,'=',v)
...
c = 3
a = 1
b = 2

因此列表生成式也可以使用两个变量来生成list:

1
2
3
>>> d={'a':'b','c':'v','d':'g'}
>>> [k + '=' + v for k,v in d.items()]
['d=g', 'c=v', 'a=b']

最后把一个list中的所有字符串变为小写:

1
2
3
>>> L=['Hello','DSFA','DFAC']
>>> [s.lower() for s in L]
['hello', 'dsfa', 'dfac']

练习

L1=[‘Hello’,’World’,18,’Apple’,None],添加if语句,期待L2=[‘hello’, ‘world’, ‘apple’]

1
2
3
4
>>> L2=[s.lower() for s in L1 if isinstance(s,str)]
>>>
>>> L2
['hello', 'world', 'apple']

生成器

通过列表生成式,我们可以直接创建一个列表。但是受到内存的限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的空间,如果我们仅仅需要访问前面几个元素,后面的绝大多数元素占用的空间就被白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环过程中不断推算出后面的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多中方法。第一种很简答,只要把一个列表生成式的[] 改为(),就创建了一个generator:

1
2
3
4
5
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g=(x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000020E84788E60>

可以看出创建L和g的区别仅在于最外层是[]还是(),L是一个list,而g是一个generator。我们可以直接打出list的么一个元素,但是我们怎么打出generator的每一个元素呢,如果要一个一个打,可以通过next()函数来获得generator的下一个返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)

我们前面讲过,generator保存的是算法,每次调用next(g),就计算出下一个g的值,直到计算到最后一个元素,没有更多的元素时,抛出stopiteration的错误。generator非常强大,如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。比如著名的斐波拉契数列(Fibonacci),除了第一个和第二个数外,任意一个数可由前面两个数相加得到:
1,1,2,3,5,8,13,21,34…

斐波拉契数列用列表生成式写不出来,但是,用函数可以把它打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> def fib(max):
... n,a,b=0,0,1
... while n<max:
... print(b)
... a,b=b,a+b
... n=n+1
... return 'done'
...
>>> fib(10)
1
1
2
3
5
8
13
21
34
55
'done'

仔细看,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑非常类似generator。也就是说,上面个的函数和generator只有一步之遥。要把fib变为generator,只需要把print(b)改为yield b就可以了:

1
2
3
4
5
6
7
8
9
10
>>> def fib(max):
... n,a,b=0,0,1
... while n<max:
... yield b
... a,b=b,a+b
... n=n+1
... return 'done'
...
>>> fib(10)
<generator object fib at 0x0000020E84788BA0>

这就是定义generator的另一种方法。如果一个函数定义包含关键字yield,那么这个函数就不再是一个普通的函数,而是一个generator。

这里最难理解的是generator和函数的执行流程不一样。函数是顺序执行的,遇到return语句或者最后一行函数语句就返回。而变为generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句出继续执行。

举个例子,定义一个generator,一次返回数字1,3,5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> def odd():
... print('step1')
... yield 1
... print('step2')
... yield (3)
... print('step3')
... yield (5)
...
>>> o=odd()
>>> next(o)
step1
1
>>> next(o)
step2
3
>>> next(o)
step3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

可以看出,odd()不是普通函数,而是generator,在执行过程中,遇到yield就会中断,下次又继续执行。执行3次之后,没有yield可以执行了,所以第四次调用next(o)就报错。回到fib的例子,我们在循环过程中不断用yield,就会不断中断。当然要给循环设置一个条件来跳出循环,不然就会产生一个无限数列出来。同样的,把函数改为generator之后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> for n in fib(10):
... print(n)
...
1
1
2
3
5
8
13
21
34
55

但是使用for循环时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> f=fib(6)
>>> while True:
... try:
... x=next(f)
... print('g',x)
... except StopIteration as e:
... print('Generator return value:',e.value)
... break
...
g 1
g 1
g 2
g 3
g 5
g 8
Generator return value: done

练习
杨辉三角,把每一行看做一个list,试着写一个generator,不断输出下一行的list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> def tri():
... L=[1]
... while True:
... yield L
... L=[L[x]+L[x+1] for x in range(len(L)-1)]
... L.insert(0,1)
... L.append(1)
...
>>> n=0
>>> for t in tri():
... print(t)
... n=n+1
... if n==10:
... break
...
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]

小结
generator是非常强大的工具,在Python中,可以简单的把列表生成式改为generator。要理解generator的工作原理,他是在for循环的过程中不断计算出下一个元素,并在适当的条件结束循环。对于函数改成的generator来说,遇到return语句或者执行到函数体最后一行语句,就要结束generator的指令,for循环随之结束。

请注意区分普通函数和generator函数,普通函数调用直接返回结果,generator函数调用实际返回一个generator对象。


迭代器

我们已经知道,可以直接作用关于for循环的数据类型有以下几种:一类是集合数据类型,如list,tuple,dict,set,str等;一类是generator,包括生成器和带yield的generator function。这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。可以使用isinstance()来判断一个对象是否是Iterable对象:

1
2
3
4
5
6
7
8
>>> isinstance([],Iterable)
True
>>> isinstance({},Iterable)
True
>>> isinstance('abc',Iterable)
True
>>> isinstance((x for x in range(10)),Iterable)
True

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。

可以用isinstance()来判断一个对象是否是Iterator对象:

1
2
3
4
>>> from collections import Iterator
>>> isinstance((x for x in range(10)),Iterator)
True
False

生成器都是Iterator对象,但是list,dict,str却不是。把这些Iterable变为Iterator可以使用iter()函数:

1
2
3
4
>>> isinstance(iter([]),Iterator)
True
>>> isinstance(iter('adsaf'),Iterator)
True

为什么list,dict,str不是Iterator???

这是因为Python的Iterator对象表示一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序的序列,但是我们却不能提前得知序列的长度,只能不断通过next()函数实现按需计算的下一个数据,所以Iterator的计算时惰性的,只有在需要的时候才会计算。Iterator甚至可以表示一个无限大的数据流,例如全体自然数,但是list是永远不可能存储全体的自然数的。

小结
凡是可以用作for循环的对象都是Iterable类型;
凡是可作用关于next()函数的对象都是Iterator类型,他们表示一个惰性计算的序列;
集合数据类型如list,dict,str等都是Iterable,但是不是Iterator,不过可以通过iter()获得一个Iterator对象。

·Python上的for循环本质上是通过不断调用next()函数来实现的:

1
2
3
4
5
6
7
8
9
10
11
>>> for x in [1,2,3,4,5,6]:
... pass
... #等价的
>>> it=iter([1,2,3,4,5,6])
>>> while True:
... try:
... x=next(it)
... except StopIteration:
... break
...
>>>

1. 函数

发表于 2019-04-21 | 分类于 Python

函数

我们知道圆的计算公式是:

S=πrr

当我们知道圆的半径r时,就可以很快的计算出圆的面积:

r1=12.34
s1=3.14r1r1

当代码出现规律的重复时,你就需要当心了,每次写3.14*x*x不仅很麻烦,而且,如果要把3.14改为3.14159265359 的时候就要全部替换。

有了函数,我们就不用再每次 再写s=3.14*x*x而是写成更有意义的函数调用s=area_of_circle(x),而函数area_of _circle(x)本身只需要写一次就可以多次调用。

基本上所有的高级语言都支持函数,Python也不例外。Python不但能非常灵活的定义函数,而且本身内置了很多有用的函数,可以直接调用。


调用函数


Python内置了很多有用的函数,我们可以直接调用。

要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs,只有一个参数。可以直接在Python的官方网站查看文档:

http://docs.python.org/3/library/functions.html#abs

也可以在交互式命令行通过help(abs)查看abs函数的帮助信息。
调用abs函数:

1
2
3
4
5
6
>>> abs(100)
100
>>> abs(-256)
256
>>> abs(-12.88)
12.88

调用函数时,如果传入的参数的数量不对,会报TperError的 错误,并且给出错位信息:str是错误的参数类型:

1
2
3
4
>>> abs('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'

而max函数max()可以接收任意多个参数,并返回最大的那个:

1
2
>>> max(1,2,3,45,65,334)
334

数据类型转换

Python内置的常用函数还包括数据类型转换函数,比如int()函数可以把其他数据类型转换为整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> int('1234')
1234
>>> int(12.34)
12
>>> float(12.34)
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个别名:

1
2
3
>>> a=abs #变量a指向abs函数
>>> a(-1) #所以可以通过a来调用函数
1

练习

请用Python内置的hex()函数把一个整数转换成十六进制表示的字符串:

1
2
3
4
5
6
>>> n1=255
>>> n2=1000
>>> print(str(hex(n1)))
0xff
>>> print(str(hex(n2)))
0x3e8

定义函数


在Python中,定义一个函数要使用def语句,依次写出函数名,括号,括号中的参数和冒号:,然后在缩进块中编写函数体,函数的返回值用return语句返回。

我们自定义一个求绝对值的my_abs函数为例:

1
2
3
4
5
6
7
8
>>> def my_abs(x):
... if x>=0:
... return x
... else:
... return -x
...
>>> my_abs(-1524)
1524

请注意,函数内部结构的的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部可以通过条件判断和循环实现非常复杂的逻辑。
如果没有return语句,函数执行完也会返回结果,只是结果为None.

空函数

如果想定义一个什么事业不做的空函数,可以用pass语句:

1
2
3
>>> def nop():
... pass
...

pass语句什么都不做,有什么用?实际上pass可以用来作为占位符,比如现在还没有想好怎么写函数的代码,可以先放一个pass,让代码能运行起来。

pass还可以用在其他语句里,比如:

1
2
3
>> if age>=18:
.. pass
..

缺少了pass,代码运行就会有语法错误。

参数检查

调用函数是,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError:

1
2
3
4
>>> my_abs(1,12,-5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_abs() takes 1 positional argument but 3 were given

单数如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs和内置函数abs的区别:

1
2
3
4
5
6
7
8
9
>>> my_abs('x')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in my_abs
TypeError: unorderable types: str() >= int()
>>> abs('x')
Traceback (most recent call last):
File "<stdin>", in <module>
TypeError: bad operand type for abs(): 'str'

当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,会导致if语句出错,出错信息和abs不一样,所以这个函数定义的不够好。
让我们修改一下my_abs的定义,对参数进行检查,只允许整数和浮点数类型的参数。数据类型的检查可以用内置函数isinstance()来实现:

1
2
3
4
5
6
7
>>> def my_abs(x):
... if not isinstance(x,(int,float)):
... raise TypeError('bad operand type')
... if x>=0:
... return x
... if x<0:
... return -x

返回多个值

函数可以返回多个值吗?答案是肯定的。

比如在游戏中经常需要从一个点移动到另一个点,给出坐标,位移和角度,就可以计算出新的坐标:

1
2
3
4
5
6
7
>>> import math
>>> def move(x,y,step,angle=0):
... nx=x+step*math.cos(angle)
... ny=y+step*math.sin(angle)
... return nx, ny
...
>>>

import math语句表示导入math包,并允许后续代码引用math包里面的sin ,cos等函数。
然后我们就可以同时获得返回值:

1
2
3
>>> x,y=move(100,100,60,math.pi/6)
>>> print(x,y)
151.96152422706632 130.0

但是其实这只是一个假象,Python函数返回的仍然是单一值:

1
2
3
>>> r=move(100,100,60,math.pi/6)
>>> print(r)
(151.96152422706632, 130.0)

原来返回的是一个tuple,但是,在语法上面,返回一个tuple是可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数的返回多值,实际上就是返回一个tuple,但是写起来方便很多。

练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# -*- coding: utf-8 -*-
import math
def quadratic(a,b,c):
if not isinstance(a,(int,float)):
raise TypeError('输入的数据类型错误')
if not isinstance(b,(int,float)):
raise TypeError('输入的数据类型错误')
if not isinstance(c,(int,float)):
raise TypeError('输入的数据类型错误')
d=b*b-4*a*c
if a!=0:
if d>0:
s='方程有两个不同的实数根'
x1=(-b+math.sqrt(d))/2/a
x2=(-b-math.sqrt(d))/2/a
return s,x1,x2
elif d==0:
s='方程有两个相同的实数根'
x1=-b/2/a
x2=-b/2/a
return s,x1,x2
else:
s='方程有两个不同的复数根'
return s
elif b!=0:
s='方程有一个实数根'
x1=-c/b
return s,x1
elif c!=0:
s='方程无解'
return s
else:
s='方程无意义'
return s
r=quadratic(1,4,2)
print(r)
F:\python>python jiefangcheng.py
('方程有两个不同的实数根', -0.5857864376269049, -3.414213562373095)

函数的参数

定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就足够了,函数内部的复杂逻辑被封装起来,调用者无需了解。

Python的函数定义非常简单,但灵活度却很大。除了正常定义的必选参数外,还可以使用默认参数,可变参数和关键字参数,是的函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。

位置参数

我们先写一个计算x的平方的函数:

1
2
3
4
5
6
7
8
>>> def power(x):
... return x*x
...
>>> power(5)
25
>>> power(52)
2704
>>>

对于power(x)的函数,参数x就是一个位置参数。当我们调用power函数时,必须传入有且仅有一参数x。

现在,我们要计算x³怎么办?计算xⁿ怎么办,我们可以把power(x)改为`power(x,n),用来计算xⁿ:

1
2
3
4
5
6
7
8
9
>>> def power(x,n):
... s=1
... while n>0:
... n=n-1
... s=s*x
... return s
...
>>> power(5,5)
3125

修改后的power(x,n)函数有两个参数:x和n,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给x和n。

默认参数

新的power(x,n)函数没有问题,但是,旧的调用代码失败了,原因是我们增加了一个参数,导致旧的代码因为缺少一个参数而无法正常调用:

1
2
3
4
>>> power(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'n'

Python的错误信息很明确:调用函数power( )缺少了一个位置信息n。

这个时候默认参数就派上用场了。由于我们经常计算x²,所以,完全可以把第二个参数n默认值设为2:

1
2
3
4
5
6
7
8
9
10
11
>>> def power(x,n=2):
... s=1
... while n>0:
... n=n-1
... s=s*x
... return s
...
>>> power(5)
25
>>> power(5,5)
3125

这个例子可以看出,默认参数可以简化函数的调用。设置默认参数时,要注意以下几点:

1.必选参数在前,默认参数在后,否则Python的解释器会报错
2.如何设置默认参数:
把变化大的参数放在前面,变化小的参数放后面。变化小的参数可以做默认参数。

使用默认参数的好处是能降低调用函数的难度。

举个例子,我们写个一年级小学生的注册的函数,需要传入name和gender两个参数:

1
2
3
4
5
6
7
>>> def enroll(name,gender):
... print('name:',name)
... print('gender:',gender)
...
>>> enroll('James','D')
name: James
gender: D

这样,调用enroll()函数需要传入两个参数。如果要继续传入年龄,城市信息等信息怎么办?这样会使得调用函数的复杂度大大增大。我们可以把年龄和城市设为默认参数,这样大多数学生注册时不需要提供年龄和城市,只需要提供必需的两个参数,只有与默认的参数不符的学生才需要提供额外信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def enroll(name,gender,age=7,city='Beijing'):
... print('name:',name)
... print('gender:',gender)
... print('age:',age)
... print('city:',city)
...
>>> enroll('James','D')
name: James
gender: D
age: 7
city: Beijing
>>> enroll('James','D',6,'Shanghai')
name: James
gender: D
age: 6
city: Shanghai

可见,默认参数降低了函数的复杂度,而一旦函数需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。

有多个默认参数时,调用的时候,既可以按照顺序提供默认参数,比如调用enroll('Bob','M',7),意思是除了name和gender两个参数外,最后一个参数应用在参数age上,city上则使用默认值。

也可以不按照顺序提供默认参数。当不按照顺序提供默认参数的时候,需要把参数名写上。比如调用enroll('Adam','B',city='Tianjin'),意思是city参数用传进去的值,其他默认参数继续使用默认值。

默认参数使用很有效,但是使用不当也会产生误导,如下:

先定义一个函数,传入一个list,添加一个END再返回:

1
2
3
4
5
6
7
8
>>> def add_end(L=[]):
... L.append('END')
... return L
...
>>> add_end([1,2,3,4,5])
[1, 2, 3, 4, 5, 'END']
>>> add_end(['x','t','xc'])
['x', 't', 'xc', 'END']

调用没有问题,一开始调用默认参数也没有问题,但是再调用add_end()时,结果就不对了。

1
2
3
4
5
6
>>> add_end()
['END']
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

这是因为Python在函数定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是定义的[]了,所以定义默认参数时要牢记一点:默认参数必须指向不变对象来实现!

可以用None来修改:

1
2
3
4
5
6
7
8
9
10
11
12
>>> def add_end(L=None):
... if L is None:
... L=[]
... L.append('END')
... return L
...
>>> add_end()
['END']
>>> add_end()
['END']
>>> add_end(['x',5,'dfs'])
['x', 5, 'dfs', 'END']

设计str,None这样的不变对象的目的在于:不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读取一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,就尽量设计成不变的对象。

可变参数

在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入参数的个数是可变的,可以是1个,2个,到任意个。

我们以数学题为例,给定一组数字a,b,c……,计算a²+b²+c²+……。

要定义出这个函数,我们必须确定输入的从参数。由于参数的个数不确定,我们首先想到把a,b,c,……作为一个list或者 tuple传进来,这样函数可以定义如下:

1
2
3
4
5
6
7
8
9
10
>>> def cal(numbers):
... sum=0
... for n in numbers:
... sum=sum+n*n
... return sum
...
>>> cal([1,2,3])
14
>>> cal([1,2,3,4,5,6,7,8,9])
285

但是这样调用的时候需要先组装成一个list和tuple,如果利用可变参数,调用函数的方式可以变为:

1
2
3
4
5
6
7
8
9
10
>>> def cal(*numbers):
... sum=0
... for n in numbers:
... sum=sum+n*n
... return sum
...
>>> cal(1,2,3)
14
>>> cal()
0

定义可变参数和定义一个list或者tuple相比,仅仅在参数前面加了个*号,在函数内部,参数numbers接受到的是一个tuple,因此,函数代码完全不变,但是,调用该函数时,可以传入任意个参数,包括零个参数。

如果已经有了一个list或者tuple,要调用一个可变参数时,可以这样做:

1
2
3
>>> numbers=[1,2,3,4]
>>> cal(*numbers)
30

关键字参数

可变参数允许传入0个或者任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装成一个dictionary:

1
2
3
4
5
6
7
8
9
>>> def person(name,age,**kw):
... print('name:',name,'age:',age,'others:',kw)
...
>>> person('Mike',30)
name: Mike age: 30 others: {}
>>> person('james',22,city='Beijing')
name: james age: 22 others: {'city': 'Beijing'}
>>> person('james',22,city='Beijing',gender='b')
name: james age: 22 others: {'gender': 'b', 'city': 'Beijing'}

函数person除了必选参数name和age'之外,还可以接受关键字参数kw。在调用函数时,可以只传入必选参数,也可以传入任意个数的关键字参数。

关键字参数可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age两个参数,但是如果调用者愿意提供更多的参数,我们也能接收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填选项,其他都是可选项,利用关键字这个函数就可以很容易满足注册的需求。

和可变参数类似,也可以先组装一个dict,然后把该dict转换为关键字参数传入:

1
2
3
>>> extra={'city':'beijing','job':'teacher'}
>>> person('jack',24,**extra)
name: jack age: 24 others: {'job': 'teacher', 'city': 'beijing'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到**kw中,kw将获得一个dict,注意kw获得的dict是extra的一个拷贝,对kw的改动不会影响到原始数据。

命名关键字参数

对于关键字参数,函数调用者可以传入任意不受限制的关键字采纳数。至于到底传入了哪些,需要函数内部通过kw检查。仍以person为例,我们希望检查是否有city和job的参数,这时候仍可以传入不受限制的关键字参数。

1
2
3
4
5
6
7
8
9
10
11
>>> def person(name,age,**kw):
... if 'city' in kw:
... pass
... if 'job' in kw:
... pass
... print('name:',name,'age:',age,'others:',kw)
...
>>> person('james',22,city='Beijing',gender='b')
name: james age: 22 others: {'gender': 'b', 'city': 'Beijing'}
>>> person('james',22,city='Beijing',job='teacher')
name: james age: 22 others: {'job': 'teacher', 'city': 'Beijing'}

如果我们要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数:

1
2
3
4
5
6
7
8
9
>>> def person(name,age,*,city,job):
... print(name,age,city,job)
...
>>> person('james',22,city='Beijing',job='teacher')
james 22 Beijing teacher
>>> person('james',22,city='Beijing',gender='b')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() got an unexpected keyword argument 'gender'

和关键字参数kw不同,命名关键字需要一个特殊分隔符*,*后面的参数为命名关键字参数,如果函数定义中已经有一个可变参数,后面就不需要一个特殊分隔符*。 命名关键字可以有缺省**可以简化调用:

1
2
3
4
5
>>> def person(name,age,*,city='beijing',job):
... print(name,age,city,job)
...
>>> person('Jack',24,job='Engineer')
Jack 24 beijing Engineer

使用命名关键字参数时需要注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数

参数组合

在Python中定义函数,可以选用必选参数,默认参数,可变参数,关键字参数和命名关键字参数,这五种参数都可以组合使用。但是请注意,参数定义的顺序是:必选参数,默认参数,可变参数,命名关键字参数和关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def f1(a,b,c=0,*args,**kw):
... print('a=',a, 'b=',b, 'c=',c, 'args=',args, 'kw=',kw)
...
>>> def f2(a,b,c=0,*,d,**kw):
... print('a=',a, 'b=',b, 'c=',c, 'd=',d, 'kw=',kw)
...
>>> f1(1,2)
a= 1 b= 2 c= 0 args= () kw= {}
>>> f1(1,2,c=3)
a= 1 b= 2 c= 3 args= () kw= {}
>>> f1(1,2,3,'a','b')
a= 1 b= 2 c= 3 args= ('a', 'b') kw= {}
>>> f1(1,2,3,'a','b',x=999)
a= 1 b= 2 c= 3 args= ('a', 'b') kw= {'x': 999}
>>> f2(1,2,d=99,ext=None)
a= 1 b= 2 c= 0 d= 99 kw= {'ext': None}

在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去,最神奇的是通过一个tuple和dict,你也可以调用上述函数:

1
2
3
4
5
6
7
8
>>> args=(1,2,3,4)
>>> kw={'d':88,'x':'#'}
>>> f1(*args,**kw)
a= 1 b= 2 c= 3 args= (4,) kw= {'d': 88, 'x': '#'}
>>> args=(1,2,3)
>>> kw={'d':88,'x':'#'}
>>> f2(*args,**kw)
a= 1 b= 2 c= 3 d= 88 kw= {'x': '#'}

所以,对于任意函数,都可以通过类似func(*args,**kw)的形式调用它,无论他的参数是如何定义的。

小结

Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。默认参数一定要用不可变对象,如果是可变对象程序运行时会出现逻辑错误!

要注意定义可变参数和关键字参数的语法:

*args是可变参数,args接收的是一个tuple;

**kw是关键字参数,kw接收的是一个dict。

以及调用函数时如何传入可变参数和关键字参数的语法:

可变参数既可以直接传入:func(1,2,3),又可以先组装list或者tuple,再通过*args传入:func(*(1,2,3));

关键字参数既可以直接传入:func(a=1,b=2)又可以先组装dict,再通过**kw传入:func(**{'a':1,'b':2})。

使用*args和**kw是Python的习惯洗发,当然也可以用其他参数名,但是最好使用习惯用法。命名关键字参数时为了限制调用者可以传入的参数名,同时也可以提供默认值。

定义命名的关键字参数在没有可变参数的情况下不要忘记加分隔符*。


递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自己本身,这个函数就是递归函数。举个例子,我们计算阶乘n!=1*2*3*...*n,用函数fact()来表示,可以看出:fact(n)=n!=123…n=fact(n-1)n,所以fact(n)可以表示为`nfact(n-1)`,只有在n=1时需要特殊处理。于是,fact(n)用递归的方式写出来就是:

1
2
3
4
5
6
7
8
9
10
>>> def fact(n):
... if n==1:
... return 1
... return n*fact(n-1)
...
>>> fact(1)
1
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915
608941463976156518286253697920827223758251185210916864000000000000000000000000

递归函数的优点是定义简单,逻辑清楚。理论上,所有的递归函数都可以写成循环的方式,但是循环的逻辑不如递归的逻辑清楚。

使用递归函数是需要防止栈溢出。在计算机中,函数调用时通过栈这种数据结构来实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以递归的次数过多,会导致栈溢出:

1
2
3
4
5
File "<stdin>", line 4, in fact
File "<stdin>", line 4, in fact
File "<stdin>", line 2, in fact
RecursionError: maximum recursion depth exceeded in comparison
>>> fact(5000)

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归也是可以的。

尾递归是指,在函数返回的时候,调用函数本身,并且return语句不能包含表达式。这样,编译器或者解释器就可以吧尾递归做优化,使递归本身无论调用多少次,都只占一个栈帧,不会出现栈溢出的情况。

上面的fact(n)函数由于return n*fact(n-1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归的方式,需要多一点代码,主要是把每一步的成绩传入到递归函数中:

1
2
3
4
5
6
7
8
>>> def fact(n):
... return fact_iter(n,1)
...
>>> def fact_iter(num,product):
... if num==1:
... return product
... return fact_iter(num-1,num*product)
...

可以看到,return fact_iter(num-1,num*product)仅仅返回递归函数本身,num-1和num*product在函数调用前就会被计算出来,不会影响函数调用。

fact(5)对应的fact_iter(5,1)的调用如下:

fact_iter(5,1)
fact_iter(4,5)
fact_iter(3,20)
fact_iter(2,60)
fact_iter(1,120)
120

尾递归调用时,如果做了优化,栈就不会增长,因此,无论多少次调用都不会导致栈溢出。

遗憾的是,大多数编程语言都没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改为尾递归方式,也会导致栈溢出。

练习

汉诺塔的移动可以用递归函数非常简单的实现,请编写move函数,它接收参数n,表示3个柱子A,B,C中第一个柱子A的盘子数量,然后打印出所有盘子从A借助B移动到C的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> def move(n, a, b, c):
... if n == 1:
... print('move', a, '-->', c)
... return
... move(n-1, a, c, b)
... print('move', a, '-->', c)
... move(n-1, b, a, c)
...
>>> move(4, 'A', 'B', 'C')
move A --> B
move A --> C
move B --> C
move A --> B
move C --> A
move C --> B
move A --> B
move A --> C
move B --> C
move B --> A
move C --> A
move B --> C
move A --> B
move A --> C
move B --> C

17. 异步IO

发表于 2019-04-21 | 分类于 Python

异步IO

在IO编程中我们知道CPU的速度远远快于磁盘网络等IO。在一个线程中,CPU执行代码的速度极快,然而一旦遇到IO操作,比如读写文件、发送网络数据时,就需要等待IO操作完成,才能进行下一步操作,这种情况称为同步IO。

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。因为一个IO操作就阻塞了当前线程,导致代码无法执行,所以我们必须使用多线程或者多进程并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,线程一旦过多,CPU的时间就花在线程切换上了,导致性能下降。

由于我们要解决的问题是CPU告诉执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。另一种方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令并不等待IO结果。然后去执行其他代码,一段时间后,当IO结果返回时,再通知CPU进行处理。

可以知道,普通顺序写的代码是无法完成异步IO的,异步IO模型需要一个消息循环,在消息循环中,主线程不断的重复着“读取消息-处理消息”这一过程:

1
2
3
4
loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)

消息模型其实早就应用在桌面程序中了,一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。由于GUI线程处理键盘鼠标等消息的速度非常快、所以用户感觉不到延迟。某些情况下,GUI线程会在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时用户会感觉到整个GUI程序停止响应了,敲键盘鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

当遇到IO操作时,代码只负责发送IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮操作处理过程。当IO操作完成后,将受到一条IO完成的消息,处理该消息时就可以直接获取IO操作的结果。

在发出IO请求到受到IO完成这段时间里面,同步IO模型下,主线程只能挂起,但是异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样在异步IO下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对大多数IO密集型程序,异步IO会大大提高系统的多任务处理能力。


协程

协程,又称微线程、 钎程。英文名Coroutine。

子程序或者成为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口一个返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但是在执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。注意在一个子程序中中断,去执行其他子程序,不是调用函数,有点类似CPU中断。比如子程序A、B:

1
2
3
4
5
6
7
8
9
def A():
print('1')
print('2')
print('3')
def B():
print('x')
print('y')
print('z')

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行中中断再去执行A,结果可能是:

1
2
3
4
5
6
1
2
x
y
3
z

但是再A总是没有调用B的,所以协程的调用比起函数调用理解要难。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行。和多线程相比,协程最大的优势就是极高的执行效率。因为子程序切换不是线程切换,而是程序自身控制的 ,因此没有线程切换的开销,和多线程相比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不休要多线程的锁机制,因为只有一个线程,也不存在同时写和变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

协程是一个线程执行,为了利用多核CPU,可以用多进程+协程,即充分利用多核,又充分发挥协程的高效率,可获得奇高的性能。

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回下一个值。但是Python的yield不但可以返回一个值,还可以接收调用者发出的参数。

例如:传统的生产者-消费者模型就是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,等消费者执行完毕后,切换回生产者继续生产,效率极高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
>>> def consumer():
... r=''
... while True:
... n = yield r
... if not n:
... return
... print('[CONSUMER] Consuming %s...' % n )
... r='200 OK'
...
>>> def produce(c):
... c.send(None)
... n=0
... while n < 5:
... n = n + 1
... print('[PRODUCER] Producing %s...' %n)
... r = c.send(n)
... print('[PRODUCER] Consumer return: %s' % r )
... c.close()
...
>>> c = consumer()
>>> produce(c)
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

1、首先调用c.send(None)启动生成器;
2、一旦产生了东西,通过c.send(n)切换到consumer执行
3、consumer通过yield拿到消息,处理,又通过yield把结果传回。
4、produce拿到consumer处理的结果,继续生产下一条消息
5、produce决定不生产了,通过c.close()关闭consumer整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为协程。


asyncio

asyncio是python3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个Eventloop的引用,然后把需要执行的协程扔到Eventloop中执行,就实现了异步IO。

用asyncio实现Hello world代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import asyncio
>>>
>>> @asyncio.coroutine
... def hello():
... print("Hello world!!")
... r=yield from asyncio.sleep(1)
... print("Hello again!")
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(hello())
Hello world!!
Hello again!
>>> loop.close()

@asyncio.coroutine把一个generator标记为coroutine类型,然后,我们就把这个Coroutine扔到Eventloop中执行。

hello()首先打印出Hello world!!,然后,yield from语法可以让我们方便的调用另一个generator。由于asyncio.sleep()也是一个Coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值,然后接着执行下一行语句。

把asyncio.sleep(1)看做一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行Eventloop中其他可以执行的coroutine了,因此可以实现并发执行。

用Task封装两个coroutine:

1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
import asyncio
@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

结果:

1
2
3
4
5
6
D:\笔记\Python\Notepad++>python 1.1.py
Hello world! (<_MainThread(MainThread, started 20036)>)
Hello world! (<_MainThread(MainThread, started 20036)>)
#(停顿1s)
Hello again! (<_MainThread(MainThread, started 20036)>)
Hello again! (<_MainThread(MainThread, started 20036)>)

由打印出来的当前线程可以看出,两个continue是由同一个线程并发执行的。如果把asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' %host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' %(host, line.decode('utf-8').rstrip()))
writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
D:\笔记\Python\Notepad++>python 1.2.py
wget www.sina.com.cn...
wget www.163.com...
wget www.sohu.com...
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Server: nginx
www.sina.com.cn header > Date: Mon, 14 Nov 2016 12:17:29 GMT
www.sina.com.cn header > Content-Type: text/html
www.sina.com.cn header > Last-Modified: Mon, 14 Nov 2016 12:17:02 GMT
www.sina.com.cn header > Vary: Accept-Encoding
www.sina.com.cn header > Expires: Mon, 14 Nov 2016 12:18:29 GMT
www.sina.com.cn header > Cache-Control: max-age=60
www.sina.com.cn header > X-Powered-By: shci_v1.03
www.sina.com.cn header > Age: 50
www.sina.com.cn header > Content-Length: 595123
www.sina.com.cn header > X-Cache: HIT from ja108-181.sina.com.cn
www.sina.com.cn header > Connection: close
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
www.163.com header > Date: Mon, 14 Nov 2016 12:21:18 GMT
www.163.com header > Content-Length: 0
www.163.com header > Location: http://www.163.com/special/0077jt/error_isp.html
www.163.com header > Connection: close
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
www.sohu.com header > Content-Length: 90665
www.sohu.com header > Connection: close
www.sohu.com header > Date: Mon, 14 Nov 2016 12:18:29 GMT
www.sohu.com header > Server: SWS
www.sohu.com header > Vary: Accept-Encoding
www.sohu.com header > Cache-Control: no-transform, max-age=120
www.sohu.com header > Expires: Mon, 14 Nov 2016 12:20:29 GMT
www.sohu.com header > Last-Modified: Mon, 14 Nov 2016 12:04:28 GMT
www.sohu.com header > Content-Encoding: gzip
www.sohu.com header > X-RS: 10511343.19686393.11189627
www.sohu.com header > FSS-Cache: HIT from 9580427.16723861.11355128
www.sohu.com header > FSS-Proxy: Powered by 3944245.5451583.5718860

三个连接是由一个线程通过Coroutine并发完成。

asyncio提供了完善的异步IO支持,异步操作需要在coroutine中通过yield from完成,多个coroutine可以封装一组Task然后并发执行。


async/await

用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

为了简化并更好地标示异步IO,从python3.5开始引入了新的语法async和await,可以让Coroutine的代码更加简洁易读。

async和await是针对coroutine的新语法,要使用新的语法,只需要做两步简单的替换:

1、把@asyncio.coroutine替换为async
2、把yield from替换为await

对比一下上一节的代码:

1
2
3
4
5
6
7
8
9
10
11
12
#old
@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())
#new
async def hello():
print('Hello world! (%s)' % threading.currentThread())
await asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())

剩下代码保持不变。


aiohttp

asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+cortinue实现多用户的高并发支持。

asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现了HTTP框架。

安装aiohttp然后编写一个HTTP服务器,分别处理以下URL:

/-首页返回b'<h1>Index</h1>'
/hello/{name}-根据URL参数返回文本hello, %s!

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
from aiohttp import web
async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')
async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()

注意aiohttp的初始化函数init()也是一个coroutine,loop.creat_server()则利用asyncio创建TCP服务。

16. Web编程

发表于 2019-04-21 | 分类于 Python

Web开发

随着互联网的兴起,人们发现CS架构不适合Web,最大的原因是Web应用程序的修改和升级非常迅速,而CS架构需要每个客户端逐个升级桌面APP,因此BS架构开始流行。

在BS架构下,客户端只需浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器获取Web页面,并把Web页面展示给用户即可。Web页面也具有极强的交互性。由于Web页面使用HTML编写的,而HTML具备超强的表现力,并且服务端升级后,客户端无需任何部署就可以使用到最新的版本,因此BS架构迅速流行起来。

Web开发的阶段:
1、静态Web页面
2、CGI:Common Gateway Interface
3、ASP/JSP/PHP:脚本语言开发与HTML结合紧密。
4、MVC:Model-View-Controller的模式,ASP发展为ASP.Net,JSP和PHP也有一大堆MVC框架。

Python是一种解释型脚本语言,开发效率高,所以很适合来做WEB开发。Python有上百种Web开发框架,有很多成熟的模板技术。

HTTP协议简介

在Web应用中,服务器把网页传给浏览器,实际上就是把网页的HTML代码发送给浏览器,让浏览器显示出来。而浏览器和服务器之间的传输协议就是HTTP,所以:

HTML是一种用来定义网页的文本,会HTML,就可以编写网页
HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。

chrome浏览器视图,开发者,开发者工具,就可以显示开发者工具:
Alt text

Elements显示网页的结构,Network显示浏览器和服务器的通信。我们点Network,确保第一个小红灯亮着,Chrome就会记录所有浏览器和服务器之间的通信:
Alt text

当我们在地址栏输入www.sina.com.cn时,浏览器将显示新浪的首页。在Network中,定位到第一条记录,点击,右侧将显示Request Headers,点击右侧的view source,我们就可以看到浏览器发给新浪服务器的请求了:
Alt text

最主要的头两行分析如下,第一行为

GET / HTTP/1.1

GET表示一个读取请求,将从服务器获得网页数据,/表示URL的路径,URL总是以/开头,/就表示首页,最后的HTTP/1.1表示采用的HTTP版本是1.1。目前HTTP协议的版本就是1.1,但是大部分服务器也支持1.0版本,主要区别在于1.1版本允许多个HTTP请求复用一个TCP连接,以加快传输速度。

第二行开始,每一行都类似于Xxx:abcdefg:

Host: www.sina.com.cn

表示请求的域名是www.sina.com.cn,如果一天服务器有多个网站,服务器就需要通过Host来区分浏览器请求的是哪个网站。

继续往下找到Response Headers,点击view source,显示服务器返回的原始相应数据:
Alt text

HTTP相应分为Header和Body两部分,我们在Network中看到的Header最重要的几行如下:

200 OK

200表示一个成功的响应,后面的OK是说明。失败的响应有404 Not Found,500Internal Server Error等。

Content-Type:text/html

Content-Type指示响应的内容,这里是text/html表示HTML网页。请注意浏览器就是靠Content-Type来判断响应的内容是网页还是图片,是时频还是音乐,不是靠URL来判断响应的内容的。

HTTP响应的Body就是HTML源码,我们可以直接查看:
Alt text

当浏览器读取到新浪首页的HTML源码后,它会解析HTML,显示页面,然后根据HTML里面的各种链接,再发送HTTP请求到新浪服务器,拿到相应的图片、时频、Flash。JavaScript脚本、CSS等各种资源,最终显示出一个完整的页面。我们在Network下回看到很多额外的请求。

HTTP请求

1、浏览器首先向服务器发送HTTP请求,请求包括:

方法:GET–仅请求资源、POST–附带用户数据
路径:由Host头指定–Host:www.sina.com.cn
以及其他相关的Header

2、服务器向浏览器返回HTTP相应,相应包括:

响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误;
响应类型:由Content-Type指定;
以及其他相关的Header;
通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中。

3、如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤1、2。

Web采用的HTTP协议采用了非常简单的请求-响应模式,从而大大简化了开发。当我们编写一个页面时,我们只需要在HTTP请求中把HTML发送出去,不需要考虑如何附带图片、视频等,浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源。

HTTP协议同时具备极强的扩展性,虽然浏览器请求的是http://www.sina.com.cn/的首页,但是新浪在HTML中可以链入其他服务器的资源,比如``=>,从而将请求压力分散到各个服务器上,并且,一个站点可以链接到其他站点,无数个站点互相链接起来,就形成了World Wide Web,简称WWW。

HTTP格式

每个HTTP请求和相应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。

HTTP协议是一种文本协议,所以它的格式非常简单。HTTP GET请求的格式:

1
2
3
4
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

每个Header一行一个,换行符是\r\n。HTTP POST请求的格式是:

1
2
3
4
5
6
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...

当遇到连续两个\r\n时,Header部分结束,后面的数据都是Body。

HTTP相应的格式:

1
2
3
4
5
6
200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...

HTTP响应如果包含body,也是通过\r\n\r\n来分割的。Body数据类型是由Content-Type来确定的。


HTML简介

HTML定义了一套语法规则,来告诉浏览器如何把一个丰富多彩的页面显示出来。最简单的HTML长这样:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>

可以用文本编辑器编写HTML,然后保存为hello.html,双击或者把文件拖到浏览器中,就可以看到效果:
Alt text
HTML文档就是一些列的Tag组成的,最外层的Tag是<html>。规范的HTML也包含<head>...</head>和<body>...<body>注意不要和HTTP中的Header和Body搞混了。由于HTML是富文档模型,所以还有一系列的Tag来表示链接、图片、表格、表单等等。

CSS简介

CSS是Cascading Style Sheets(层叠样式表),CSS用来控制HTML里的所有元素如何展现,比如,给标题元素<h1>加一个样式,变成48号字体,灰色,带阴影。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>

效果如下:
Alt text

JavaScript简介

JavaScript和Java没有关系。JavaScript是为了让HTML具有交互性而作为脚本语言添加的,JavaScript既可以内嵌到HTML中,也可以从外部链接到HTML中。如果我们希望当用户点击标题时把标题变为红色,就必须通过JavaScript来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
<script>
function change() {
document.getElementsByTagName('h1')[0].style.color = '#ff0000';
}
</script>
</head>
<body>
<h1 onclick="change()">Hello, world!</h1>
</body>
</html>

Alt text

如果要学习Web开发,首先要对HTML、CSS和JavaScript作一定的了解。HTML定义了页面的内容,CSS来控制页面元素的样式,而JavaScript负责页面的交互逻辑。


WSGI接口

了解HTTP协议和HTML文档,一个Web应用的本质是:

浏览器发送一个请求—>服务器收到请求,生成一个HTML文档—>服务器把HTML文档作为HTTP相应的Body发送给浏览器—>浏览器收到HTTP响应,从HTTP Body中取出HTML并显示。

最简单的WEB应用就是先把HTML用文件保存好,用一个线程的HTTP服务器软件接收用户请求,从文件中读取HTML,返回。Apache、Nginx、Lighttpd等这些常见的静态服务器就是做这件事情的。

如果要动态生成HTML,就需要把上述步骤自己实现。不过那些工作量很大,所以底层的代码由专门的服务器软件实现,我们用python专注于生成HTML文档。我们不希望接触到TCP连接。HTTP原始请求和响应格式,所以需要一个统一的接口,让我们专心用Python编写Web业务。

这个接口就是WSGI:Web Server Gateway Interface,只要求开发者实现一个函数,就可以相应HTTP请求。最简单的实例如下:

1
2
3
4
>>> def application(envision, start_response):
... start_response('200 OK',[('Content-Type', 'text/html')])
... return [b'<h1>Hello, world!</h1>']
...

上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:

environ:一个包含所有HTTP请求信息的dict对象
start_response:一个发送HTTP响应的函数。

在application()函数中,调用:

start_response(‘200 OK’,[(‘Content-Type’, ‘text/html’)])

就发送了HTTP响应的Header,注意Header只能发送一次,也就是只能调用一次start_response()函数。start_response()函数接收两个参数,一个是HTTP响应码,一个是一组list表示的HTTP Header,每个Header用一个包含两个str的tuple表示。

通常情况下,都应该把Connect-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。

然后函数的返回值b'<h1>Hello, world!</h1>'将作为HTTP响应的Body发送给浏览器。

有了WSGI,我们关心的就是如何从environ这个dict对象中拿到HTTP请求信息然后构造HTML,通过start_response()函数发送Header,最后返回Body。

整个application()函数本身并没有涉及到任何解析HTTP的部分,也就是说,底层代码不需要我们自己编写,我们只负责在更高层次上考虑如何响应请求就可以了。并且application()必须由WSGI服务器来调用。有很多符合WSGI规范的服务器,我们可以挑选一个来用。Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。

运行WSGI服务

我们先编写hello.py,实现WEB应用程序的WSGI处理函数:

1
2
3
def application(environ, start_response):
start_response('200 OK',[('Content-Type','text/html')])
return [b'<h1>Hello, world!</h1>']

然后再编写一个server.py,负责启动WSGI服务器,加载application()函数:

1
2
3
4
5
6
7
8
9
from wsgiref.simple_server import make_server
from hello import application
#创建一个服务器,IP地址为空,端口为8000,处理函数是application
httpd = make_server('', 8000, application)
print('Serving HTTP on port 8000...')
httpd.serve_forever()

两个文件在一个目录下,执行服务器程序:

1
2
3
4
D:\笔记\Python\Notepad++>python server.py
Serving HTTP on port 8000...
127.0.0.1 - - [10/Nov/2016 20:47:17] "GET / HTTP/1.1" 200 22
127.0.0.1 - - [10/Nov/2016 20:47:17] "GET /favicon.ico HTTP/1.1" 200 22

启动成功后,打开浏览器,输入http://localhost:8000/就可以看到结果了。
Alt text

按Ctrl+c终止服务器。

为了丰富WEB内容,可以在environ里读取PATH_INFO,这样可以显示更加动态的内容:

1
2
3
4
def application(environ, start_response):
start_response('200 OK',[('Content-Type','text/html')])
body='<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
return [body.encode('utf-8')]

你可以在地址栏输入用户名作为URL的一部分,将返回Hello,xx!:
Alt text

无论多么复杂的WEB应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得,HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。复杂的Web应用程序,光靠一个WSGI函数来处理还是太底层了,我们需要在WSGI之上再抽象出Web框架,进一步简化开发。


使用Web框架

了解了WSGI框架,我们发现:其实一个WebAPP就是写一个WSGI的处理函数,针对每个HTTP请求进行相应。

但是如何处理HTTP请求不是问题,问题是如何处理100个不同的URL。

每一个URL可以对应GET和POST请求,当然还有PUT、DELETE等请求,但是通常我们只考虑最常见的GET和POST请求。

一个最简单的想法是从environ变量里取出HTTP请求的信息,然后逐个判断:

1
2
3
4
5
6
7
8
def application(environ, start_response):
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
if method=='GET' and path=='/':
return handle_home(environ, start_response)
if method=='POST' and path='/signin':
return handle_signin(environ, start_response)
...

这样写下去代码是没法维护的,因为WSGI提供的接口虽然比HTTP接口高级了不少,但是和Web APP的处理逻辑相比,还是比较低级的,我们需要在WSGI接口之上能进一步抽象,让我们专注于用一个函数处理一个URL,至于URL到函数 的映射就交给WEB框架来做。

Python提供了上百个开源的框架,最流行的为flask。

写一个app.py,处理三个URL,分别是:

1、GET /:首页,返回Home
2、GET /signin:登录页,显示登录表单
3、POST /signin:处理登录表单,显示登录结果。

同一个URL/signin分别有GET和POST两种请求,映射到两个处理函数争取。Flask通过Python的装饰器在内部自动地把URL和函数关联起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from flask import Flask
from flask import request
app=Flask(__name__)
@app.route('/',methods=['GET', 'Post'])
def home():
return '<h1>Home</h1>'
@app.route('/signin', methods=['GET'])
def signin_form():
return '''<form action="/signin" method="post">
<p><input name="username"></p>
<p><input name="password" type="password"></p>
<p><button type="submit">Sign In</p>
</form>'''
@app.route('/signin', methods=['POST'])
def signin():
if request.form['username']=='admin' and request.form['password']=='password':
return '<h3>Hello, admin!</h3>'
return '<h3>Bad username or password.</h3>'
if __name__=='__main__':
app.run()

运行python app.py,Flask自带的Server在端口5000上监听:

1
2
3
4
D:\笔记\Python\Notepad++>python app.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [11/Nov/2016 18:07:12] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [11/Nov/2016 18:07:12] "GET /favicon.ico HTTP/1.1" 404 -

在浏览器首页输入:http://localhost:5000/:
Alt text

首页显示正确,地址栏输入:http://localhost:5000/signin:
Alt text

输入预设的用户名和口令:
Alt text
Alt text

实际的Web APP应该拿到用户名和口令后,去数据库查询再对比,来判断用户是否能登录成功。除了Flask,常见的python框架还有:Django、web.py、Bottle、Tornado

有了Web框架,我们在编写Web应用时,注意力就从WSGI处理函数转移到URL+对应的处理函数,编写变得简单。在编写URL处理函数时,除了配置URL外,从HTTP请求拿到用户数据也是非常重要的。Web框架都提供了自己的API来实现这些功能。Flask通过request.form['name']来获取表单的内容。


使用模板

Web框架把我们从WSGI中拯救出来了,我们只需要不断地编写函数,带上URL,就可以继续Web APP的开发了。Web App最复杂的部分在HTML页面。HTML不仅要正确,还要通过CSS美化,再加上复杂的JavaScript脚本来实现各种交互和动画效果。用python的字符串是无法实现的。所以出现了模板技术。

使用模板首先需要预先准备一个HTML文档,这个HTMl文档不是普通的HTML,而是嵌入了一些变量和指令,然后根据我们传入的数据,替换后得到最终的HTMl,发送给用户:
Alt text

这就是传说中的MVC:Model-View-Controller,模型-视图-控制器

Python处理URL的函数就是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等。包含变量的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。

上面的例子中,Model就是一个dict:

{ ‘name’: ‘Michael’}

因为Python支持关键字参数,很多Web框架允许传入关键字参数,然后在框架内部组装出一个dict作为Model。

对之前的app.py进行改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask
from flask import request, render_template
app=Flask(__name__)
@app.route('/',methods=['GET', 'Post'])
def home():
return render_template('home.html')
@app.route('/signin', methods=['GET'])
def signin_form():
return render_template('form.html')
@app.route('/signin', methods=['POST'])
def signin():
username=request.form['username']
password=request.form['password']
if username=='admin' and password=='password':
return render_template('signin-ok.html', username=username)
return render_template('form.html', message='Bad username or password',username=username)
if __name__=='__main__':
app.run()

Flask通过render_template()函数来实现模板的渲染。和Web框架类似,Python的模板也有很多中。Flask默认支持的模板是jinja2,用jinja2来编写模板home.html、显示首页:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Home</title>
</head>
<body>
<h1 style="font-style:italic">Home</h1>
</body>
</html

form.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<title>Please Sign In</title>
</head>
<body>
{% if message %}
<p style="color:red">{{ message }}</p>
{% endif %}
<form action="/signin" method="post">
<legend>Please sign in:</legend>
<p><input name="username" placeholder="Username" value="{{ username }}"></p>
<p><input name="password" placeholder="Password" type="password"></p>
<p><button type="submit">Sign In</button></p>
</form>
</body>
</html>

signin-ok.html:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Welcome, {{ username }}</title>
</head>
<body>
<p>Welcome, {{ username }}!</p>
</body>
</html>

15. 电子邮件

发表于 2019-04-21 | 分类于 Python

电子邮件

电子邮件软件成为MUA:Mail User Agent—-邮件用户代理。E-mail从MUA发出去,不是直接到达对方电脑而是发到MTA(Mail Transfer Agent—邮件传输代理),就是Email服务提供商,比如网易,新浪等。MTA会把email投递到MDA(Mail Delivery Agent—邮件投递代理),存储在某个文件或特殊的数据库里也就是邮箱。要想获取邮件,必须通过MUA从MDA上获取到自己电脑上。

发件人—>MUA—>MTA—>MTA—>若干个MTA—>MDA<—MUA<—收件人

要编写程序来发送和接收邮件,本质上就是:

1、编写MUA把邮件发到MTA
2、编写MUA从MDA上收邮件

发邮件时MUA和MTA使用的协议是SMTP:Simple Mail Transfer Protocol。
收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol,目前版本是3,俗称POP3;IMAP:Internet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱。

邮件客户端在发邮件时,会让你先配置SMTP服务器,也就是你要发到哪个MTA上。比如你在用163邮箱,就不能直接发送到新浪的MTA上,你要填163提供的SMTP服务器地址:smtp.163.com,还需要输入邮箱地址和邮箱口令来证明你是163用户,这样MUA才能正常地把Email通过SMTP协议发送到MTA。

从MDA收取邮件时,MDA服务器会要求验证邮箱口令,这样MUA才能顺利地通过POP或IMAP协议从MDA获取邮件。

SMTP发送邮件

SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件,HTML邮件及其附件。

Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,stmplib负责发送邮件。首先,我们来构造一个最简单的纯文本邮件:

1
2
>>> from email.mime.text import MIMEText
>>> msg=MIMEText('Hello, send by Python...','plain','utf-8')

注意到构造MIMEText对象时,第一个参数就是邮件正文,第二个参数是MIME的subtype,传入plain表示纯文本,最终的MIME就是text/plain,最后一定要用utf-8编码保证多语言兼容性。然后通过SMTP发送出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
from_addr=input('From:')
password=input('Password:')
to_addr=input('To:')
smtp_server=input('SMTP server: ')
import smtplib
server=smtplib.SMTP(smtp_server, 25)#smtp默认的端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr,[to_addr],msg.as_string())
server.quit()

我们用set_debuglevel(1)就可以打印出和SMTP服务器交互的所有信息。SMTP协议就是简单的文本命令和相应。login()方法用来登录SMTP服务器,sendmail()方法就是发邮件,由于可以一次发给对个人,所以传入一个list,邮件正文是一个str,as_string()把MIMEText对象编程str。收到的邮件的问题是没有主题收件人没有显示为友好的名字,且收到了邮件却提示你不在收件人中。这是因为邮件主题、如何显示发件人、收件人的信息并不是通过SMTP协议发给MTA,而是包含在发给MTA的文本中的,所以,我们必须把From、To、Subject添加到MIMEText中才是一封完整的邮件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import smtplib
from email import encoders
from email.mime.text import MIMEText
from email.header import Header
from email.utils import parseaddr, formataddr
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
from_addr=input('From:')
password=input('Password:')
to_addr=input('To:')
smtp_server=input('SMTP server: ')
msg=MIMEText('hello, send by python...', 'plain' ,'utf-8')
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()
server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()

我们编写了一个函数_format_addr()来格式化一个邮件地址。注意不能简单地传入name <addr@example.com>,因为如果包含中文,需要通过Header对象编码。

msg['To']发送的是字符串而不是list,如果有多个地址邮件,用,分隔即可。发送邮箱显示:
Alt text

你看到的收件人的名字可能不是我们传入的管理员,因为很多邮件服务商在显示邮件时会把收件人名字自动替换为用户注册的名字。我们插卡Email原始内容如下:

From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= xxxxxx@163.com
To: =?utf-8?b?566h55CG5ZGY?= xxxxxx@qq.com
Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=

发送HTML邮件

如果我们要发送HTML邮件而不是普通的纯文本文件怎么办,就在构造MIMEText对象时,把HTML字符串传进去,再把第二个参数由plain改为plain就可以了:

1
msg=MIMEText('<html><body><h1>Hello</h1>' + '<p>send by <a href="http://www.python.org">Python</a>...</p>'+'</body></html>', 'html', 'utf-8')

再发送一遍邮件:
Alt text

发送附件

如果Email中要加上附件,可以把带附件的邮件看做包含若干部分的邮件:文本和各个附件本身,所以可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 邮件对象:
msg = MIMEMultipart()
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候……', 'utf-8').encode()
# 邮件正文是MIMEText:
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
# 添加附件就是加上一个MIMEBase,从本地读取一个图片:
with open('/Users/michael/Downloads/test.png', 'rb') as f:
# 设置附件的MIME和文件名,这里是png类型:
mime = MIMEBase('image', 'png', filename='test.png')
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename='test.png')
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)

发送结果:
Alt text

发送图片

如果要把一个图片嵌入到邮件正文中,直接在HTML邮件中连接图片地址是不行的,我们需要按照发送附件的方法,把邮件作为附件添加进去,然后在HTML中通过引用src="cid:0"就可以把附件作为图片嵌入。如果有多个图片,可以依次编号,然后引用不同的cid:x就可以。

把上面代码中加入MIMEMultipart的MIMEText从plain改为html,然后在适当的位置引用图片:

1
2
3
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))

发送结果:
Alt text

同时支持HTML和Plain格式

如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是如果收件人的设备无法查看HTML邮件时,我们可以在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。

利用MIMEMultipart可以组合一个HTML和Plain,要注意指定subtype是alternative:

1
2
3
4
5
6
7
8
msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...
msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))
# 正常发送msg对象...

加密SMTP

使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。

Gmail提供的SMTP服务必须要加密传输。首先知道Gmail的SMTP端口是587,修改代码如下:

1
2
3
4
5
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
...

只需要在创建SMTP对象后,立刻调用starttls()方法,就创建了安全链接。后面的代码和前面的发送邮件代码完全一样。

使用Python的smtplib发送邮件很简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头就可以顺利发出。构造一个邮件对象就是一个Message对象,如果构建一个MIMEText对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart,MIMEBase可以表示任何对象,继承关系如下:
Alt text


POP3收取邮件

收邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议。Python内置了一个poplib模块,实现了POP3协议。

POP3收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP一样,SMTP发送的也是经过编码后的一大段文本。要把POP3收取的文本变为可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。收取邮件分为两部分:

1、用poplib把邮件的原始文本下载到本地;
2、用email解析原始文本,还原为邮件对象。

通过POP3下载邮件

POP3的协议本身很简单,以下面的代码为例,我们来获取最新的一封邮件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from email.parser import Parser
import poplib
#输入邮箱地址,口令和POP3服务器地址:
email = input('Email:')
password =input('Password:')
pop3_server = input('POP3 server:')
#连接到POP3服务器
server = poplib.POP3(pop3_server)
#可以打开或关闭调试信息
server.set_debuglevel(1)
print(server.getwelcome().decode('utf-8'))
#身份认证
server.user(email)
server.pass_(password)
#start()返回邮件数量和占用空间
print('Messages: %s, Size: %s' %server.stat())
#list()返回所有邮件的编号
resp, mails, octets = server.list()
print(mails)
#获取最新一封邮件,索引号从1开始
index = len(mails)
resp, lines, octets = server.retr(index)
msg_content = b'\r\n'.join(lines).decode('utf-8')
msg = Parser().parsestr(msg_content)
server.quit()

用POP3协议很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可。

解析邮件

解析邮件的过程和上一节构造邮件刚好相反,必须导入必要的模块:

1
2
3
4
5
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib

一行代码可以把邮件内容解析为Message对象:

1
msg = Parser().parsestr(msg_content)

这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。所以我们要递归地打印出Message对象的层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# indent用于缩进显示:
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header=='Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type=='text/plain' or content_type=='text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))

邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:

1
2
3
4
5
def decode_Str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value

decode_header()返回一个list,因为像Cc``Bcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码我们只取了第一个元素。文本邮件的内容也是str,还需要检测编码,否则非UTF-8编码的邮件都无法正常显示:

1
2
3
4
5
6
7
8
def gusee_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset

把上面的代码整理好,我们就可以来试试获取一封邮件。先往自己的邮箱发送一封邮件,然后用Python程序把它收到本地:
Alt text

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib
# 输入邮件地址, 口令和POP3服务器地址:
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header=='Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type=='text/plain' or content_type=='text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))
# 连接到POP3服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))
# 身份认证:
server.user(email)
server.pass_(password)
# stat()返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list()返回所有邮件的编号:
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]
print(mails)
# 获取最新一封邮件, 注意索引号从1开始:
index = len(mails)
resp, lines, octets = server.retr(index)
# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)
print_info(msg)
# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()

14. 网络编程

发表于 2019-04-21 | 分类于 Python

网络编程

现在几乎所有的程序都是网络程序,很少有单机版的程序了。计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。比如当你使用浏览器浏览新浪网时,你的计算机就和新浪的某台服务器通过互联网连接起来了,然后新浪的服务器把网页内容作为数据通过互联网传输到你的电脑。由于电脑上有很多软件,不同的程序连接的别的计算机也会不同。网络通信是两台计算机上的两个进程之间的通信。

网络编程对所有开发语言都是一样的。用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。

TCP/IP简介

计算机wield联网,必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Micosoft都有各自的网络协议,互不兼容,同样的网络协议的计算机可以互相交流,不同协议的计算机就不可以。

为了实现所有不同类型的计算机都连接起来,互联网协议簇(Internet Protocol Suite)就是通用协议标准。最主要的两个协议是TCP协议和IP协议。

通信的时候,双方必须知道对方的标识,互联网中每一个计算机的唯一标识就是IP地址。如果一台计算机同时接入两个或两个以上的网络,比如路由器,它就会有两个或更多个IP地址,所以IP地址对应的实际上是计算机的网络接口,通常是网卡。

IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此路由器就负责决定如何把一个IP包转发出去。IP报的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺利到达。

TCP协议是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后对每个IP包进行编号,确保对方按顺序收到,如果包丢了,就自动重发。

一个IP包除了包含要传输的数据外,还包含源IP地址和目的IP地址,源端口和目的端口。

在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个IP包来了之后,到底是交给浏览器还是QQ就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号这样,两个进程在两台计算机之间建立的网络连接就需要各自的IP地址和各自的端口号。一个进程也可能同时与多个计算机建立链接,因此会申请很多端口。

TCP编程

Socket是网络编程的一个抽象概念。通常我们用一个Socket表示打开了一个网络链接,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。

客户端

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动相应连接的叫服务器。当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来了,后面的通信就是发送网页内容了。所以我们要创建一个基于TCP的Socket:

1
2
3
4
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('www.sina.com.cn',80))

创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。SOCK_STREAM指定使用面向流的TCP协议,这样一个Socket对象就创建成功,但是还没有建立连接。

客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。IP地址可以用域名自动转换得到,端口号需要服务器来提供。服务器提供什么样的服务,端口号就必须固定下来。80端口是WEB服务的标准端口。端口号小于1024的是Internet标准服务的端口,端口号大于1024的可以任意使用。

建立TCP连接后,我们可以向新浪服务器发送请求,要求返回首页的内容:

1
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发怎么协调要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪返回的数据了:

1
2
3
4
5
6
7
8
buffer=[]
while True:
d=s.recv(1024)
if d:
buffer.append(d)
else:
break
data=b''.join(buffer)

接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。

当我们接收完出局后,调用close()方法关闭Socket,这样一次完整的网络通信就结束了。接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,吧HTTP头打印出来,网页内容保存到文件:

1
2
3
4
5
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
with open('sina.html','wb') as f:
f.write(html)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\笔记\Python\Notepad++>python 11.py
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 07 Nov 2016 12:24:26 GMT
Content-Type: text/html
Last-Modified: Mon, 07 Nov 2016 12:23:13 GMT
Vary: Accept-Encoding
Expires: Mon, 07 Nov 2016 12:25:26 GMT
Cache-Control: max-age=60
X-Powered-By: shci_v1.03
Age: 31
Content-Length: 597288
X-Cache: HIT from ja180-183.sina.com.cn
Connection: close

服务器

和客户端相比,服务器编程要复杂一些。服务器进程首先要绑定一份端口并监听来自其他客户端的连接。服务器会打开固定端口监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以服务器要能够区分一个Socket连接是和那个客户端绑定的。一个Socket依赖四项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。

每个连接都需要一个新的进程或者线程来处理,否则服务器一次就只能服务一个客户端了。我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。首先创建一个基于IPv4和TCP协议的Socket:

1
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。

端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号,小于1024的端口号必须有管理员权限才能绑定:

1
s.blind('127.0.0.1',9999)

紧接着,调用listen()方法开始监听,传入的参数指定等待连接的最大数量:

1
2
s.listen(5)
print('Waiting for connection...')

接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:

1
2
3
4
while True:
sock.addr=s.accept()
t=threading.Thread(target==tcplink,args=(sock, addr))
t.start()

每个连接都必须创建新的线程或进程来处理,否则,单线程在处理连接的过程中无法接受其他客户端的连接:

1
2
3
4
5
6
7
8
9
10
11
def tcplink(sock, addr):
print('Accecpt new connection from %s:%s...' %addr)
sock.send(b'Welcome!')
while True:
data=sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' %data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' %addr)

建立连接后,服务器首先发一条欢迎信息,然后等待客户端数据,并加上Hello再发送给客户端,如果客户端发送了exit字符串,就直接关闭连接。要测试这个程序,我们还需要编写一个客户端程序:

1
2
3
4
5
6
7
8
9
10
11
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后对每一个新的连接,创建一个线程或进程来处理。通常,服务器会无限运行下去。


##UDP 编程

TCP建立的是可靠的连接,并且通信双方都可以以流的形式发送数据。UDP是面向无连接的协议。使用UDP时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包,但是能够到达就不知道了。UDP的有点在于速度快,对于不要求可靠到达的数据,就可以使用UDP协议。

和TCP类似,使用UDP的通信双方也分为客户端和服务器,服务器首先需要绑定端口:

1
2
3
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999))

创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据。

1
2
3
4
5
6
print('Bind UDP on 9999...')
while True:
# 接收数据:
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)

recvform()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发送给客户端了。

客户端使用UDP时,首先仍然创建基于UDP的Socket,然后不需要调用connect()直接通过sendto()给服务器发送数据:

1
2
3
4
5
6
7
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))
s.close()

从服务器接收数据仍然调用recv()方法。

UDP的使用与TCP类似,但是不需要建立连接。此外,服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。

13. virtualenv&图形界面

发表于 2019-04-21 | 分类于 Python

virtualenv&图形界面

virtualenv

在开发Python应用程序的时候,系统安装的Python只有一个3.5版本。所有的第三方包都会被安装到Python3的site-packages目录下。

如果要同时开发多个应用程序,那么这些应用程序共用一个Python3.5,如果应用A需要jinja2.7,应用B需要jinja2.6,这时候需要用virtualenv来为一个应用创建一套隔离的Python运行环境。

第一步,创建项目文件夹myproject,在该文件夹中安装虚拟环境env:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\笔记\Python\Notepad++>mkdir myproject
D:\笔记\Python\Notepad++>cd myproject
D:\笔记\Python\Notepad++\myproject>dir
驱动器 D 中的卷是 文件
卷的序列号是 0BE3-0E5C
D:\笔记\Python\Notepad++\myproject 的目录
2016/11/06 20:01 <DIR> .
2016/11/06 20:01 <DIR> ..
0 个文件 0 字节
2 个目录 104,178,057,216 可用字节
D:\笔记\Python\Notepad++\myproject>virtualenv env
Using base prefix 'c:\\users\\cdxu0\\appdata\\local\\programs\\python\\python35'
New python executable in D:\笔记\Python\Notepad++\myproject\env\Scripts\python.exe
Installing setuptools, pip, wheel...done.

第二步,启动虚拟环境,安装所需库类。在windows中虚拟环境的启动使用命令:your_env_dir\Scripts\activate,默认情况下,virtualenv已经安装好了pip。在启动虚拟环境后直接使用pip install命令就可以为该虚拟环境安装库类。

1
2
3
4
5
D:\笔记\Python\Notepad++\myproject>cd env\Scripts
D:\笔记\Python\Notepad++\myproject\env\Scripts>activate
(env) D:\笔记\Python\Notepad++\myproject\env\Scripts>pip install flask==0.9

第三步,在虚拟环境中可以运行脚本等操作,离开虚拟环境,使用deactivate命令。


图形界面

Python支持多种图形界面的第三方库,比如:Tk,wxWidgets ,Qt,GTK等。但是Python自带的库是支持Tk的TKinter,使用Tkinter,无需安装任何包就可以直接使用。

使用TKinter十分简单,第一步导入TKinter包的所有内容:

1
>>> from tkinter import *

第二步从Frame派生一个Application类,这是所有Widget的父容器:

1
2
3
4
5
6
7
8
9
10
>>> class Application(Frame):
def __init__(self,master=None):
Frame.__init__(self,master)
self.pack()
self.createWidgets()
def createWidgets(self):
self.helloLabel=Label(self, text='Hello, world!')
self.helloLabel.pack()
self.quitButton=Button(self, text='Quit', command=self.quit)
self.quitButton.pack()

在GUI中,每个Button、Label、输入框等,都是一个Widget。Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。

pack()方法把Widget加入到父容器中,并实现布局。pack()是最简单的布局,grid()可以实现更复杂的布局。

在createWidgets()方法中,我们创建一个Label和一个Button,当Button被点击时,触发self.quit()使得程序退出。

第三步,实例化Application,并启动消息循环:

1
2
3
4
>>> app=Application()
>>> app.master.title('Hello world!')
''
>>> app.mainloop()

GUI程序的主线负责监听来自操作系统的消息,并依次处理每一条消息。因此,如果消息处理非常耗时,就需要新线程中处理。运行这个GUI程序:
Alt text

点击Quit按钮或者窗口的x结束程序。

输入文本

我们再对这个GUI程序改进一下,加入一个文本框,让用户可以输入文本,然后点按钮后,弹出消息对话框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
from tkinter import *
import tkinter.messagebox as messagebox
class Application(Frame):
def __init__(self,master=None):
Frame.__init__(self,master)
self.pack()
self.createWidgets()
def createWidgets(self):
self.nameInput=Entry(self)
self.nameInput.pack()
self.alterButton=Button(self, text='Hello', command=self.hello)
self.alterButton.pack()
def hello(self):
name=self.nameInput.get() or 'world'
messagebox.showinfo('Message','Hello, %s' %name)
app=Application()
app.master.title('Hello World')
app.mainloop()

当用户点击按钮时,出发hello(),通过self.nameInput.get()获得用户输入的文本后,使用tkMessageBox.showinfo()可以弹出消息对话框:
Alt text
Alt text

12. 常用第三方模块

发表于 2019-04-21 | 分类于 Python

常用第三方模块

除了内建的模块,Python还有很多的第三方模块,在PyPI-the Python Package Index注册。

PIL

PIL:Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL的功能非常强大,API却非常简单易用。PIL只支持到2.7,PIL上的兼容版本Pillow,加了很多新特性,可以支持到3.x。

操作图像

图像缩放操作:

1
2
3
4
5
6
7
8
>>> im=Image.open('1.jpg')
>>> w,h=im.size
>>> print('Original image size: %sx%s' %(w,h))
Original image size: 600x337
>>> im.thumbnail((w//2, h//2))
>>> print('Resize image to: %sx%s' %(w//2,h//2))
Resize image to: 300x168
>>> im.save('thumbnail.jpg','jpeg')

其他功能如切片、旋转、滤镜、输出文字、调色板等一应俱全。比如,模糊效果:

1
2
3
4
5
>>> from PIL import Image,ImageFilter
>>>
>>> im=Image.open('1.jpg')
>>> im2=im.filter(ImageFilter.BLUR)
>>> im2.save('blur.jpg','jpeg')

PIL的ImageDraw提供了一系列绘图的方法,让我们可以直接绘图。如生成字幕验证码的图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
#随机字母:
def rndChar():
return chr(random.randint(65,90))
#随机颜色1:
def rndColor():
return (random.randint(64,255),random.randint(64,255),random.randint(64,255))
#随机颜色2:
def rndColor2():
return (random.randint(32,217),random.randint(32,217),random.randint(32,217))
#240*60
width=60*4
height=60
image=Image.new('RGB',(width,height),(255,255,255))
#创建Font、Draw对象:
font=ImageFont.truetype('C:\Windows\Fonts\Arial.ttf',36)
draw=ImageDraw.Draw(image)
#填充:
for x in range(width):
for y in range(height):
draw.point((x,y),fill=rndColor())
#输出文字:
for t in range(4):
draw.text((60*t+10,10),rndChar(),font=font,fill=rndColor2())
#模糊
image=image.filter(ImageFilter.BLUR)
image.save('code.jpg','jpeg')

得到的验证码图片如下:
Alt text

详细的Pillow官方文档:

11. 常用内建模块

发表于 2019-04-21 | 分类于 Python

常用内建模块

datetime

datetime是Python处理日期和时间的标准库。

获取当前的日期和时间:

1
2
3
4
5
6
>>> from datetime import datetime
>>> now=datetime.now() #获取当前的datetime
>>> print(now)
2016-11-01 20:25:50.305269
>>> print(type(now))
<class 'datetime.datetime'>

注意到datetime是模块,datetime模块还包含一个datetime类,通过from datetime import datetime导入的才是datetime这个类。如果仅导入import datetime,则必须引用全名datetime.datetime。datetime.now()返回当前日期和时间,其类型是datetime。

获取指定日期和时间:

1
2
3
4
>>> from datetime import datetime
>>> dt=datetime(2015,11,11,11,11)
>>> print(dt)
2015-11-11 11:11:00

datetime转换为timestamp,在计算机中,时间实际上是由数字表示的,我们把1970年1月1日00:00:00UTC+00:00时区的时刻称为epoch time,记为0,当前时间就是相当于epoch time的秒数,称为timestamp。

timestamp=0=1970-1-1 00:00:00 UTC+0:00

对应的北京时间是

timestamp=0=1970-1-1 08:00:00 UTC+8:00

课件timestamp的值和时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是一timestamp表示的,因为世界各地的计算机在任意时刻的timestamp都是完全相同的。

把一个datetime类型转换为timestamp只需要简单调用timestamp()方法:

1
2
>>> dt.timestamp()
1447211460.0

注意Python的timestamp是一个浮点数。如果有小数点,小数位表示毫秒数。某些编程语言的timestamp使用整数表示毫秒数,这种情况下需要把timestamp除以1000就得到Python的浮点表示方法。

timestamp转换为datetime:

1
2
3
4
>>> from datetime import datetime
>>> t=1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00

注意到timestamp是一个浮点数,它没有时区的概念,而datetime有时区的。上述转换是在timestamp和本地时间做转换。本地时间是指当前操作系统设定的时区。例如北京时区是东八区,则本地时间是2015-04-19 12:20:00,实际上就是UTC+8:00时区的时间2015-04-19 12:20:00 UTC+8:00,而此时的格林威治标准时间与北京差了八个小时,也就是UTC+0:00时区的时间:2015-04-19 04:20:00 UTC+0:00。

timestamp也可以直接转换到UTC标准时区的时间:

1
2
3
4
5
6
>>> from datetime import datetime
>>> t=1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00
>>> print(datetime.utcfromtimestamp(t))
2015-04-19 04:20:00

str转换为datetime,很多时候,用户输入的日期和时间是字符串,要处理日期和时间,首先要把str转换为datetime。转换方法是通过datetime.strptime()实现:

1
2
3
4
>>> from datetime import datetime
>>> cday=datetime.strptime('2015-6-1 18:19:59','%Y-%m-%d %H:%M:%S')
>>> print(cday)
2015-06-01 18:19:59

字符串'%Y-%m-%d %H:%M:%S'规定了日期和时间部分的格式。

datetime转换为str,如果已经有了datetime对象,要把它格式化 为字符串显示给用户,就需要转换为str,转换方法是通过strftime()实现的,同样需要一个日期和时间的格式化字符串:

1
2
3
4
5
6
>>> from datetime import datetime
>>> now=datetime.now()
>>> print(now.strftime('%a,%b %d %H:%M'))
Tue,Nov 01 21:26
>>> print(now)
2016-11-01 21:26:20.187960

datetime加减,对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime。加减可以直接用+``-运算符,需要导入timedelta这个类:

1
2
3
4
5
6
7
8
9
10
>>> from datetime import datetime,timedelta
>>> now=datetime.now()
>>> now
datetime.datetime(2016, 11, 1, 21, 29, 50, 726284)
>>> now+timedelta(hours=10)
datetime.datetime(2016, 11, 2, 7, 29, 50, 726284)
>>> now-timedelta(days=2)
datetime.datetime(2016, 10, 30, 21, 29, 50, 726284)
>>> now+timedelta(days=1,hours=10)
datetime.datetime(2016, 11, 3, 7, 29, 50, 726284)

本地时间转换为UTC时间,一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个市区,除非强行给datetime设置一个时区:

1
2
3
4
5
6
7
8
>>> from datetime import datetime,timedelta,timezone
>>> tz_utc_8=timezone(timedelta(hours=8)) #创建时区UTC+8:00
>>> now=datetime.now()
>>> now
datetime.datetime(2016, 11, 1, 21, 35, 48, 271569)
>>> dt=now.replace(tzinfo=tz_utc_8) #强制设置为UTC+8:00
>>> dt
datetime.datetime(2016, 11, 1, 21, 35, 48, 271569, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))

如果系统时区恰好是UTC+8:00,那么上述代码就是正确的,否则,不能强制设置为UTC+8:00时区。

时区转换,我们可以先通过utcnow()拿到当前的UTC时间,再转换为任意时区的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> utc_dt=datetime.utcnow().replace(tzinfo=timezone.utc) #拿到UTC时间,强制设置时区为UTC+0:00
>>> print(utc_dt)
2016-11-01 13:39:21.800814+00:00 #astimezone()将转化时区为北京时间
>>> bj_dt=utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2016-11-01 21:39:21.800814+08:00
>>> tokyo_dt=utc_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt)
2016-11-01 22:39:21.800814+09:00
>>> tokyo_dt2=bj_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt2)
2016-11-01 22:39:21.800814+09:00
>>>

时区转换的关键在于,拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。利用带时区的datetime,通过astimezone()方法,可以转换到任意时区。

datetime表示时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。如果要存储datetime,最佳方法是将其转换为timestamp再存储,因为timestamp的值与时区完全无关。


collections

collections是Python内建的一个集合模块,提供了许多有用的集合类。

namedtuple

我们知道tuple可以表示不变集合,比如,一个点的二维坐标可以表示为:

p=(1,2)```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
但是看到(1,2),很难看出这个`tuple`是用来表示一个坐标的。定义一个class又没有必要,这时候,`namedtuple`就可以使用了:
```python
>>> from collections import namedtuple
>>> Point=namedtuple('Point',['x','y'])
>>> p=Point(1,2)
>>> p.x
1
>>> p.y
2
>>> isinstance(p,Point)
True
>>> isinstance(p,tuple)
True

namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。这样我们可以用namedtuple可以很方便的定义一种数据结构,它具备tuple的不变性,又可以根据属性来引用,十分方便。可以验证创建的Point对象是tuple的一种子类。

类似的如果要用坐标和半径定义一个圆:

1
>>> Circle=namedtuple('Circle',['x','y','r'])

deque

使用list村塾数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。deque是为了高效实现插入和删除操作 的双向列表,适合用于队列和栈:

1
2
3
4
5
6
>>> from collections import deque
>>> q=deque(['a','b','c'])
>>> q.append('x')
>>> q.appendleft('y')
>>> q
deque(['y', 'a', 'b', 'c', 'x'])

deque除了实现list的append()和pop()外,还支持appendleft()和popleft()这样可以非常高效的往头部添加或删除元素。

defaultdict

使用dict时,如果引用的key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defalultdict:

1
2
3
4
5
6
7
>>> from collections import defaultdict
>>> dd=defaultdict(lambda: 'N/A')
>>> dd['key1']='abc'
>>> dd['key1']
'abc'
>>> dd['key2']
'N/A'

注意默认值是返回函数调用的,而函数在创建defaultdict对象时传入。

OrderedDict

使用dict时,key是无序的。在对dict做迭代时,我们无法确定Key的顺序,如果要保持Key的顺序,可以用OrderedDict:

1
2
3
4
5
6
7
>>> from collections import OrderedDict
>>> d=dict([('a',1),('b',2),('c',3)])
>>> d
{'a': 1, 'b': 2, 'c': 3}
>>> od=OrderedDict([('a',1),('b',2),('c',3)])
>>> od
OrderedDict([('a', 1), ('b', 2), ('c', 3)])

注意,OrderedDict的Key会按照插入的顺序排列,不是Key本身排序:

1
2
3
4
5
6
7
8
>>> od=OrderedDict()
>>> od['z']=1
>>> od['y']=2
>>> od['a']=3
>>> od
OrderedDict([('z', 1), ('y', 2), ('a', 3)])
>>> list(od.keys())
['z', 'y', 'a']

OrderedDict可以实现一个FIFO的dict,当容量超出限制时,先删除最早添加的Key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> from collections import OrderedDict
>>>
>>> class LastUpdatedOrderedDict(OrderedDict):
... def __init__(self,capacity):
... super( LastUpdatedOrderedDict,self).__init__()
... self._capacity=capacity
...
... def __setitem__(self,key,value):
... containsKey=1 if key in self else 0
... if len(self)-constainsKey>=self._capacity:
... last=self.popoitem(last=False)
... print('remove:',last)
... if constainsKey:
... del self[key]
... print('set:',(key,value))
... else:
... print('add:',(key,value))
... OrderdeDict.__setitem__(self,key,value)
...

Counter

Counter是一个简单的计数器,例如统计字符出现的个数:

1
2
3
4
5
6
7
>>> from collections import Counter
>>> c=Counter()
>>> for ch in 'programming':
... c[ch]=c[ch]+1
...
>>> c
Counter({'m': 2, 'g': 2, 'r': 2, 'o': 1, 'p': 1, 'n': 1, 'i': 1, 'a': 1})

Counter实际上也是dict的一个子类。


base64

Base64是一种用64个字符来表示任意二进制数据的方法。用记事本打开exe,jpg,pdf这些文件会看到一大堆乱码,因为二进制文件包含了很多无法显示和打印的字符,所以,如果让笔记本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。

Base64的原理很简单,首先,准备一个包含64个字符的数组:

[‘A’,’B’,’C’……’a’,’b’,’c’…….’0’,’1’,’2’…..’+’,’/‘]

然后,对二进制数据进行处理,每三个字节一组,一共是24个bit,划为四组,每组为6bit:
Alt text

这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。

所以,base64编码会把三个字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文,网页等直接显示。如果要编码的二进制数据不是3的倍数,最后剩下的1个或2个字节可以用\x00字节在末尾补足后,再在编码的末尾加上一个或两个=号,表示补了多少个字节,解码的时候会自动去掉。

Python内置的base64可以直接进行base64的编解码:

1
2
3
4
5
>>> import base64
>>> base64.b64encode(b'binary\x00string')
b'YmluYXJ5AHN0cmluZw=='
>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
b'binary\x00string'

由于标准的Base64编码后可能出现符号+和/,在URL中就不能直接作为参数,所以又有一种URL safe的base64编码,其实就是把+、/分别变为-和_:

1
2
3
4
5
6
>>> base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd++//'
>>> base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')
b'abcd--__'
>>> base64.urlsafe_b64decode('abcd--__')
b'i\xb7\x1d\xfb\xef\xff'

Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。适用于小段内容的编码,比如数字证书签名,Cookie的内容等。

由于=字符也可能出现在Base64编码中,但=用在URL,Cookie里面会造成歧义,很多Base64编码后会把=去掉。

标准Base64:
‘abcd’ -> ‘YWJjZA==’
自动去掉=:
‘abcd’ -> ‘YWJjZA’

去掉之后解码时,由于Base64是把3个字节变为4个字节的,所以Base64编码的长度永远是4的倍数,加上=后就可以正常解码了。


struct

Python没有专门处理字节的数据类型,但是由于b'str'可以表示字节,所以,字节数组=二进制str。而在C语言中,我们可以很方便的使用struct、union等来处理字节,以及字节和int、float的转换。

在Python中,比如要把一个32位无符号整数编程字节,也就是四个长度的bytes ,需要配合运算符:

1
2
3
4
5
6
7
8
>>> n=10240099
>>> b1=(n&0xff000000)>>24
>>> b2=(n&0xff0000)>>16
>>> b3=(n&0xff00)>>8
>>> b4=n&0xff
>>> bs=bytes([b1,b2,b3,b4])
>>> bs
b'\x00\x9c@c'

很麻烦,如果换成浮点数就无能为力了。Python提供了一个struct模块来解决bytes和其他二进制数据类型的转换。struct的pack函数把任意数据变为bytes:

1
2
3
>>> import struct
>>> struct.pack('>I',10240099)
b'\x00\x9c@c'

pack的第一个参数是处理指令,‘>I’的意思是:>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。后面的参数个数要和处理指令一致。unpack把bytes变成相应的数据类型:

1
2
>>> struct.unpack('>IH',b'\xf0\xf0\xf0\xf0\x80\x80')
(4042322160, 32896)

根据>IH的说明,后面的bytes一次变为I:4字节无符号数和H:2字节无符号整数。所以,尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct 很方便。

hashlib

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

摘要算法

又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串。摘要算法就是通过摘要函数f()对任意的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。摘要算法之所以可以确定数据是否被篡改过是因为摘要函数是一个单向函数,计算f(data)很容易,但是通过digest 反推data很困难。对原始数据做一个bit的修改都会导致计算出的摘要完全不同。

以MD5为例,计算一个字符串的MD5值:

1
2
3
4
5
6
>>> import hashlib
>>>
>>> md5=hashlib.md5()
>>> md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
>>> print(md5.hexdigest())
d26a53750bc40b38b65a520292f69306

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

1
2
3
4
5
6
7
>>> import hashlib
>>>
>>> md5=hashlib.md5()
>>> md5.update('how to use md5 in'.encode('utf-8'))
>>> md5.update('python hashlib?'.encode('utf-8'))
>>> print(md5.hexdigest())
047b2e59ed7530a32db954459f82266b

MD5是最常见的摘要算法,速度很快,上观南城结果是固定的128bit字节,通常用一个32位的16进制字符串表示。

另一种常见的摘要算法是SHA1:

1
2
3
4
5
6
7
>>> import hashlib
>>>
>>> sha1=hashlib.sha1()
>>> sha1.update('how to use sha1 in '.encode('utf-8'))
>>> sha1.update('python hashlib?'.encode('utf-8'))
>>> print(sha1.hexdigest())
2c76b57293ce30acef38d98f6046927161b46a44

SHA1的结果是160bit字节,通常用一个40位的16进制字符来表示。比SHA1更安全的算法是SHA256和SHA512,越安全的算法越慢,摘要长度也会更长。

摘要算法应用

任何允许用户登录的网站都会存储用户登录的用户名和口令,存储的方法是存储到数据库表中:

name passwd
Michael 123456
bob asd121
alice alice123456

如果以明文保存用户口令,如果数据库泄露,所有用户的口令都泄露。而且网站运维人员是可以访问数据库的,也就是能获取到所有用户的口令。正确保存口令的方式是不存储用户的明文口令,而是存储用户口令的摘要,比如MD5:
| name | passwd |
| :——– | ——–:|
| Michael | e10adc3949ba59abbe56e057f20f883e |
|bob | 878ef96e86145580c38c87f0410ad153 |
|alice | 99b1c2188db85afee403b1536010c2c9|

当用户登录时,首先计算机用户输入的明文口令的MD5,然后和数据库的MD5对比,结果一致说明口令输入正确,如果不一致则口令错误。


itertools

Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。首先来看itertools提供的几个无限迭代器:

cycle()会把传入的一个序列无限重复下去。

1
2
3
4
5
6
7
8
9
10
11
12
>>> import itertools
>>> cs = itertools.cycle('ABC') # 注意字符串也是序列的一种
>>> for c in cs:
... print(c)
...
'A'
'B'
'C'
'A'
'B'
'C'
...

repeat()负责把一个元素无限重复下去,不过提供第二个参数就可以限定重复次数:

1
2
3
4
5
6
7
8
9
10
11
>>> import itertools
>>>
>>> ns=itertools.repeat('a',4)
>>> for n in ns:
print(n)
a
a
a
a

无限序列只有在for迭代时才会无限地迭代下去,如果只是创建了一个迭代对象,它不会事先把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列:

1
2
3
4
>>> naturals=itertools.count(1)
>>> ns=itertools.takewhile(lambda x:x<=10,naturals)
>>> list(ns)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

itertools提供的几个迭代器操作函数更加有用:

chain()

chain()可以把一组迭代对象串联起来,形成一个更大的迭代器:

1
2
3
4
5
6
7
8
9
10
>>> for c in itertools.chain('abc','1234'):
print(c)
a
b
c
1
2
3

groupby()

groupby()把迭代器中相邻的重复元素挑出来放在一起:

1
2
3
4
5
6
7
8
9
>>> for key,group in itertools.groupby('aaabbbccaaadd'):
print(key,list(group))
a ['a', 'a', 'a']
b ['b', 'b', 'b']
c ['c', 'c']
a ['a', 'a', 'a']
d ['d', 'd']

实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,可以让元素Aa都返回相同的key:

1
2
3
4
5
6
7
8
9
10
>>> for key,group in itertools.groupby('AAaBbbcCCDcd',lambda c:c.upper()):
print(key,list(group))
A ['A', 'A', 'a']
B ['B', 'b', 'b']
C ['c', 'C', 'C']
D ['D']
C ['c']
D ['d']


XML

DOM vs SAX

操作XMl有两种方法:DOM和SAX,DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,缺点是我们需要自己处理事件。正常情况下优先考虑SAX。在Python中我们关心的事件是start_element,end_element,char_data准备好这三个函数就可以解析xml了。如当SAX解析器读到一个节点时:

python

会产生三个事件:

1.start_element事件,在读取时;
2.char_data事件,在读取python时
3.end_element事件,在读取

代码验证如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from xml.parsers.expat import ParserCreate
class DefaultSaxHandler(object):
def start_element(self, name, attrs):
print('sax:start_element: %s, attrs: %s' % (name, str(attrs)))
def end_element(self, name):
print('sax:end_element: %s' % name)
def char_data(self, text):
print('sax:char_data: %s' % text)
xml = r'''<?xml version="1.0"?>
<ol>
<li><a href="/python">Python</a></li>
<li><a href="/ruby">Ruby</a></li>
</ol>
'''
handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)

需要注意的是读取一大段字符串时,CharacterDataHandler可能被多次调用,所以需要自己保存起来,在EndElementHandler里面合并。


HTMLParser

编写一个搜索引擎,第一步是用爬虫把目标网站的页面抓下来,第二步就是解析该HTML页面,看看里面的内容到底是新闻、图片还是视频。

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或者SAX来解析HTML。可以用HTMLParser来解析HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from html.parser import HTMLParser
from html.entities import name2codepoint
class MyHTMLParser(HTMLParser):
def handle_starttag(self,tag,attrs):
print('</%s>' %tag)
def handle_endtag(self,tag):
print('</%s>' %tag)
def handle_startendtag(self ,tag,attrs):
print('</%s>' %tag)
def handle_data(self,data):
print(data)
def handle_comment(self,data):
print('<!--',data ,'-->')
def handle_entityref(self,name):
print('&%s:' %name)
def handle_charref(self,name):
print('&#%s:' %name)
parser=MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser-->
<p>Some<a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')

feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。特殊字符有两种,一种是用英文表示的&nbsp,一种是用数字表示的&#1234;,这两种字符都可以通过Parser解析出来。


urllib

urllib提供了一系列用于操作URL的功能。

Get

urllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的相应:

例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取并返回相应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> from urllib import request
>>>
>>> with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
... dada=f.read()
... print('Status:',f.status,f.reason)
... for k,v in f.getheaders():
... print('%s:%s' %(k,v))
... print('Data:',data.decode('utf-8'))
...
Status: 200 OK
Date:Sun, 06 Nov 2016 08:44:01 GMT
Content-Type:application/json; charset=utf-8
Content-Length:2055
Connection:close
Vary:Accept-Encoding
X-Ratelimit-Remaining2:96
X-Ratelimit-Limit2:100
Expires:Sun, 1 Jan 2006 01:00:00 GMT
Pragma:no-cache
Cache-Control:must-revalidate, no-cache, private
Set-Cookie:bid=L9LbgN0KHkE; Expires=Mon, 06-Nov-17 08:44:01 GMT; Domain=.douban.com; Path=/
X-DOUBAN-NEWBID:L9LbgN0KHkE
X-DAE-Node:dis16
X-DAE-App:book
Server:dae
Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6","tags":[{"count":20,"name":"spring","title":"spring"},{"count":11,"name":"Java","title":"Java"},{"count":6,"name":"javaee","title":"javaee"},{"count":4,"name":"j2ee","title":"j2ee"},{"count":3,"name":"POJO","title":"POJO"},{"count":3,"name":"计算机","title":"计算机"},{"count":3,"name":"藏书","title":"藏书"},{"count":3,"name":"编程","title":"编程"}],"origin_title":"","image":"https://img3.doubanio.com\/mpic\/s2648230.jpg","binding":"","translator":[],"catalog":"","pages":"509","images":{"small":"https://img3.doubanio.com\/spic\/s2648230.jpg","large":"https://img3.doubanio.com\/lpic\/s2648230.jpg","medium":"https://img3.doubanio.com\/mpic\/s2648230.jpg"},"alt":"https:\/\/book.douban.com\/subject\/2129650\/","id":"2129650","publisher":"电子工业","isbn10":"7121042622","isbn13":"9787121042621","title":"Spring 2.0核心技术与最佳实践","url":"https:\/\/api.douban.com\/v2\/book\/2129650","alt_title":"","author_intro":"","summary":"本书注重实践而又深入 理论,由浅入深且详细介绍了Spring 2.0框架的几乎全部的内容,并重点突出2.0版本的新特性。本书将为读者展示如何应用Spring 2.0框架创建灵活高效的JavaEE应用,并提供了一个真正 可直接部署的完整的Web应用程序——Live在线书店(http:\/\/www.livebookstore.net)。\n在介绍Spring框架的同时,本书还介绍了与Spring相关的大量第三方框架,涉及领域全面,实用 性强。本书另一大特色是实用性强,易于上手,以实际项目为出发点,介绍项目开发中应遵循 的最佳开发模式。\n本书还介绍了大量实践性极强的例子,并给出了完整的配置步骤,几乎覆 盖了Spring 2.0版本的新特性。\n本书适合有一定Java基础的读者,对JavaEE开发人员特别有 帮助。本书既可以作为Spring 2.0的学习指南,也可以作为实际项目开发的参考手册。","price":"59.80元"}

可以看到HTTP相应的头和JSON数据,如果我们要模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone6区请求豆瓣首页:

1
2
3
4
5
6
7
8
9
10
>>> from urllib import request
>>>
>>> req=request.Request('http://www.douban.com/')
>>> req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
>>> with request.urlopen(req) as f:
... print('Status:',f.status, f.reason)
... for k,v in f.getheaders():
... print('%s:%s' %(k,v))
... print('Data:',f.read().decode('utf-8'))
...

这样豆瓣就会返回适合iPhone的移动版网页。

Post

如果要以POST发送一个请求,只需要把参数data以bytes形式传入。

我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from urllib import request, parse
print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
('username', email),
('password', passwd),
('entry', 'mweibo'),
('client_id', ''),
('savestate', '1'),
('ec', ''),
('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])
req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')
with request.urlopen(req, data=login_data.encode('utf-8')) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))

Handler

如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,实例如下:

1
2
3
4
5
6
proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})
proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open('http://www.example.com/login.html') as f:
pass

urllib提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标示浏览器的。

10. 正则表达式

发表于 2019-04-21 | 分类于 Python

正则表达式

字符串是编程时涉及到最多的一种数据结构,对于字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址,虽然可以编程提取@前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦,而且代码难以复用。

正则表达式是一种用来匹配字符串的强有力的武器。它设计的思想是用一种描述性的语言来给字符串定义了一个规则,凡是符合规则 对字符串,我们就认为它匹配了,否则,该字符串就是不合法的。

所以我们判断一个字符串是否是合法的Email的方法是:
1、创建一个匹配Email的正则表达式
2、用该正则表达式去匹配用户的输入来判断是否合法。

因为正则表达式也是用字符串表示的,首先需要了解如何用字符来描述字符。

在正则表达式中,如果直接给出字符,就是精确匹配。用\d可以匹配一个数字,\w可以匹配一个字母或数字,所以:

00\d可以匹配007,但是无法匹配00A
\d\d\d可以匹配010
\w\w\d可以匹配py3
.可以匹配任意字符,py.可以匹配pyc``py0``py!等。

要匹配变长的字符,在正则表达式中,用*表示任意个字符,用+表示至少一个字符,用?表示0个或1个字符,用{n}表示n个字符,用{n,m}表示n-m个字符:

\d{3}\s+\d{3,8}
1、\d{3}表示匹配三个数字
2、\s表示可以匹配一个空格(包括Tab等空白符),\s+表示至少有一个空白符,
3、\d{3.8}表示3-8个数字。
综合起来,上面的正则表达式可以匹配任意个空格隔开的带区号的电话号码。如果要匹配010-12345这样的号码时,由于-是特殊字符,在正则表达式中要用\转义,所以上面的正则为:\d{3}\-\d{3,8},但是仍然无法匹配010 - 12345这样的电话号码。

进阶

要做更精确的匹配,可以用[]表示范围:

[0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线
[0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的祖字符串,比如a100,0_z,Py300等
[a-zA-z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串。
[a-zA-z\_][0-9a-zA-Z\_]{0,19}更精确地限制了变量的长度是1-20个字符

A|B可以匹配A或B,所以(P|p)ython可以匹配python或者Python。

^表示行的开头,^\d表示必须以数字开头。

$表示行的结束,\d$表示必须以数字结束。

re模块

Python提供re模块,包含所有正则表达式的功能。又有Python的字符串本身也用\转义,要特别注意:

1
2
3
4
>>> s='ABC\\-001'
#python的字符串,对应的正则表达式'ABC\-001'
>>> s=r'ABC\-001'
#加r就不再考虑转义的问题

如何判断正则表达式是否匹配:

1
2
3
4
>>> import re
>>> re.match(r'^\d{3}\-\d{3,8}$', '010-12345')
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> re.match(r'^\d{3}\-\d{3,8}$', '010 12345')

match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None。常见的判断方法是:

1
2
3
4
5
test='用户输入的字符串'
if re.match(r'正则表达式',est):
print('ok')
else:
print('failed')

切分字符串

用正则表达式切分字符串比用固定的字符更灵活,正常的切分代码为:

1
2
>>> 'a b c'.split()
['a', 'b', 'c']

无法识别连续的空格,用正则表达式:

1
2
>>> re.split(r'\s+','a b c')
['a', 'b', 'c']

无论多少个空格都可以正常分割。加入,:

1
2
>>> re.split(r'[\s\.]+','a, b, c d')
['a,', 'b,', 'c', 'd']

加入;:

1
2
>>> re.split(r'[\s\.\;]+','a,b;; c d')
['a,b', 'c', 'd']

分组

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组。比如:

^(\d{3}-(\d{3,8}))$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:

1
2
3
4
5
6
7
8
9
>>> m=re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'

如果正则表达式中定义了组,就可以在Match对象上用group()方法提取出子串来。

group(0)永远是原始字符串,group(1)、group(2)……表示第一个、第二个子串。

提取子串很有用:

1
2
3
4
>>> t='19:13:12'
>>> m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', t)
>>> m.groups()
('19', '13', '12')

这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:

‘^(0[1-9]|1[0-2]|[0-9])-(0[0-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$’

对于2-30,4-31这样的非法日期,用正则还是识别不了。

贪婪匹配

正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。如匹配出数字后面的0:

1
2
>>> re.match(r'^(\d+)(0*)$','102300').groups()
('102300', '')

由于\d+采用贪婪匹配,直接把后面的0全匹配了,结果0*只能匹配空字符串了。

必须让\d+采用非贪婪匹配,才能把后面的0匹配出来,加个?就可以了:

1
2
>>> re.match(r'^(\d+?)(0*)$','102300').groups()
('1023', '00')

编译

当我们在Python中使用正则表达式时,re模块会做两件事情:

1、编译正则表达式,如果正则表达式的字符串本身不合法会报错。
2、用编译后的正则表达式去匹配字符串

如果一个正则表达式要重复机器那次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:

1
2
3
4
5
6
>>> import re
>>> re_telephone=re.compile(r'^(\d{3})-(\d{3,8})$')
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8989').groups()
('010', '8989')

编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。

1…1011
cdx

cdx

Be a better man!

110 日志
36 分类
31 标签
GitHub E-Mail
© 2020 cdx
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.2