1. 程式人生 > >python內部常用模組

python內部常用模組

1.datetime
datetime是Python處理日期和時間的標準庫。


獲取當前日期和時間
我們先看如何獲取當前日期和時間:


>>> from datetime import datetime
>>> now = datetime.now() # 獲取當前datetime
>>> print(now)
2015-05-18 16:28:07.198690
>>> print(type(now))
<class 'datetime.datetime'>
注意到datetime是模組,datetime模組還包含一個datetime類,通過from datetime import datetime匯入的才是datetime這個類。


如果僅匯入import datetime,則必須引用全名datetime.datetime。


datetime.now()返回當前日期和時間,其型別是datetime。


獲取指定日期和時間
要指定某個日期和時間,我們直接用引數構造一個datetime:


>>> from datetime import datetime
>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期時間建立datetime
>>> print(dt)
2015-04-19 12:20:00
datetime轉換為timestamp
在計算機中,時間實際上是用數字表示的。我們把1970年1月1日 00:00:00 UTC+00:00時區的時刻稱為epoch time,記為0(1970年以前的時間timestamp為負數),當前時間就是相對於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()方法:


>>> from datetime import datetime
>>> dt = datetime(2015, 4, 19, 12, 20) # 用指定日期時間建立datetime
>>> dt.timestamp() # 把datetime轉換為timestamp
1429417200.0
注意Python的timestamp是一個浮點數。如果有小數位,小數位表示毫秒數。


某些程式語言(如Java和JavaScript)的timestamp使用整數表示毫秒數,這種情況下只需要把timestamp除以1000就得到Python的浮點表示方法。


timestamp轉換為datetime
要把timestamp轉換為datetime,使用datetime提供的fromtimestamp()方法:


>>> from datetime import datetime
>>> t = 1429417200.0
>>> print(datetime.fromtimestamp(t))
2015-04-19 12:20:00
注意到timestamp是一個浮點數,它沒有時區的概念,而datetime是有時區的。上述轉換是在timestamp和本地時間做轉換。


本地時間是指當前作業系統設定的時區。例如北京時區是東8區,則本地時間:


2015-04-19 12:20:00
實際上就是UTC+8:00時區的時間:


2015-04-19 12:20:00 UTC+8:00
而此刻的格林威治標準時間與北京時間差了8小時,也就是UTC+0:00時區的時間應該是:


2015-04-19 04:20:00 UTC+0:00
timestamp也可以直接被轉換到UTC標準時區的時間:


>>> 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
很多時候,使用者輸入的日期和時間是字串,要處理日期和時間,首先必須把str轉換為datetime。轉換方法是通過datetime.strptime()實現,需要一個日期和時間的格式化字串:


>>> 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'規定了日期和時間部分的格式。詳細的說明請參考Python文件。


注意轉換後的datetime是沒有時區資訊的。


datetime轉換為str
如果已經有了datetime物件,要把它格式化為字串顯示給使用者,就需要轉換為str,轉換方法是通過strftime()實現的,同樣需要一個日期和時間的格式化字串:


>>> from datetime import datetime
>>> now = datetime.now()
>>> print(now.strftime('%a, %b %d %H:%M'))
Mon, May 05 16:28
datetime加減
對日期和時間進行加減實際上就是把datetime往後或往前計算,得到新的datetime。加減可以直接用+和-運算子,不過需要匯入timedelta這個類:


>>> 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)
可見,使用timedelta你可以很容易地算出前幾天和後幾天的時刻。


本地時間轉換為UTC時間
本地時間是指系統設定時區的時間,例如北京時間是UTC+8:00時區的時間,而UTC時間指UTC+0:00時區的時間。


一個datetime型別有一個時區屬性tzinfo,但是預設為None,所以無法區分這個datetime到底是哪個時區,除非強行給datetime設定一個時區:


>>> 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時間,再轉換為任意時區的時間:


