python4

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

獲取當前日期和時間

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

>>> from datetime import datetime>>> now = datetime.now()# 獲取當前datetime>>>print(now)2015-05-1816:28:07.198690>>>print(type(now))

注意到datetime是模塊,datetime模塊還包含一個datetime類,通過from datetime import datetime導入的才是datetime這個類。

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

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

獲取指定日期和時間

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

>>> fromdatetimeimportdatetime>>> dt = datetime(2015,4,19,12,20)# 用指定日期時間創建datetime>>> print(dt)2015-04-1912: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-100:00:00UTC+0:00

對應的北京時間是:

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

可見timestamp的值與時區毫無關系,因為timestamp一旦確定,其UTC時間就確定了,轉換到任意時區的時間也是完全確定的,這就是為什么計算機存儲的當前時間是以timestamp表示的,因為全球各地的計算機在任意時刻的timestamp都是完全相同的(假定時間已校準)。

把一個datetime類型轉換為timestamp只需要簡單調用timestamp()方法:

>>> fromdatetimeimportdatetime>>> dt = datetime(2015,4,19,12,20)# 用指定日期時間創建datetime>>> dt.timestamp()# 把datetime轉換為timestamp1429417200.0

注意Python的timestamp是一個浮點數。如果有小數位,小數位表示毫秒數。

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

timestamp轉換為datetime

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

>>> fromdatetimeimportdatetime>>> t =1429417200.0>>> print(datetime.fromtimestamp(t))2015-04-1912:20:00

注意到timestamp是一個浮點數,它沒有時區的概念,而datetime是有時區的。上述轉換是在timestamp和本地時間做轉換。

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

2015-04-19 12:20:00

實際上就是UTC+8:00時區的時間:

2015-04-19 12:20:00UTC+8:00

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

2015-04-19 04:20:00UTC+0:00

timestamp也可以直接被轉換到UTC標準時區的時間:

>>> fromdatetimeimportdatetime>>> t =1429417200.0>>> print(datetime.fromtimestamp(t))# 本地時間2015-04-1912:20:00>>> print(datetime.utcfromtimestamp(t))# UTC時間2015-04-1904:20:00

str轉換為datetime

很多時候,用戶輸入的日期和時間是字符串,要處理日期和時間,首先必須把str轉換為datetime。轉換方法是通過datetime.strptime()實現,需要一個日期和時間的格式化字符串:

>>> fromdatetimeimportdatetime>>> cday = datetime.strptime('2015-6-1 18:19:59','%Y-%m-%d %H:%M:%S')>>> print(cday)2015-06-0118:19:59

字符串'%Y-%m-%d %H:%M:%S'規定了日期和時間部分的格式。詳細的說明請參考Python文檔

注意轉換后的datetime是沒有時區信息的。

datetime轉換為str

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

>>> fromdatetimeimportdatetime>>> now = datetime.now()>>> print(now.strftime('%a, %b %d %H:%M'))Mon, May0516:28

datetime加減

對日期和時間進行加減實際上就是把datetime往后或往前計算,得到新的datetime。加減可以直接用+和-運算符,不過需要導入timedelta這個類:

>>> fromdatetimeimportdatetime, timedelta>>> now = datetime.now()>>> nowdatetime.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設置一個時區:

>>> fromdatetimeimportdatetime, timedelta, timezone>>> tz_utc_8 = timezone(timedelta(hours=8))# 創建時區UTC+8:00>>> now = datetime.now()>>> nowdatetime.datetime(2015,5,18,17,2,10,871012)>>> dt = now.replace(tzinfo=tz_utc_8)# 強制設置為UTC+8:00>>> dtdatetime.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-1809:05:12.377316+00:00# astimezone()將轉換時區為北京時間:>>> bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))>>> print(bj_dt)2015-05-1817:05:12.377316+08:00# astimezone()將轉換時區為東京時間:>>> tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))>>> print(tokyo_dt)2015-05-1818:05:12.377316+09:00# astimezone()將bj_dt轉換時區為東京時間:>>> tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))>>> print(tokyo_dt2)2015-05-1818:05:12.377316+09:00

時區轉換的關鍵在于,拿到一個datetime時,要獲知其正確的時區,然后強制設置時區,作為基準時間。

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

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

collections是Python內建的一個集合模塊,提供了許多有用的集合類。

namedtuple

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

>>> p = (1,2)

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

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

>>> fromcollectionsimportnamedtuple>>> Point = namedtuple('Point', ['x','y'])>>> p = Point(1,2)>>> p.x1>>> p.y2

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是為了高效實現插入和刪除操作的雙向列表,適合用于隊列和棧:

>>> fromcollectionsimportdeque>>> q = deque(['a','b','c'])>>> q.append('x')>>> q.appendleft('y')>>> qdeque(['y','a','b','c','x'])

deque除了實現list的append()和pop()外,還支持appendleft()和popleft(),這樣就可以非常高效地往頭部添加或刪除元素。

defaultdict

使用dict時,如果引用的Key不存在,就會拋出KeyError。如果希望key不存在時,返回一個默認值,就可以用defaultdict:

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

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

fromcollectionsimportOrderedDictclassLastUpdatedOrderedDict(OrderedDict):def__init__(self, capacity):super(LastUpdatedOrderedDict, self).__init__()? ? ? ? self._capacity = capacitydef__setitem__(self, key, value):containsKey =1ifkeyinselfelse0iflen(self) - containsKey >= self._capacity:? ? ? ? ? ? last = self.popitem(last=False)? ? ? ? ? ? print('remove:', last)ifcontainsKey:delself[key]? ? ? ? ? ? print('set:', (key, value))else:? ? ? ? ? ? print('add:', (key, value))? ? ? ? OrderedDict.__setitem__(self, key, value)

ChainMap

ChainMap可以把一組dict串起來并組成一個邏輯上的dict。ChainMap本身也是一個dict,但是查找的時候,會按照順序在內部的dict依次查找。

什么時候使用ChainMap最合適?舉個例子:應用程序往往都需要傳入參數,參數可以通過命令行傳入,可以通過環境變量傳入,還可以有默認參數。我們可以用ChainMap實現參數的優先級查找,即先查命令行參數,如果沒有傳入,再查環境變量,如果沒有,就使用默認參數。

下面的代碼演示了如何查找user和color這兩個參數:

fromcollectionsimportChainMapimportos, 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: vfork, vinvars(namespace).items()ifv }# 組合成ChainMap:combined = ChainMap(command_line_args, os.environ, defaults)# 打印參數:print('color=%s'% combined['color'])print('user=%s'% combined['user'])

沒有任何參數時,打印出默認參數:

$ python3 use_chainmap.py color=reduser=guest

當傳入命令行參數時,優先使用命令行參數:

$ python3 use_chainmap.py -u bobcolor=reduser=bob

同時傳入命令行參數和環境變量,命令行參數的優先級較高:

$ user=admin color=green python3 use_chainmap.py -u bobcolor=greenuser=bob

