python学习笔记,常用内建库
datetime
获取当前时间
1 2 3 4 5 6 from datetime import datetimenow = datetime.now() print (now) print (type (now))
datetime
模块中包含一个datetime
类,now()
是datetime
类中的一个方法。datetime.now()
返回的是一个datetime
类。
获取指定日期与时间
直接使用参数构造一个datetime
对象即可:
1 2 3 4 5 from datetime import datetimedt = datetime(2015 , 4 , 19 , 12 , 20 ) print (dt)
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 datetimedt = datetime(2015 , 4 , 19 , 12 , 20 ) dt.timestamp()
timestamp
是一个浮点数,整数位表示秒。
某些编程语言(如Java和JavaScript)的timestamp
使用整数表示毫秒数,这种情况下只需要把timestamp
除以1000就得到Python的浮点表示方法。
timestamp转换为datetime
要把timestamp
转换为datetime
,使用datetime
提供的fromtimestamp()
方法:
1 2 3 4 5 from datetime import datetimet = 1429417200.0 print (datetime.fromtimestamp(t))
timestamp
是没有时区的,而datetime
是有市区的,使用fromtimestamp()
方法转换后的时间是按照系统设置的当地时区进行转化的,同样也支持转化到UTC标准市区,即UTC+0:00:
1 2 3 4 5 6 7 8 from datetime import datetimet = 1429417200.0 print (datetime.fromtimestamp(t)) print (datetime.utcfromtimestamp(t))
str转datetime
从前端传送的时间大多是String
格式的时间,要处理日期和时间,首先必须把str转换为datetime。转换方法是通过datetime.strptime()
实现,需要一个日期和时间的格式化字符串:
1 2 3 4 5 from datetime import datetimecday = datetime.strptime('2015-6-1 18:19:59' , '%Y-%m-%d %H:%M:%S' ) print (cday)
字符串'%Y-%m-%d %H:%M:%S'
规定了日期和时间部分的格式。详细的说明请参考:
注意转换后的datetime是没有时区信息的。
datetime转换为str
如果已经有了datetime对象,要把它格式化为字符串显示给用户,就需要转换为str,转换方法是通过strftime()
实现的,同样需要一个日期和时间的格式化字符串:
1 2 3 4 5 from datetime import datetimenow = datetime.now() print (now.strftime('%a, %b %d %H:%M' ))
datetime加减
对日期和时间进行加减实际上就是把datetime往后或往前计算,得到新的datetime。加减可以直接用+
和-
运算符,不过需要导入timedelta
这个类:
1 2 3 4 5 6 7 8 9 10 >>> from datetime import datetime, timedelta>>> now = datetime.now()>>> nowdatetime.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 )) >>> now = datetime.now()>>> nowdatetime.datetime(2015 , 5 , 18 , 17 , 2 , 10 , 871012 ) >>> dt = now.replace(tzinfo=tz_utc_8) >>> dtdatetime.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_dt = datetime.utcnow().replace(tzinfo=timezone.utc)>>> print (utc_dt)2021 -08-17 02:34 :19.972780 +00 :00 >>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8 )))>>> print (bj_dt)2021 -08-17 10 :34 :19.972780 +08:00 >>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9 )))>>> print (tokyo_dt)2021 -08-17 11 :34 :19.972780 +09:00 >>> 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_dt
到tokyo_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 import refrom datetime import datetime, timezone, timedeltadef 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 , t1t2 = to_timestamp('2015-5-31 16:10:30' , 'UTC-09:00' ) assert t2 == 1433121030.0 , t2print ('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.x1 >>> p.y2
namedtuple
是一个函数,它用来创建一个自定义的tuple
对象,并且规定了tuple
元素的个数,并可以用属性而不是索引来引用tuple
的某个元素。
可以验证namedtuple
是tuple
的子类:
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' )>>> qdeque(['y' , 'a' , 'b' , 'c' , 'x' ])
对尾部进行增删时可使用append()
和pop()
方法,而对头部进行增删时则可使用appendleft()
和popleft()
方法
defaultdict
defaultdict
在dict
的基础上,当发生keyerror
异常时,允许设置自定义返回值:
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
对象时传入。
除了在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 {'a' : 1 , 'c' : 3 , 'b' : 2 } >>> od = OrderedDict([('a' , 1 ), ('b' , 2 ), ('c' , 3 )])>>> od 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()) ['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 OrderedDictclass 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
串起来并组成一个逻辑上的dict
。ChainMap
本身也是一个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 ChainMapimport os, argparsedefaults = { '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 } 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 ... >>> cCounter({'g' : 2 , 'm' : 2 , 'r' : 2 , 'a' : 1 , 'i' : 1 , 'o' : 1 , 'n' : 1 , 'p' : 1 }) >>> c.update('hello' ) >>> cCounter({'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个字符来表示任意二进制数据的方法。
通常用记事本打开exe
、jpg
、pdf
等文件会看到一些乱码,这是因为二进制文件包含很多无法显示和打印的字符,如果需要让文本处理软件能够处理二进制数据,就需要一个二进制转字符串的方法,Base64
就是一种最常见的二进制编码方法
Base64原理
Base64的原理很简单,首先,准备一个包含64个字符的数组:
1 ['A' , 'B' , 'C' , ... 'a' , 'b' , 'c' , ... '0' , '1' , ... '+' , '/' ]
然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24
bit,划为4组,每组正好6个bit:
base64-encode
这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
原本长为3 × 8 = 24 3 \times 8 = 24 3 × 8 = 2 4 bit的二进制序列,重新划分为4组,则每组6bit,则每组需要增加2bit来构成一个新的字节,则增长2 × 4 = 8 2 \times 4 = 8 2 × 4 = 8 bit,则增长8 / 24 ∗ 100 % = 33.3333 % 8/24 *100\% = 33.3333\% 8 / 2 4 ∗ 1 0 0 % = 3 3 . 3 3 3 3 %
如果要编码的二进制数据不是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 '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 import base64def safe_base64_decode (s ): 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])>>> bsb'\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字节无符号整数。
下面我们复习一下内存中的两种存储顺序
字节顺序
big-endian:字节存储方式为大端在末尾,即顺序存储,第一个字节存储在开头位置,如 01234567,存储为 01 23 45 67(即权重高的位在前 )
little-endian:字节存储方式为小端在末尾,即逆序存储,第一个字节存储在末尾位置,如 01234567,存储为 67 45 23 01(即权重低的位在前 )
struct应用
后面的参数个数要和处理指令一致。
unpack
把bytes
变成相应的数据类型:
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 import base64, structbmp_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 hashlibmd5 = 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 hashlibmd5 = 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 hashlibsha1 = 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 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 import hashlib, randomclass 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' ) } def register (username, password ): newUser = User(username, password) db[username] = User.password return newUser 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 hmacmessage = 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 import hmac, randomclass 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' ) } def register (username, password ): newUser = User(username, password) db[username] = User.password return newUser 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' )
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)
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 import itertoolsdef pi (N ): ' 计算pi的值 ' odd_num = itertools.count(start = 1 , step = 2 ) firstN = itertools.takewhile(lambda x: x <= 2 *N-1 , odd_num) firstN_devided4 = [4 /val if i%2 == 0 else -4 /val for i,val in enumerate (firstN)] 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 ): 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 contextmanagerclass 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 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): pass
文件打开后自动管理的实现
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 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" )
运行结果如下:
代码的执行顺序是:
with
语句首先执行yield
之前的语句,因此打印出<h1>
;
yield
调用会执行with
语句内部的所有语句,因此打印出hello
和world
;
最后执行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 contextmanagerfrom contextlib import nestedfrom contextlib import closing@contextmanager def my_context (name ): print ("enter" ) try : yield name finally : print ("exit" ) print ("---------使用nested函数调用多个管理器-----------" )with nested(my_context("管理器一" ), my_context("管理器二" ),my_context("管理器三" )) as (m1,m2,m3): print (m1) print (m2) print (m3) 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 = thing20 def __enter__ (self ):21 return self .thing22 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, sysfrom contextlib import closingwith 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 requestwith 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 requestreq = 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, parseprint ('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有两种方法:
DOM
SAX。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_element
,end_element
和char_data
,准备好这3个函数,然后就可以解析xml了。
举个例子,当SAX解析器读到一个节点时:
会产生3个事件:
start_element事件,在读取<a href="/">
时;
char_data事件,在读取python
时;
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 ParserCreateclass 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 HTMLParserfrom html.entities import name2codepointclass 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 tutorial...<br>END</p> </body></html>''' )
feed()
方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。
特殊字符有两种,一种是英文表示的
,一种是数字表示的Ӓ
,这两种字符都可以通过Parser解析出来。