Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

python学习笔记,常用内建库

datetime

collection

base64

struct

hashlib

itertolls

contexlib

urllib

xml

htmlparser

datetime

获取当前时间

1
2
3
4
5
6
from datetime import datetime


now = datetime.now() # 获取当前时间
print(now) # 2021-08-17 09:10:59.819930
print(type(now)) # <class 'datetime.datetime'>

datetime模块中包含一个datetime类,now()datetime类中的一个方法。datetime.now()返回的是一个datetime类。

获取指定日期与时间

直接使用参数构造一个datetime对象即可:

1
2
3
4
5
from datetime import datetime


dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime
print(dt) # 2015-04-19 12:20:00

datetime转换为timestamp

`timestamp`时间戳

时间戳是指格林威治时间1970年01月01日00时00分00秒起至当下的总秒数。通俗的讲, 时间戳是一份能够表示一份数据在一个特定时间点已经存在的完整的可验证的数据。 它的提出主要是为用户提供一份电子证据, 以证明用户的某些数据的产生时间。

1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数)

即对数据产生的时间进行一个唯一认证,不可更改。

则北京时间的1970年01月01日00时00分00秒为:

1
1970-1-1 08:00:00 UTC+8:00

可见timestamp的值与时区毫无关系,因为timestamp一旦确定,其UTC时间就确定了,转换到任意时区的时间也是完全确定的,这就是为什么计算机存储的当前时间是以timestamp表示的,全球各地的计算机在任意时刻的timestamp都是完全相同的(假定时间已校准)。

转换的方式只需要调用timestamp()即可:

1
2
3
4
5
6
from datetime import datetime


dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime
dt.timestamp() # 把datetime转换为timestamp
# 1429417200.0

timestamp是一个浮点数,整数位表示秒。

某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数,这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。

timestamp转换为datetime

要把timestamp转换为datetime,使用datetime提供的fromtimestamp()方法:

1
2
3
4
5
from datetime import datetime


t = 1429417200.0
print(datetime.fromtimestamp(t)) # 2015-04-19 12:20:00

timestamp是没有时区的,而datetime是有市区的,使用fromtimestamp()方法转换后的时间是按照系统设置的当地时区进行转化的,同样也支持转化到UTC标准市区,即UTC+0:00:

1
2
3
4
5
6
7
8
from datetime import datetime


t = 1429417200.0
print(datetime.fromtimestamp(t)) # 本地时间
# 2015-04-19 12:20:00
print(datetime.utcfromtimestamp(t)) # UTC时间
# 2015-04-19 04:20:00

str转datetime

从前端传送的时间大多是String格式的时间,要处理日期和时间,首先必须把str转换为datetime。转换方法是通过datetime.strptime()实现,需要一个日期和时间的格式化字符串:

1
2
3
4
5
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是没有时区信息的。

datetime转换为str

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

1
2
3
4
5
from datetime import datetime


now = datetime.now()
print(now.strftime('%a, %b %d %H:%M')) # Tue, Aug 17 09:10

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(2021, 8, 17, 10, 11, 27, 797823)
>>> now + timedelta(hours = 10)
datetime.datetime(2021, 8, 17, 20, 11, 27, 797823)
>>> now - timedelta(days = 1)
datetime.datetime(2021, 8, 16, 10, 11, 27, 797823)
>>> now + timedelta(days = 2, hours = 12)
datetime.datetime(2021, 8, 19, 22, 11, 27, 797823)

可见,使用timedelta你可以很容易地算出前几天和后几天的时刻。

本地时间转换为UTC时间

本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而UTC时间指UTC+0:00时区的时间。

一个datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区,该参数需要接收一个timezone类型的对象:

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(2015, 5, 18, 17, 2, 10, 871012)
>>> dt = now.replace(tzinfo=tz_utc_8) # 强制设置为UTC+8:00
>>> dt
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012, 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
14
15
16
# 拿到UTC时间,并强制设置时区为UTC+0:00:
>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
>>> print(utc_dt)
2021-08-17 02:34:19.972780+00:00
# astimezone()将转换时区为北京时间:
>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2021-08-17 10:34:19.972780+08:00
# astimezone()将转换时区为东京时间:
>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt)
2021-08-17 11:34:19.972780+09:00
# astimezone()将bj_dt转换时区为东京时间:
>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt2)
2021-08-17 11:34:19.972780+09:00