Counter

Counter是一個簡單的計數器,例如,統計字符出現的個數:

>>> fromcollectionsimportCounter>>> c = Counter()>>> forchin'programming':... c[ch] = c[ch] +1...>>> cCounter({'g':2,'m':2,'r':2,'a':1,'i':1,'o':1,'n':1,'p':1})

Counter實際上也是dict的一個子類,上面的結果可以看出,字符'g'、'm'、'r'各出現了兩次,其他字符各出現了一次。

Base64是一種用64個字符來表示任意二進制數據的方法。

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

Base64的原理很簡單,首先,準備一個包含64個字符的數組:

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

然后,對二進制數據進行處理,每3個字節一組,一共是3x8=24bit,劃為4組,每組正好6個bit:

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

所以,Base64編碼會把3字節的二進制數據編碼為4字節的文本數據,長度增加33%,好處是編碼后的文本數據可以在郵件正文、網頁等直接顯示。

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

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

>>> importbase64>>> 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的倍數,就可以正常解碼了。

準確地講,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])>>> bsb'\x00\x9c@c'

非常麻煩。如果換成浮點數就無能為力了。

好在Python提供了一個struct模塊來解決bytes和其他二進制數據類型的轉換。

struct的pack函數把任意數據類型變成bytes:

>>> importstruct>>> struct.pack('>I',10240099)b'\x00\x9c@c'

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

結果顯示,b'B'、b'M'說明是Windows位圖,位圖大小為640x360,顏色數為24。

請編寫一個bmpinfo.py,可以檢查任意文件是否是位圖文件,如果是,打印出圖片大小和顏色數。

摘要算法簡介

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

importhashlibmd5 = hashlib.md5()md5.update('how to use md5 in python hashlib?'.encode('utf-8'))print(md5.hexdigest())

計算結果如下:

d26a53750bc40b38b65a520292f69306

如果數據量很大,可以分塊多次調用update(),最后計算的結果是一樣的:

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

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

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

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

摘要算法應用

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

任何允許用戶登錄的網站都會存儲用戶登錄的用戶名和口令。如何存儲用戶名和口令呢?方法是存到數據庫表中:

namepassword

michael123456

bobabc999

alicealice2008

如果以明文保存用戶口令,如果數據庫泄露,所有用戶的口令就落入黑客的手里。此外,網站運維人員是可以訪問數據庫的,也就是能獲取到所有用戶的口令。

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

usernamepassword

michaele10adc3949ba59abbe56e057f20f883e

bob878ef96e86145580c38c87f0410ad153

alice99b1c2188db85afee403b1536010c2c9

當用戶登錄時,首先計算用戶輸入的明文口令的MD5,然后和數據庫存儲的MD5對比,如果一致,說明口令輸入正確,如果不一致,口令肯定錯誤。通過哈希算法,我們可以驗證一段數據是否有效,方法就是對比該數據的哈希值,例如,判斷用戶口令是否正確,我們用保存在數據庫中的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的代碼如下:

>>> importhmac>>> 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。

Python的內建模塊itertools提供了非常有用的用于操作迭代對象的函數。

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

>>> importitertools>>> natuals = itertools.count(1)>>> forninnatuals:... print(n)...123...

因為count()會創建一個無限的迭代器,所以上述代碼會打印出自然數序列,根本停不下來,只能按Ctrl+C退出。

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

>>> importitertools>>> cs = itertools.cycle('ABC')# 注意字符串也是序列的一種>>> forcincs:... print(c)...'A''B''C''A''B''C'...

同樣停不下來。

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

>>> ns = itertools.repeat('A',3)>>> forninns:... print(n)...AAA

無限序列只有在for迭代時才會無限地迭代下去,如果只是創建了一個迭代對象,它不會事先把無限個元素生成出來,事實上也不可能在內存中創建無限多個元素。

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

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

itertools提供的幾個迭代器操作函數更加有用:

chain()

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

>>> forcinitertools.chain('ABC','XYZ'):... print(c)# 迭代效果:'A' 'B' 'C' 'X' 'Y' 'Z'

groupby()

groupby()把迭代器中相鄰的重復元素挑出來放在一起:

>>> forkey, groupinitertools.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:

>>> forkey, groupinitertools.groupby('AaaBBbcCAAa',lambdac: c.upper()):... print(key, list(group))...A ['A','a','a']B ['B','B','b']C ['c','C']A ['A','A','a']

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

try:? ? f = open('/path/to/file','r')? ? f.read()finally:iff:? ? ? ? f.close()

寫try...finally非常繁瑣。Python的with語句允許我們非常方便地使用資源,而不必擔心資源沒有關閉,所以上面的代碼可以簡化為:

withopen('/path/to/file','r')asf:? ? f.read()

并不是只有open()函數返回的fp對象才能使用with語句。實際上,任何對象,只要正確實現了上下文管理,就可以用于with語句。

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

classQuery(object):def__init__(self, name):self.name = namedef__enter__(self):print('Begin')returnselfdef__exit__(self, exc_type, exc_value, traceback):ifexc_type:? ? ? ? ? ? print('Error')else:? ? ? ? ? ? print('End')defquery(self):print('Query info about %s...'% self.name)

這樣我們就可以把自己寫的資源對象用于with語句:

withQuery('Bob')asq:? ? q.query()

@contextmanager

編寫__enter__和__exit__仍然很繁瑣,因此Python的標準庫contextlib提供了更簡單的寫法,上面的代碼可以改寫如下:

fromcontextlibimportcontextmanagerclassQuery(object):def__init__(self, name):self.name = namedefquery(self):print('Query info about %s...'% self.name)@contextmanagerdefcreate_query(name):print('Begin')? ? q = Query(name)yieldq? ? print('End')

@contextmanager這個decorator接受一個generator,用yield語句把with ... as var把變量輸出出去,然后,with語句就可以正常地工作了:

withcreate_query('Bob')asq:? ? q.query()

很多時候,我們希望在某段代碼執行前后自動執行特定代碼,也可以用@contextmanager實現。例如:

@contextmanagerdeftag(name):print("<%s>"% name)yieldprint("</%s>"% name)withtag("h1"):? ? print("hello")? ? print("world")

上述代碼執行結果為:

helloworld

代碼的執行順序是:

with語句首先執行yield之前的語句,因此打印出<h1>;

yield調用會執行with語句內部的所有語句,因此打印出hello和world;

最后執行yield之后的語句,打印出</h1>。

因此,@contextmanager讓我們通過編寫generator來簡化上下文管理。

@closing

如果一個對象沒有實現上下文,我們就不能把它用于with語句。這個時候,可以用closing()來把該對象變為上下文對象。例如,用with語句使用urlopen():

fromcontextlibimportclosingfromurllib.requestimporturlopenwithclosing(urlopen('https://www.python.org'))aspage:forlineinpage:? ? ? ? print(line)

closing也是一個經過@contextmanager裝飾的generator,這個generator編寫起來其實非常簡單:

