如何使用 urllib 包獲取 Internet 資源

作者:

Michael Foord

引言

urllib.request 是一個用於獲取 URL (統一資源定位符) 的 Python 模組。它以 `urlopen` 函式的形式提供了一個非常簡單的介面。該函式能夠使用各種不同的協議獲取 URL。它還提供了一個稍微複雜一些的介面,用於處理常見情況,例如基本認證、cookie、代理等。這些功能由稱為 `handler` 和 `opener` 的物件提供。

urllib.request 支援使用其關聯的網路協議(例如 FTP、HTTP)獲取許多“URL 方案”(由 URL 中 ":" 前的字串標識 - 例如 "ftp""ftp://python.club.tw/" 的 URL 方案)的 URL。本教程重點介紹最常見的情況,即 HTTP。

對於簡單的情況,`urlopen` 非常易於使用。但是,一旦您在開啟 HTTP URL 時遇到錯誤或非簡單情況,您將需要對超文字傳輸協議有所瞭解。HTTP 最全面和權威的參考資料是 RFC 2616。這是一份技術文件,並不旨在易於閱讀。本 HOWTO 旨在說明如何使用 `urllib`,並提供足夠的 HTTP 詳細資訊以幫助您。它不旨在取代 urllib.request 文件,而是對其進行補充。

獲取 URL

使用 urllib.request 的最簡單方法如下:

import urllib.request
with urllib.request.urlopen('https://python.club.tw/') as response:
   html = response.read()

如果您希望透過 URL 檢索資源並將其儲存在臨時位置,您可以透過 shutil.copyfileobj()tempfile.NamedTemporaryFile() 函式來實現

import shutil
import tempfile
import urllib.request

with urllib.request.urlopen('https://python.club.tw/') as response:
    with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
        shutil.copyfileobj(response, tmp_file)

with open(tmp_file.name) as html:
    pass

urllib 的許多用法都將如此簡單(請注意,我們可以使用以“ftp:”、“file:”等開頭的 URL,而不是“http:”URL)。然而,本教程的目的是解釋更復雜的情況,重點關注 HTTP。

HTTP 是基於請求和響應的——客戶端發出請求,伺服器傳送響應。urllib.request 透過 Request 物件反映了這一點,該物件表示您正在發出的 HTTP 請求。在其最簡單的形式中,您建立一個指定要獲取的 URL 的 Request 物件。使用此 Request 物件呼叫 urlopen 將返回所請求 URL 的響應物件。此響應是一個類似檔案的物件,這意味著您可以例如對響應呼叫 .read()

import urllib.request

req = urllib.request.Request('https://python.club.tw/')
with urllib.request.urlopen(req) as response:
   the_page = response.read()

請注意,urllib.request 使用相同的 Request 介面來處理所有 URL 方案。例如,您可以像這樣發出 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 請求沒有副作用。資料也可以透過將其編碼到 URL 本身中,在 HTTP GET 請求中傳遞。

這可以透過以下方式完成

>>> 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 後新增 ?,再跟上編碼後的值,即可建立完整的 URL。

請求頭

我們在這裡討論一個特定的 HTTP 頭,以說明如何將頭新增到您的 HTTP 請求中。

有些網站[1]不喜歡被程式瀏覽,或者向不同的瀏覽器傳送不同版本[2]。預設情況下,urllib 會將自己識別為 Python-urllib/x.y(其中 xy 是 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 的通常情況一樣,內建異常如 ValueErrorTypeError 等也可能被引發)。

HTTPError 是在 HTTP URL 的特定情況下引發的 URLError 的子類。

異常類從 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”(需要身份驗證)。

有關所有 HTTP 錯誤程式碼的參考,請參閱 RFC 2616 的第 10 節。

引發的 HTTPError 例項將具有一個整數“code”屬性,它對應於伺服器傳送的錯誤。

錯誤程式碼

由於預設處理器會處理重定向(300 範圍內的程式碼),並且 100-299 範圍內的程式碼表示成功,您通常只會看到 400-599 範圍內的錯誤程式碼。

http.server.BaseHTTPRequestHandler.responses 是一個有用的響應程式碼字典,其中顯示了 RFC 2616 使用的所有響應程式碼。該字典的摘錄如下

responses = {
    ...
    <HTTPStatus.OK: 200>: ('OK', 'Request fulfilled, document follows'),
    ...
    <HTTPStatus.FORBIDDEN: 403>: ('Forbidden',
                                  'Request forbidden -- authorization will '
                                  'not help'),
    <HTTPStatus.NOT_FOUND: 404>: ('Not Found',
                                  'Nothing matches the given URI'),
    ...
    <HTTPStatus.IM_A_TEAPOT: 418>: ("I'm a Teapot",
                                    'Server refuses to brew coffee because '
                                    'it is a teapot'),
    ...
    <HTTPStatus.SERVICE_UNAVAILABLE: 503>: ('Service Unavailable',
                                            'The server cannot process the '
                                            'request due to a high load'),
    ...
    }