时区转换的关键在于,拿到一个datetime时,要获知其正确的时区,然后强制设置时区,作为基准时间。

利用带时区的datetime,通过astimezone()方法,可以转换到任意时区。

注:不是必须从UTC+0:00时区转换到其他时区,任何带时区的datetime都可以正确转换,例如上述bj_dttokyo_dt的转换。

练习

例如假设你获取了用户输入的日期和时间如2015-1-21 9:01:30,以及一个时区信息如UTC+5:00,均是str,请编写一个函数将其转换为timestamp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Hello.py
# -*- coding:utf-8 -*-

import re
from datetime import datetime, timezone, timedelta

def to_timestamp(dt_str, tz_str):
timeNow = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S')
tz = re.match(r'UTC(\+|\-)0?(\d{1,2}):00', tz_str)
tz = int((str(tz[1])+str(tz[2])))
dt = timeNow.replace(tzinfo=timezone(timedelta(hours=tz)))
return dt.timestamp()


# 测试:
t1 = to_timestamp('2015-6-1 08:10:30', 'UTC+7:00')
assert t1 == 1433121030.0, t1

t2 = to_timestamp('2015-5-31 16:10:30', 'UTC-09:00')
assert t2 == 1433121030.0, t2

print('ok')

collection

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

namedtuple

tuple可以表示不变集合,比如二位坐标

namedtuple则允许我们为tuple设置一个名字。

1
2
3
4
5
6
7
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)
>>> p.x
1
>>> p.y
2

namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。

可以验证namedtupletuple的子类:

1
2
3
4
>>> isinstance(p, Point)
True
>>> isinstance(p, tuple)
True

deque

python中list为线性表,在访问时速度很快,但在插入和删除时速度比较慢

python中提供了一种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'])

对尾部进行增删时可使用append()pop()方法,而对头部进行增删时则可使用appendleft()popleft()方法

defaultdict

defaultdictdict的基础上,当发生keyerror异常时,允许设置自定义返回值:

1
2
3
4
5
6
7
>>> from collections import defaultdict
>>> dd = defaultdict(lambda: 'N/A')
>>> dd['key1'] = 'abc'
>>> dd['key1'] # key1存在
'abc'
>>> dd['key2'] # key2不存在,返回默认值
'N/A'

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

除了在Key不存在时返回默认值,defaultdict的其他行为跟dict是完全一样的。

OrderedDict

普通的dict中,key时无序的,迭代dict时无法确定key的顺序,如果需要保持则需要使用OrderedDict

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

OrderedDict的Key会按照插入的顺序排列:

1
2
3
4
5
6
>>> od = OrderedDict()
>>> od['z'] = 1
>>> od['y'] = 2
>>> od['x'] = 3
>>> list(od.keys()) # 按照插入的Key的顺序返回
['z', 'y', 'x']

rderedDict可以实现一个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) - containsKey >= self._capacity:
last = self.popitem(last=False)
print('remove:', last)
if containsKey:
del self[key]
print('set:', (key, value))
else:
print('add:', (key, value))
OrderedDict.__setitem__(self, key, value)

ChainMap

ChainMap可以把一组dict串起来并组成一个逻辑上的dictChainMap本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找。

ChainMap可以用于实现参数的优先级查找,例如:应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。我们可以用ChainMap实现参数的优先级查找,即先查命令行参数,如果没有传入,再查环境变量,如果没有,就使用默认参数。

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from collections import ChainMap
import os, argparse

# 构造缺省参数:
defaults = {
'color': 'red',
'user': 'guest'
}

# 构造命令行参数:
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = { k: v for k, v in vars(namespace).items() if v }

# 组合成ChainMap:
combined = ChainMap(command_line_args, os.environ, defaults)

# 打印参数:
print('color=%s' % combined['color'])
print('user=%s' % combined['user'])

没有任何参数时,打印出默认参数:

1
2
3
$ python3 use_chainmap.py 
color=red
user=guest

当传入命令行参数时,优先使用命令行参数:

1
2
3
$ python3 use_chainmap.py -u bob
color=red
user=bob

同时传入命令行参数和环境变量,命令行参数的优先级较高:

1
2
3
$ user=admin color=green python3 use_chainmap.py -u bob
color=green
user=bob

Counter

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

