datetime
datetime是Python處理日期和時(shí)間的標(biāo)準(zhǔn)庫
# 獲取當(dāng)前日期和時(shí)間
>>> from datetime import datetime
>>> now = datetime.now() # 獲取當(dāng)前datetime
>>> print(now)
2015-05-18 16:28:07.198690
>>> print(type(now))
<class 'datetime.datetime'>
# datetime是模塊,datetime模塊還包含一個(gè)datetime類,通過from datetime import datetime導(dǎo)入的才是datetime這個(gè)類
# 如果僅導(dǎo)入import datetime,則必須引用全名datetime.datetime
# datetime.now()返回當(dāng)前日期和時(shí)間,其類型是datetime
# 獲取指定日期和時(shí)間
>>> from datetime import datetime
>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期時(shí)間創(chuàng)建datetime
>>> print(dt)
2015-04-19 12:20:00
# datetime轉(zhuǎn)換為timestamp
# 在計(jì)算機(jī)中,時(shí)間實(shí)際上是用數(shù)字表示的。我們把1970年1月1日 00:00:00 UTC+00:00時(shí)區(qū)的時(shí)刻稱為epoch time,記為0(1970年以前的時(shí)間timestamp為負(fù)數(shù)),當(dāng)前時(shí)間就是相對(duì)于epoch time的秒數(shù),稱為timestamp。
# 把一個(gè)datetime類型轉(zhuǎn)換為timestamp只需要簡單調(diào)用timestamp()方法:
>>> from datetime import datetime
>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期時(shí)間創(chuàng)建datetime
>>> dt.timestamp() # 把datetime轉(zhuǎn)換為timestamp
1429417200.0
# Python的timestamp是一個(gè)浮點(diǎn)數(shù)。如果有小數(shù)位,小數(shù)位表示毫秒數(shù)
# 某些編程語言(如Java和JavaScript)的timestamp使用整數(shù)表示毫秒數(shù),這種情況下只需要把timestamp除以1000就得到Python的浮點(diǎn)表示方法
# timestamp轉(zhuǎn)換為datetime
>>> from datetime import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00
# timestamp是一個(gè)浮點(diǎn)數(shù),它沒有時(shí)區(qū)的概念,而datetime是有時(shí)區(qū)的。上述轉(zhuǎn)換是在timestamp和本地時(shí)間做轉(zhuǎn)換。
# 本地時(shí)間是指當(dāng)前操作系統(tǒng)設(shè)定的時(shí)區(qū)。例如北京時(shí)區(qū)是東8區(qū),則本地時(shí)間:
2015-04-19 12:20:00
# 實(shí)際上就是UTC+8:00時(shí)區(qū)的時(shí)間:
2015-04-19 12:20:00 UTC+8:00
# 而此刻的格林威治標(biāo)準(zhǔn)時(shí)間與北京時(shí)間差了8小時(shí),也就是UTC+0:00時(shí)區(qū)的時(shí)間應(yīng)該是:
2015-04-19 04:20:00 UTC+0:00
# timestamp也可以直接被轉(zhuǎn)換到UTC標(biāo)準(zhǔn)時(shí)區(qū)的時(shí)間:
>>> from datetime import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t)) # 本地時(shí)間
2015-04-19 12:20:00
>>> print(datetime.utcfromtimestamp(t)) # UTC時(shí)間
2015-04-19 04:20:00
# str轉(zhuǎn)換為datetime
>>> 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'規(guī)定了日期和時(shí)間部分的格式
# datetime轉(zhuǎn)換為str
>>> from datetime import datetime
>>> now = datetime.now()
>>> print(now.strftime('%a, %b %d %H:%M'))
Mon, May 05 16:28
# datetime加減
# 對(duì)日期和時(shí)間進(jìn)行加減實(shí)際上就是把datetime往后或往前計(jì)算,得到新的datetime。加減可以直接用+和-運(yùn)算符,不過需要導(dǎo)入timedelta這個(gè)類
>>> from datetime import datetime, timedelta
>>> now = datetime.now()
>>> now
datetime.datetime(2015, 5, 18, 16, 57, 3, 540997)
>>> now + timedelta(hours=10)
datetime.datetime(2015, 5, 19, 2, 57, 3, 540997)
>>> now - timedelta(days=1)
datetime.datetime(2015, 5, 17, 16, 57, 3, 540997)
>>> now + timedelta(days=2, hours=12)
datetime.datetime(2015, 5, 21, 4, 57, 3, 540997)
# 本地時(shí)間轉(zhuǎn)換為UTC時(shí)間
# 本地時(shí)間是指系統(tǒng)設(shè)定時(shí)區(qū)的時(shí)間,例如北京時(shí)間是UTC+8:00時(shí)區(qū)的時(shí)間,而UTC時(shí)間指UTC+0:00時(shí)區(qū)的時(shí)間
# 一個(gè)datetime類型有一個(gè)時(shí)區(qū)屬性tzinfo,但是默認(rèn)為None,所以無法區(qū)分這個(gè)datetime到底是哪個(gè)時(shí)區(qū),除非強(qiáng)行給datetime設(shè)置一個(gè)時(shí)區(qū)
>>> from datetime import datetime, timedelta, timezone
>>> tz_utc_8 = timezone(timedelta(hours=8)) # 創(chuàng)建時(shí)區(qū)UTC+8:00
>>> now = datetime.now()
>>> now
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012)
>>> dt = now.replace(tzinfo=tz_utc_8) # 強(qiáng)制設(shè)置為UTC+8:00
>>> dt
datetime.datetime(2015, 5, 18, 17, 2, 10, 871012, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))
# 如果系統(tǒng)時(shí)區(qū)恰好是UTC+8:00,那么上述代碼就是正確的,否則,不能強(qiáng)制設(shè)置為UTC+8:00時(shí)區(qū)
# 時(shí)區(qū)轉(zhuǎn)換
# 我們可以先通過utcnow()拿到當(dāng)前的UTC時(shí)間,再轉(zhuǎn)換為任意時(shí)區(qū)的時(shí)間:
# 拿到UTC時(shí)間,并強(qiáng)制設(shè)置時(shí)區(qū)為UTC+0:00:
>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
>>> print(utc_dt)
2015-05-18 09:05:12.377316+00:00
# astimezone()將轉(zhuǎn)換時(shí)區(qū)為北京時(shí)間:
>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2015-05-18 17:05:12.377316+08:00
# astimezone()將轉(zhuǎn)換時(shí)區(qū)為東京時(shí)間:
>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt)
2015-05-18 18:05:12.377316+09:00
# astimezone()將bj_dt轉(zhuǎn)換時(shí)區(qū)為東京時(shí)間:
>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt2)
2015-05-18 18:05:12.377316+09:00
# 時(shí)區(qū)轉(zhuǎn)換的關(guān)鍵在于,拿到一個(gè)datetime時(shí),要獲知其正確的時(shí)區(qū),然后強(qiáng)制設(shè)置時(shí)區(qū),作為基準(zhǔn)時(shí)間。
# 利用帶時(shí)區(qū)的datetime,通過astimezone()方法,可以轉(zhuǎn)換到任意時(shí)區(qū)。
datetime表示的時(shí)間需要時(shí)區(qū)信息才能確定一個(gè)特定的時(shí)間,否則只能視為本地時(shí)間
如果要存儲(chǔ)datetime,最佳方法是將其轉(zhuǎn)換為timestamp再存儲(chǔ),因?yàn)閠imestamp的值與時(shí)區(qū)完全無關(guān)
collections
提供了許多有用的集合類
# namedtuple
# namedtuple是一個(gè)函數(shù),它用來創(chuàng)建一個(gè)自定義的tuple對(duì)象,并且規(guī)定了tuple元素的個(gè)數(shù),并可以用屬性而不是索引來引用tuple的某個(gè)元素
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)
>>> p.x
1
>>> p.y
2
# 用namedtuple可以很方便地定義一種數(shù)據(jù)類型,它具備tuple的不變性,又可以根據(jù)屬性來引用,使用十分方便
# 類似的,如果要用坐標(biāo)和半徑表示一個(gè)圓,也可以用namedtuple定義:
# namedtuple('名稱', [屬性list]):
Circle = namedtuple('Circle', ['x', 'y', 'r'])
# deque
# deque是為了高效實(shí)現(xiàn)插入和刪除操作的雙向列表,適合用于隊(duì)列和棧
>>> from collections import deque
>>> q = deque(['a', 'b', 'c'])
>>> q.append('x')
>>> q.appendleft('y')
>>> q
deque(['y', 'a', 'b', 'c', 'x'])
# deque除了實(shí)現(xiàn)list的append()和pop()外,還支持appendleft()和popleft(),這樣就可以非常高效地往頭部添加或刪除元素
# defaultdict
# 使用dict時(shí),如果引用的Key不存在,就會(huì)拋出KeyError。如果希望key不存在時(shí),返回一個(gè)默認(rèn)值,就可以用defaultdict
>>> from collections import defaultdict
>>> dd = defaultdict(lambda: 'N/A')
>>> dd['key1'] = 'abc'
>>> dd['key1'] # key1存在
'abc'
>>> dd['key2'] # key2不存在,返回默認(rèn)值
'N/A'
# 除了在Key不存在時(shí)返回默認(rèn)值,defaultdict的其他行為跟dict是完全一樣的。
# OrderedDict
# 如果要保持Key的順序,可以用OrderedDict
>>> 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會(huì)按照插入的順序排列,不是Key本身排序
>>> od = OrderedDict()
>>> od['z'] = 1
>>> od['y'] = 2
>>> od['x'] = 3
>>> list(od.keys()) # 按照插入的Key的順序返回
['z', 'y', 'x']
# OrderedDict可以實(shí)現(xiàn)一個(gè)FIFO(先進(jìn)先出)的dict,當(dāng)容量超出限制時(shí),先刪除最早添加的Key
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)
# Counter
# Counter是一個(gè)簡單的計(jì)數(shù)器,例如,統(tǒng)計(jì)字符出現(xiàn)的個(gè)數(shù)
>>> 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})
# Counter實(shí)際上也是dict的一個(gè)子類,上面的結(jié)果可以看出,字符'g'、'm'、'r'各出現(xiàn)了兩次,其他字符各出現(xiàn)了一次
base64
Base64是一種用64個(gè)字符來表示任意二進(jìn)制數(shù)據(jù)的方法。
# 首先,準(zhǔn)備一個(gè)包含64個(gè)字符的數(shù)組:
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
# 然后,對(duì)二進(jìn)制數(shù)據(jù)進(jìn)行處理,每3個(gè)字節(jié)一組,一共是3x8=24bit,劃為4組,每組正好6個(gè)bit
# 這樣我們得到4個(gè)數(shù)字作為索引,然后查表,獲得相應(yīng)的4個(gè)字符,就是編碼后的字符串。
# 所以,Base64編碼會(huì)把3字節(jié)的二進(jìn)制數(shù)據(jù)編碼為4字節(jié)的文本數(shù)據(jù),長度增加33%,好處是編碼后的文本數(shù)據(jù)可以在郵件正文、網(wǎng)頁等直接顯示。
# 如果要編碼的二進(jìn)制數(shù)據(jù)不是3的倍數(shù),最后會(huì)剩下1個(gè)或2個(gè)字節(jié)怎么辦?Base64用\x00字節(jié)在末尾補(bǔ)足后,再在編碼的末尾加上1個(gè)或2個(gè)=號(hào),表示補(bǔ)了多少字節(jié),解碼的時(shí)候,會(huì)自動(dòng)去掉。
# Python內(nèi)置的base64可以直接進(jìn)行base64的編解碼
>>> import base64
>>> base64.b64encode(b'binary\x00string')
b'YmluYXJ5AHN0cmluZw=='
>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
b'binary\x00string'
# 由于標(biāo)準(zhǔn)的Base64編碼后可能出現(xiàn)字符+和/,在URL中就不能直接作為參數(shù),所以又有一種"url safe"的base64編碼,其實(shí)就是把字符+和/分別變成-和_:
>>> 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個(gè)字符的排列順序,這樣就可以自定義Base64編碼
# Base64是一種通過查表的編碼方法,不能用于加密
# Base64適用于小段內(nèi)容的編碼,比如數(shù)字證書簽名、Cookie的內(nèi)容等
# 由于=字符也可能出現(xiàn)在Base64編碼中,但=用在URL、Cookie里面會(huì)造成歧義,所以,很多Base64編碼后會(huì)把=去掉
# 標(biāo)準(zhǔn)Base64:
'abcd' -> 'YWJjZA=='
# 自動(dòng)去掉=:
'abcd' -> 'YWJjZA'
# 去掉=后怎么解碼呢?因?yàn)锽ase64是把3個(gè)字節(jié)變?yōu)?個(gè)字節(jié),所以,Base64編碼的長度永遠(yuǎn)是4的倍數(shù),因此,需要加上=把Base64字符串的長度變?yōu)?的倍數(shù),就可以正常解碼了
Base64是一種任意二進(jìn)制到文本字符串的編碼方法,常用于在URL、Cookie、網(wǎng)頁中傳輸少量二進(jìn)制數(shù)據(jù)
struct
struct模塊來解決bytes和其他二進(jìn)制數(shù)據(jù)類型的轉(zhuǎn)換
# struct的pack函數(shù)把任意數(shù)據(jù)類型變成bytes:
>>> import struct
>>> struct.pack('>I', 10240099)
b'\x00\x9c@c'
# pack的第一個(gè)參數(shù)是處理指令,'>I'的意思是:
# >表示字節(jié)順序是big-endian,也就是網(wǎng)絡(luò)序,I表示4字節(jié)無符號(hào)整數(shù)。
# 后面的參數(shù)個(gè)數(shù)要和處理指令一致。
# unpack把bytes變成相應(yīng)的數(shù)據(jù)類型:
>>> struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')
(4042322160, 32896)
# 根據(jù)>IH的說明,后面的bytes依次變?yōu)镮:4字節(jié)無符號(hào)整數(shù)和H:2字節(jié)無符號(hào)整數(shù)。
# Windows的位圖文件(.bmp)是一種非常簡單的文件格式,我們來用struct分析一下。
# 首先找一個(gè)bmp文件,讀入前30個(gè)字節(jié)來分析:
>>> 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格式采用小端方式存儲(chǔ)數(shù)據(jù),文件頭的結(jié)構(gòu)按順序如下:
# 兩個(gè)字節(jié):'BM'表示W(wǎng)indows位圖,'BA'表示OS/2位圖;
一個(gè)4字節(jié)整數(shù):表示位圖大?。?一個(gè)4字節(jié)整數(shù):保留位,始終為0;
一個(gè)4字節(jié)整數(shù):實(shí)際圖像的偏移量;
一個(gè)4字節(jié)整數(shù):Header的字節(jié)數(shù);
一個(gè)4字節(jié)整數(shù):圖像寬度;
一個(gè)4字節(jié)整數(shù):圖像高度;
一個(gè)2字節(jié)整數(shù):始終為1;
一個(gè)2字節(jié)整數(shù):顏色數(shù)。
# 所以,組合起來用unpack讀?。?>>> struct.unpack('<ccIIIIIIHH', s)
(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)
# 結(jié)果顯示,b'B'、b'M'說明是Windows位圖,位圖大小為640x360,顏色數(shù)為24。
hashlib
Python的hashlib提供了常見的摘要算法,如MD5,SHA1等等。
摘要算法又稱哈希算法、散列算法。它通過一個(gè)函數(shù),把任意長度的數(shù)據(jù)轉(zhuǎn)換為一個(gè)長度固定的數(shù)據(jù)串(通常用16進(jìn)制的字符串表示)。
摘要算法就是通過摘要函數(shù)f()對(duì)任意長度的數(shù)據(jù)data計(jì)算出固定長度的摘要digest,目的是為了發(fā)現(xiàn)原始數(shù)據(jù)是否被人篡改過。
# 以常見的摘要算法MD5為例,計(jì)算出一個(gè)字符串的MD5值:
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
# 計(jì)算結(jié)果如下:
d26a53750bc40b38b65a520292f69306
# 如果數(shù)據(jù)量很大,可以分塊多次調(diào)用update(),最后計(jì)算的結(jié)果是一樣的:
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是最常見的摘要算法,速度很快,生成結(jié)果是固定的128 bit字節(jié),通常用一個(gè)32位的16進(jìn)制字符串表示。
# 另一種常見的摘要算法是SHA1,調(diào)用SHA1和調(diào)用MD5完全類似:
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的結(jié)果是160 bit字節(jié),通常用一個(gè)40位的16進(jìn)制字符串表示
# 比SHA1更安全的算法是SHA256和SHA512,不過越安全的算法不僅越慢,而且摘要長度更長
# 摘要算法應(yīng)用
# 如果以明文保存用戶口令,如果數(shù)據(jù)庫泄露,所有用戶的口令就落入黑客的手里。此外,網(wǎng)站運(yùn)維人員是可以訪問數(shù)據(jù)庫的,也就是能獲取到所有用戶的口令。
# 正確的保存口令的方式是不存儲(chǔ)用戶的明文口令,而是存儲(chǔ)用戶口令的摘要,比如MD5:
username | password
---------+---------------------------------
michael | e10adc3949ba59abbe56e057f20f883e
bob | 878ef96e86145580c38c87f0410ad153
alice | 99b1c2188db85afee403b1536010c2c9
# 當(dāng)用戶登錄時(shí),首先計(jì)算用戶輸入的明文口令的MD5,然后和數(shù)據(jù)庫存儲(chǔ)的MD5對(duì)比,如果一致,說明口令輸入正確,如果不一致,口令肯定錯(cuò)誤。
# 由于常用口令的MD5值很容易被計(jì)算出來,所以,要確保存儲(chǔ)的用戶口令不是那些已經(jīng)被計(jì)算出來的常用口令的MD5,這一方法通過對(duì)原始口令加一個(gè)復(fù)雜字符串來實(shí)現(xiàn),俗稱“加鹽”:
def calc_md5(password):
return get_md5(password + 'the-Salt')
# 經(jīng)過Salt處理的MD5口令,只要Salt不被黑客知道,即使用戶輸入簡單口令,也很難通過MD5反推明文口令。
# 但是如果有兩個(gè)用戶都使用了相同的簡單口令比如123456,在數(shù)據(jù)庫中,將存儲(chǔ)兩條相同的MD5值,這說明這兩個(gè)用戶的口令是一樣的。有沒有辦法讓使用相同口令的用戶存儲(chǔ)不同的MD5呢?
# 如果假定用戶無法修改登錄名,就可以通過把登錄名作為Salt的一部分來計(jì)算MD5,從而實(shí)現(xiàn)相同口令的用戶也存儲(chǔ)不同的MD5。
摘要算法在很多地方都有廣泛的應(yīng)用。要注意摘要算法不是加密算法,不能用于加密(因?yàn)闊o法通過摘要反推明文),只能用于防篡改,但是它的單向計(jì)算特性決定了可以在不存儲(chǔ)明文口令的情況下驗(yàn)證用戶口令。
itertools
Python的內(nèi)建模塊itertools提供了非常有用的用于操作迭代對(duì)象的函數(shù)
# itertools提供的幾個(gè)“無限”迭代器:
>>> import itertools
>>> natuals = itertools.count(1)
>>> for n in natuals:
... print(n)
...
1
2
3
...
# 因?yàn)閏ount()會(huì)創(chuàng)建一個(gè)無限的迭代器,所以上述代碼會(huì)打印出自然數(shù)序列,根本停不下來,只能按Ctrl+C退出。
# cycle()會(huì)把傳入的一個(gè)序列無限重復(fù)下去:
>>> import itertools
>>> cs = itertools.cycle('ABC') # 注意字符串也是序列的一種
>>> for c in cs:
... print(c)
...
'A'
'B'
'C'
'A'
'B'
'C'
...
# repeat()負(fù)責(zé)把一個(gè)元素?zé)o限重復(fù)下去,不過如果提供第二個(gè)參數(shù)就可以限定重復(fù)次數(shù):
>>> ns = itertools.repeat('A', 3)
>>> for n in ns:
... print(n)
...
A
A
A
# 無限序列雖然可以無限迭代下去,但是通常我們會(huì)通過takewhile()等函數(shù)根據(jù)條件判斷來截取出一個(gè)有限的序列:
>>> natuals = itertools.count(1)
>>> ns = itertools.takewhile(lambda x: x <= 10, natuals)
>>> list(ns)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# itertools提供的幾個(gè)迭代器操作函數(shù)更加有用:
# chain()
# chain()可以把一組迭代對(duì)象串聯(lián)起來,形成一個(gè)更大的迭代器:
>>> for c in itertools.chain('ABC', 'XYZ'):
... print(c)
# 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'
# groupby()
# groupby()把迭代器中相鄰的重復(fù)元素挑出來放在一起:
>>> 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']
# 實(shí)際上挑選規(guī)則是通過函數(shù)完成的,只要作用于函數(shù)的兩個(gè)元素返回的值相等,這兩個(gè)元素就被認(rèn)為是在一組的,而函數(shù)返回值作為組的key。如果我們要忽略大小寫分組,就可以讓元素'A'和'a'都返回相同的key:
>>> 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']
itertools模塊提供的全部是處理迭代功能的函數(shù),它們的返回值不是list,而是Iterator,只有用for循環(huán)迭代的時(shí)候才真正計(jì)算。
contextlib
# Python的with語句允許我們非常方便地使用資源,而不必?fù)?dān)心資源沒有關(guān)閉:
with open('/path/to/file', 'r') as f:
f.read()
# 并不是只有open()函數(shù)返回的fp對(duì)象才能使用with語句。實(shí)際上,任何對(duì)象,只要正確實(shí)現(xiàn)了上下文管理,就可以用于with語句。
# 實(shí)現(xiàn)上下文管理是通過__enter__和__exit__這兩個(gè)方法實(shí)現(xiàn)的。例如,下面的class實(shí)現(xiàn)了這兩個(gè)方法:
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)
# 這樣我們就可以把自己寫的資源對(duì)象用于with語句:
with Query('Bob') as q:
q.query()
# @contextmanager
# 編寫__enter__和__exit__仍然很繁瑣,因此Python的標(biāo)準(zhǔn)庫contextlib提供了更簡單的寫法,上面的代碼可以改寫如下:
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)
yield q
print('End')
# @contextmanager這個(gè)decorator接受一個(gè)generator,用yield語句把with ... as var把變量輸出出去,然后,with語句就可以正常地工作了:
with create_query('Bob') as q:
q.query()
# 很多時(shí)候,我們希望在某段代碼執(zhí)行前后自動(dòng)執(zhí)行特定代碼,也可以用@contextmanager實(shí)現(xiàn)。例如:
@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)
with tag("h1"):
print("hello")
print("world")
# 上述代碼執(zhí)行結(jié)果為:
<h1>
hello
world
</h1>
# 代碼的執(zhí)行順序是:
# with語句首先執(zhí)行yield之前的語句,因此打印出<h1>;
# yield調(diào)用會(huì)執(zhí)行with語句內(nèi)部的所有語句,因此打印出hello和world;
# 最后執(zhí)行yield之后的語句,打印出</h1>。
# 因此,@contextmanager讓我們通過編寫generator來簡化上下文管理。
# @closing
# 如果一個(gè)對(duì)象沒有實(shí)現(xiàn)上下文,我們就不能把它用于with語句。這個(gè)時(shí)候,可以用closing()來把該對(duì)象變?yōu)樯舷挛膶?duì)象。例如,用with語句使用urlopen():
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen('https://www.python.org')) as page:
for line in page:
print(line)
# closing也是一個(gè)經(jīng)過@contextmanager裝飾的generator,這個(gè)generator編寫起來其實(shí)非常簡單:
@contextmanager
def closing(thing):
try:
yield thing
finally:
thing.close()
# 它的作用就是把任意對(duì)象變?yōu)樯舷挛膶?duì)象,并支持with語句。
# @contextlib還有一些其他decorator,便于我們編寫更簡潔的代碼。
XML
DOM vs SAX
操作XML有兩種方法:DOM和SAX。DOM會(huì)把整個(gè)XML讀入內(nèi)存,解析為樹,因此占用內(nèi)存大,解析慢,優(yōu)點(diǎn)是可以任意遍歷樹的節(jié)點(diǎn)。SAX是流模式,邊讀邊解析,占用內(nèi)存小,解析快,缺點(diǎn)是我們需要自己處理事件。
正常情況下,優(yōu)先考慮SAX,因?yàn)镈OM實(shí)在太占內(nèi)存。
在Python中使用SAX解析XML非常簡潔,通常我們關(guān)心的事件是start_element,end_element和char_data,準(zhǔn)備好這3個(gè)函數(shù),然后就可以解析xml了。
# 舉個(gè)例子,當(dāng)SAX解析器讀到一個(gè)節(jié)點(diǎn)時(shí):
<a href="/">python</a>
# 會(huì)產(chǎn)生3個(gè)事件:
# start_element事件,在讀取<a href="/">時(shí);
# char_data事件,在讀取python時(shí);
# end_element事件,在讀取</a>時(shí)。
# 用代碼實(shí)驗(yàn)一下:
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)
# 需要注意的是讀取一大段字符串時(shí),CharacterDataHandler可能被多次調(diào)用,所以需要自己保存起來,在EndElementHandler里面再合并。
# 除了解析XML外,如何生成XML呢?99%的情況下需要生成的XML結(jié)構(gòu)都是非常簡單的,因此,最簡單也是最有效的生成XML的方法是拼接字符串:
L = []
L.append(r'<?xml version="1.0"?>')
L.append(r'<root>')
L.append(encode('some & data'))
L.append(r'</root>')
return ''.join(L)
# 如果要生成復(fù)雜的XML呢?建議你不要用XML,改成JSON。
HTMLParser
如果我們要編寫一個(gè)搜索引擎,第一步是用爬蟲把目標(biāo)網(wǎng)站的頁面抓下來,第二步就是解析該HTML頁面,看看里面的內(nèi)容到底是新聞、圖片還是視頻。
假設(shè)第一步已經(jīng)完成了,第二步應(yīng)該如何解析HTML呢?
HTML本質(zhì)上是XML的子集,但是HTML的語法沒有XML那么嚴(yán)格,所以不能用標(biāo)準(zhǔn)的DOM或SAX來解析HTML。
好在Python提供了HTMLParser來非常方便地解析HTML,只需簡單幾行代碼:
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 tutorial...<br>END</p>
</body></html>''')
# feed()方法可以多次調(diào)用,也就是不一定一次把整個(gè)HTML字符串都塞進(jìn)去,可以一部分一部分塞進(jìn)去。
# 特殊字符有兩種,一種是英文表示的 ,一種是數(shù)字表示的Ӓ,這兩種字符都可以通過Parser解析出來。
urllib
urllib提供了一系列用于操作URL的功能
# Get
# urllib的request模塊可以非常方便地抓取URL內(nèi)容,也就是發(fā)送一個(gè)GET請(qǐng)求到指定的頁面,然后返回HTTP的響應(yīng):
# 例如,對(duì)豆瓣的一個(gè)URLhttps://api.douban.com/v2/book/2129650進(jìn)行抓取,并返回響應(yīng):
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響應(yīng)的頭和JSON數(shù)據(jù):
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","tags":[{"count":20,"name":"spring","title":"spring"}...}
# 如果我們要想模擬瀏覽器發(fā)送GET請(qǐng)求,就需要使用Request對(duì)象,通過往Request對(duì)象添加HTTP頭,我們就可以把請(qǐng)求偽裝成瀏覽器。例如,模擬iPhone 6去請(qǐng)求豆瓣首頁:
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'))
# 這樣豆瓣會(huì)返回適合iPhone的移動(dòng)版網(wǎng)頁:
...
<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" />
...
# Post
# 如果要以POST發(fā)送一個(gè)請(qǐng)求,只需要把參數(shù)data以bytes形式傳入。
# 我們模擬一個(gè)微博登錄,先讀取登錄的郵箱和口令,然后按照weibo.cn的登錄頁的格式以u(píng)sername=xxx&password=xxx的編碼傳入:
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'))
# 如果登錄成功,我們獲得的響應(yīng)如下:
Status: 200 OK
Server: nginx/1.2.0
...
Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn
...
Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}
# 如果登錄失敗,我們獲得的響應(yīng)如下:
...
Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"example@python.org","errline":536}}
# Handler
# 如果還需要更復(fù)雜的控制,比如通過一個(gè)Proxy去訪問網(wǎng)站,我們需要利用ProxyHandler來處理,示例代碼如下:
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