如何使用 urllib 程序包获取 Internet 资源

Note

此 HOWTO 的较早版本的法语翻译为urllib2-Le Manuel manquant

Introduction

Related Articles

以下有关使用 Python 提取 Web 资源的文章也可能会有用:

Note

基本认证教程,带有 Python 示例。

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

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

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

Fetching URLs

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

import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
   html = response.read()

如果您希望pass URL 检索资源并将其存储在一个临时位置,则可以passshutil.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 基于请求和响应-Client 端发出请求,服务器发送响应。 urllib.request 使用Request对象(代表您正在发出的 HTTP 请求)进行镜像。以最简单的形式,创建一个 Request 对象,该对象指定要获取的 URL。使用此 Request 对象调用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“Headers”发送。让我们依次看一下这些。

Data

有时您想将数据发送到 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 请求通常具有“副作用”:它们以某种方式更改系统状态(例如,pass向网站下订单发送一百重量的罐装垃圾邮件)到你的门)。尽管 HTTP 标准明确指出 POST 旨在“总是”引起副作用,而 GET 请求“绝不”引起副作用,但是没有什么可以阻止 GET 请求具有副作用,也不能阻止 POST 请求没有副作用。副作用。也可以pass在 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 是pass在 URL 上添加?后跟编码值来创建的。

Headers

我们将在这里讨论一个特定的 HTTPHeaders,以说明如何向 HTTP 请求中添加 Headers。

一些网站[1]不喜欢被程序浏览,或将不同的版本发送到不同的浏览器[2]。默认情况下,urllib 将自己标识为Python-urllib/x.y(其中xy是 Python 发行版的主要和次要版本号,例如Python-urllib/2.5),这可能会混淆该网站,或者只是无法正常工作。浏览器标识自己的方式是passUser-AgentHeaders[3]。创建请求对象时,可以传入 Headers 的字典。以下示例提出与上述相同的请求,但将其自身标识为 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()

响应也有两种有用的方法。请参阅关于信息和 geturl的小节,我们将在发生问题时查看发生了什么。

Handling Exceptions

当无法处理响应时,* urlopen *会引发URLError(尽管通常使用 Python API,也可能引发诸如ValueErrorTypeError等内置异常)。

HTTPError是在特定的 HTTP URL 中引发的URLError的子类。

异常类是从urllib.error模块导出的。

URLError

通常,由于没有网络连接(到指定服务器的路由)或指定的服务器不存在,所以引发 URLError。在这种情况下,引发的异常将具有“原因”属性,该属性是一个包含错误代码和文本错误消息的 Tuples。

e.g.

>>> 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 响应均包含数字“状态代码”。有时状态码指示服务器无法满足请求。默认处理程序将为您处理其中一些响应(例如,如果响应是“重定向”,请求 Client 端从其他 URL 提取文档,则 urllib 会为您处理)。对于无法处理的内容,urlopen 将引发HTTPError。典型的错误包括“ 404”(找不到页面),“ 403”(禁止请求)和“ 401”(需要认证)。

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

引发的HTTPError实例将具有整数“代码”属性,该属性对应于服务器发送的错误。

Error Codes

因为默认处理程序会处理重定向(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.'),
    }

引发错误时,服务器将pass返回 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做准备,则有两种基本方法。我更喜欢第二种方法。

Number 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

Note

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

信息和 geturl

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

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

info -这将返回一个类似于字典的对象,该对象描述获取的页面,尤其是服务器发送的 Headers。当前是http.client.HTTPMessage个实例。

典型的 Headers 包括“ Content-length”,“ Content-type”等。有关 HTTPHeaders 的有用列表,请参见HTTPHeaders 快速参考,并简要说明其含义和用法。

开瓶器和处理程序

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

如果要获取安装了特定处理程序的 URL,则需要创建打开程序,例如,获取用于处理 cookie 的打开程序或获取不处理重定向的打开程序。

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

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

您可能希望使用的其他类型的处理程序可以处理代理,身份验证以及其他常见但稍有特殊的情况。

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

Opener 对象具有open方法,可以直接调用它以与urlopen函数相同的方式获取 url:除了方便起见,不需要调用install_opener

Basic Authentication

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

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

e.g.

WWW-Authenticate: Basic realm="cPanel Users"

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

HTTPBasicAuthHandler使用称为密码 Management 器的对象来处理 URL 和领域到密码和用户名的 Map。如果您知道领域是什么(pass服务器发送的身份验证 Headers),则可以使用HTTPPasswordMgr。通常,人们并不关心领域是什么。在这种情况下,使用HTTPPasswordMgrWithDefaultRealm很方便。这使您可以为 URL 指定默认的用户名和密码。如果您没有为特定领域提供替代组合,将提供此文件。我们pass提供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)

Note

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

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

Proxies

urllib **将自动检测您的代理设置并使用它们。这是passProxyHandler进行的,当检测到代理设置时,ProxyHandler是常规处理程序链的一部分。通常这是一件好事,但在某些情况下[5]可能没有帮助。一种方法是设置我们自己的ProxyHandler,但未定义代理。这可以pass类似的步骤来设置Basic Authentication处理程序来完成:

>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)

Note

目前urllib.request 支持pass代理获取https个位置。但是,可以pass扩展 urllib.request 来启用它,如配方[6]所示。

Note

如果设置了变量REQUEST_METHOD,则HTTP_PROXY将被忽略;请参阅getproxies()上的文档。

套接字和层

Python 对从 Web 上获取资源的支持是分层的。 urllib 使用http.client库,而该库又使用套接字库。

从 Python 2.3 开始,您可以指定套接字在超时之前应 await 响应的时间。这在必须获取网页的应用程序中很有用。默认情况下,套接字模块没有超时,可以挂起。当前,套接字超时未在 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)

Footnotes

该文档已由 John Lee 审查和修订。

  • [1]

    • 以 Google 为例。
  • [2]

    • 浏览器嗅探对于网站设计而言是非常糟糕的做法-使用 Web 标准构建网站更为明智。不幸的是,许多站点仍然向不同的浏览器发送不同的版本。
  • [3]

    • MSIE 6 的用户代理为*'Mozilla/4.0(兼容; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)'*
  • [4]

  • [5]

    • 就我而言,我必须在工作中使用代理访问互联网。如果您trypass此代理获取* localhost * URL,则会阻止它们。 IE 被设置为使用 urllib 所使用的代理。为了使用 localhost 服务器测试脚本,我必须防止 urllib 使用代理。
  • [6]