1
2
3
4
5
6
7
8
9
10
>>> from collections import Counter
>>> c = Counter()
>>> for ch in 'programming':
... c[ch] = c[ch] + 1
...
>>> c
Counter({'g': 2, 'm': 2, 'r': 2, 'a': 1, 'i': 1, 'o': 1, 'n': 1, 'p': 1})
>>> c.update('hello') # 也可以一次性update
>>> c
Counter({'r': 2, 'o': 2, 'g': 2, 'm': 2, 'l': 2, 'p': 1, 'a': 1, 'i': 1, 'n': 1, 'h': 1, 'e': 1})

Counter实际上也是dict的一个子类,上面的结果可以看出每个字符出现的次数。

Base64

Base64是一种用64个字符来表示任意二进制数据的方法。

通常用记事本打开exejpgpdf等文件会看到一些乱码,这是因为二进制文件包含很多无法显示和打印的字符,如果需要让文本处理软件能够处理二进制数据,就需要一个二进制转字符串的方法,Base64就是一种最常见的二进制编码方法

Base64原理

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

1
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']

然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24bit,划为4组,每组正好6个bit:

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

所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。

原本长为$3 \times 8 = 24$​bit的二进制序列,重新划分为4组,则每组6bit,则每组需要增加2bit来构成一个新的字节,则增长$2 \times 4 = 8$​bit,则增长$8/24 *100% = 33.3333%$​

如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候,会自动去掉。

Base64使用

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'

还可以自己定义64个字符的排列顺序,这样就可以自定义Base64编码,不过,通常情况下完全没有必要。

Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。

Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。

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

1
2
3
4
# 标准Base64:
'abcd' -> 'YWJjZA=='
# 自动去掉=:
'abcd' -> 'YWJjZA'

去掉=后怎么解码呢?因为Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-
import base64


def safe_base64_decode(s): # 拿到去掉=后的base64字符串后,为其自动补充=
l = len(s)
n = 4 - l%4
if not n == 4:
while n:
s += '='
n -= 1
print(s)
return base64.b64decode(s)


# 测试:
assert b'abcd' == safe_base64_decode('YWJjZA=='), safe_base64_decode('YWJjZA==')
assert b'abcd' == safe_base64_decode('YWJjZA'), safe_base64_decode('YWJjZA')
print('ok')

Base64是一种任意二进制到文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。

struct

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

在Python中,比方说要把一个32位无符号整数变成字节,也就是4个长度的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和其他二进制数据类型的转换。

structpack函数把任意数据类型变成bytes

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

pack的第一个参数是处理指令,'>I'的意思是:

>表示字节顺序是big-endian,也就是网络序(大端),I表示4字节无符号整数。

下面我们复习一下内存中的两种存储顺序