# 拿到UTC時間,並強制設定時區為UTC+0:00:
>>> utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
>>> print(utc_dt)
2015-05-18 09:05:12.377316+00:00
# astimezone()將轉換時區為北京時間:
>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
>>> print(bj_dt)
2015-05-18 17:05:12.377316+08:00
# astimezone()將轉換時區為東京時間:
>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt)
2015-05-18 18:05:12.377316+09:00
# astimezone()將bj_dt轉換時區為東京時間:
>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
>>> print(tokyo_dt2)
2015-05-18 18:05:12.377316+09:00
時區轉換的關鍵在於,拿到一個datetime時,要獲知其正確的時區,然後強制設定時區,作為基準時間。


利用帶時區的datetime,通過astimezone()方法,可以轉換到任意時區。


注:不是必須從UTC+0:00時區轉換到其他時區,任何帶時區的datetime都可以正確轉換,例如上述bj_dt到tokyo_dt的轉換。


小結
datetime表示的時間需要時區資訊才能確定一個特定的時間,否則只能視為本地時間。


如果要儲存datetime,最佳方法是將其轉換為timestamp再儲存,因為timestamp的值與時區完全無關。


2.collections
閱讀: 90194
collections是Python內建的一個集合模組,提供了許多有用的集合類。


namedtuple
我們知道tuple可以表示不變集合,例如,一個點的二維座標就可以表示成:


>>> p = (1, 2)
但是,看到(1, 2),很難看出這個tuple是用來表示一個座標的。


定義一個class又小題大做了,這時,namedtuple就派上了用場:


>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)
>>> p.x
1
>>> p.y
2
namedtuple是一個函式,它用來建立一個自定義的tuple物件,並且規定了tuple元素的個數,並可以用屬性而不是索引來引用tuple的某個元素。


這樣一來,我們用namedtuple可以很方便地定義一種資料型別,它具備tuple的不變性,又可以根據屬性來引用,使用十分方便。


可以驗證建立的Point物件是tuple的一種子類:


>>> isinstance(p, Point)
True
>>> isinstance(p, tuple)
True
類似的,如果要用座標和半徑表示一個圓,也可以用namedtuple定義:


# namedtuple('名稱', [屬性list]):
Circle = namedtuple('Circle', ['x', 'y', 'r'])
deque
使用list儲存資料時,按索引訪問元素很快,但是插入和刪除元素就很慢了,因為list是線性儲存,資料量大的時候,插入和刪除效率很低。


deque是為了高效實現插入和刪除操作的雙向列表,適合用於佇列和棧:


>>> 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不存在時,返回一個預設值,就可以用defaultdict:


>>> 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的順序。


如果要保持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會按照插入的順序排列,不是Key本身排序:


>>> od = OrderedDict()
>>> od['z'] = 1
>>> od['y'] = 2
>>> od['x'] = 3
>>> list(od.keys()) # 按照插入的Key的順序返回
['z', 'y', 'x']
OrderedDict可以實現一個FIFO(先進先出)的dict,當容量超出限制時,先刪除最早新增的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是一個簡單的計數器,例如,統計字元出現的個數:


>>> 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實際上也是dict的一個子類,上面的結果可以看出,字元'g'、'm'、'r'各出現了兩次,其他字元各出現了一次。


小結
collections模組提供了一些有用的集合類,可以根據需要選用。


3.base64
閱讀: 66156
Base64是一種用64個字元來表示任意二進位制資料的方法。


用記事本開啟exe、jpg、pdf這些檔案時,我們都會看到一大堆亂碼,因為二進位制檔案包含很多無法顯示和列印的字元,所以,如果要讓記事本這樣的文字處理軟體能處理二進位制資料,就需要一個二進位制到字串的轉換方法。Base64是一種最常見的二進位制編碼方法。


Base64的原理很簡單,首先,準備一個包含64個字元的陣列:


