網絡編程

目錄

1.網絡模塊

1.1 模塊socket

1.2 模塊urllib和urllib2

1.2.1 打開遠程文件

1.2.2 獲取遠程文件

1.2.3 SocketServer及相關類

1.3 多個連接

1.3.1 使用SocketServer實現分叉和線程化

1.3.2 使用select和poll實現異步IO

1.4 Twisted

2.屏幕抓取

2.1 Tidy和XHTML解析

2.1.1 Tidy

2.1.2 HTMLParser

2.2 Beautiful Soup


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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章