字节顺序

  • big-endian:字节存储方式为大端在末尾,即顺序存储,第一个字节存储在开头位置,如 01234567,存储为 01 23 45 67(即权重高的位在前
  • little-endian:字节存储方式为小端在末尾,即逆序存储,第一个字节存储在末尾位置,如 01234567,存储为 67 45 23 01(即权重低的位在前

struct应用

后面的参数个数要和处理指令一致。

unpackbytes变成相应的数据类型:

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

根据>IH的说明,后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数。

所以,尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct就方便多了。

struct模块定义的数据类型可以参考Python官方文档:

Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用struct分析一下。

首先找一个bmp文件,没有的话用“画图”画一个。

读入前30个字节来分析:

1
>>> s = b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'

BMP格式采用小端方式存储数据,文件头的结构按顺序如下:

两个字节:'BM'表示Windows位图,'BA'表示OS/2位图; 一个4字节整数:表示位图大小; 一个4字节整数:保留位,始终为0; 一个4字节整数:实际图像的偏移量; 一个4字节整数:Header的字节数; 一个4字节整数:图像宽度; 一个4字节整数:图像高度; 一个2字节整数:始终为1; 一个2字节整数:颜色数。

所以,组合起来用unpack读取:

1
2
>>> struct.unpack('<ccIIIIIIHH', s)
(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)

结果显示,b'B'b'M'说明是Windows位图,位图大小为640x360,颜色数为24。

练习

接下来编写一个bmpinfo.py,可以检查任意文件是否是位图文件,如果是,打印出图片大小和颜色数作为练习:

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
# -*- coding: utf-8 -*-
import base64, struct


bmp_data = base64.b64decode('Qk1oAgAAAAAAADYAAAAoAAAAHAAAAAoAAAABABAAAAAAADICAAASCwAAEgsAA' +
'AAAAAAAAAAA/3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3/' +
'/f/9//3//f/9//3//f/9/AHwAfAB8AHwAfAB8AHwAfP9//3//fwB8AHwAfAB8/3//f/9/A' + 'HwAfAB8AHz/f/9//3//f/9//38AfAB8AHwAfAB8AHwAfAB8AHz/f/9//38AfAB8/3//f/9' +
'//3//fwB8AHz/f/9//3//f/9//3//f/9/AHwAfP9//3//f/9/AHwAfP9//3//fwB8AHz/f' +
'/9//3//f/9/AHwAfP9//3//f/9//3//f/9//38AfAB8AHwAfAB8AHwAfP9//3//f/9/AHw' +
'AfP9//3//f/9//38AfAB8/3//f/9//3//f/9//3//fwB8AHwAfAB8AHwAfAB8/3//f/9//' +
'38AfAB8/3//f/9//3//fwB8AHz/f/9//3//f/9//3//f/9/AHwAfP9//3//f/9/AHwAfP9' +
'//3//fwB8AHz/f/9/AHz/f/9/AHwAfP9//38AfP9//3//f/9/AHwAfAB8AHwAfAB8AHwAf' +
'AB8/3//f/9/AHwAfP9//38AfAB8AHwAfAB8AHwAfAB8/3//f/9//38AfAB8AHwAfAB8AHw' +
'AfAB8/3//f/9/AHwAfAB8AHz/fwB8AHwAfAB8AHwAfAB8AHz/f/9//3//f/9//3//f/9//' +
'3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//38AAA==')



def bmp_info(data):
data = data[0:30]
byteStr = struct.unpack('<ccIIIIIIHH', data)
if byteStr[0] == b'B' and byteStr[1] == b'M' or b'A':
return {
'width': byteStr[-4],
'height': byteStr[-3],
'color': byteStr[-1]
}


# 测试
bi = bmp_info(bmp_data)
assert bi['width'] == 28
assert bi['height'] == 10
assert bi['color'] == 16
print('ok')

hashlib

python的hashlib提供了常见的摘要算法支持:MD5、SHA1等等

摘要算法

摘要算法,即哈希算法或者说散列算法。通过一个函数将任意长度的数据转化为一个长度固定的数据串(通常用16进制的字符串表示)

摘要算法作用

下面引用一个例子:

举个例子,你写了一篇文章,内容是一个字符串'how to use python hashlib - by Michael',并附上这篇文章的摘要是'2d73d4f15c0db7f5ecb321b6a65e5d6d'。如果有人篡改了你的文章,并发表为'how to use python hashlib - by Bob',你可以一下子指出Bob篡改了你的文章,因为根据'how to use python hashlib - by Bob'计算出的摘要不同于原始文章的摘要。

总的来说,摘要算法的主要作用就是对任意长度数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。

需要注意的是,摘要算法通常是一个单向函数,即从原文本生成摘要相对容易,但从摘要生成原文非常困难。并且对原始数据做哪怕一个bit的修改,都会导致计算出的摘要完全不同。

摘要算法使用

先使用MD5算法计算一段文本的摘要:

1
2
3
4
5
6
import hashlib


md5 = hashlib.md5()
md5.update('how to use md5 in puthon hashlib'.encode('utf-8'))
print(md5)

计算结果如下:

1
bd3c2bf3a86acaa6ee89efda6b5ceae4

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

1
2
3
4
5
6
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())

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

另一种常见的摘要算法是SHA1,调用SHA1和调用MD5完全类似:

1
2
3
4
5
6
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())

SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。

结果如下:

1
2c76b57293ce30acef38d98f6046927161b46a44

由于任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。因此完全有可能发生两个不同的数据集通过某个摘要算法得到了相同的结果。这种情况称为碰撞,比如Bob试图根据你的摘要反推出一篇文章'how to learn hashlib in python - by Bob',并且这篇文章的摘要恰好和你的文章完全一致,这种情况也并非不可能出现,但是非常非常困难。

摘要算法应用

摘要算法在数据持久化时应用广泛,比如为了保证平台用户的账号安全,通常不会将用户的密码明文存入数据库,通常会将其MD5摘要存入数据库。用户登陆时先计算出明文口令的MD5摘要,再与数据库中的摘要进行对比。

但是采用存储MD5口令摘要的方式也不一定安全。