['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
然後,對二進位制資料進行處理,每3個位元組一組,一共是3x8=24bit,劃為4組,每組正好6個bit:


base64-encode


這樣我們得到4個數字作為索引,然後查表,獲得相應的4個字元,就是編碼後的字串。


所以,Base64編碼會把3位元組的二進位制資料編碼為4位元組的文字資料,長度增加33%,好處是編碼後的文字資料可以在郵件正文、網頁等直接顯示。


如果要編碼的二進位制資料不是3的倍數,最後會剩下1個或2個位元組怎麼辦?Base64用\x00位元組在末尾補足後,再在編碼的末尾加上1個或2個=號,表示補了多少位元組,解碼的時候,會自動去掉。


Python內建的base64可以直接進行base64的編解碼:


>>> import base64
>>> base64.b64encode(b'binary\x00string')
b'YmluYXJ5AHN0cmluZw=='
>>> base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
b'binary\x00string'
由於標準的Base64編碼後可能出現字元+和/,在URL中就不能直接作為引數,所以又有一種"url safe"的base64編碼,其實就是把字元+和/分別變成-和_:


>>> 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編碼後會把=去掉:


# 標準Base64:
'abcd' -> 'YWJjZA=='
# 自動去掉=:
'abcd' -> 'YWJjZA'
去掉=後怎麼解碼呢?因為Base64是把3個位元組變為4個位元組,所以,Base64編碼的長度永遠是4的倍數,因此,需要加上=把Base64字串的長度變為4的倍數,就可以正常解碼了。


小結
Base64是一種任意二進位制到文字字串的編碼方法,常用於在URL、Cookie、網頁中傳輸少量二進位制資料。




4.struct
閱讀: 58179
準確地講,Python沒有專門處理位元組的資料型別。但由於b'str'可以表示位元組,所以,位元組陣列=二進位制str。而在C語言中,我們可以很方便地用struct、union來處理位元組,以及位元組和int,float的轉換。


在Python中,比方說要把一個32位無符號整數變成位元組,也就是4個長度的bytes,你得配合位運算子這麼寫:


>>> 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\
[email protected]
'
非常麻煩。如果換成浮點數就無能為力了。


好在Python提供了一個struct模組來解決bytes和其他二進位制資料型別的轉換。


struct的pack函式把任意資料型別變成bytes:


>>> import struct
>>> struct.pack('>I', 10240099)
b'\x00\[email protected]'
pack的第一個引數是處理指令,'>I'的意思是:


>表示位元組順序是big-endian,也就是網路序,I表示4位元組無符號整數。


後面的引數個數要和處理指令一致。


unpack把bytes變成相應的資料型別:


>>> struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')
(4042322160, 32896)
根據>IH的說明,後面的bytes依次變為I:4位元組無符號整數和H:2位元組無符號整數。


所以,儘管Python不適合編寫底層操作位元組流的程式碼,但在對效能要求不高的地方,利用struct就方便多了。


struct模組定義的資料型別可以參考Python官方文件:


https://docs.python.org/3/library/struct.html#format-characters


Windows的點陣圖檔案(.bmp)是一種非常簡單的檔案格式,我們來用struct分析一下。


首先找一個bmp檔案,沒有的話用“畫圖”畫一個。


讀入前30個位元組來分析:


>>> 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讀取:


>>> struct.unpack('<ccIIIIIIHH', s)
(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)
結果顯示,b'B'、b'M'說明是Windows點陣圖,點陣圖大小為640x360,顏色數為24。


5.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'計算出的摘要不同於原始文章的摘要。


可見,摘要演算法就是通過摘要函式f()對任意長度的資料data計算出固定長度的摘要digest,目的是為了發現原始資料是否被人篡改過。


摘要演算法之所以能指出資料是否被篡改過,就是因為摘要函式是一個單向函式,計算f(data)很容易,但通過digest反推data卻非常困難。而且,對原始資料做一個bit的修改,都會導致計算出的摘要完全不同。


我們以常見的摘要演算法MD5為例,計算出一個字串的MD5值:


import hashlib


md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
計算結果如下:


d26a53750bc40b38b65a520292f69306
如果資料量很大,可以分塊多次呼叫update(),最後計算的結果是一樣的:


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完全類似:


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進位制字串表示。


比SHA1更安全的演算法是SHA256和SHA512,不過越安全的演算法不僅越慢,而且摘要長度更長。


有沒有可能兩個不同的資料通過某個摘要演算法得到了相同的摘要?完全有可能,因為任何摘要演算法都是把無限多的資料集合對映到一個有限的集合中。這種情況稱為碰撞,比如Bob試圖根據你的摘要反推出一篇文章'how to learn hashlib in python - by Bob',並且這篇文章的摘要恰好和你的文章完全一致,這種情況也並非不可能出現,但是非常非常困難。


摘要演算法應用
摘要演算法能應用到什麼地方?舉個常用例子:


任何允許使用者登入的網站都會儲存使用者登入的使用者名稱和口令。如何儲存使用者名稱和口令呢?方法是存到資料庫表中:


namepassword
michael123456
bobabc999
alicealice2008
如果以明文儲存使用者口令,如果資料庫洩露,所有使用者的口令就落入黑客的手裡。此外,網站運維人員是可以訪問資料庫的,也就是能獲取到所有使用者的口令。


正確的儲存口令的方式是不儲存使用者的明文口令,而是儲存使用者口令的摘要,比如MD5:


usernamepassword
michaele10adc3949ba59abbe56e057f20f883e
bob878ef96e86145580c38c87f0410ad153
alice99b1c2188db85afee403b1536010c2c9
當用戶登入時,首先計算使用者輸入的明文口令的MD5,然後和資料庫儲存的MD5對比,如果一致,說明口令輸入正確,如果不一致,口令肯定錯誤


6.hmac
通過雜湊演算法,我們可以驗證一段資料是否有效,方法就是對比該資料的雜湊值,例如,判斷使用者口令是否正確,我們用儲存在資料庫中的password_md5對比計算md5(password)的結果,如果一致,使用者輸入的口令就是正確的。


為了防止黑客通過彩虹表根據雜湊值反推原始口令,在計算雜湊的時候,不能僅針對原始輸入計算,需要增加一個salt來使得相同的輸入也能得到不同的雜湊,這樣,大大增加了黑客破解的難度。


如果salt是我們自己隨機生成的,通常我們計算MD5時採用md5(message + salt)。但實際上,把salt看做一個“口令”,加salt的雜湊就是:計算一段message的雜湊時,根據不通口令計算出不同的雜湊。要驗證雜湊值,必須同時提供正確的口令。


這實際上就是Hmac演算法:Keyed-Hashing for Message Authentication。它通過一個標準演算法,在計算雜湊的過程中,把key混入計算過程中。


和我們自定義的加salt演算法不同,Hmac演算法針對所有雜湊演算法都通用,無論是MD5還是SHA-1。採用Hmac替代我們自己的salt演算法,可以使程式演算法更標準化,也更安全。


Python自帶的hmac模組實現了標準的Hmac演算法。我們來看看如何使用hmac實現帶key的雜湊。


我們首先需要準備待計算的原始訊息message,隨機key,雜湊演算法,這裡採用MD5,使用hmac的程式碼如下:


>>> import hmac
>>> message = b'Hello, world!'
>>> key = b'secret'
>>> h = hmac.new(key, message, digestmod='MD5')
>>> # 如果訊息很長,可以多次呼叫h.update(msg)
>>> h.hexdigest()
'fa4ee7d173f2d97ee79022d1a7355bcf'
可見使用hmac和普通hash演算法非常類似。hmac輸出的長度和原始雜湊演算法的長度一致。需要注意傳入的key和message都是bytes型別,str型別需要首先編碼為bytes。


7.itertools
Python的內建模組itertools提供了非常有用的用於操作迭代物件的函式。


首先,我們看看itertools提供的幾個“無限”迭代器:


>>> import itertools
>>> natuals = itertools.count(1)
>>> for n in natuals:
...     print(n)
...
1
2
3
...
因為count()會建立一個無限的迭代器,所以上述程式碼會打印出自然數序列,根本停不下來,只能按Ctrl+C退出。


cycle()會把傳入的一個序列無限重複下去:


>>> import itertools
>>> cs = itertools.cycle('ABC') # 注意字串也是序列的一種
>>> for c in cs:
...     print(c)
...
'A'
'B'
'C'
'A'
'B'
'C'
...
同樣停不下來。


repeat()負責把一個元素無限重複下去,不過如果提供第二個引數就可以限定重複次數:


>>> ns = itertools.repeat('A', 3)
>>> for n in ns:
...     print(n)
...
A
A
A
無限序列只有在for迭代時才會無限地迭代下去,如果只是建立了一個迭代物件,它不會事先把無限個元素生成出來,事實上也不可能在記憶體中建立無限多個元素。


無限序列雖然可以無限迭代下去,但是通常我們會通過takewhile()等函式根據條件判斷來截取出一個有限的序列:


>>> 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提供的幾個迭代器操作函式更加有用:


chain()
chain()可以把一組迭代物件串聯起來,形成一個更大的迭代器:


>>> for c in itertools.chain('ABC', 'XYZ'):
...     print(c)
# 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'
groupby()
groupby()把迭代器中相鄰的重複元素挑出來放在一起:


>>> 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:


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




8.contextlib
在Python中,讀寫檔案這樣的資源要特別注意,必須在使用完畢後正確關閉它們。正確關閉檔案資源的一個方法是使用try...finally:


try:
    f = open('/path/to/file', 'r')
    f.read()
finally:
    if f:
        f.close()
寫try...finally非常繁瑣。Python的with語句允許我們非常方便地使用資源,而不必擔心資源沒有關閉,所以上面的程式碼可以簡化為:


with open('/path/to/file', 'r') as f:
    f.read()
並不是只有open()函式返回的fp物件才能使用with語句。實際上,任何物件,只要正確實現了上下文管理,就可以用於with語句。


實現上下文管理是通過__enter__和__exit__這兩個方法實現的。例如,下面的class實現了這兩個方法:


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語句:


with Query('Bob') as q:
    q.query()
@contextmanager
編寫__enter__和__exit__仍然很繁瑣,因此Python的標準庫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這個decorator接受一個generator,用yield語句把with ... as var把變數輸出出去,然後,with語句就可以正常地工作了:


with create_query('Bob') as q:
    q.query()
很多時候,我們希望在某段程式碼執行前後自動執行特定程式碼,也可以用@contextmanager實現。例如:


@contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)