@contextmanagerdefclosing(thing):try:yieldthingfinally:? ? ? ? thing.close()

它的作用就是把任意對象變為上下文對象,并支持with語句。

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

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

Get

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

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

fromurllibimportrequestwithrequest.urlopen('https://api.douban.com/v2/book/2129650')asf:? ? data = f.read()? ? print('Status:', f.status, f.reason)fork, vinf.getheaders():? ? ? ? print('%s: %s'% (k, v))? ? print('Data:', data.decode('utf-8'))

可以看到HTTP響應的頭和JSON數據:

Status: 200 OKServer: nginxDate: Tue, 26 May 2015 10:02:27 GMTContent-Type: application/json; charset=utf-8Content-Length:2049Connection:closeExpires: Sun,1Jan200601:00:00GMTPragma:no-cacheCache-Control: must-revalidate,no-cache, privateX-DAE-Node: pidl1Data: {"rating":{"max":10,"numRaters":16,"average":"7.4","min":0},"subtitle":"","author":["廖雪峰編著"],"pubdate":"2007-6",...}

如果我們要想模擬瀏覽器發送GET請求,就需要使用Request對象,通過往Request對象添加HTTP頭,我們就可以把請求偽裝成瀏覽器。例如,模擬iPhone 6去請求豆瓣首頁:

fromurllibimportrequestreq = 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')withrequest.urlopen(req)asf:? ? print('Status:', f.status, f.reason)fork, vinf.getheaders():? ? ? ? print('%s: %s'% (k, v))? ? print('Data:', f.read().decode('utf-8'))

這樣豆瓣會返回適合iPhone的移動版網頁:

......

Post

如果要以POST發送一個請求,只需要把參數data以bytes形式傳入。

我們模擬一個微博登錄,先讀取登錄的郵箱和口令,然后按照weibo.cn的登錄頁的格式以username=xxx&password=xxx的編碼傳入:

fromurllibimportrequest, 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')withrequest.urlopen(req, data=login_data.encode('utf-8'))asf:? ? print('Status:', f.status, f.reason)fork, vinf.getheaders():? ? ? ? print('%s: %s'% (k, v))? ? print('Data:', f.read().decode('utf-8'))

如果登錄成功,我們獲得的響應如下:

Status: 200 OKServer: 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":"example@python.org","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)withopener.open('http://www.example.com/login.html')asf:? ? pass

XML雖然比JSON復雜,在Web中應用也不如以前多了,不過仍有很多地方在用,所以,有必要了解如何操作XML。

DOM vs SAX

操作XML有兩種方法:DOM和SAX。DOM會把整個XML讀入內存,解析為樹,因此占用內存大,解析慢,優點是可以任意遍歷樹的節點。SAX是流模式,邊讀邊解析,占用內存小,解析快,缺點是我們需要自己處理事件。

正常情況下,優先考慮SAX,因為DOM實在太占內存。

在Python中使用SAX解析XML非常簡潔,通常我們關心的事件是start_element,end_element和char_data,準備好這3個函數,然后就可以解析xml了。

舉個例子,當SAX解析器讀到一個節點時:

python

會產生3個事件:

start_element事件,在讀取<a href="/">時;

char_data事件,在讀取python時;

end_element事件,在讀取</a>時。

用代碼實驗一下:

fromxml.parsers.expatimportParserCreateclassDefaultSaxHandler(object):defstart_element(self, name, attrs):print('sax:start_element: %s, attrs: %s'% (name, str(attrs)))defend_element(self, name):print('sax:end_element: %s'% name)defchar_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_elementparser.EndElementHandler = handler.end_elementparser.CharacterDataHandler = handler.char_dataparser.Parse(xml)

需要注意的是讀取一大段字符串時,CharacterDataHandler可能被多次調用,所以需要自己保存起來,在EndElementHandler里面再合并。

除了解析XML外,如何生成XML呢?99%的情況下需要生成的XML結構都是非常簡單的,因此,最簡單也是最有效的生成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)

如果要生成復雜的XML呢?建議你不要用XML,改成JSON。

如果我們要編寫一個搜索引擎,第一步是用爬蟲把目標網站的頁面抓下來,第二步就是解析該HTML頁面,看看里面的內容到底是新聞、圖片還是視頻。

假設第一步已經完成了,第二步應該如何解析HTML呢?

HTML本質上是XML的子集,但是HTML的語法沒有XML那么嚴格,所以不能用標準的DOM或SAX來解析HTML。

好在Python提供了HTMLParser來非常方便地解析HTML,只需簡單幾行代碼:

fromhtml.parserimportHTMLParserfromhtml.entitiesimportname2codepointclassMyHTMLParser(HTMLParser):defhandle_starttag(self, tag, attrs):print('<%s>'% tag)defhandle_endtag(self, tag):print('</%s>'% tag)defhandle_startendtag(self, tag, attrs):print('<%s/>'% tag)defhandle_data(self, data):print(data)defhandle_comment(self, data):print('<!--', data,'-->')defhandle_entityref(self, name):print('&%s;'% name)defhandle_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解析出來。

PIL:Python Imaging Library,已經是Python平臺事實上的圖像處理標準庫了。PIL功能非常強大,但API卻非常簡單易用。

由于PIL僅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基礎上創建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了許多新特性,因此,我們可以直接安裝使用Pillow。

安裝Pillow

如果安裝了Anaconda,Pillow就已經可用了。否則,需要在命令行下通過pip安裝:

$ pip install pillow

如果遇到Permission denied安裝失敗,請加上sudo重試。

操作圖像

來看看最常見的圖像縮放操作,只需三四行代碼:

from PIL import Image# 打開一個jpg圖像文件,注意是當前路徑:im = Image.open('test.jpg')# 獲得圖像尺寸:w, h = im.sizeprint('Original image size: %sx%s'% (w, h))# 縮放到50%:im.thumbnail((w//2, h//2))print('Resize image to: %sx%s'% (w//2, h//2))# 把縮放后的圖像用jpeg格式保存:im.save('thumbnail.jpg','jpeg')

其他功能如切片、旋轉、濾鏡、輸出文字、調色板等一應俱全。

比如,模糊效果也只需幾行代碼:

fromPILimportImage, ImageFilter# 打開一個jpg圖像文件,注意是當前路徑:im = Image.open('test.jpg')# 應用模糊濾鏡:im2 = im.filter(ImageFilter.BLUR)im2.save('blur.jpg','jpeg')

效果如下:

PIL的ImageDraw提供了一系列繪圖方法,讓我們可以直接繪圖。比如要生成字母驗證碼圖片:

