使用 urllib 包获取互联网资源 HOWTO

作者:

Michael Foord

简介

urllib.request 是一个用于获取 URL(统一资源定位符)的 Python 模块。它提供了一个非常简单的接口,以urlopen函数的形式。它能够使用各种不同的协议获取 URL。它还提供了一个稍微复杂一点的接口来处理常见情况——比如基本身份验证、cookie、代理等等。这些由称为处理器和打开器的对象提供。

urllib.request 支持使用其关联的网络协议获取许多“URL 方案”(由 URL 中 ":" 之前的字符串标识——例如 "ftp""ftp://www.pythonlang.cn/" 的 URL 方案)的 URL(例如 FTP、HTTP)。本教程重点介绍最常见的情况,即 HTTP。

对于简单的场景,urlopen 非常易于使用。但是,一旦您在打开 HTTP URL 时遇到错误或非平凡情况,您将需要了解一些关于超文本传输协议的知识。HTTP 最全面和权威的参考是 RFC 2616。这是一份技术文档,并非旨在易于阅读。本 HOWTO 旨在说明使用urllib,并提供足够的 HTTP 细节来帮助您完成操作。它并非旨在取代 urllib.request 文档,而是对其进行补充。

获取 URL

使用 urllib.request 的最简单方法如下

import urllib.request
with urllib.request.urlopen('https://www.pythonlang.cn/') as response:
   html = response.read()

如果您希望通过 URL 检索资源并将其存储在临时位置,您可以通过 shutil.copyfileobj()tempfile.NamedTemporaryFile() 函数来实现

import shutil
import tempfile
import urllib.request

with urllib.request.urlopen('https://www.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 的许多用途都非常简单(请注意,我们可以使用以“http:”开头的 URL,而不是“http:” URL)。但是,本教程的目的是解释更复杂的情况,重点介绍 HTTP。

HTTP 基于请求和响应 - 客户端发出请求,服务器发送响应。urllib.request 通过一个 Request 对象来反映这一点,该对象代表您正在发出的 HTTP 请求。在最简单的情况下,您创建一个 Request 对象,指定您要获取的 URL。使用此 Request 对象调用 urlopen 会返回所请求 URL 的响应对象。此响应是一个类似文件的对象,这意味着您可以例如在响应上调用 .read()

import urllib.request

req = urllib.request.Request('https://www.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** 请求的方式完成。这通常是您在 Web 上提交您填写过的 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 等也可能被引发)。

HTTPErrorURLError 的子类,在 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”(需要身份验证)。

有关所有 HTTP 错误代码的参考,请参见 RFC 2616 的第 10 节。

引发的 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 属性之外,它还具有 read、geturl 和 info 方法,这些方法由 urllib.response 模块返回

>>> req = urllib.request.Request('https://www.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 做好准备,有两种基本方法。我更喜欢第二种方法。

方法一

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

注意

The except HTTPError 必须放在首位,否则 except URLError 也会捕获 HTTPError

Number 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 标头,并简要说明了它们的含义和用途。

Openers 和 Handlers

当您获取 URL 时,您使用一个 opener(可能是令人困惑的 urllib.request.OpenerDirector 的实例)。通常我们使用默认的 opener - 通过 urlopen - 但您可以创建自定义的 opener。Openers 使用 handlers。所有“繁重的工作”都由 handlers 完成。每个 handler 都知道如何为特定 URL 方案(http、ftp 等)打开 URL,或者如何处理 URL 打开的某个方面,例如 HTTP 重定向或 HTTP cookie。

如果您想使用安装了特定 handlers 的 URL 获取 URL,例如获取处理 cookie 的 opener 或获取不处理重定向的 opener,则需要创建 openers。

要创建 opener,请实例化一个 OpenerDirector,然后重复调用 .add_handler(some_handler_instance)

或者,您可以使用 build_opener,这是一个用于使用单个函数调用创建 opener 对象的便捷函数。 build_opener 默认添加了几个 handlers,但提供了一种快速添加更多 handlers 或覆盖默认 handlers 的方法。

您可能想要的其他类型的 handlers 可以处理代理、身份验证和其他常见但略微专业的情况。

install_opener 可用于将 opener 对象设为(全局)默认 opener。这意味着对 urlopen 的调用将使用您安装的 opener。

Opener 对象有一个 open 方法,可以直接调用它以与 urlopen 函数相同的方式获取 url:无需调用 install_opener,除非作为一种便利。

基本身份验证

为了说明创建和安装处理程序,我们将使用 HTTPBasicAuthHandler。有关此主题的更详细讨论(包括对基本身份验证工作原理的解释),请参阅 基本身份验证教程

当需要身份验证时,服务器会发送一个标题(以及 401 错误代码)请求身份验证。这指定了身份验证方案和“领域”。标题看起来像:WWW-Authenticate: SCHEME realm="REALM"

例如:

WWW-Authenticate: Basic realm="cPanel Users"

然后,客户端应该使用包含在请求标题中的该领域的适当名称和密码重新尝试请求。这就是“基本身份验证”。为了简化此过程,我们可以创建一个 HTTPBasicAuthHandler 实例和一个使用此处理程序的打开器。

HTTPBasicAuthHandler 使用一个名为密码管理器(password manager)的对象来处理 URL 和领域的映射到密码和用户名。如果您知道领域是什么(来自服务器发送的身份验证标题),那么您可以使用 HTTPPasswordMgr。通常情况下,您并不关心领域是什么。在这种情况下,使用 HTTPPasswordMgrWithDefaultRealm 很方便。这允许您为 URL 指定默认用户名和密码。如果您没有为特定领域提供替代组合,则将提供此组合。我们通过将 None 作为领域参数提供给 add_password 方法来指示这一点。

顶级 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)

注意

在上面的示例中,我们只将 HTTPBasicAuthHandler 提供给了 build_opener。默认情况下,打开器具有针对正常情况的处理程序 - ProxyHandler(如果设置了代理设置,例如 http_proxy 环境变量),UnknownHandlerHTTPHandlerHTTPDefaultErrorHandlerHTTPRedirectHandlerFTPHandlerFileHandlerDataHandlerHTTPErrorProcessor

top_level_url 实际上是完整的 URL(包括“http:”方案组件和主机名,以及可选的端口号),例如 "http://example.com/"或者是“授权”(即主机名,可选地包括端口号),例如 "example.com""example.com:8080"(后一个示例包含端口号)。如果存在授权,则必须包含“用户信息”组件 - 例如 "joe:[email protected]" 是不正确的。

代理

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 从网络获取资源的支持是分层的。urllib 使用 http.client 库,而该库又使用 socket 库。

从 Python 2.3 开始,您可以指定套接字在超时之前等待响应的时间。这在需要获取网页的应用程序中很有用。默认情况下,socket 模块没有超时,可能会挂起。目前,socket 超时在 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 审核和修订。