目錄
Python提供了強大的網絡編程支持,有很多庫實現了常見的網絡協議以及基於這些協議的抽象層。
1.網絡模塊
1.1 模塊socket
網絡編程中的一個基本組件是套接字,套接字基本上是一個信息通道,兩端各有一個程序。這些程序可能位於不同的計算機上,通過套接字向對方發送消息。在Python中,大多數網絡編程都隱藏了模塊socket的基本工作原理,不與套接字直接交互。
套接字是模塊socket中socket類的實例,實例化套接字時最多指定三個參數:一個地址族(默認爲socket.AF_INET);流套接字(socket.SOCK_STREAM,默認設置)還是數據報套接字(socket.SOCK_DGRAM);協議(使用默認值0就好)。創建普通套接字時,可以不用提供任何參數。方法listen接收一個參數,即最多有多少個客戶端連接在隊列中等待連接,到達這個數量以後,服務端就可以拒絕連接。服務端套接字開始監聽後,就可以使用accept接收客戶端的連接了。
爲了傳輸數據,套接字提供兩個方法:send和recv。要發送數據,可調用方法send並提供一個字符串;要接收數據,可調用recv並指定接收多少個字節的數據。實例代碼如下:
服務端
import socket
s = socket.socket()
host = socket.gethostname()
port = 8080
s.bind((host, port))
s.listen(5)
while True:
c, addr = s.accept()
print('get connect from:', addr)
c.send('hello!'.encode())
c.close()
客戶端:
import socket
s = socket.socket()
host = socket.gethostname()
port = 8080
s.connect((host, port))
print(s.recv(1024).decode())
1.2 模塊urllib和urllib2
urllib和urlib2讓我們能夠通過網絡訪問文件,就像這些文件位於我們本地計算機一樣。urllib2相比於urllib1功能更加強大一些,它提供了HTTP身份認證和cookie。
1.2.1 打開遠程文件
打開遠程文件就像打開本地文件一樣,差別是隻能使用只讀模式。實例代碼如下:
from urllib.request import urlopen
webpage = urlopen('http://www.python.org')
print(webpage)
變量webpage包含一個類似於文件的對象,該對象與網頁http://www.python.org 相關聯。urlopen返回的類似於文件的對象支持方法close、read、readline和readlines,還支持迭代等。如果我們要提取網頁中的鏈接About的相對URL,可使用正則表達式匹配:
from urllib.request import urlopen
import re
webpage = urlopen('http://www.python.org')
text = webpage.read()
m = re.search(b'<a href="([^"]+)".*?>about</a>', text, re.IGNORECASE)
print(m.group(1))
當然,如果網頁發生了變化,我們需要修改使用的正則表達式。
1.2.2 獲取遠程文件
如果要讓urllib下載文件,並保存到本地文件中,可使用urlretrieve。實現代碼如下:
from urllib.request import urlretrieve
urlretrieve('http://www.python.org', r'D:\text\python.html')
除了這些模塊以外,Python庫還包含了很多其他與網絡相關的模塊:
1.2.3 SocketServer及相關類
前面我們編寫了一個簡單的套接字服務器,這對我們來說相當容易。如果我們實現的服務器非常複雜,此時我們需要使用服務器模塊。模塊SocketServer是標準庫提供的服務器框架的基石,該框架包括BasedHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer和DocXMLRPCServer等服務器,它們在基本服務器的基礎上添加了很多功能。如下代碼我們實現了簡單服務器的SocketServer版本。
from socketserver import TCPServer, StreamRequestHandler
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('get connection from', addr)
self.wfile.write(b'hello!')
server = TCPServer(('', 8080), Handler)
server.serve_forever()
1.3 多個連接
前面實現的服務器都是同步的,不能同時處理多個客戶端的連接請求。如果連接持續的時間較長,就需要我們同時處理多個連接。處理多個連接的主要方式有三種:分叉(forking)、線程化和異步I/O。通過結合使用SocketServer中的混合類和服務器類,很容易實現分叉和線程化。然而,它們確實存在缺點,分叉佔用的資源很多,且在客戶端很多時可伸縮性不佳;而線程化可能帶來同步問題。
分叉是一個UNIX術語,windows不支持分叉。對進程進行分叉時,基本上是複製它,而這樣得到的兩個進程都將從當前位置開始繼續往下執行,且每個進程都有自己的內存副本(變量等)。原來的進程爲父進程,複製的進程爲子進程。進程能判斷它們是原始進程還是子進程(通常查看函數fork的返回值),因此能夠執行不同的操作。
在分叉服務器中,對於每個客戶端連接,都將通過分叉創建一個子進程。父進程繼續監聽新連接,而子進程負責處理客戶端請求。客戶端請求結束後,子進程直接退出。由於分叉出來的進程並行的運行,因此客戶端無需等待。
鑑於分叉佔用的資源較多(每個分叉出來的進程都必須有自己的內存),還有一種解決方案:線程化。線程是輕量級的進程,都位於同一個進程中共享內存。這減少了佔用的資源,但也帶來了一個缺點:由於線程共享內存,所以必須解決同步問題。本章將介紹基於函數select的其他解決方案。
1.3.1 使用SocketServer實現分叉和線程化
使用框架SocketServer創建分叉服務器非常簡單,代碼如下:
from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler
class Server(ForkingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('get connection from', addr)
self.wfile.write('hello!')
server = Server(('', 8080), Handler)
server.serve_forever()
在windows執行報錯,這是因爲windows是不支持分叉的。
ImportError: cannot import name 'ForkingMixIn' from 'socketserver' (F:\Python3.7.3\lib\socketserver.py)
線程化服務器執行如下:
from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler
class Server(ThreadingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('get connection from', addr)
self.wfile.write(b'hello!')
server = Server(('', 8080), Handler)
server.serve_forever()
1.3.2 使用select和poll實現異步IO
當服務器與客戶端通信時,來自客戶端的數據可能時斷時續。如果使用了分叉和線程化,這就不是問題:因爲一個進程(線程)等待數據時,其他進程(線程)可繼續處理其他客戶端的請求。另外一種做法是隻處理當前正在通信的客戶端,無需不斷監聽,只需要監聽後將客戶端加入隊列即可。這就是框架asyncore/asynchat和Twisted採取的方法。這種功能的基石是系統函數select或poll。這兩個函數都是位於模塊select中,其中poll的可伸縮性更好,但是隻有Unix系統支持它。
下面的實例代碼展示了select來爲多個連接提供服務。該服務器是一個簡單的日誌程序,將來自客戶端的數據都打印出來。
import socket, select
s = socket.socket()
host = socket.gethostname()
port = 8080
s.bind((host, port))
s.listen(5)
inputs = [s]
while True:
rs, ws, es = select.select(inputs, [], [])
for r in rs:
if r is s:
c, addr = s.accept()
print('get connection from', addr)
inputs.append(c)
else:
try:
data = r.recv(1024)
disconnected = not data
except socket.error:
disconnected = True
if disconnected:
print(r.getpeername(), 'disconnected')
inputs.remove(r)
else:
print(data)
方法poll使用起來比select容易。調用poll時,將返回一個輪詢對象。我們使用方法register向這個對象註冊文件描述符,註冊後可使用方法unregister將它們刪除。註冊對象後,可調用方法poll,該方法返回一個包含(fd,event)元組的列表,其中fd爲文件描述符,而event是發生的事件。event是一個位掩碼,這意味着它是一個整數,其各個位對應於不同的事件。各個事件使用select模塊中的常量表示。要檢查指定的位是否爲1,可以直接使用按位與運算符(&)。select模塊中的輪詢事件常量如下:
實例代碼如下:
import socket, select
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
fdmap = {s.fileno(): s}
s.listen(5)
p = select.poll()
p.register(s)
while True:
events = p.poll()
for fd, event in events:
if fd in fdmap:
c, addr = s.accept()
print('Got connection from', addr)
p.register(c)
fdmap[c.fileno()] = c
elif event & select.POLLIN:
data = fdmap[fd].recv(1024)
if not data: # 沒有數據 --連接已關閉
print(fdmap[fd].getpeername(), 'disconnected')
p.unregister(fd)
del fdmap[fd]
else:
print(data)
1.4 Twisted
Twisted是一個事件驅動的Python網絡框架,最初是爲編寫網絡遊戲開發的,但是現在被各種網絡軟件使用。Twisted支持許多常見的傳輸及應用層協議,包括TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC以及FTP。Twisted對於其支持的所有協議都帶有客戶端和服務器實現,同時附帶有基於命令行的工具,使得配置和部署產品級的Twisted應用變得非常方便。如果想要深入地瞭解,可參閱網站http://twistedmatrix.com的在線文檔。
2.屏幕抓取
屏幕抓取是通過程序下載網頁並從中提取信息的過程,這種技術很有用,在網頁中有我們在程序中使用的信息時,就可以使用它。例如,我們可以使用前面介紹的urllib來獲取網頁的HTML代碼,再使用正則表達式或其他技術從中提取信息,示例代碼如下:
from urllib.request import urlopen
import re
p = re.compile('<a href="(/jobs/\\d+)/">(.*?)</a>')
text = urlopen('http://python.org/jobs').read().decode()
for url, name in p.findall(text):
print('{} ({})'.format(name, url))
上面的代碼中,正則表達式依賴於HTML代碼的細節,而不是更抽象的結構。這意味着只要網頁的結構發生細微的變化,改程序可能就不管用。針對正則表達式存在的問題,有兩種解決方法:一是結合使用程序Tidy和XHTML解析;二是使用專門爲屏幕抓取而設計的Beautiful Soup庫。
2.1 Tidy和XHTML解析
XHTML和舊式HTML的主要區別在於,XHTML非常嚴格,要求顯式地結束所有的元素。因此在HTML中,可以通過一個標籤開始與結束段落,但在XHTML中,必須顯式地開始與結束當前段落。這讓XHTML解析起來非常方便,而且XHTML是一種標準的XML格式,可以直接使用XML工具進行解析。
當然,如果對於格式不正確且不嚴謹的HTML,Tidy可以幫助我們進行修復。
2.1.1 Tidy
假設我們有一個混亂的HTML文件messy.html,格式如下:
<h1>Pet Shop
<h2>Complaints</h3>
<p>There is <b>no <i>way</b> at all</i> we can accept returned
parrots.
<h1><i>Dead Pets</h1>
<p>Our pets may tend to rest at times, but rarely die within the
warranty period.
<i><h2>News</h2></i>
<p>We have just received <b>a really nice parrot.
<p>It's really nice.</b>
<h3><hr>The Norwegian Blue</h3>
<h4>Plumage and <hr>pining behavior</h4>
<a href="#norwegian-blue">More information<a>
<p>Features:
<body>
<li>Beautiful plumage
使用Tidy進行修復,修復代碼如下:
from subprocess import Popen, PIPE
text = open(r'd:\test\mess.html').read()
tidy = Popen('tidy', stdin=PIPE, stdout=PIPE, stderr=PIPE)
tidy.stdin.write(text.encode())
tidy.stdin.close()
print(tidy.stdout.read().decode())
2.1.2 HTMLParser
要對Tidy生成的格式良好的XHTML進行解析,一種非常簡單的方式是使用標準庫模塊html.parser中的HTMLParser類
2.2 Beautiful Soup
Beautiful Soup是一個小巧而出色的模塊,用於解析我們在Web上可能遇到的不嚴謹且格式糟糕的HTML。實例代碼如下:
from urllib.request import urlopen
from bs4 import BeautifulSoup
text = urlopen('https://www.python.org/jobs/').read()
soup = BeautifulSoup(text, 'html.parser')
jobs = set()
for job in soup.body.section('h2'):
jobs.add('{}({})'.format(job.a.string, job.a['href']))
print('\n'.join(sorted(jobs, key=str.lower)))
我們使用要從中抓取文本的HTML代碼實例化Beautiful Soup類,然後提取解析樹的不同部位。例如,使用soup.body來獲取文檔體,再訪問其中的第一個section。使用參數‘h2’調用返回的對象,這與find_all等效——返回其中的所有h2元素。每個h2都表示一個職位,而我們提取的是它包含的第一個鏈接job.a。屬性string是鏈接的文本內容,而a['href']爲屬性href。