黑客往往会保存一个MD5口令对应数据库,对于简单的密码,只需要对数据库进行暴力匹配便可得到有限个可能的选项。

例如:

明文 摘要
123456 e10adc3949ba59abbe56e057f20f883e
888888 21218cca77804d2ba1922c33e0151105
password 5f4dcc3b5aa765d61d8327deb882cf99

那么我们如何保护那些密码设置简单的用户呢?

首先想到的有两条方案:

  1. 要求用户使用复杂密码
  2. 数据保护方使用特殊的方法避免简易密码遭泄露

方案一大多数网站都会使用,比如强制要求用户使用字母+数字+大小写+特殊字符的离奇组合。

这种方案虽然高效,但作为服务提供者,如此要求客户,这种做法显然无异于甩锅。

那么服务提供商们有了另一个做法,就是在你的口令中加

1
2
def calc_md5(password):
return get_md5(password + 'the-Salt')

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

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

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

现在我们模拟一下使用该方法的登陆与注册:

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
# -*- coding: utf-8 -*-
import hashlib, random


# 用户类
class User(object):
def __init__(self, username, password):
self.username = username
self.salt = ''.join([chr(random.randint(48, 122)) for i in range(20)])
self.password = get_md5(password + self.salt)

# 模拟用户信息数据库
db = {
'michael': User('michael', '123456'),
'bob': User('bob', 'abc999'),
'alice': User('alice', 'alice2008')
}
# 根据用户输入的登录名和口令模拟用户注册,计算更安全的MD5
def register(username, password):
newUser = User(username, password)
db[username] = User.password
return newUser


# 然后,根据修改后的MD5算法实现用户登录的验证
def get_md5(s):
return hashlib.md5(s.encode('utf-8')).hexdigest()


# 登陆验证
def login(username, password):
user = db[username]
return user.password == get_md5(password + user.salt)


# 测试:
assert login('michael', '123456')
assert login('bob', 'abc999')
assert login('alice', 'alice2008')
assert not login('michael', '1234567')
assert not login('bob', '123456')
assert not login('alice', 'Alice2008')
print('ok')

需要注意的是摘要算法并不是加密算法,不能用于加密(因为无法解秘)

hmac

上一节中,我们通过在口令中加入salt来保护一些比较简单的算法。实际上,Hmac算法也是通过一个标准算法, 在计算哈希的过程中,把key混入计算过程中。

和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。

python自带的hmac模块实现了标准的Hmac算法。

我们准备好原始信息,随机key,哈希算法,使用hmac的过程如下:

1
2
3
4
5
6
7
import hmac


message = b'Hello, world!'
key = b'secret'
h = hmac.new(key, message, digestmod = 'MD5')
print(h.hexdigest())

计算结果如下:

1
fa4ee7d173f2d97ee79022d1a7355bcf

可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes类型,str类型需要首先编码为bytes

我们可以用hmac模块代替上一节提到的增加salt的方法:

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
# -*- coding: utf-8 -*-
import hmac, random


# 用户类
class User(object):
def __init__(self, username, password):
self.username = username
self.salt = ''.join([chr(random.randint(48, 122)) for i in range(20)])
self.password = hmac_md5(self.salt, password)

# 模拟用户信息数据库
db = {
'michael': User('michael', '123456'),
'bob': User('bob', 'abc999'),
'alice': User('alice', 'alice2008')
}
# 根据用户输入的登录名和口令模拟用户注册,计算更安全的MD5
def register(username, password):
newUser = User(username, password)
db[username] = User.password
return newUser


# 然后,根据修改后的MD5算法实现用户登录的验证
def hmac_md5(key, s):
return hmac.new(key.encode('utf-8'), s.encode('utf-8'), 'MD5').hexdigest()


# 登陆验证
def login(username, password):
user = db[username]
return user.password == get_md5(user.salt, password)


# 测试:
assert login('michael', '123456')
assert login('bob', 'abc999')
assert login('alice', 'alice2008')
assert not login('michael', '1234567')
assert not login('bob', '123456')
assert not login('alice', 'Alice2008')
print('ok')

itertools

python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。

无限迭代器

count

count会从传入的参数开始不断的输出该数的下一个自然数

1
2
3
4
5
6
7
8
9
>>> import itertools
>>> natuals = itertools.count(1)
>>> for n in natuals:
... print(n)
...
1
2
3
...

cycle

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