fromPILimportImage, ImageDraw, ImageFont, ImageFilterimportrandom# 隨機字母:defrndChar():returnchr(random.randint(65,90))# 隨機顏色1:defrndColor():return(random.randint(64,255), random.randint(64,255), random.randint(64,255))# 隨機顏色2:defrndColor2():return(random.randint(32,127), random.randint(32,127), random.randint(32,127))# 240 x 60:width =60*4height =60image = Image.new('RGB', (width, height), (255,255,255))# 創建Font對象:font = ImageFont.truetype('Arial.ttf',36)# 創建Draw對象:draw = ImageDraw.Draw(image)# 填充每個像素:forxinrange(width):foryinrange(height):? ? ? ? draw.point((x, y), fill=rndColor())# 輸出文字:fortinrange(4):? ? draw.text((60* t +10,10), rndChar(), font=font, fill=rndColor2())# 模糊:image = image.filter(ImageFilter.BLUR)image.save('code.jpg','jpeg')

我們用隨機顏色填充背景,再畫上文字,最后對圖像進行模糊,得到驗證碼圖片如下:

如果運行的時候報錯:

IOError: cannot open resource

這是因為PIL無法定位到字體文件的位置,可以根據操作系統提供絕對路徑,比如:

'/Library/Fonts/Arial.ttf'

要詳細了解PIL的強大功能,請請參考Pillow官方文檔:

https://pillow.readthedocs.org/

我們已經講解了Python內置的urllib模塊,用于訪問網絡資源。但是,它用起來比較麻煩,而且,缺少很多實用的高級功能。

更好的方案是使用requests。它是一個Python第三方庫,處理URL資源特別方便。

安裝requests

如果安裝了Anaconda,requests就已經可用了。否則,需要在命令行下通過pip安裝:

$ pip install requests

如果遇到Permission denied安裝失敗,請加上sudo重試。

使用requests

要通過GET訪問一個頁面,只需要幾行代碼:

>>> import requests>>> r = requests.get('https://www.douban.com/') # 豆瓣首頁>>> r.status_code200>>> r.textr.text'<!DOCTYPE HTML>\n\n\n

對于帶參數的URL,傳入一個dict作為params參數:

>>> r = requests.get('https://www.douban.com/search', params={'q':'python','cat':'1001'})>>> r.url# 實際請求的URL'https://www.douban.com/search?q=python&cat=1001'

requests自動檢測編碼,可以使用encoding屬性查看:

>>> r.encoding'utf-8'

無論響應是文本還是二進制內容,我們都可以用content屬性獲得bytes對象:

>>> r.contentb'<!DOCTYPE html>\n\n\n\n...'

requests的方便之處還在于,對于特定類型的響應,例如JSON,可以直接獲取:

>>> r = requests.get('https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20%3D%202151330&format=json')

>>> r.json()