with tag("h1"):
    print("hello")
    print("world")
上述程式碼執行結果為:


<h1>
hello
world
</h1>
程式碼的執行順序是:


with語句首先執行yield之前的語句,因此打印出<h1>;
yield呼叫會執行with語句內部的所有語句,因此打印出hello和world;
最後執行yield之後的語句,打印出</h1>。
因此,@contextmanager讓我們通過編寫generator來簡化上下文管理。


@closing
如果一個物件沒有實現上下文,我們就不能把它用於with語句。這個時候,可以用closing()來把該物件變為上下文物件。例如,用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也是一個經過@contextmanager裝飾的generator,這個generator編寫起來其實非常簡單:


@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()
它的作用就是把任意物件變為上下文物件,並支援with語句。


@contextlib還有一些其他decorator,便於我們編寫更簡潔的程式碼。


9.urllib
urllib提供了一系列用於操作URL的功能。


Get
urllib的request模組可以非常方便地抓取URL內容,也就是傳送一個GET請求到指定的頁面,然後返回HTTP的響應:


例如,對豆瓣的一個URLhttps://api.douban.com/v2/book/2129650進行抓取,並返回響應:


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資料:


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去請求豆瓣首頁:


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的移動版網頁:


...
    <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的編碼傳入:


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'))
如果登入成功,我們獲得的響應如下:


Status: 200 OK
Server: nginx/1.2.0
...
Set-Cookie: SSOLoginState=1432620126; path=/; domain=weibo.cn
...
Data: {"retcode":20000000,"msg":"","data":{...,"uid":"1658384301"}}
如果登入失敗,我們獲得的響應如下:


...
Data: {"retcode":50011015,"msg":"\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef","data":{"username":"
[email protected]
","errline":536}}
Handler
如果還需要更復雜的控制,比如通過一個Proxy去訪問網站,我們需要利用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
小結
urllib提供的功能就是利用程式去執行各種HTTP請求。如果要模擬瀏覽器完成特定功能,需要把請求偽裝成瀏覽器。偽裝的方法是先監控瀏覽器發出的請求,再根據瀏覽器的請求頭來偽裝,User-Agent頭就是用來標識瀏覽器的。