Skip to main content

如何使用urllib包获取Internet资源

Author:

迈克尔·福德

注解

有一个早期版本的HOWTO的法语翻译,可在 urllib2 - Le Manuel manquant

介绍

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

urllib.request支持使用其相关的网络协议(例如FTP,HTTP)获取许多“URL方案”的URL(由URL中的“:”之前的字符串标识,例如“ftp”是“ ftp://python.org/ ”的URL方案) 。本教程重点介绍最常见的情况,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检索资源并将其存储在临时位置,可以通过 urlretrieve() 功能执行此操作:

import urllib.request
local_filename, headers = urllib.request.urlretrieve('http://python.org/')
html = open(local_filename)

urllib的许多使用将是那么简单(注意,而不是一个“http:”URL,我们可以使用以’ftp:’,’file:’开头的URL等)。但是,本教程的目的是解释更复杂的情况,专注于HTTP。

HTTP基于请求和响应 - 客户端发出请求和服务器发送响应。 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使用相同的请求接口来处理所有URL方案。例如,您可以发出如此的FTP请求:

req = urllib.request.Request('ftp://example.com/')

在HTTP的情况下,Request对象允许你做两件额外的事情:首先,你可以传递要发送到服务器的数据。第二,您可以传递额外的信息(“元数据”) about 的数据或关于请求本身,到服务器 - 此信息作为HTTP“headers”发送。让我们依次看看这些。

数据

有时您想要将数据发送到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和POST请求不同的一种方式是POST请求通常具有“副作用”:它们以某种方式改变系统的状态(例如通过向网站下达一个订单来递送一定量的罐头垃圾邮件到你的门)。尽管HTTP标准清楚地表明POST旨在用于 always 导致副作用,并且GET请求 never 导致副作用,但是没有任何东西阻止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)

请注意,完整网址是通过向网址中添加 ?,然后是编码值创建的。

标题

我们将在这里讨论一个特定的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 and 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 实例将具有整数“代码”属性,其对应于服务器发送的错误。

错误代码

由于默认处理程序处理重定向(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错误代码 and 一个错误页面来做出响应。您可以在返回的页面上使用 HTTPError 实例作为响应。这意味着,除了代码属性,它也有read,geturl和info,urllib.response 模块返回的方法:

>>> 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 or 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 must 先来,否则 except URLErroralso 捕获 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

信息和geturl

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

geturl - 这会返回所获取网页的实际网址。这是有用的,因为 urlopen (或使用的开放程序对象)可能跟随重定向。提取的网页的网址可能与请求的网址不同。

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

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

Openers和处理程序

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

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

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

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

其他类型的处理程序,你可能想要处理代理,身份验证和其他常见但略有特殊的情况。

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

开放者对象有一个 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 方法的领域参数来指示这一点。

顶级网址是需要验证的第一个网址。比传递给.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)

注解

在上面的例子中,我们只提供我们的 HTTPBasicAuthHandlerbuild_opener。默认情况下,开放程序具有正常情况下的处理程序 - ProxyHandler (如果设置了代理设置,例如设置了 http_proxy 环境变量),UnknownHandlerHTTPHandlerHTTPDefaultErrorHandlerHTTPRedirectHandlerFTPHandlerFileHandlerDataHandlerHTTPErrorProcessor

top_level_url 实际上是 either 一个完整的URL(包括’http:’scheme组件和主机名以及可选的端口号)。 “ http://example.com/or “授权”(即主机名,可选地包括端口号)。 “example.com”或“example.com:8080”(后一示例包括端口号)。权限(如果存在)不能包含“userinfo”组件,例如“joe:password@example.com”不正确。

代理

urllib 将自动检测您的代理设置并使用它们。这是通过 ProxyHandler,当检测到代理设置时,它是正常处理程序链的一部分。通常这是一件好事,但有时候它可能没有什么帮助 [5]。一种方法是设置我们自己的 ProxyHandler,没有定义代理。这是通过使用类似的步骤来设置 Basic Authentication 处理程序:

>>> 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审查和修订。

[1]

Google为例。

[2]

浏览器嗅探是网站设计的一个非常糟糕的做法 - 使用网络标准的建筑网站更加明智。不幸的是,很多网站仍然发送不同的版本到不同的浏览器。

[3]

MSIE 6的用户代理是 ‘Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)’

[4]

有关更多HTTP请求标头的详细信息,请参阅 Quick Reference to HTTP Headers

[5]

在我的情况下,我必须使用代理访问互联网在工作。如果您尝试通过此代理获取 localhost URL,则会阻止它们。 IE设置为使用代理,urllib选择。为了使用localhost服务器测试脚本,我必须防止urllib使用代理。

[6]

urllib打开工具用于SSL代理(CONNECT方法):ASPN食谱