{'query': {'count': 1, 'created': '2017-11-17T07:14:12Z', ...

需要傳入HTTP Header時,我們傳入一個dict作為headers參數:

>>> r = requests.get('https://www.douban.com/', headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit'})>>> r.text'<!DOCTYPE html>\n\n\n\n豆瓣(手機版)...'

要發送POST請求,只需要把get()方法變成post(),然后傳入data參數作為POST請求的數據:

>>> r = requests.post('https://accounts.douban.com/login', data={'form_email':'abc@example.com','form_password':'123456'})

requests默認使用application/x-www-form-urlencoded對POST數據編碼。如果要傳遞JSON數據,可以直接傳入json參數:

params ={'key': 'value'}r =requests.post(url, json=params) # 內部自動序列化為JSON

類似的,上傳文件需要更復雜的編碼格式,但是requests把它簡化成files參數:

>>> upload_files = {'file': open('report.xls','rb')}>>> r = requests.post(url, files=upload_files)

在讀取文件時,注意務必使用'rb'即二進制模式讀取,這樣獲取的bytes長度才是文件的長度。

把post()方法替換為put(),delete()等,就可以以PUT或DELETE方式請求資源。

除了能輕松獲取響應內容外,requests對獲取HTTP響應的其他信息也非常簡單。例如,獲取響應頭:

>>> r.headers{Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Content-Encoding': 'gzip', ...}

>>> r.headers['Content-Type']

'text/html;charset=utf-8'

requests對Cookie做了特殊處理,使得我們不必解析Cookie就可以輕松獲取指定的Cookie:

>>> r.cookies['ts']'example_cookie_12345'

要在請求中傳入Cookie,只需準備一個dict傳入cookies參數:

>>> cs = {'token':'12345','status':'working'}>>> r = requests.get(url, cookies=cs)

最后,要指定超時,傳入以秒為單位的timeout參數:

>>> r = requests.get(url, timeout=2.5)# 2.5秒后超時

字符串編碼一直是令人非常頭疼的問題,尤其是我們在處理一些不規范的第三方網頁的時候。雖然Python提供了Unicode表示的str和bytes兩種數據類型,并且可以通過encode()和decode()方法轉換,但是,在不知道編碼的情況下,對bytes做decode()不好做。

對于未知編碼的bytes,要把它轉換成str,需要先“猜測”編碼。猜測的方式是先收集各種編碼的特征字符,根據特征字符判斷,就能有很大概率“猜對”。

當然,我們肯定不能從頭自己寫這個檢測編碼的功能,這樣做費時費力。chardet這個第三方庫正好就派上了用場。用它來檢測編碼,簡單易用。

安裝chardet

如果安裝了Anaconda,chardet就已經可用了。否則,需要在命令行下通過pip安裝:

$ pip install chardet

如果遇到Permission denied安裝失敗,請加上sudo重試。

使用chardet

當我們拿到一個bytes時,就可以對其檢測編碼。用chardet檢測編碼,只需要一行代碼:

>>> chardet.detect(b'Hello, world!'){'encoding':'ascii','confidence':1.0,'language':''}

檢測出的編碼是ascii,注意到還有個confidence字段,表示檢測的概率是1.0(即100%)。

我們來試試檢測GBK編碼的中文:

>>> data ='離離原上草,一歲一枯榮'.encode('gbk')>>> chardet.detect(data){'encoding':'GB2312','confidence':0.7407407407407407,'language':'Chinese'}

檢測的編碼是GB2312,注意到GBK是GB2312的超集,兩者是同一種編碼,檢測正確的概率是74%,language字段指出的語言是'Chinese'。

對UTF-8編碼進行檢測:

>>> data ='離離原上草,一歲一枯榮'.encode('utf-8')>>> chardet.detect(data){'encoding':'utf-8','confidence':0.99,'language':''}

我們再試試對日文進行檢測:

>>> data ='最新の主要ニュース'.encode('euc-jp')>>> chardet.detect(data){'encoding':'EUC-JP','confidence':0.99,'language':'Japanese'}

可見,用chardet檢測編碼,使用簡單。獲取到編碼后,再轉換為str,就可以方便后續處理。

chardet支持檢測的編碼列表請參考官方文檔Supported encodings

用Python來編寫腳本簡化日常的運維工作是Python的一個重要用途。在Linux下,有許多系統命令可以讓我們時刻監控系統運行的狀態,如ps,top,free等等。要獲取這些系統信息,Python可以通過subprocess模塊調用并獲取結果。但這樣做顯得很麻煩,尤其是要寫很多解析代碼。

在Python中獲取系統信息的另一個好辦法是使用psutil這個第三方模塊。顧名思義,psutil = process and system utilities,它不僅可以通過一兩行代碼實現系統監控,還可以跨平臺使用,支持Linux/UNIX/OSX/Windows等,是系統管理員和運維小伙伴不可或缺的必備模塊。

安裝psutil

如果安裝了Anaconda,psutil就已經可用了。否則,需要在命令行下通過pip安裝:

$ pip install psutil

如果遇到Permission denied安裝失敗,請加上sudo重試。

獲取CPU信息

我們先來獲取CPU的信息:

>>> importpsutil>>> psutil.cpu_count()# CPU邏輯數量4>>> psutil.cpu_count(logical=False)# CPU物理核心2# 2說明是雙核超線程, 4則是4核非超線程

統計CPU的用戶/系統/空閑時間:

>>> psutil.cpu_times()scputimes(user=10963.31, nice=0.0, system=5138.67, idle=356102.45)

再實現類似top命令的CPU使用率,每秒刷新一次,累計10次:

>>> forxinrange(10):... psutil.cpu_percent(interval=1, percpu=True)... [14.0,4.0,4.0,4.0][12.0,3.0,4.0,3.0][8.0,4.0,3.0,4.0][12.0,3.0,3.0,3.0][18.8,5.1,5.9,5.0][10.9,5.0,4.0,3.0][12.0,5.0,4.0,5.0][15.0,5.0,4.0,4.0][19.0,5.0,5.0,4.0][9.0,3.0,2.0,3.0]

獲取內存信息

使用psutil獲取物理內存和交換內存信息,分別使用:

>>> psutil.virtual_memory()svmem(total=8589934592, available=2866520064, percent=66.6, used=7201386496, free=216178688, active=3342192640, inactive=2650341376, wired=1208852480)>>> psutil.swap_memory()sswap(total=1073741824, used=150732800, free=923009024, percent=14.0, sin=10705981440, sout=40353792)

返回的是字節為單位的整數,可以看到,總內存大小是8589934592 = 8 GB,已用7201386496 = 6.7 GB,使用了66.6%。

而交換區大小是1073741824 = 1 GB。

獲取磁盤信息

可以通過psutil獲取磁盤分區、磁盤使用率和磁盤IO信息:

>>> psutil.disk_partitions()# 磁盤分區信息[sdiskpart(device='/dev/disk1', mountpoint='/', fstype='hfs', opts='rw,local,rootfs,dovolfs,journaled,multilabel')]>>> psutil.disk_usage('/')# 磁盤使用情況sdiskusage(total=998982549504, used=390880133120, free=607840272384, percent=39.1)>>> psutil.disk_io_counters()# 磁盤IOsdiskio(read_count=988513, write_count=274457, read_bytes=14856830464, write_bytes=17509420032, read_time=2228966, write_time=1618405)

可以看到,磁盤'/'的總容量是998982549504 = 930 GB,使用了39.1%。文件格式是HFS,opts中包含rw表示可讀寫,journaled表示支持日志。

獲取網絡信息

psutil可以獲取網絡接口和網絡連接信息:

>>> psutil.net_io_counters() # 獲取網絡讀寫字節/包的個數snetio(bytes_sent=3885744870, bytes_recv=10357676702, packets_sent=10613069, packets_recv=10423357, errin=0, errout=0, dropin=0, dropout=0)>>> psutil.net_if_addrs() # 獲取網絡接口信息{'lo0': [snic(family=, address='127.0.0.1', netmask='255.0.0.0'), ...],? 'en1': [snic(family=, address='10.0.1.80', netmask='255.255.255.0'), ...],? 'en0': [...],? 'en2': [...],? 'bridge0': [...]}>>> psutil.net_if_stats() # 獲取網絡接口狀態{? 'lo0': snicstats(isup=True, duplex=, speed=0, mtu=16384),? 'en0': snicstats(isup=True, duplex=, speed=0, mtu=1500),? 'en1': snicstats(...),? 'en2': snicstats(...),? 'bridge0': snicstats(...)}

要獲取當前網絡連接信息,使用net_connections():

>>> psutil.net_connections()Traceback (most recentcalllast):? ...PermissionError: [Errno1] OperationnotpermittedDuring handlingofthe aboveexception, anotherexceptionoccurred:Traceback (most recentcalllast):? ...psutil.AccessDenied: psutil.AccessDenied (pid=3847)

你可能會得到一個AccessDenied錯誤,原因是psutil獲取信息也是要走系統接口,而獲取網絡連接信息需要root權限,這種情況下,可以退出Python交互環境,用sudo重新啟動:

$ sudo python3Password: ******Python3.6.3... on darwinType"help", ...formore information.>>> import psutil>>> psutil.net_connections()[? ? sconn(fd=83, family=, type=1, laddr=addr(ip='::127.0.0.1', port=62911), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),? ? sconn(fd=84, family=, type=1, laddr=addr(ip='::127.0.0.1', port=62905), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),? ? sconn(fd=93, family=, type=1, laddr=addr(ip='::', port=8080), raddr=(), status='LISTEN', pid=3725),? ? sconn(fd=103, family=, type=1, laddr=addr(ip='::127.0.0.1', port=62918), raddr=addr(ip='::127.0.0.1', port=3306), status='ESTABLISHED', pid=3725),? ? sconn(fd=105, family=, type=1, ..., pid=3725),? ? sconn(fd=106, family=, type=1, ..., pid=3725),? ? sconn(fd=107, family=, type=1, ..., pid=3725),? ? ...? ? sconn(fd=27, family=, type=2, ..., pid=1)]

獲取進程信息

通過psutil可以獲取到所有進程的詳細信息:

>>> psutil.pids()# 所有進程ID[3865,3864,3863,3856,3855,3853,3776, ...,45,44,1,0]>>> p = psutil.Process(3776)# 獲取指定進程ID=3776,其實就是當前Python交互環境>>> p.name()# 進程名稱'python3.6'>>> p.exe()# 進程exe路徑'/Users/michael/anaconda3/bin/python3.6'>>> p.cwd()# 進程工作目錄'/Users/michael'>>> p.cmdline()# 進程啟動的命令行['python3']>>> p.ppid()# 父進程ID3765>>> p.parent()# 父進程>>> p.children()# 子進程列表[]>>> p.status()# 進程狀態'running'>>> p.username()# 進程用戶名'michael'>>> p.create_time()# 進程創建時間1511052731.120333>>> p.terminal()# 進程終端'/dev/ttys002'>>> p.cpu_times()# 進程使用的CPU時間pcputimes(user=0.081150144, system=0.053269812, children_user=0.0, children_system=0.0)>>> p.memory_info()# 進程使用的內存pmem(rss=8310784, vms=2481725440, pfaults=3207, pageins=18)>>> p.open_files()# 進程打開的文件[]>>> p.connections()# 進程相關網絡連接[]>>> p.num_threads()# 進程的線程數量1>>> p.threads()# 所有線程信息[pthread(id=1, user_time=0.090318, system_time=0.062736)]>>> p.environ()# 進程環境變量{'SHELL':'/bin/bash','PATH':'/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:...','PWD':'/Users/michael','LANG':'zh_CN.UTF-8', ...}>>> p.terminate()# 結束進程Terminated:15<-- 自己把自己結束了

和獲取網絡連接類似,獲取一個root用戶的進程需要root權限,啟動Python交互環境或者.py文件時,需要sudo權限。

psutil還提供了一個test()函數,可以模擬出ps命令的效果:

$sudopython3Password: ******Python3.6.3...ondarwinType"help", ...formoreinformation.>>>importpsutil>>>psutil.test()USERPID%MEMVSZRSSTTYSTARTTIMECOMMANDroot0 24.074270628 2016380 ?Nov1840:51kernel_taskroot1? 0.12494140? ? 9484 ?Nov1801:39launchdroot44? 0.42519872? 36404 ?Nov1802:02UserEventAgentroot45? ? ? 2474032? ? 1516 ?Nov1800:14syslogdroot47? 0.12504768? ? 8912 ?Nov1800:03kextdroot48? 0.12505544? ? 4720 ?Nov1800:19fseventsd_appleeven52? 0.12499748? ? 5024 ?Nov1800:00appleeventsdroot53? 0.12500592? ? 6132 ?Nov1800:02configd...

在開發Python應用程序的時候,系統安裝的Python3只有一個版本:3.4。所有第三方的包都會被pip安裝到Python3的site-packages目錄下。

如果我們要同時開發多個應用程序,那這些應用程序都會共用一個Python,就是安裝在系統的Python 3。如果應用A需要jinja 2.7,而應用B需要jinja 2.6怎么辦?

這種情況下,每個應用可能需要各自擁有一套“獨立”的Python運行環境。virtualenv就是用來為一個應用創建一套“隔離”的Python運行環境。

首先,我們用pip安裝virtualenv:

$ pip3 install virtualenv

然后,假定我們要開發一個新的項目,需要一套獨立的Python運行環境,可以這么做:

第一步,創建目錄:

Mac:~michael$ mkdir myprojectMac:~michael$ cd myproject/Mac:myprojectmichael$

第二步,創建一個獨立的Python運行環境,命名為venv:

Mac:myprojectmichael$ virtualenv --no-site-packages venvUsingbase prefix'/usr/local/.../Python.framework/Versions/3.4'Newpython executableinvenv/bin/python3.4Alsocreating executableinvenv/bin/pythonInstallingsetuptools, pip, wheel...done.

命令virtualenv就可以創建一個獨立的Python運行環境,我們還加上了參數--no-site-packages,這樣,已經安裝到系統Python環境中的所有第三方包都不會復制過來,這樣,我們就得到了一個不帶任何第三方包的“干凈”的Python運行環境。

新建的Python環境被放到當前目錄下的venv目錄。有了venv這個Python環境,可以用source進入該環境:

Mac:myprojectmichael$ source venv/bin/activate(venv)Mac:myprojectmichael$

注意到命令提示符變了,有個(venv)前綴,表示當前環境是一個名為venv的Python環境。

下面正常安裝各種第三方包,并運行python命令:

(venv)Mac:myprojectmichael$ pip install jinja2...Successfullyinstalled jinja2-2.7.3markupsafe-0.23(venv)Mac:myprojectmichael$ python myapp.py...

在venv環境下,用pip安裝的包都被安裝到venv這個環境下,系統Python環境不受任何影響。也就是說,venv環境是專門針對myproject這個應用創建的。

退出當前的venv環境,使用deactivate命令:

(venv)Mac:myprojectmichael$ deactivateMac:myprojectmichael$

此時就回到了正常的環境,現在pip或python均是在系統Python環境下執行。

完全可以針對每個應用創建獨立的Python運行環境,這樣就可以對每個應用的Python環境進行隔離。

virtualenv是如何創建“獨立”的Python運行環境的呢?原理很簡單,就是把系統Python復制一份到virtualenv的環境,用命令source venv/bin/activate進入一個virtualenv環境時,virtualenv會修改相關環境變量,讓命令python和pip均指向當前的virtualenv環境。

Python支持多種圖形界面的第三方庫,包括:

Tk

wxWidgets

Qt

GTK

等等。

但是Python自帶的庫是支持Tk的Tkinter,使用Tkinter,無需安裝任何包,就可以直接使用。本章簡單介紹如何使用Tkinter進行GUI編程。

Tkinter

我們來梳理一下概念:

我們編寫的Python代碼會調用內置的Tkinter,Tkinter封裝了訪問Tk的接口;

Tk是一個圖形庫,支持多個操作系統,使用Tcl語言開發;

Tk會調用操作系統提供的本地GUI接口,完成最終的GUI。

所以,我們的代碼只需要調用Tkinter提供的接口就可以了。

第一個GUI程序

使用Tkinter十分簡單,我們來編寫一個GUI版本的“Hello, world!”。

第一步是導入Tkinter包的所有內容:

fromtkinterimport*

第二步是從Frame派生一個Application類,這是所有Widget的父容器:

classApplication(Frame):def__init__(self, master=None):Frame.__init__(self, master)? ? ? ? self.pack()? ? ? ? self.createWidgets()defcreateWidgets(self):self.helloLabel = Label(self, text='Hello, world!')? ? ? ? self.helloLabel.pack()? ? ? ? self.quitButton = Button(self, text='Quit', command=self.quit)? ? ? ? self.quitButton.pack()

在GUI中,每個Button、Label、輸入框等,都是一個Widget。Frame則是可以容納其他Widget的Widget,所有的Widget組合起來就是一棵樹。

pack()方法把Widget加入到父容器中,并實現布局。pack()是最簡單的布局,grid()可以實現更復雜的布局。

在createWidgets()方法中,我們創建一個Label和一個Button,當Button被點擊時,觸發self.quit()使程序退出。

第三步,實例化Application,并啟動消息循環:

app =Application()# 設置窗口標題:app.master.title('Hello World')# 主消息循環:app.mainloop()

GUI程序的主線程負責監聽來自操作系統的消息,并依次處理每一條消息。因此,如果消息處理非常耗時,就需要在新線程中處理。

運行這個GUI程序,可以看到下面的窗口:

點擊“Quit”按鈕或者窗口的“x”結束程序。

輸入文本

我們再對這個GUI程序改進一下,加入一個文本框,讓用戶可以輸入文本,然后點按鈕后,彈出消息對話框。

fromtkinterimport*importtkinter.messageboxasmessageboxclassApplication(Frame):def__init__(self, master=None):Frame.__init__(self, master)? ? ? ? self.pack()? ? ? ? self.createWidgets()defcreateWidgets(self):self.nameInput = Entry(self)? ? ? ? self.nameInput.pack()? ? ? ? self.alertButton = Button(self, text='Hello', command=self.hello)? ? ? ? self.alertButton.pack()defhello(self):name = self.nameInput.get()or'world'messagebox.showinfo('Message','Hello, %s'% name)app = Application()# 設置窗口標題:app.master.title('Hello World')# 主消息循環:app.mainloop()

當用戶點擊按鈕時,觸發hello(),通過self.nameInput.get()獲得用戶輸入的文本后,使用tkMessageBox.showinfo()可以彈出消息對話框。

程序運行結果如下:

雖然大家現在對互聯網很熟悉,但是計算機網絡的出現比互聯網要早很多。

計算機為了聯網,就必須規定通信協議,早期的計算機網絡,都是由各廠商自己規定一套協議,IBM、Apple和Microsoft都有各自的網絡協議,互不兼容,這就好比一群人有的說英語,有的說中文,有的說德語,說同一種語言的人可以交流,不同的語言之間就不行了。

為了把全世界的所有不同類型的計算機都連接起來,就必須規定一套全球通用的協議,為了實現互聯網這個目標,互聯網協議簇(Internet Protocol Suite)就是通用協議標準。Internet是由inter和net兩個單詞組合起來的,原意就是連接“網絡”的網絡,有了Internet,任何私有網絡,只要支持這個協議,就可以聯入互聯網。

因為互聯網協議包含了上百種協議標準,但是最重要的兩個協議是TCP和IP協議,所以,大家把互聯網的協議簡稱TCP/IP協議。

通信的時候,雙方必須知道對方的標識,好比發郵件必須知道對方的郵件地址。互聯網上每個計算機的唯一標識就是IP地址,類似123.123.123.123。如果一臺計算機同時接入到兩個或更多的網絡,比如路由器,它就會有兩個或多個IP地址,所以,IP地址對應的實際上是計算機的網絡接口,通常是網卡。

IP協議負責把數據從一臺計算機通過網絡發送到另一臺計算機。數據被分割成一小塊一小塊,然后通過IP包發送出去。由于互聯網鏈路復雜,兩臺計算機之間經常有多條線路,因此,路由器就負責決定如何把一個IP包轉發出去。IP包的特點是按塊發送,途徑多個路由,但不保證能到達,也不保證順序到達。

IP地址實際上是一個32位整數(稱為IPv4),以字符串表示的IP地址如192.168.0.1實際上是把32位整數按8位分組后的數字表示,目的是便于閱讀。

IPv6地址實際上是一個128位整數,它是目前使用的IPv4的升級版,以字符串表示類似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。

TCP協議則是建立在IP協議之上的。TCP協議負責在兩臺計算機之間建立可靠連接,保證數據包按順序到達。TCP協議會通過握手建立連接,然后,對每個IP包編號,確保對方按順序收到,如果包丟掉了,就自動重發。

許多常用的更高級的協議都是建立在TCP協議基礎上的,比如用于瀏覽器的HTTP協議、發送郵件的SMTP協議等。

一個TCP報文除了包含要傳輸的數據外,還包含源IP地址和目標IP地址,源端口和目標端口。

端口有什么作用?在兩臺計算機通信時,只發IP地址是不夠的,因為同一臺計算機上跑著多個網絡程序。一個TCP報文來了之后,到底是交給瀏覽器還是QQ,就需要端口號來區分。每個網絡程序都向操作系統申請唯一的端口號,這樣,兩個進程在兩臺計算機之間建立網絡連接就需要各自的IP地址和各自的端口號。

一個進程也可能同時與多個計算機建立鏈接,因此它會申請很多端口。

了解了TCP/IP協議的基本概念,IP地址和端口的概念,我們就可以開始進行網絡編程了。

Socket是網絡編程的一個抽象概念。通常我們用一個Socket表示“打開了一個網絡鏈接”,而打開一個Socket需要知道目標計算機的IP地址和端口號,再指定協議類型即可。

客戶端

大多數連接都是可靠的TCP連接。創建TCP連接時,主動發起連接的叫客戶端,被動響應連接的叫服務器。

舉個例子,當我們在瀏覽器中訪問新浪時,我們自己的計算機就是客戶端,瀏覽器會主動向新浪的服務器發起連接。如果一切順利,新浪的服務器接受了我們的連接,一個TCP連接就建立起來的,后面的通信就是發送網頁內容了。

所以,我們要創建一個基于TCP連接的Socket,可以這樣做:

# 導入socket庫:importsocket# 創建一個socket:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立連接:s.connect(('www.sina.com.cn',80))

創建Socket時,AF_INET指定使用IPv4協議,如果要用更先進的IPv6,就指定為AF_INET6。SOCK_STREAM指定使用面向流的TCP協議,這樣,一個Socket對象就創建成功,但是還沒有建立連接。

客戶端要主動發起TCP連接,必須知道服務器的IP地址和端口號。新浪網站的IP地址可以用域名www.sina.com.cn自動轉換到IP地址,但是怎么知道新浪服務器的端口號呢?

答案是作為服務器,提供什么樣的服務,端口號就必須固定下來。由于我們想要訪問網頁,因此新浪提供網頁服務的服務器必須把端口號固定在80端口,因為80端口是Web服務的標準端口。其他服務都有對應的標準端口號,例如SMTP服務是25端口,FTP服務是21端口,等等。端口號小于1024的是Internet標準服務的端口,端口號大于1024的,可以任意使用。

因此,我們連接新浪服務器的代碼如下:

s.connect(('www.sina.com.cn', 80))

注意參數是一個tuple,包含地址和端口號。

建立TCP連接后,我們就可以向新浪服務器發送請求,要求返回首頁的內容:

# 發送數據:s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP連接創建的是雙向通道,雙方都可以同時給對方發數據。但是誰先發誰后發,怎么協調,要根據具體的協議來決定。例如,HTTP協議規定客戶端必須先發請求給服務器,服務器收到后才發數據給客戶端。

發送的文本格式必須符合HTTP標準,如果格式沒問題,接下來就可以接收新浪服務器返回的數據了:

# 接收數據:buffer = []whileTrue:# 每次最多接收1k字節:d = s.recv(1024)ifd:? ? ? ? buffer.append(d)else:breakdata =b''.join(buffer)

接收數據時,調用recv(max)方法,一次最多接收指定的字節數,因此,在一個while循環中反復接收,直到recv()返回空數據,表示接收完畢,退出循環。

當我們接收完數據后,調用close()方法關閉Socket,這樣,一次完整的網絡通信就結束了:

# 關閉連接:s.close()

接收到的數據包括HTTP頭和網頁本身,我們只需要把HTTP頭和網頁分離一下,把HTTP頭打印出來,網頁內容保存到文件:

header, html = data.split(b'\r\n\r\n',1)print(header.decode('utf-8'))# 把接收的數據寫入文件:withopen('sina.html','wb')asf:? ? f.write(html)

現在,只需要在瀏覽器中打開這個sina.html文件,就可以看到新浪的首頁了。

服務器

和客戶端編程相比,服務器編程就要復雜一些。

服務器進程首先要綁定一個端口并監聽來自其他客戶端的連接。如果某個客戶端連接過來了,服務器就與該客戶端建立Socket連接,隨后的通信就靠這個Socket連接了。

所以,服務器會打開固定端口(比如80)監聽,每來一個客戶端連接,就創建該Socket連接。由于服務器會有大量來自客戶端的連接,所以,服務器要能夠區分一個Socket連接是和哪個客戶端綁定的。一個Socket依賴4項:服務器地址、服務器端口、客戶端地址、客戶端端口來唯一確定一個Socket。

但是服務器還需要同時響應多個客戶端的請求,所以,每個連接都需要一個新的進程或者新的線程來處理,否則,服務器一次就只能服務一個客戶端了。

我們來編寫一個簡單的服務器程序,它接收客戶端連接,把客戶端發過來的字符串加上Hello再發回去。

首先,創建一個基于IPv4和TCP協議的Socket:

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

然后,我們要綁定監聽的地址和端口。服務器可能有多塊網卡,可以綁定到某一塊網卡的IP地址上,也可以用0.0.0.0綁定到所有的網絡地址,還可以用127.0.0.1綁定到本機地址。127.0.0.1是一個特殊的IP地址,表示本機地址,如果綁定到這個地址,客戶端必須同時在本機運行才能連接,也就是說,外部的計算機無法連接進來。

端口號需要預先指定。因為我們寫的這個服務不是標準服務,所以用9999這個端口號。請注意,小于1024的端口號必須要有管理員權限才能綁定:

# 監聽端口:s.bind(('127.0.0.1',9999))

緊接著,調用listen()方法開始監聽端口,傳入的參數指定等待連接的最大數量:

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

接下來,服務器程序通過一個永久循環來接受來自客戶端的連接,accept()會等待并返回一個客戶端的連接:

whileTrue:# 接受一個新連接:sock, addr = s.accept()# 創建新線程來處理TCP連接:t = threading.Thread(target=tcplink, args=(sock, addr))? ? t.start()

每個連接都必須創建新線程(或進程)來處理,否則,單線程在處理連接的過程中,無法接受其他客戶端的連接:

deftcplink(sock, addr):print('Accept new connection from %s:%s...'% addr)? ? sock.send(b'Welcome!')whileTrue:? ? ? ? data = sock.recv(1024)? ? ? ? time.sleep(1)ifnotdataordata.decode('utf-8') =='exit':breaksock.send(('Hello, %s!'% data.decode('utf-8')).encode('utf-8'))? ? sock.close()? ? print('Connection from %s:%s closed.'% addr)

連接建立后,服務器首先發一條歡迎消息,然后等待客戶端數據,并加上Hello再發送給客戶端。如果客戶端發送了exit字符串,就直接關閉連接。

要測試這個服務器程序,我們還需要編寫一個客戶端程序:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 建立連接:s.connect(('127.0.0.1',9999))# 接收歡迎消息:print(s.recv(1024).decode('utf-8'))fordatain[b'Michael',b'Tracy',b'Sarah']:# 發送數據:s.send(data)? ? print(s.recv(1024).decode('utf-8'))s.send(b'exit')s.close()

我們需要打開兩個命令行窗口,一個運行服務器程序,另一個運行客戶端程序,就可以看到效果了:

┌────────────────────────────────────────────────────────┐

│Command Prompt? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? - □ x │

├────────────────────────────────────────────────────────┤

│$ python echo_server.py? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│Waiting for connection...? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│Accept new connection from 127.0.0.1:64398...? ? ? ? ? │

│Connection from 127.0.0.1:64398 closed.? ? ? ? ? ? ? ? │

│? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│? ? ? ┌────────────────────────────────────────────────┴───────┐

│? ? ? │Command Prompt? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? - □ x │

│? ? ? ├────────────────────────────────────────────────────────┤

│? ? ? │$ python echo_client.py? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│? ? ? │Welcome!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│? ? ? │Hello, Michael!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

└───────┤Hello, Tracy!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │Hello, Sarah!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │$? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? └────────────────────────────────────────────────────────┘

需要注意的是,客戶端程序運行完畢就退出了,而服務器程序會永遠運行下去,必須按Ctrl+C退出程序。

CP是建立可靠連接,并且通信雙方都可以以流的形式發送數據。相對TCP,UDP則是面向無連接的協議。

使用UDP協議時,不需要建立連接,只需要知道對方的IP地址和端口號,就可以直接發數據包。但是,能不能到達就不知道了。

雖然用UDP傳輸數據不可靠,但它的優點是和TCP比,速度快,對于不要求可靠到達的數據,就可以使用UDP協議。

我們來看看如何通過UDP協議傳輸數據。和TCP類似,使用UDP的通信雙方也分為客戶端和服務器。服務器首先需要綁定端口:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# 綁定端口:s.bind(('127.0.0.1',9999))

創建Socket時,SOCK_DGRAM指定了這個Socket的類型是UDP。綁定端口和TCP一樣,但是不需要調用listen()方法,而是直接接收來自任何客戶端的數據:

print('Bind UDP on 9999...')whileTrue:# 接收數據:data, addr = s.recvfrom(1024)print('Received from %s:%s.'% addr)? ? s.sendto(b'Hello, %s!'% data, addr)

recvfrom()方法返回數據和客戶端的地址與端口,這樣,服務器收到數據后,直接調用sendto()就可以把數據用UDP發給客戶端。

注意這里省掉了多線程,因為這個例子很簡單。

客戶端使用UDP時,首先仍然創建基于UDP的Socket,然后,不需要調用connect(),直接通過sendto()給服務器發數據:

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)fordatain[b'Michael',b'Tracy',b'Sarah']:# 發送數據:s.sendto(data, ('127.0.0.1',9999))# 接收數據:print(s.recv(1024).decode('utf-8'))s.close()

從服務器接收數據仍然調用recv()方法。

仍然用兩個命令行分別啟動服務器和客戶端測試,結果如下:

┌────────────────────────────────────────────────────────┐

│Command Prompt? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? - □ x │

├────────────────────────────────────────────────────────┤

│$ python udp_server.py? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│Bind UDP on 9999...? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│Received from 127.0.0.1:63823...? ? ? ? ? ? ? ? ? ? ? ? │

│Received from 127.0.0.1:63823...? ? ? ? ? ? ? ? ? ? ? ? │

│Received from 127.0.0.1:63823...? ? ? ? ? ? ? ? ? ? ? ? │

│? ? ? ┌────────────────────────────────────────────────┴───────┐

│? ? ? │Command Prompt? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? - □ x │

│? ? ? ├────────────────────────────────────────────────────────┤

│? ? ? │$ python udp_client.py? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│? ? ? │Welcome!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

│? ? ? │Hello, Michael!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

└───────┤Hello, Tracy!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │Hello, Sarah!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │$? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? │? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? └────────────────────────────────────────────────────────┘

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容