HOWTO 使用 urllib 包获取 Internet 资源 — Python 文档

来自菜鸟教程
Python/docs/3.9/howto/urllib2
跳转至:导航、​搜索

HOWTO 使用 urllib 包获取 Internet 资源

作者
迈克尔·福特

笔记

本 HOWTO 的早期修订版有法语翻译,可从 urllib2 - Le Manuel manquant 获得。


简介

相关文章

您可能还会发现以下有关使用 Python 获取 Web 资源的文章很有用:

  • 基本认证

    关于 基本身份验证 的教程,以及 Python 中的示例。

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

urllib.request 支持获取许多“URL 方案”的 URL(由 URL 中 ":" 之前的字符串标识 - 例如 "ftp""ftp://python.org/%22 的 URL 方案)使用它们的相关的网络协议(例如 FTP、HTTP)。 本教程重点介绍最常见的情况,HTTP。

对于简单的情况 urlopen 非常容易使用。 但是,一旦您在打开 HTTP URL 时遇到错误或非平凡情况,您就需要对超文本传输协议有所了解。 HTTP 最全面、最权威的参考文献是 RFC 2616。 这是一份技术文档,并非旨在易于阅读。 本 HOWTO 旨在说明如何使用 urllib,其中包含有关 HTTP 的足够详细信息以帮助您完成。 它不是要替换 urllib.request 文档,而是对它们的补充。


获取网址

最简单的使用 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 的许多用途就是这么简单(请注意,我们可以使用以 'ftp:'、'file:' 等开头的 URL,而不是 'http:' URL)。 但是,本教程的目的是解释更复杂的情况,重点是 HTTP。

HTTP 基于请求和响应 - 客户端发出请求,服务器发送响应。 urllib.request 用 Request 对象反映了这一点,该对象代表您正在发出的 HTTP 请求。 以最简单的形式创建一个 Request 对象,该对象指定要获取的 URL。 使用此请求对象调用 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 接口来处理所有 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。 在这种情况下,引发的异常将具有“原因”属性,它是一个包含错误代码和文本错误消息的元组。

例如

>>> 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')

HTTP错误

来自服务器的每个 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 属性外,它还具有 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


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

信息和获取网址

urlopen(或 HTTPError 实例)返回的响应有两个有用的方法 info()geturl() 并在模块 urllib.response 中定义..

geturl - 这将返回所获取页面的真实 URL。 这很有用,因为 urlopen(或使用的 opener 对象)可能跟随重定向。 获取的页面的 URL 可能与请求的 URL 不同。

info - 这将返回一个类似字典的对象,描述所获取的页面,特别是服务器发送的标头。 它目前是一个 http.client.HTTPMessage 实例。

典型的标头包括“Content-length”、“Content-type”等。 请参阅 HTTP 标头快速参考 以获取有用的 HTTP 标头列表及其含义和用途的简要说明。


开瓶器和处理程序

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

如果您想获取安装了特定处理程序的 URL,您将需要创建 opener,例如获取处理 cookie 的 opener,或获取不处理重定向的 opener。

要创建开启器,请实例化 OpenerDirector,然后重复调用 .add_handler(some_handler_instance)

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

您可能想要的其他类型的处理程序可以处理代理、身份验证和其他常见但稍微特殊的情况。

install_opener 可用于使 opener 对象成为(全局)默认开启器。 这意味着对 urlopen 的调用将使用您安装的开启程序。

Opener 对象有一个 open 方法,可以像 urlopen 函数一样直接调用它来获取 url:不需要调用 install_opener,除非是为了方便.


基本认证

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

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

例如

WWW-Authenticate: Basic realm="cPanel Users"

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

HTTPBasicAuthHandler 使用一个称为密码管理器的对象来处理 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 环境变量)、UnknownHandler、[ X162X]、HTTPDefaultErrorHandlerHTTPRedirectHandlerFTPHandlerFileHandlerDataHandlerHTTPErrorProcessor.


top_level_url 实际上是 或者 一个完整的 URL(包括“http:”方案组件和主机名以及可选的端口号)例如 "http://example.com/%22 一个“权威”(即 主机名,可选地包括端口号)例如 "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() 的文档。


套接字和层

从 Web 获取资源的 Python 支持是分层的。 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
以谷歌为例。
2
浏览器嗅探对于网站设计来说是一种非常糟糕的做法 - 使用网络标准构建网站更为明智。 不幸的是,许多网站仍然向不同的浏览器发送不同的版本。
3
MSIE 6 的用户代理是 Mozilla/4.0(兼容;MSIE 6.0;Windows NT 5.1;SV1;.NET CLR 1.1.4322)
4
更多 HTTP 请求头的详细信息,请参见 HTTP 头快速参考
5
就我而言,我必须使用代理才能在工作中访问互联网。 如果您尝试通过此代理获取 localhost URL,它会阻止它们。 IE 设置为使用 urllib 接收的代理。 为了使用本地主机服务器测试脚本,我必须阻止 urllib 使用代理。
6
SSL 代理的 urllib 开启器(CONNECT 方法):ASPN Cookbook Recipe