HOWTO 使用 urllib 包獲取網絡資源?
注解
這份 HOWTO 文檔的早期版本有一份法語的譯文,可在 urllib2 - Le Manuel manquant 處查閱。
概述?
urllib.request 是一個用于獲取 URL (統一資源定位地址)的 Python 模塊。它以 urlopen 函數的形式提供了一個非常簡單的接口。該接口能夠使用不同的協議獲取 URL。同時它也提供了一個略微復雜的接口來處理常見情形——如:基本驗證、cookies、代理等等。這些功能是通過叫做 handlers 和 opener 的對象來提供的。
urllib.request 支持多種 "URL 網址方案" (通過 URL中 ":" 之前的字符串加以區分——如 URL 地址 "ftp://python.org/"` 中的 ``"ftp"`) ,使用與之相關的網絡協議(如:FTP、 HTTP)來獲取 URL 資源。本指南重點關注最常用的情形—— HTTP。
對于簡單場景而言, urlopen 用起來十分容易。但只要在打開 HTTP URL 時遇到錯誤或非常情況,就需要對超文本傳輸協議有所了解才行。最全面、最權威的 HTTP 參考是 RFC 2616 。那是一份技術文檔,并沒有追求可讀性。本 文旨在說明 urllib 的用法,為了便于閱讀也附帶了足夠詳細的 HTTP 信息。本文并不是為了替代 urllib.request 文檔,只是其補充說明而已。
提取URL?
下面是使用 urllib.request 最簡單的方式:
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
html = response.read()
如果你想通過 URL 獲取資源并保存某個臨時的地方,你可以通過 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很易于使用(注意URL不僅僅可以以'http:'開頭,也可以是'ftp:','file:'等)。但是,這篇教程的目的是介紹更加復雜的用法,大多數是以HTTP舉例。
HTTP基于請求和回應——客戶端像服務器請求,服務器回應。urllib.request將你的HTTP請求保存為一個``Request``對象。在最簡單的情況下,一個Request對象里包含你所請求的特定URL。以當前的Request對象作為參數調用``urlopen``返回服務器對你正在請求的URL的回應?;貞莻€文件類對象,所以你可以調用如``.read()``等命令。
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中的Request接口也支持處理所有的協議。比如,你可以像這樣做一個 FTP 請求:
req = urllib.request.Request('ftp://example.com/')
在 HTTP 的情況下,Request 對象允許你做兩件額外的事:一,你可以向服務器發送數據。二,你可以向服務器發送額外的信息(“元數據”): 關于 數據或請求本身的。信息將以“HTTP頭”的方式發過去。讓我們一個個看過去。
數據?
有時候你想要給一個 URL 發送數據(通常這個URL指向一個CGI(通用網關接口)腳本或者其他 web 應用)。 對于 HTTP,這通常使用一個 POST 請求來完成。 比如在瀏覽器上提交一個 HTML 表單。 但并不是所有的 POST 都來自表單:你能使用一個 POST 來傳輸任何數據到你自己的應用上。 在使用常見的 HTML 表單的情況下,數據需要以標準的方式編碼,然后再作為 data 參數傳給 Request 對象。 編碼需要使用一個來自 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()
請注意,有時還需要采用其他編碼,比如由 HTML 表單上傳文件——更多細節請參見 HTML 規范,提交表單 。
如果不傳遞 data 參數,urllib 將采用 GET 請求。GET 和 POST 請求有一點不同,POST 請求往往具有“副作用”,他們會以某種方式改變系統的狀態。例如,從網站下一個訂單,購買一大堆罐裝垃圾并運送到家。 盡管 HTTP 標準明確指出 POST 總是 要導致副作用,而 GET 請求 從來不會 導致副作用。但沒有什么辦法能阻止 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 是通過在其中添加 ? 創建的,后面跟著經過編碼的數據。
HTTP 頭部信息?
下面介紹一個具體的 HTTP 頭部信息,以此說明如何在 HTTP 請求加入頭部信息。
有些網站 1 不愿被程序瀏覽到,或者要向不同的瀏覽器發送不同版本 2 的網頁。默認情況下,urllib 將自身標識為“Python-urllib/xy”(其中 x 、 y 是 Python 版本的主、次版本號,例如 Python-urllib/2.5),這可能會讓網站不知所措,或者干脆就使其無法正常工作。瀏覽器是通過頭部信息 User-Agent 3 來標識自己的。在創建 Request 對象時,可以傳入字典形式的頭部信息。以下示例將生成與之前相同的請求,只是將自身標識為某個版本的 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()
響應對象也有兩個很有用的方法。請參閱有關 info 和 geturl 部分,了解出現問題時會發生什么。
處理異常?
如果 urlopen 無法處理響應信息,就會觸發 URLError 。盡管與通常的 Python API 一樣,也可能觸發 ValueError 、 TypeError 等內置異常。
HTTPError 是 URLError 的子類,當 URL 是 HTTP 的情況時將會觸發。
異常類從 urllib.error 模塊中導出。
URLError?
通常,引發 URLError 的原因是沒有網絡連接(或者沒有到指定服務器的路由),或者指定的服務器不存在。該情況下,將會引發該異常,并帶有一個 'reason' 屬性,該屬性是一個包含錯誤代碼和文本錯誤信息的元組。
例如
>>> 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?
從服務器返回的每個 HTTP 響應都包含一個數字的 “狀態碼”。有時該狀態碼表明服務器無法完成該請求。默認的處理器(函數?)將會為你處理這其中的一些響應。(例如,如果響應包含了 "redirection",將會要求客戶端去向另外的 URL 獲取文檔,urllib 將會為你處理該情形)。對于那些它無法處理的(狀態代碼),urlopen 將會引發一個 HTTPError 。典型的錯誤包括:‘404’(頁面無法找到)、‘403’(請求遭拒絕)和 ’401‘ (需要身份驗證)。
全部的 HTTP 錯誤碼請參閱 RFC 2616?。
HTTPError 實例將包含一個整數型的“code”屬性,對應于服務器發來的錯誤。
錯誤代碼?
由于默認處理函數會自行處理重定向(300 以內的錯誤碼),而且 100--299 的狀態碼表示成功,因此通常只會出現 400--599 的錯誤碼。
http.server.BaseHTTPRequestHandler.responses 是很有用的響應碼字典,其中給出了 RFC 2616 用到的所有響應代碼。為方便起見,下面將此字典轉載如下:
# 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.'),
}
當觸發錯誤時,服務器通過返回 HTTP 錯誤碼 和 錯誤頁面進行響應??梢詫?HTTPError 實例用作返回頁面的響應。這意味著除了 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 ,有兩種簡單的方案。推薦使用第二種方案。
數字1?
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 將會 同時 捕獲 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 and geturl?
由 urlopen (或者 HTTPError 實例)所返回的響應包含兩個有用的方法: info() 和 geturl(),該響應由模塊 urllib.response 定義。
geturl - 返回所獲取頁面的真實 URL。該方法很有用,因為 urlopen (或者所使用的 opener 對象)可能回包括一次重定向。所獲取頁面的 URL 未必就是所請求的 URL 。
info - 該方法返回一個類似字典的對象,描述了所獲取的頁面,特別是由服務器送出的頭部信息(headers) 。目前它是一個 http.client.HTTPMessage 實例。
典型的 HTTP 頭部信息包括“Content-length”、“Content-type”等。有關 HTTP 頭部信息的清單,包括含義和用途的簡要說明,請參閱 HTTP Header 快速參考 。
Opener 和 Handler?
當獲取 URL 時,會用到了一個 opener(一個類名可能經過混淆的 urllib.request.OpenerDirector 的實例)。通常一直會用默認的 opener ——通過 urlopen ——但也可以創建自定義的 opener 。opener 會用到 handler。所有的“繁重工作”都由 handler 完成。每種 handler 知道某種 URL 方案(http、ftp 等)的 URL 的打開方式,或是某方面 URL 的打開方式,例如 HTTP 重定向或 HTTP cookie。
若要用已安裝的某個 handler 獲取 URL,需要創建一個 opener 對象,例如處理 cookie 的 handler,或對重定向不做處理的 handler。
若要創建 opener,請實例化一個 OpenerDirector ,然后重復調用 .add_handler(some_handler_instance) 。
或者也可以用 build_opener ,這是個用單次調用創建 opener 對象的便捷函數。build_opener 默認會添加幾個 handler,不過還提供了一種快速添加和/或覆蓋默認 handler 的方法。
可能還需要其他類型的 handler,以便處理代理、身份認證和其他常見但稍微特殊的情況。
install_opener 可用于讓 opener 對象成為(全局)默認 opener。這意味著調用 urlopen 時會采用已安裝的 opener。
opener 對象帶有一個 `open 方法,可供直接調用以獲取 url,方式與 urlopen 函數相同。除非是為了調用方便,否則沒必要去調用 install_opener 。
基本認證?
To illustrate creating and installing a handler we will use the
HTTPBasicAuthHandler. For a more detailed discussion of this subject --
including an explanation of how Basic Authentication works - see the Basic
Authentication Tutorial.
如果需要身份認證,服務器會發送一條請求身份認證的頭部信息(以及 401 錯誤代碼)。這條信息中指明了身份認證方式和“安全區域(realm)”。格式如下所示:WWW-Authenticate: SCHEME realm="REALM" 。
例如
WWW-Authenticate: Basic realm="cPanel Users"
然后,客戶端應重試發起請求,請求數據中的頭部信息應包含安全區域對應的用戶名和密碼。這就是“基本身份認證”。為了簡化此過程,可以創建 HTTPBasicAuthHandler 的一個實例及使用它的 opener。
HTTPBasicAuthHandler 用一個名為密碼管理器的對象來管理 URL、安全區域與密碼、用戶名之間的映射關系。如果知道確切的安全區域(來自服務器發送的身份認證頭部信息),那就可以用到 HTTPPasswordMgr 。通常人們并不關心安全區域是什么,這時用``HTTPPasswordMgrWithDefaultRealm`` 就很方便,允許為 URL 指定默認的用戶名和密碼。當沒有為某個安全區域提供用戶名和密碼時,就會用到默認值。下面用 None 作為 add_password 方法的安全區域參數,表明采用默認用戶名和密碼。
首先需要身份認證的是頂級 URL。比傳給 .add_password() 的 URL 級別“更深”的 URL 也會得以匹配:
# 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 會有用于處理常見狀況的 handler ——ProxyHandler (如果設置代理的話,比如設置了環境變量 http_proxy ),UnknownHandler 、HTTPHandler 、 HTTPDefaultErrorHandler 、 HTTPRedirectHandler 、 FTPHandler 、 FileHandler 、 DataHandler 、 HTTPErrorProcessor 。
top_level_url 其實 要么 是一條完整的 URL(包括 “http:” 部分和主機名及可選的端口號),比如 "http://example.com/" , 要么 是一條“訪問權限”(即主機名,及可選的端口號),比如 "example.com" 或 "example.com:8080" (后一個示例包含了端口號)。訪問權限 不得 包含“用戶信息”部分——比如 "joe:password@example.com" 就不正確。
代理?
urllib 將自動檢測并使用代理設置。 這是通過 ProxyHandler 實現的,當檢測到代理設置時,是正常 handler 鏈中的一部分。通常這是一件好事,但有時也可能會無效 5。 一種方案是配置自己的 ProxyHandler ,不要定義代理。 設置的步驟與 Basic Authentication handler 類似:
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
注解
目前 urllib.request 尚不 支持通過代理抓取 https 鏈接地址。 但此功能可以通過擴展 urllib.request 來啟用,如以下例程所示 6。
注解
如果設置了 REQUEST_METHOD 變量,則會忽略 HTTP_PROXY ;參閱 getproxies() 文檔。
套接字與分層?
Python 獲取 Web 資源的能力是分層的。urllib 用到的是 http.client 庫,而后者又用到了套接字庫。
從 Python 2.3 開始,可以指定套接字等待響應的超時時間。這對必須要讀到網頁數據的應用程序會很有用。默認情況下,套接字模塊 不會超時 并且可以掛起。目前,套接字超時機制未暴露給 http.client 或 urllib.request 層使用。不過可以為所有用到的套接字設置默認的全局超時。
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
對于網站設計而言,探測不同的瀏覽器是非常糟糕的做法——更為明智的做法是采用 web 標準構建網站。不幸的是,很多網站依然向不同的瀏覽器發送不同版本的網頁。
- 3
MSIE 6 的 user-agent 信息是 “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)”
- 4
有關 HTTP 請求的頭部信息,詳情請參閱 Quick Reference to HTTP Headers。
- 5
本人必須使用代理才能在工作中訪問互聯網。如果嘗試通過代理獲取 localhost URL,將會遭到阻止。IE 設置為代理模式,urllib 就會獲取到配置信息。為了用 localhost 服務器測試腳本,我必須阻止 urllib 使用代理。
- 6
urllib 的 SSL 代理 opener(CONNECT? 方法): ASPN Cookbook Recipe 。
