如何使用 urllib 包获取 Internet 资源¶
- 作者:
引言¶
urllib.request 是一个用于获取 URL (统一资源定位符) 的 Python 模块。它以 `urlopen` 函数的形式提供了一个非常简单的接口。该函数能够使用各种不同的协议获取 URL。它还提供了一个稍微复杂一些的接口,用于处理常见情况,例如基本认证、cookie、代理等。这些功能由称为 `handler` 和 `opener` 的对象提供。
urllib.request 支持使用其关联的网络协议(例如 FTP、HTTP)获取许多“URL 方案”(由 URL 中 ":"
前的字符串标识 - 例如 "ftp"
是 "ftp://pythonlang.cn/"
的 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://pythonlang.cn/') as response:
html = response.read()
如果您希望通过 URL 检索资源并将其存储在临时位置,您可以通过 shutil.copyfileobj()
和 tempfile.NamedTemporaryFile()
函数来实现
import shutil
import tempfile
import urllib.request
with urllib.request.urlopen('https://pythonlang.cn/') 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://pythonlang.cn/')
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
(其中 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
是在 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://pythonlang.cn/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
环境变量)、UnknownHandler
、HTTPHandler
、HTTPDefaultErrorHandler
、HTTPRedirectHandler
、FTPHandler
、FileHandler
、DataHandler
、HTTPErrorProcessor
。
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 审阅和修订。