《深入Python》-11. HTTP Web 服务
《深入Python》-11. HTTP Web 服務
11. HTTP Web 服務
出處: http://www.woodpecker.org.cn/diveintopython/http_web_services/index.html
第?11?章?HTTP Web 服務
- 11.1. 概覽
- 11.2. 避免通過 HTTP 重復地獲取數據
- 11.3. HTTP 的特性
- 11.3.1. 用戶代理 (User-Agent)
- 11.3.2. 重定向 (Redirects)
- 11.3.3. Last-Modified/If-Modified-Since
- 11.3.4. ETag/If-None-Match
- 11.3.5. 壓縮 (Compression)
- 11.4. 調試 HTTP web 服務
- 11.5. 設置 User-Agent
- 11.6. 處理 Last-Modified 和 ETag
- 11.7. 處理重定向
- 11.8. 處理壓縮數據
- 11.9. 全部放在一起
- 11.10. 小結
11.1.?概覽
在講解如何下載 web 頁和如何從 URL 解析 XML時,你已經學習了關于 HTML 處理和 XML 處理,接下來讓我們來更全面地探討有關 HTTP web 服務的主題。
簡單地講,HTTP web 服務是指以編程的方式直接使用 HTTP 操作從遠程服務器發送和接收數據。如果你要從服務器獲取數據,直接使用 HTTP GET;如果您想發送新數據到服務器,使用 HTTP POST。(一些較高級的 HTTP web 服務 API 也定義了使用 HTTP PUT 和 HTTP DELETE 修改和刪除現有數據的方法。) 換句話說,構建在 HTTP 協議中的 “verbs (動作)” (GET, POST, PUT 和 DELETE) 直接映射為接收、發送、修改和刪除等應用級別的操作。
這種方法的主要優點是簡單,并且許多不同的站點充分印證了這樣的簡單性是受歡迎的。數據 (通常是 XML 數據) 能靜態創建和存儲,或通過服務器端腳本和所有主流計算機語言 (包括用于下載數據的 HTTP 庫) 動態生成。調試也很簡單,因為您可以在任意瀏覽器中調用網絡服務來查看這些原始數據。現代瀏覽器甚至可以為您進行良好的格式化并漂亮地打印這些 XML 數據,以便讓您快速地瀏覽。
HTTP web 服務上的純 XML 應用舉例:
- Amazon API 允許您從 Amazon.com 在線商店獲取產品信息。
- National Weather Service (美國) 和 Hong Kong Observatory (香港) 通過 web 服務提供天氣警報。
- Atom API 用來管理基于 web 的內容。
- Syndicated feeds 應用于 weblogs 和新聞站點中帶給您來自眾多站點的最新消息。
在后面的幾章里,我們將探索使用 HTTP 進行數據發送和接收傳輸的 API,但是不會將應用語義映射到潛在的 HTTP 語義。(所有這些都是通過 HTTP POST 這個管道完成的。) 但是本章將關注使用 HTTP GET 從遠程服務器獲取數據,并且將探索幾個由純 HTTP web 服務帶來最大利益的 HTTP 特性。
如下所示為上一章曾經看到過的 openanything 模塊的更高級版本:
例?11.1.?openanything.py
如果您還沒有下載本書附帶的樣例程序, 可以 下載本程序和其他樣例程序。
import urllib2, urlparse, gzip from StringIO import StringIOUSER_AGENT = 'OpenAnything/1.0 +http://diveintopython.org/http_web_services/'class SmartRedirectHandler(urllib2.HTTPRedirectHandler): def http_error_301(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers) result.status = code return result def http_error_302(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers) result.status = code return result class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): def http_error_default(self, req, fp, code, msg, headers):result = urllib2.HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT):'''URL, filename, or string --> streamThis function lets you define parsers that take any input source(URL, pathname to local or network file, or actual data as a string)and deal with it in a uniform manner. Returned object is guaranteedto have all the basic stdio read methods (read, readline, readlines).Just .close() the object when you're done with it.If the etag argument is supplied, it will be used as the value of anIf-None-Match request header.If the lastmodified argument is supplied, it must be a formatteddate/time string in GMT (as returned in the Last-Modified header ofa previous request). The formatted date/time will be usedas the value of an If-Modified-Since request header.If the agent argument is supplied, it will be used as the value of aUser-Agent request header.'''if hasattr(source, 'read'):return sourceif source == '-':return sys.stdinif urlparse.urlparse(source)[0] == 'http': # open URL with urllib2 request = urllib2.Request(source) request.add_header('User-Agent', agent) if etag: request.add_header('If-None-Match', etag) if lastmodified: request.add_header('If-Modified-Since', lastmodified) request.add_header('Accept-encoding', 'gzip') opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler())return opener.open(request) # try to open with native open function (if source is a filename)try:return open(source)except (IOError, OSError):pass# treat source as stringreturn StringIO(str(source))def fetch(source, etag=None, last_modified=None, agent=USER_AGENT): '''Fetch data and metadata from a URL, file, stream, or string'''result = {} f = openAnything(source, etag, last_modified, agent) result['data'] = f.read() if hasattr(f, 'headers'): # save ETag, if the server sent one result['etag'] = f.headers.get('ETag') # save Last-Modified header, if the server sent one result['lastmodified'] = f.headers.get('Last-Modified') if f.headers.get('content-encoding', '') == 'gzip': # data came back gzip-compressed, decompress it result['data'] = gzip.GzipFile(fileobj=StringIO(result['data']])).read()if hasattr(f, 'url'): result['url'] = f.url result['status'] = 200 if hasattr(f, 'status'): result['status'] = f.status f.close() return result進一步閱讀
- Paul Prescod 認為純 HTTP web 服務是 Internet 的未來。
11.2.?避免通過 HTTP 重復地獲取數據
假如說你想用 HTTP 下載資源,例如一個 Atom feed 匯聚。你不僅僅想下載一次;而是想一次又一次地下載它,如每小時一次,從提供 news feed 的站點獲得最新的消息。讓我們首先用一種直接而原始的方法來實現它,然后看看如何改進它。
例?11.2.?用直接而原始的方法下載 feed
>>> import urllib >>> data = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read() >>> print data <?xml version="1.0" encoding="iso-8859-1"?> <feed version="0.3"xmlns="http://purl.org/atom/ns#"xmlns:dc="http://purl.org/dc/elements/1.1/"xml:lang="en"><title mode="escaped">dive into mark</title><link rel="alternate" type="text/html" href="http://diveintomark.org/"/><-- rest of feed omitted for brevity -->11.3.?HTTP 的特性
- 11.3.1. 用戶代理 (User-Agent)
- 11.3.2. 重定向 (Redirects)
- 11.3.3. Last-Modified/If-Modified-Since
- 11.3.4. ETag/If-None-Match
- 11.3.5. 壓縮 (Compression)
這里有五個你必須關注的 HTTP 重要特性。
11.3.1.?用戶代理 (User-Agent)
User-Agent 是一種客戶端告知服務器誰在什么時候通過 HTTP 請求了一個 web 頁、feed 匯聚或其他類型的 web 服務的簡單途徑。當客戶端請求一個資源時,應該盡可能明確發起請求的是誰,以便當產生異常錯誤時,允許服務器端的管理員與客戶端的開發者取得聯系。
默認情況下 Python 發送一個通用的 User-Agent:Python-urllib/1.15。下一節,您將看到更加有針對性的 User-Agent。
11.3.2.?重定向 (Redirects)
有時資源移來移去。Web 站點重組內容,頁面移動到了新的地址。甚至是 web 服務重組。原來位于 http://example.com/index.xml 的 feed 匯聚可能被移動到 http://example.com/xml/atom.xml。或者因為一個機構的擴展或重組,整個域被遷移。例如,http://www.example.com/index.xml 可能被重定向到 http://server-farm-1.example.com/index.xml。
您每次從 HTTP 服務器請求任何類型的資源時,服務器的響應中均包含一個狀態代碼。狀態代碼 200 的意思是 “一切正常,這就是您請求的頁面”。狀態代碼 404 的意思是 “頁面沒找到”。 (當瀏覽 web 時,你可能看到過 404 errors。)
HTTP 有兩種不同的方法表示資源已經被移動。狀態代碼 302 表示臨時重定向;這意味著 “哎呀,訪問內容被臨時移動” (然后在 Location: 頭信息中給出臨時地址)。狀態代碼 301 表示永久重定向;這意味著 “哎呀,訪問內容被永久移動” (然后在 Location: 頭信息中給出新地址)。如果您獲得了一個 302 狀態代碼和一個新地址,HTTP 規范說您應該使用新地址獲取您的請求,但是下次您要訪問同一資源時,應該使用原地址重試。但是如果您獲得了一個 301 狀態代碼和一個新地址,您應該從此使用新地址。
當從 HTTP 服務器接受到一個適當的狀態代碼時,urllib.urlopen 將自動 “跟蹤” 重定向,但不幸的是,當它做了重定向時不會告訴你。 你將最終獲得所請求的數據,卻絲毫不會察覺到在這個過程中一個潛在的庫 “幫助” 你做了一次重定向操作。因此你將繼續不斷地使用舊地址,并且每次都將獲得被重定向的新地址。這一過程要往返兩次而不是一次:太沒效率了!本章的后面,您將看到如何改進這一點,從而適當地且有效率地處理永久重定向。
11.3.3.?Last-Modified/If-Modified-Since
有些數據隨時都在變化。CNN.com 的主頁經常幾分鐘就更新。另一方面,Google.com 的主頁幾個星期才更新一次 (當他們上傳特殊的假日 logo,或為一個新服務作廣告時)。 Web 服務是不變的:通常服務器知道你所請求的數據的最后修改時間,并且 HTTP 為服務器提供了一種將最近修改數據連同你請求的數據一同發送的方法。
如果你第二次 (或第三次,或第四次) 請求相同的數據,你可以告訴服務器你上一次獲得的最后修改日期:在你的請求中發送一個 If-Modified-Since 頭信息,它包含了上一次從服務器連同數據所獲得的日期。如果數據從那時起沒有改變,服務器將返回一個特殊的 HTTP 狀態代碼 304,這意味著 “從上一次請求后這個數據沒有改變”。這一點有何進步呢?當服務器發送狀態編碼 304 時,不再重新發送數據。您僅僅獲得了這個狀態代碼。所以當數據沒有更新時,你不需要一次又一次地下載相同的數據;服務器假定你有本地的緩存數據。
所有現代的瀏覽器都支持最近修改 (last-modified) 的數據檢查。如果你曾經訪問過某頁,一天后重新訪問相同的頁時發現它沒有變化,并奇怪第二次訪問時頁面加載得如此之快——這就是原因所在。你的瀏覽器首次 訪問時會在本地緩存頁面內容,當你第二次訪問,瀏覽器自動發送首次訪問時從服務器獲得的最近修改日期。服務器簡單地返回 304: Not Modified (沒有修改),因此瀏覽器就會知道從本地緩存加載頁面。在這一點上,Web 服務也如此智能。
Python 的 URL 庫沒有提供內置的最近修改數據檢查支持,但是你可以為每一個請求添加任意的頭信息并在每一個響應中讀取任意頭信息,從而自己添加這種支持。
11.3.4.?ETag/If-None-Match
ETag 是實現與最近修改數據檢查同樣的功能的另一種方法:沒有變化時不重新下載數據。其工作方式是:服務器發送你所請求的數據的同時,發送某種數據的 hash (在 ETag 頭信息中給出)。hash 的確定完全取決于服務器。當第二次請求相同的數據時,你需要在 If-None-Match: 頭信息中包含 ETag hash,如果數據沒有改變,服務器將返回 304 狀態代碼。與最近修改數據檢查相同,服務器僅僅 發送 304 狀態代碼;第二次將不為你發送相同的數據。在第二次請求時,通過包含 ETag hash,你告訴服務器:如果 hash 仍舊匹配就沒有必要重新發送相同的數據,因為你還有上一次訪問過的數據。
Python 的 URL 庫沒有對 ETag 的內置支持,但是在本章后面你將看到如何添加這種支持。
11.3.5.?壓縮 (Compression)
最后一個重要的 HTTP 特性是 gzip 壓縮。 關于 HTTP web 服務的主題幾乎總是會涉及在網絡線路上傳輸的 XML。XML 是文本,而且還是相當冗長的文本,而文本通常可以被很好地壓縮。當你通過 HTTP 請求一個資源時,可以告訴服務器,如果它有任何新數據要發送給我時,請以壓縮的格式發送。在你的請求中包含 Accept-encoding: gzip 頭信息,如果服務器支持壓縮,它將返回由 gzip 壓縮的數據并且使用 Content-encoding: gzip 頭信息標記。
Python 的 URL 庫本身沒有內置對 gzip 壓縮的支持,但是你能為請求添加任意的頭信息。Python 還提供了一個獨立的 gzip 模塊,它提供了對數據進行解壓縮的功能。
注意我們用于下載 feed 匯聚的小單行腳本并不支持任何這些 HTTP 特性。讓我們來看看如何改善它。
11.4.?調試 HTTP web 服務
首先,讓我們開啟 Python HTTP 庫的調試特性并查看網絡線路上的傳輸過程。這對本章的全部內容都很有用,因為你將添加越來越多的特性。
例?11.3.?調試 HTTP
>>> import httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> import urllib >>> feeddata = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read() connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/1.15 ' reply: 'HTTP/1.1 200 OKrn' header: Date: Wed, 14 Apr 2004 22:27:30 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT header: ETag: "e8284-68e0-4de30f80" header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close11.5.?設置 User-Agent
改善你的 HTTP web 服務客戶端的第一步就是用 User-Agent 適當地鑒別你自己。為了做到這一點,你需要遠離基本的 urllib 而深入到 urllib2。
例?11.4.?urllib2 介紹
>>> import httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> import urllib2 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> opener = urllib2.build_opener() >>> feeddata = opener.open(request).read() connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OKrn' header: Date: Wed, 14 Apr 2004 23:23:12 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT header: ETag: "e8284-68e0-4de30f80" header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close11.6.?處理 Last-Modified 和 ETag
既然你知道如何在你的 web 服務請求中添加自定義的 HTTP 頭信息,接下來看看如何添加 Last-Modified 和 ETag 頭信息的支持。
下面的這些例子將以調試標記置為關閉的狀態來顯示輸出結果。如果你還停留在上一部分的開啟狀態,可以使用 httplib.HTTPConnection.debuglevel = 0 將其設置為關閉狀態。或者,如果你認為有幫助也可以保持為開啟狀態。
例?11.6.?測試 Last-Modified
>>> import urllib2 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> opener = urllib2.build_opener() >>> firstdatastream = opener.open(request) >>> firstdatastream.headers.dict {'date': 'Thu, 15 Apr 2004 20:42:41 GMT', 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 'content-type': 'application/atom+xml','last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'etag': '"e842a-3e53-55d97640"','content-length': '15955', 'accept-ranges': 'bytes', 'connection': 'close'} >>> request.add_header('If-Modified-Since', ... firstdatastream.headers.get('Last-Modified')) >>> seconddatastream = opener.open(request) Traceback (most recent call last):File "<stdin>", line 1, in ?File "c:python23liburllib2.py", line 326, in open'_open', req)File "c:python23liburllib2.py", line 306, in _call_chainresult = func(*args)File "c:python23liburllib2.py", line 901, in http_openreturn self.do_open(httplib.HTTP, req)File "c:python23liburllib2.py", line 895, in do_openreturn self.parent.error('http', req, fp, code, msg, hdrs)File "c:python23liburllib2.py", line 352, in errorreturn self._call_chain(*args)File "c:python23liburllib2.py", line 306, in _call_chainresult = func(*args)File "c:python23liburllib2.py", line 412, in http_error_defaultraise HTTPError(req.get_full_url(), code, msg, hdrs, fp) urllib2.HTTPError: HTTP Error 304: Not Modified11.7.?處理重定向
你可以使用兩種不同的自定義 URL 處理器來處理永久重定向和臨時重定向。
首先,讓我們來看看重定向處理的必要性。
例?11.10.?沒有重定向處理的情況下,訪問 web 服務
>>> import urllib2, httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> request = urllib2.Request( ... 'http://diveintomark.org/redir/example301.xml') >>> opener = urllib2.build_opener() >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /redir/example301.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 301 Moved Permanentlyrn' header: Date: Thu, 15 Apr 2004 22:06:25 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Location: http://diveintomark.org/xml/atom.xml header: Content-Length: 338 header: Connection: close header: Content-Type: text/html; charset=iso-8859-1 connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OKrn' header: Date: Thu, 15 Apr 2004 22:06:25 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Content-Length: 15955 header: Connection: close header: Content-Type: application/atom+xml >>> f.url 'http://diveintomark.org/xml/atom.xml' >>> f.headers.dict {'content-length': '15955', 'accept-ranges': 'bytes', 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'connection': 'close', 'etag': '"e842a-3e53-55d97640"', 'date': 'Thu, 15 Apr 2004 22:06:25 GMT', 'content-type': 'application/atom+xml'} >>> f.status Traceback (most recent call last):File "<stdin>", line 1, in ? AttributeError: addinfourl instance has no attribute 'status'11.8.?處理壓縮數據
你要支持的最后一個重要的 HTTP 特性是壓縮。許多 web 服務具有發送壓縮數據的能力,這可以將網絡線路上傳輸的大量數據消減 60% 以上。這尤其適用于 XML web 服務,因為 XML 數據 的壓縮率可以很高。
服務器不會為你發送壓縮數據,除非你告訴服務器你可以處理壓縮數據。
例?11.14.?告訴服務器你想獲得壓縮數據
>>> import urllib2, httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> request.add_header('Accept-encoding', 'gzip') >>> opener = urllib2.build_opener() >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 Accept-encoding: gzip ' reply: 'HTTP/1.1 200 OKrn' header: Date: Thu, 15 Apr 2004 22:24:39 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Vary: Accept-Encoding header: Content-Encoding: gzip header: Content-Length: 6289 header: Connection: close header: Content-Type: application/atom+xml11.9.?全部放在一起
你已經看到了構造一個智能的 HTTP web 客戶端的所有片斷。現在讓我們看看如何將它們整合到一起。
例?11.17.?openanything 函數
這個函數定義在 openanything.py 中。
def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT):# non-HTTP code omitted for brevityif urlparse.urlparse(source)[0] == 'http': # open URL with urllib2 request = urllib2.Request(source) request.add_header('User-Agent', agent) if etag: request.add_header('If-None-Match', etag) if lastmodified: request.add_header('If-Modified-Since', lastmodified) request.add_header('Accept-encoding', 'gzip') opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler()) return opener.open(request)11.10.?小結
openanything.py 及其函數現在可以完美地工作了。
每個客戶端都應該支持 HTTP web 服務的以下 5 個重要特性:
- 通過設置適當的 User-Agent 識別你的應用。
- 適當地處理永久重定向。
- 支持 Last-Modified 日期檢查從而避免在數據未改變的情況下重新下載數據。
- 支持 ETag hash 從而避免在數據未改變的情況下重新下載數據。
- 支持 gzip 壓縮從而在數據已經 改變的情況下盡可能地減少傳輸帶寬。
總結
以上是生活随笔為你收集整理的《深入Python》-11. HTTP Web 服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .Net Framework 4.0 中
- 下一篇: fieldset在ie8下的margin