當引發錯誤時,伺服器透過返回 HTTP 錯誤程式碼 *和* 錯誤頁面進行響應。您可以將 HTTPError 例項用作返回頁面上的響應。這意味著除了 `code` 屬性之外,它還具有由 urllib.response 模組返回的 `read`、`geturl` 和 `info` 方法

>>> req = urllib.request.Request('https://python.club.tw/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

方法 2

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 標頭快速參考

開路器和處理器

當您獲取 URL 時,您會使用一個開路器(`urllib.request.OpenerDirector` 的例項,這個名稱可能有點令人困惑)。通常我們透過 urlopen 使用預設開路器,但您可以建立自定義開路器。開路器使用處理器。所有“繁重工作”都由處理器完成。每個處理器都知道如何開啟特定 URL 方案(http、ftp 等)的 URL,或者如何處理 URL 開啟的某個方面,例如 HTTP 重定向或 HTTP cookie。

如果您希望使用安裝了特定處理器的 opener 來獲取 URL,例如獲取處理 cookie 的 opener,或者獲取不處理重定向的 opener,您將需要建立 opener。

要建立一個 opener,請例項化一個 OpenerDirector,然後重複呼叫 .add_handler(some_handler_instance)

或者,您可以使用 build_opener,這是一個方便的函式,只需一次函式呼叫即可建立 opener 物件。build_opener 預設添加了幾個處理器,但提供了一種快速新增更多處理器和/或覆蓋預設處理器的方法。

您可能希望處理的其他型別的處理器包括代理、身份驗證和其他常見但略微特殊的情況。

install_opener 可用於將一個 opener 物件設定為(全域性)預設 opener。這意味著對 urlopen 的呼叫將使用您已安裝的 opener。

Opener 物件有一個 open 方法,可以直接呼叫它來獲取 URL,就像 urlopen 函式一樣:除了為了方便之外,沒有必要呼叫 install_opener

基本認證

為了說明如何建立和安裝處理器,我們將使用 HTTPBasicAuthHandler。有關此主題的更詳細討論——包括基本身份驗證的工作原理的解釋——請參閱 基本身份驗證教程

當需要身份驗證時,伺服器會發送一個請求身份驗證的標頭(以及 401 錯誤程式碼)。這指定了身份驗證方案和“領域”。標頭看起來像:WWW-Authenticate: SCHEME realm="REALM"

例如

WWW-Authenticate: Basic realm="cPanel Users"

客戶端隨後應重試請求,並在請求中包含適當的域名和密碼作為標頭。這就是“基本認證”。為了簡化此過程,我們可以建立 HTTPBasicAuthHandler 的例項以及使用此處理器的 opener。

HTTPBasicAuthHandler 使用一個名為密碼管理器的物件來處理 URL 和域到密碼和使用者名稱的對映。如果您知道域是什麼(從伺服器傳送的身份驗證頭),那麼您可以使用 HTTPPasswordMgr。通常人們並不關心域是什麼。在這種情況下,使用 HTTPPasswordMgrWithDefaultRealm 很方便。這允許您為 URL 指定預設的使用者名稱和密碼。如果您沒有為特定域提供替代組合,則會提供此組合。我們透過向 add_password 方法提供 None 作為域引數來指示這一點。

頂層 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 具有處理常規情況的處理器 —— ProxyHandler(如果設定了代理設定,例如 http_proxy 環境變數)、UnknownHandlerHTTPHandlerHTTPDefaultErrorHandlerHTTPRedirectHandlerFTPHandlerFileHandlerDataHandlerHTTPErrorProcessor

top_level_url 實際上是 *一個* 完整的 URL(包括“http:”方案元件、主機名和可選的埠號),例如 "http://example.com/",*或者* 一個“許可權”(即主機名,可選地包括埠號),例如 "example.com""example.com:8080"(後一個示例包含埠號)。如果存在,許可權不得包含“userinfo”元件 - 例如 "joe:password@example.com" 是不正確的。

代理

urllib 將自動檢測您的代理設定並使用它們。這是透過 ProxyHandler 實現的,當檢測到代理設定時,它是正常處理程式鏈的一部分。通常這是一件好事,但有時可能無益[5]。一種方法是設定我們自己的 ProxyHandler,不定義任何代理。這與設定 基本認證 處理程式類似

>>> 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 審閱和修訂。