如何利用 urllib 包獲取網(wǎng)絡(luò )資源?
備注
這份 HOWTO 文檔的早期版本有一份法語(yǔ)的譯文,可在 urllib2 - Le Manuel manquant 處查閱。
概述?
urllib.request 是用于獲取 URL (統一資源定位符)的 Python 模塊。它以 urlopen 函數的形式提供了一個(gè)非常簡(jiǎn)單的接口,能用不同的協(xié)議獲取 URL。同時(shí)它還為處理各種常見(jiàn)情形提供了一個(gè)稍微復雜一些的接口——比如:基礎身份認證、cookies、代理等等。這些功能是由名為 handlers 和 opener 的對象提供的。
urllib.request 支持多種 "URL 方案" (通過(guò) URL中 ":"
之前的字符串加以區分——如 "ftp://python.org/"` 中的 ``"ftp"`
)即為采用其關(guān)聯(lián)網(wǎng)絡(luò )協(xié)議(FTP、HTTP 之類(lèi))的 URL 方案 。本教程重點(diǎn)關(guān)注最常用的 HTTP 場(chǎng)景。
對于簡(jiǎn)單場(chǎng)景而言, urlopen 用起來(lái)十分容易。但只要在打開(kāi) HTTP URL 時(shí)遇到錯誤或非常情況,就需要對超文本傳輸協(xié)議有所了解才行。最全面、最權威的 HTTP 參考是 RFC 2616 。那是一份技術(shù)文檔,并沒(méi)有追求可讀性。本 文旨在說(shuō)明 urllib 的用法,為了便于閱讀也附帶了足夠詳細的 HTTP 信息。本文并不是為了替代 urllib.request
文檔,只是其補充說(shuō)明而已。
獲取 URL 資源?
urllib.request 最簡(jiǎn)單的使用方式如下所示:
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
html = response.read()
如果想通過(guò) URL 獲取資源并臨時(shí)存儲一下,可以采用 shutil.copyfileobj()
和 tempfile.NamedTemporaryFile()
函數:
import shutil
import tempfile
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
shutil.copyfileobj(response, tmp_file)
with open(tmp_file.name) as html:
pass
urllib 的很多用法就是這么簡(jiǎn)單(注意 URL 不僅可以 http: 開(kāi)頭,還可以是 ftp: 、file: 等)。不過(guò)本教程的目的是介紹更加復雜的應用場(chǎng)景,重點(diǎn)還是關(guān)注 HTTP。
HTTP 以請求和響應為基礎——客戶(hù)端生成請求,服務(wù)器發(fā)送響應。urllib.request 用 Request
對象來(lái)表示要生成的 HTTP 請求。最簡(jiǎn)單的形式就是創(chuàng )建一個(gè) Request 對象,指定了想要獲取的 URL。用這個(gè) Request 對象作為參數調用``urlopen`` ,將會(huì )返回該 URL 的響應對象。響應對象類(lèi)似于文件對象,就是說(shuō)可以對其調用 .read()
之類(lèi)的命令:
import urllib.request
req = urllib.request.Request('http://www.voidspace.org.uk')
with urllib.request.urlopen(req) as response:
the_page = response.read()
請注意,urllib.request 用同一個(gè) Request 接口處理所有 URL 方案。比如可生成 FTP 請求如下:
req = urllib.request.Request('ftp://example.com/')
就 HTTP 而言,Request 對象能夠做兩件額外的事情:首先可以把數據傳給服務(wù)器。其次,可以將 有關(guān) 數據或請求本身的額外信息(metadata)傳給服務(wù)器——這些信息將會(huì )作為 HTTP “頭部”數據發(fā)送。下面依次看下。
數據?
有時(shí)需要向某個(gè) URL 發(fā)送數據,通常此 URL 會(huì )指向某個(gè)CGI(通用網(wǎng)關(guān)接口)腳本或其他 web 應用。對于 HTTP 而言,這通常會(huì )用所謂的 POST 請求來(lái)完成。當要把 Web 頁(yè)填寫(xiě)的 HTML 表單提交時(shí),瀏覽器通常會(huì )執行此操作。但并不是所有的 POST 都來(lái)自表單:可以用 POST 方式傳輸任何數據到自己的應用上。對于通常的 HTML 表單,數據需要以標準的方式編碼,然后作為 data
參數傳給 Request 對象。編碼過(guò)程是用 urllib.parse
庫的函數完成的:
import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
'location' : 'Northampton',
'language' : 'Python' }
data = urllib.parse.urlencode(values)
data = data.encode('ascii') # data should be bytes
req = urllib.request.Request(url, data)
with urllib.request.urlopen(req) as response:
the_page = response.read()
請注意,有時(shí)還需要采用其他編碼,比如由 HTML 表單上傳文件——更多細節請參見(jiàn) HTML 規范,提交表單 。
如果不傳遞 data
參數,urllib 將采用 GET 請求。GET 和 POST 請求有一點(diǎn)不同,POST 請求往往具有“副作用”,他們會(huì )以某種方式改變系統的狀態(tài)。例如,從網(wǎng)站下一個(gè)訂單,購買(mǎi)一大堆罐裝垃圾并運送到家。 盡管 HTTP 標準明確指出 POST 總是 要導致副作用,而 GET 請求 從來(lái)不會(huì ) 導致副作用。但沒(méi)有什么辦法能阻止 GET 和 POST 請求的副作用。數據也可以在 HTTP GET 請求中傳遞,只要把數據編碼到 URL 中即可。
做法如下所示:
>>> import urllib.request
>>> import urllib.parse
>>> data = {}
>>> data['name'] = 'Somebody Here'
>>> data['location'] = 'Northampton'
>>> data['language'] = 'Python'
>>> url_values = urllib.parse.urlencode(data)
>>> print(url_values) # The order may differ from below.
name=Somebody+Here&language=Python&location=Northampton
>>> url = 'http://www.example.com/example.cgi'
>>> full_url = url + '?' + url_values
>>> data = urllib.request.urlopen(full_url)
請注意,完整的 URL 是通過(guò)在其中添加 ?
創(chuàng )建的,后面跟著(zhù)經(jīng)過(guò)編碼的數據。
HTTP 頭部信息?
下面介紹一個(gè)具體的 HTTP 頭部信息,以此說(shuō)明如何在 HTTP 請求加入頭部信息。
有些網(wǎng)站 1 不愿被程序瀏覽到,或者要向不同的瀏覽器發(fā)送不同版本 2 的網(wǎng)頁(yè)。默認情況下,urllib 將自身標識為“Python-urllib/xy”(其中 x
、 y
是 Python 版本的主、次版本號,例如 Python-urllib/2.5
),這可能會(huì )讓網(wǎng)站不知所措,或者干脆就使其無(wú)法正常工作。瀏覽器是通過(guò)頭部信息 User-Agent
3 來(lái)標識自己的。在創(chuàng )建 Request 對象時(shí),可以傳入字典形式的頭部信息。以下示例將生成與之前相同的請求,只是將自身標識為某個(gè)版本的 Internet Explorer 4 :
import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'
values = {'name': 'Michael Foord',
'location': 'Northampton',
'language': 'Python' }
headers = {'User-Agent': user_agent}
data = urllib.parse.urlencode(values)
data = data.encode('ascii')
req = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(req) as response:
the_page = response.read()
響應對象也有兩個(gè)很有用的方法。請參閱有關(guān) info 和 geturl 部分,了解出現問(wèn)題時(shí)會(huì )發(fā)生什么。
異常的處理?
如果 urlopen 無(wú)法處理響應信息,就會(huì )觸發(fā) URLError
。盡管與通常的 Python API 一樣,也可能觸發(fā) ValueError
、 TypeError
等內置異常。
HTTPError
是 URLError
的子類(lèi),當 URL 是 HTTP 的情況時(shí)將會(huì )觸發(fā)。
上述異常類(lèi)是從 urllib.error
模塊中導出的。
URLError?
觸發(fā) URLError 的原因,通常是網(wǎng)絡(luò )不通(或者沒(méi)有到指定服務(wù)器的路由),或者指定的服務(wù)器不存在。這時(shí)觸發(fā)的異常會(huì )帶有一個(gè) reason 屬性,是一個(gè)包含錯誤代碼和文本錯誤信息的元組。
例如:
>>> req = urllib.request.Request('http://www.pretend_server.org')
>>> try: urllib.request.urlopen(req)
... except urllib.error.URLError as e:
... print(e.reason)
...
(4, 'getaddrinfo failed')
HTTPError?
從服務(wù)器返回的每個(gè) HTTP 響應都包含一個(gè)數字的 “狀態(tài)碼”。有時(shí)該狀態(tài)碼表明服務(wù)器無(wú)法完成該請求。默認的處理函數將會(huì )處理這其中的一部分響應。如若響應是“redirection”,這是要求客戶(hù)端從另一 URL 處獲取數據,urllib 將會(huì )自行處理。對于那些無(wú)法處理的狀況,urlopen 將會(huì )引發(fā) HTTPError
。典型的錯誤包括:“404”(頁(yè)面無(wú)法找到)、“403”(請求遭拒絕)和“401”(需要身份認證)。
全部的 HTTP 錯誤碼請參閱 RFC 2616 。
HTTPError
實(shí)例將包含一個(gè)整數型的“code”屬性,對應于服務(wù)器發(fā)來(lái)的錯誤。
錯誤代碼?
由于默認處理函數會(huì )自行處理重定向(300 以?xún)鹊腻e誤碼),而且 100--299 的狀態(tài)碼表示成功,因此通常只會(huì )出現 400--599 的錯誤碼。
http.server.BaseHTTPRequestHandler.responses
是很有用的響應碼字典,其中給出了 RFC 2616 用到的所有響應代碼。為方便起見(jiàn),將此字典轉載如下:
# Table mapping response codes to messages; entries have the
# form {code: (shortmessage, longmessage)}.
responses = {
100: ('Continue', 'Request received, please continue'),
101: ('Switching Protocols',
'Switching to new protocol; obey Upgrade header'),
200: ('OK', 'Request fulfilled, document follows'),
201: ('Created', 'Document created, URL follows'),
202: ('Accepted',
'Request accepted, processing continues off-line'),
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
204: ('No Content', 'Request fulfilled, nothing follows'),
205: ('Reset Content', 'Clear input form for further input.'),
206: ('Partial Content', 'Partial content follows.'),
300: ('Multiple Choices',
'Object has several resources -- see URI list'),
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
302: ('Found', 'Object moved temporarily -- see URI list'),
303: ('See Other', 'Object moved -- see Method and URL list'),
304: ('Not Modified',
'Document has not changed since given time'),
305: ('Use Proxy',
'You must use proxy specified in Location to access this '
'resource.'),
307: ('Temporary Redirect',
'Object moved temporarily -- see URI list'),
400: ('Bad Request',
'Bad request syntax or unsupported method'),
401: ('Unauthorized',
'No permission -- see authorization schemes'),
402: ('Payment Required',
'No payment -- see charging schemes'),
403: ('Forbidden',
'Request forbidden -- authorization will not help'),
404: ('Not Found', 'Nothing matches the given URI'),
405: ('Method Not Allowed',
'Specified method is invalid for this server.'),
406: ('Not Acceptable', 'URI not available in preferred format.'),
407: ('Proxy Authentication Required', 'You must authenticate with '
'this proxy before proceeding.'),
408: ('Request Timeout', 'Request timed out; try again later.'),
409: ('Conflict', 'Request conflict.'),
410: ('Gone',
'URI no longer exists and has been permanently removed.'),
411: ('Length Required', 'Client must specify Content-Length.'),
412: ('Precondition Failed', 'Precondition in headers is false.'),
413: ('Request Entity Too Large', 'Entity is too large.'),
414: ('Request-URI Too Long', 'URI is too long.'),
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
416: ('Requested Range Not Satisfiable',
'Cannot satisfy request range.'),
417: ('Expectation Failed',
'Expect condition could not be satisfied.'),
500: ('Internal Server Error', 'Server got itself in trouble'),
501: ('Not Implemented',
'Server does not support this operation'),
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
503: ('Service Unavailable',
'The server cannot process the request due to a high load'),
504: ('Gateway Timeout',
'The gateway server did not receive a timely response'),
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
}
當觸發(fā)錯誤時(shí),服務(wù)器通過(guò)返回 HTTP 錯誤碼 和 錯誤頁(yè)面進(jìn)行響應??梢詫?HTTPError
實(shí)例用作返回頁(yè)面的響應。這意味著(zhù)除了 code 屬性之外,錯誤對象還像 urllib.response
模塊返回的那樣具有 read、geturl 和 info 方法:
>>> req = urllib.request.Request('http://www.python.org/fish.html')
>>> try:
... urllib.request.urlopen(req)
... except urllib.error.HTTPError as e:
... print(e.code)
... print(e.read())
...
404
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n\n\n<html
...
<title>Page Not Found</title>\n
...
總之?
若要準備處理 HTTPError
或 URLError
,有兩種簡(jiǎn)單的方案。推薦使用第二種方案。
第一種方案?
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
req = Request(someurl)
try:
response = urlopen(req)
except HTTPError as e:
print('The server couldn\'t fulfill the request.')
print('Error code: ', e.code)
except URLError as e:
print('We failed to reach a server.')
print('Reason: ', e.reason)
else:
# everything is fine
備注
except HTTPError
必須 首先處理,否則 except URLError
將會(huì ) 同時(shí) 捕獲 HTTPError
。
第二種方案?
from urllib.request import Request, urlopen
from urllib.error import URLError
req = Request(someurl)
try:
response = urlopen(req)
except URLError as e:
if hasattr(e, 'reason'):
print('We failed to reach a server.')
print('Reason: ', e.reason)
elif hasattr(e, 'code'):
print('The server couldn\'t fulfill the request.')
print('Error code: ', e.code)
else:
# everything is fine
info 和 geturl 方法?
由 urlopen (或者 HTTPError
實(shí)例)所返回的響應包含兩個(gè)有用的方法: info()
和 geturl()
,該響應由模塊 urllib.response
定義。
geturl ——返回所獲取頁(yè)面的真實(shí) URL。該方法很有用,因為 urlopen
(或 opener 對象)可能已經(jīng)經(jīng)過(guò)了一次重定向。已獲取頁(yè)面的 URL 未必就是所請求的 URL 。
info - 該方法返回一個(gè)類(lèi)似字典的對象,描述了所獲取的頁(yè)面,特別是由服務(wù)器送出的頭部信息(headers) 。目前它是一個(gè) http.client.HTTPMessage
實(shí)例。
典型的 HTTP 頭部信息包括“Content-length”、“Content-type”等。有關(guān) HTTP 頭部信息的清單,包括含義和用途的簡(jiǎn)要說(shuō)明,請參閱 HTTP Header 快速參考 。
Opener 和 Handler?
當獲取 URL 時(shí),會(huì )用到了一個(gè) opener(一個(gè)類(lèi)名可能經(jīng)過(guò)混淆的 urllib.request.OpenerDirector
的實(shí)例)。通常一直會(huì )用默認的 opener ——通過(guò) urlopen
——但也可以創(chuàng )建自定義的 opener 。opener 會(huì )用到 handler。所有的“繁重工作”都由 handler 完成。每種 handler 知道某種 URL 方案(http、ftp 等)的 URL 的打開(kāi)方式,或是某方面 URL 的打開(kāi)方式,例如 HTTP 重定向或 HTTP cookie。
若要用已安裝的某個(gè) handler 獲取 URL,需要創(chuàng )建一個(gè) opener 對象,例如處理 cookie 的 opener,或對重定向不做處理的 opener。
若要創(chuàng )建 opener,請實(shí)例化一個(gè) OpenerDirector
,然后重復調用 .add_handler(some_handler_instance)
。
或者也可以用 build_opener
,這是個(gè)用單次調用創(chuàng )建 opener 對象的便捷函數。build_opener
默認會(huì )添加幾個(gè) handler,不過(guò)還提供了一種快速添加和/或覆蓋默認 handler 的方法。
可能還需要其他類(lèi)型的 handler,以便處理代理、身份認證和其他常見(jiàn)但稍微特殊的情況。
install_opener
可用于讓 opener
對象成為(全局)默認 opener。這意味著(zhù)調用 urlopen
時(shí)會(huì )采用已安裝的 opener。
opener 對象帶有一個(gè) `open
方法,可供直接調用以獲取 url,方式與 urlopen
函數相同。除非是為了調用方便,否則沒(méi)必要去調用 install_opener
。
基本認證?
為了說(shuō)明 handler 的創(chuàng )建和安裝過(guò)程,會(huì )用到 HTTPBasicAuthHandler
。有關(guān)該主題的更詳細的介紹——包括基本身份認證的工作原理——請參閱 Basic Authentication Tutorial 。
如果需要身份認證,服務(wù)器會(huì )發(fā)送一條請求身份認證的頭部信息(以及 401 錯誤代碼)。這條信息中指明了身份認證方式和“安全區域(realm)”。格式如下所示:WWW-Authenticate: SCHEME realm="REALM"
。
例如
WWW-Authenticate: Basic realm="cPanel Users"
然后,客戶(hù)端應重試發(fā)起請求,請求數據中的頭部信息應包含安全區域對應的用戶(hù)名和密碼。這就是“基本身份認證”。為了簡(jiǎn)化此過(guò)程,可以創(chuàng )建 HTTPBasicAuthHandler
的一個(gè)實(shí)例及使用它的 opener。
HTTPBasicAuthHandler
用一個(gè)名為密碼管理器的對象來(lái)管理 URL、安全區域與密碼、用戶(hù)名之間的映射關(guān)系。如果知道確切的安全區域(來(lái)自服務(wù)器發(fā)送的身份認證頭部信息),那就可以用到 HTTPPasswordMgr
。通常人們并不關(guān)心安全區域是什么,這時(shí)用``HTTPPasswordMgrWithDefaultRealm`` 就很方便,允許為 URL 指定默認的用戶(hù)名和密碼。當沒(méi)有為某個(gè)安全區域提供用戶(hù)名和密碼時(shí),就會(huì )用到默認值。下面用 None
作為 add_password
方法的安全區域參數,表明采用默認用戶(hù)名和密碼。
首先需要身份認證的是頂級 URL。比傳給 .add_password() 的 URL 級別“更深”的 URL 也會(huì )得以匹配:
# create a password manager
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Add the username and password.
# If we knew the realm, we could use it instead of None.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
# create "opener" (OpenerDirector instance)
opener = urllib.request.build_opener(handler)
# use the opener to fetch a URL
opener.open(a_url)
# Install the opener.
# Now all calls to urllib.request.urlopen use our opener.
urllib.request.install_opener(opener)
備注
在以上例子中,只向 build_opener
給出了 HTTPBasicAuthHandler
。默認情況下,opener 會(huì )有用于處理常見(jiàn)狀況的 handler ——ProxyHandler
(如果設置代理的話(huà),比如設置了環(huán)境變量 http_proxy
),UnknownHandler
、HTTPHandler
、 HTTPDefaultErrorHandler
、 HTTPRedirectHandler
、 FTPHandler
、 FileHandler
、 DataHandler
、 HTTPErrorProcessor
。
top_level_url
其實(shí) 要么 是一條完整的 URL(包括 “http:” 部分和主機名及可選的端口號),比如 "http://example.com/"
, 要么 是一條“訪(fǎng)問(wèn)權限”(即主機名,及可選的端口號),比如 "example.com"
或 "example.com:8080"
(后一個(gè)示例包含了端口號)。訪(fǎng)問(wèn)權限 不得 包含“用戶(hù)信息”部分——比如 "joe:password@example.com"
就不正確。
代理?
urllib 將自動(dòng)檢測并使用代理設置。 這是通過(guò) ProxyHandler
實(shí)現的,當檢測到代理設置時(shí),是正常 handler 鏈中的一部分。通常這是一件好事,但有時(shí)也可能會(huì )無(wú)效 5。 一種方案是配置自己的 ProxyHandler
,不要定義代理。 設置的步驟與 Basic Authentication handler 類(lèi)似:
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
備注
目前 urllib.request
尚不 支持通過(guò)代理抓取 https
鏈接地址。 但此功能可以通過(guò)擴展 urllib.request 來(lái)啟用,如以下例程所示 6。
備注
如果設置了 REQUEST_METHOD
變量,則會(huì )忽略 HTTP_PROXY
;參閱 getproxies()
文檔。
套接字與分層?
Python 獲取 Web 資源的能力是分層的。urllib 用到的是 http.client
庫,而后者又用到了套接字庫。
從 Python 2.3 開(kāi)始,可以指定套接字等待響應的超時(shí)時(shí)間。這對必須要讀到網(wǎng)頁(yè)數據的應用程序會(huì )很有用。默認情況下,套接字模塊 不會(huì )超時(shí) 并且可以?huà)炱?。目前,套接字超時(shí)機制未暴露給 http.client 或 urllib.request 層使用。不過(guò)可以為所有套接字應用設置默認的全局超時(shí)。
import socket
import urllib.request
# timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)
# this call to urllib.request.urlopen now uses the default timeout
# we have set in the socket module
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)
備注?
這篇文檔由 John Lee 審訂。
- 1
例如 Google。
- 2
對于網(wǎng)站設計而言,探測不同的瀏覽器是非常糟糕的做法——更為明智的做法是采用 web 標準構建網(wǎng)站。不幸的是,很多網(wǎng)站依然向不同的瀏覽器發(fā)送不同版本的網(wǎng)頁(yè)。
- 3
MSIE 6 的 user-agent 信息是 “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)”
- 4
有關(guān) HTTP 請求的頭部信息,詳情請參閱 Quick Reference to HTTP Headers。
- 5
本人必須使用代理才能在工作中訪(fǎng)問(wèn)互聯(lián)網(wǎng)。如果嘗試通過(guò)代理獲取 localhost URL,將會(huì )遭到阻止。IE 設置為代理模式,urllib 就會(huì )獲取到配置信息。為了用 localhost 服務(wù)器測試腳本,我必須阻止 urllib 使用代理。
- 6
urllib 的 SSL 代理 opener(CONNECT 方法): ASPN Cookbook Recipe 。