repeat负责把一个元素无限重复下去,可以通过设置第二个参数来设置重复的次数

1
2
3
4
5
6
7
>>> ns = itertools.repeat('A', 3)
>>> for n in ns:
... print(n)
...
A
A
A

由于生成器的特性,在使用for循环遍历生成器时,才会生成这些元素。

takewhile

无限序列虽然可以无限迭代下去,但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列:

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

迭代器操作函数

chain

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

1
2
3
>>> for c in itertools.chain('ABC', 'XYZ'):
... print(c)
# 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'

groupby()

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

1
2
3
4
5
6
7
>>> for key, group in itertools.groupby('AAABBBCCAAA'):
... print(key, list(group))
...
A ['A', 'A', 'A']
B ['B', 'B', 'B']
C ['C', 'C']
A ['A', 'A', 'A']

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

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

例子

我们使用刚刚学习的指示来打印一个PI的近似过程:

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
# -*- coding: utf-8 -*-
import itertools


def pi(N):
' 计算pi的值 '
# step 1: 创建一个奇数序列: 1, 3, 5, 7, 9, ...
odd_num = itertools.count(start = 1, step = 2)
# step 2: 取该序列的前N项: 1, 3, 5, 7, 9, ..., 2*N-1.
firstN = itertools.takewhile(lambda x: x <= 2*N-1, odd_num)
# step 3: 添加正负符号并用4除: 4/1, -4/3, 4/5, -4/7, 4/9, ...
firstN_devided4 = [4/val if i%2 == 0 else -4/val for i,val in enumerate(firstN)]
# step 4: 求和:
return sum(firstN_devided4)


# 测试:
print(pi(10))
print(pi(100))
print(pi(1000))
print(pi(10000))
assert 3.04 < pi(10) < 3.05
assert 3.13 < pi(100) < 3.14
assert 3.140 < pi(1000) < 3.141
assert 3.1414 < pi(10000) < 3.1415
print('ok')e

contextlib

在之前的基础章节提到过with是通过上下文管理来实现文件的打开与关闭。

而事实上,实现了上下文管理的对象都能用于with语句。

只要实现了__enter____exit__两个方法,就能称之为上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Query(object):

def __init__(self, name):
self.name = name

def __enter__(self):
# 返回with as var语句中的var
print('Begin')
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')

def query(self):
print('Query info about %s...' % self.name)

这样我们就能使用with将其包裹:

1
2
with Query('Bob') as q:
q.query()

运行结果如下:

1
2
3
Begin
Query info about Bob...
End

此外python还提供了一种构造上下文处理器的装饰器@contextmanager,能让我们更方便的构造上下文处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from contextlib import contextmanager

class Query(object):

def __init__(self, name):
self.name = name

def query(self):
print('Query info about %s...' % self.name)

@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
yeild q
print('End')

@contextmanager这个装饰器接受一个generator,用yield语句把with expression as var中的var输出出去,然后,with语句就可以正常地工作了:

1
2
with create_query('Bob') as q:
q.query()

以下是其他集中应用场景:

  1. 锁资源自动获取和释放的例子
1
2
3
4
5
6
7
8
9
10
11
12
@contextmanager
def locked(lock):
lock.acquire()
try:
yield
finally:
lock.release()

with locked(myLock):
#代码执行到这里时,myLock已经自动上锁
pass
#执行完后会,会自动释放锁
  1. 文件打开后自动管理的实现
1
2
3
4
5
6
7
8
9
10
11
@contextmanager
def myopen(filename, mode="r"):
f = open(filename,mode)
try:
yield f
finally:
f.close()

with myopen("test.txt") as f:
for line in f:
print(line)
  1. 数据库事务的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@contextmanager
def transaction(db):
db.begin()
try
yield
except:
db.rollback()
raise
else:
db.commit()

with transaction(mydb):
mydb.cursor.execute(sql)
mydb.cursor.execute(sql)
mydb.cursor.execute(sql)
mydb.cursor.execute(sql)

有时我们希望在某段代码执行前后,自动执行一些代码,就可以将其变成一个上下文管理器:

1
2
3
4
5
6
7
8
9
@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)

with tag("h1"):
print("hello")
print("world")

运行结果如下:

1
2
3
4
<h1>
hello
world
</h1>

代码的执行顺序是:

  1. with语句首先执行yield之前的语句,因此打印出<h1>
  2. yield调用会执行with语句内部的所有语句,因此打印出helloworld
  3. 最后执行yield之后的语句,打印出</h1>

