介紹
改教程翻譯自python官網的一篇文檔。
urllib.request是一個用於訪問URL(統一資源定位符)的Python模塊。它以urlopen函數的形式提供了一個非常簡單的接口,可以訪問使用多種不同協議的URL。它也提供了一個稍微復雜一些的接口,用來處理常用的情況——如基本的認證,cookies,代理等等。這些服務由叫做handlers和openers的對象提供。
urllib.request支持訪問多種“URL模式”(模式由URL中“:”前面的字符串確定——比如“ftp”就是“ftp://python.org/”的URL模式),使用的是它們對應的網絡協議(如FTP,HTTP)。這個教程集中於最常用的的類型,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下載一個資源並把它存儲在一個臨時文件中,你可以利用urlretrieve()
函數:
import urllib.request
local_filename,headers = urllib.request.urlretrieve('http://pythpn.org/')
html = open(local_filename)
下圖是演示效果:
uellib的很多用法就是這么簡單(注意除了‘http:’類型的url,我們也可以使用以‘ftp:’、‘file:’等開頭的url)。然而,這個教程的目的是解釋HTTP中一些更復雜的情形。
HTTP是基於請求和響應的——客戶端發起請求,服務端返回響應。urllib.request用Request類來表示你做出的HTTP請求。它的最簡形式就是只指定你需要訪問的url。Request對象調用urlopen
方法會為這個請求返回一個響應對象。這個響應是一個類文件對象,意味着你可以對其調用.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使用相同的接口來處理所有類型的url。比如說,你也可以這樣發起一個FTP請求:
req = urllib.request.urlopen('ftp://example.com/')
在HTTP的情況下,Request對象還允許你做兩件事:第一,你可以傳遞要發送到服務端的數據;第二,你可以發送關於數據或請求自身的額外信息(元數據)給服務器——這個信息會作為HTTP的“請求頭”進行發送。讓我們分別看一下。
數據
有時你想使用HTTP發送數據到一個url(通常這個url會指向一個CGI(通用網關接口,Common Gateway Interface)腳本或其它網絡應用),這通常使用一個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()
如果不傳遞data
參數,那urllib就會使用GET請求方式。GET方式和POST方式的其中一個區別在於POST請求經常有副作用:它們會以某種方式改變系統的狀態(比如,在網上下訂單,會有一英擔的午餐肉罐頭送到你家門口)。盡管HTTP標准明確說POST方式總是會造成副作用,而GET方式從來不會,但是並沒有保證措施。數據也可以用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)
注意full_url 是通過在url后面加一個?,然后再加上編碼后的數據進行創建的。
頭信息
我們在這里討論一個特殊的HTTP頭信息,來說明如何在你的HTTP請求中添加頭信息。
一些網站不喜歡被程序訪問,或者會給不同的瀏覽器發送不同的版本。默認情況下,urllib會把自身標記為Python-urllib/x.y
(其中x和y表示Python的版本號,如Python-urllib/2.5
),這可能會迷惑網站,或者干脆不起作用。瀏覽器標識自己的方式就是通過User-agent
頭信息。當你創建一個Request對象時,你可以傳遞一個頭信息的字典。下面的例子發起的是跟上面一樣的請求,但是把自己標識為一個IE瀏覽器的版本。
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](#info 和 geturl) 這一節。
處理異常
urlopen在不能處理某個響應的時候會拋出URLError(雖然一般使用Python API時,ValueError、TypeError等內建異常也可能被拋出)。
HTTPError是URLError的子類,在遇到HTTP URL的特殊情況時被拋出。
異常類出自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響應都包含一個數字的“狀態碼”。有時狀態碼表明服務器不能執行請求。默認的處理程序會為你處理其中的部分響應(比如,如果響應是“重定向”,要求客戶端從一個不同的URL中獲取資料,那么urllib
將會為你處理這個)。對於那些不能處理的響應,urlopen
將會拋出一個HTTPError。典型的錯誤包括‘404’(頁面未找到),‘403’(請求禁止),和‘401’(請求認證)。
查看RFC 2616的第10節,作為對所有HTTP錯誤碼的參考。
拋出的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.info())
print (e.geturl())
print (e.read())
上面程序的效果如下,
簡單起見,上圖中后面的頁面內容沒有全部截圖。
包裝一下
如果你希望程序對HTTPError和URLError有所准備,有兩種基本方法可以使用,我更喜歡第二個。
第一種
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和geturl
由urlopen
(或HTTPError實例)返回的響應有兩個實用的方法info()
和geturl()
,是在模塊urllib.response中定義的。
geturl—這個方法返回的是所獲取頁面的真正URL路徑。這個是有用的,因為urlopen
(或者使用的opener對象)可能經過了重定向。因此所得頁面的URL可能不是請求的URL。
info—這個方法返回一個類似字典的對象,描述所得到的頁面,尤其是服務器發回的頭信息。它實際上是一個http.client.HTTPMessage實例。
典型的頭信息包括‘Content-length’、‘Content-type’等等。你可以查看關於HTTP 頭信息的快速指南,這里有關於HTTP頭信息的含義和使用的簡短說明。
Openers和Handlers
當你訪問URL時,你使用的就是一個opener(這是urllib.reuest.OpenerDirector的一個實例)。一般來說我們使用的是默認的opener—通過urlopen
—但是你可以創建自定義的opener。opener會使用handler。所有的“重活”都是由handler完成的。每一個handler知道如何去處理某種類型的URL(http,ftp等等),或者是如何處理訪問URL的某一方面,如HTTP重定向或HTTP cookies。
如果你想要用特定的handler來訪問URL,你可以創建一個opener。比如,得到一個處理cookies的opener或者是一個不處理重定向的opener。
要創建一個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
。
基本認證
為了說明創建和安裝一個handler,我們使用HTTPBasicAuthHandler為例。關於這個話題的更多細節—包括關於基本認證如何工作的說明—請參看基本認證教程。
當需要認證(或授權)時,服務器會發送一個頭信息(還有一個401錯誤碼)請求認證。它指定了認證模式和一個“realm(領域)”。這個頭信息的形式看起來是:WWW-Authenticate:SCHEME realm="REALM"
。
例如,WWW-Authenticate: Basic realm="cPanel Users"
。
如何客戶端應該重新發起請求,並把對應realm的用戶名與密碼加入請求的頭信息中。這就是“基礎認證”。為了簡化這個過程,我們創建一個HTTPBasicAuthHandler實例,再有一個opener來使用這個handler。
HTTPBasicAuthHandler使用一個叫做密碼管理器的對象處理url和用戶名密碼域的映射。如果你知道領域是什么(根據服務器發送的認證頭信息得知),那么你可以使用HTTPPasswordMgr
。人們往往不在乎領域是什么。在那種情況下,最方便的就是使用HTTPPasswordMgrWithDefaultRealm
。它允許你為一個url指定一個默認的用戶名和密碼。如果你沒有為某個領域提供可選的組合,anemia就會使用這個。我們通過把None
作為add_password
方法的realm參數來表示它。
頂層url就是需要認證的第一個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
.
事實上,頂層url要么是一個完整的url(包括“http:”模式和主機名以及可選的端口號),例如“http://example.com/” ,或者是一個“授權機構”(即主機名,可以再加一個端口號)如“example.com” 或“example.com:8080” 。如果使用“授權機構”的話,一定不能包含“userinfo”元素—比如“joe:password@example.com”就是不正確的。
代理
urllib會自動地檢測並使用你的代理設置。這是通過ProxyHandler
完成的,它是在檢測到一個代理設置時的正常處理程序鏈的一部分。通常這是個好事,但是有些時候它可能沒有用。還有一個方法就是設置我們自己的ProxyHandler,不定義代理。做法與設置一個Basic Authentication的步驟相同:
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
Note:現在urllib.request不支持通過代理訪問一個https網址。不過,可以通過這篇教程實現。
如果設置一個變量
REQUEST_METHOD
,那么HTTP_PROXY就會被忽略,具體可以查看getproxies()
的文檔
套接字層
Python對於獲取網絡數據的支持是有層次的。urllib使用的是http.client庫,而他又轉而使用socket庫。
在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)