因此,@contextmanager让我们通过编写generator来简化上下文管理。

nested函数

contextlib模块还提供了一个函数给我们:nested(mgr1,mgr2…mgrn)函数,用来嵌套多个上下文管理器,等同于下面的形式:  

1
2
3
4
5
with mgr1:
with mgr2:
...
with mgrn:
pass

示例如下:

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 contextlib import contextmanager
from contextlib import nested
from contextlib import closing

@contextmanager
def my_context(name):
print("enter")
try:
yield name
finally:
print("exit")

#使用nested函数来调用多个管理器
print("---------使用nested函数调用多个管理器-----------")
with nested(my_context("管理器一"), my_context("管理器二"),my_context("管理器三")) as (m1,m2,m3):
print(m1)
print(m2)
print(m3)

#直接使用with来调用调用多个管理器
print("---------使用with调用多个管理器-----------")
with my_context("管理器一") as m1, my_context("管理器二") as m2, my_context("管理器三") as m3:
print(m1)
print(m2)
print(m3)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---------使用nested函数调用多个管理器-----------
enter
enter
enter
管理器一
管理器二
管理器三
exit
exit
exit
---------使用with调用多个管理器-----------
enter
enter
enter
管理器一
管理器二
管理器三
exit
exit
exit

closing对象

contextlib中还包含一个closing对象,这个对象就是一个上下文管理器,它的__exit__函数仅仅调用传入参数的close函数,closing对象的源码如下:

1
2
3
4
5
6
7
1 class closing(object):
18 def __init__(self, thing):
19 self.thing = thing
20 def __enter__(self):
21 return self.thing
22 def __exit__(self, *exc_info):
23 self.thing.close()

所以closeing上下文管理器仅使用于具有close()方法的资源对象。例如,如果我们通过urllib.urlopen打开一个网页,urlopen返回的request有close方法,所以我们就可以使用closing上下文管理器,如下:

1
2
3
4
5
6
import urllib, sys
from contextlib import closing

with closing(urllib.urlopen('http://www.yahoo.com')) as f:
for line in f:
sys.stdout.write(line)

urllib

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

Get

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

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

1
2
3
4
5
6
7
8
from urllib import request

with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
data = 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'))

可以看到HTTP响应的头和JSON数据:

1
2
3
4
5
6
7
8
9
10
11
Status: 200 OK
Server: nginx
Date: Tue, 26 May 2015 10:02:27 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2049
Connection: close
Expires: Sun, 1 Jan 2006 01:00:00 GMT
Pragma: no-cache
Cache-Control: must-revalidate, no-cache, private
X-DAE-Node: pidl1
Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰编著"],"pubdate":"2007-6",...}

如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页:

1
2
3
4
5
6
7
8
9
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的移动版网页:

1
2
3
4
5
...
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
<meta name="format-detection" content="telephone=no">
<link rel="apple-touch-icon" sizes="57x57" href="http://img4.douban.com/pics/cardkit/launcher/57.png" />
...

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

如果登录成功,我们获得的响应如下:

1
2
3
4
5
6
Status: 200 OK
Server: nginx/1.2.0
...
Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn
...
Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}

如果登录失败,我们获得的响应如下:

1
2
...
Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"example@python.org","errline":536}}

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

XML

DOM vs SAX

操作XML有两种方法:

  1. DOM

  2. SAX。

  • DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。

  • SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。

正常情况下,优先考虑SAX,因为DOM实在太占内存。

在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_elementend_elementchar_data,准备好这3个函数,然后就可以解析xml了。

举个例子,当SAX解析器读到一个节点时:

1
<a href="/">python</a>

会产生3个事件:

  1. start_element事件,在读取<a href="/">时;
  2. char_data事件,在读取python时;
  3. end_element事件,在读取</a>时。

用代码实验一下:

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里面再合并。

如果需要生成XML,99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也是最有效的生成XML的方法是拼接字符串:

1
2
3
4
5
6
L = []
L.append(r'<?xml version="1.0"?>')
L.append(r'<root>')
L.append(encode('some & data'))
L.append(r'</root>')
return ''.join(L)

如果要生成复杂的XML则不建议使用XML,而改成JSON。

HTMLParser

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

假设第一步已经完成了,第二步应该如何解析HTML呢?

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

好在Python提供了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
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解析出来。

评论