HOWTO Fetch Internet Resources Using The urllib Package

作者:Michael Foord

注意

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

Introduction

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

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

对于直接的情况,urlopen非常容易使用。但是,一旦您在打开HTTP URL时遇到错误或非常简单的情况,您将需要了解超文本传输​​协议。对HTTP最全面和最权威的参考是 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()

如果您希望通过URL检索资源并将其存储在临时位置,可以通过urlretrieve()函数执行此操作:

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

urllib的许多使用将是那么简单(注意,我们可以使用以'ftp:','file:'等开头的URL代替“http:”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对象允许你做两件额外的事情:首先,你可以传递要发送到服务器的数据。其次,您可以向服务器传递关于数据或关于请求本身的额外信息(“元数据”) - 此信息以HTTP“标头”形式发送。让我们依次看看这些。

Data

有时你想要发送数据到一个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标准清楚地表明POSTs的目的是总是导致副作用,并且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,后跟编码值。

Headers

我们将在这里讨论一个特定的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部分,后面我们将看看当出现问题时会发生什么。

Handling Exceptions

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实例将有一个整数'code'属性,它对应于服务器发送的错误。

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.'),
    }

当出现错误时,服务器通过返回HTTP错误代码错误页面来做出响应。您可以在返回的页面上使用HTTPError实例作为响应。这意味着除了代码属性之外,它还具有由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
  ...

Wrapping it Up

因此,如果您想为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

注意

The except HTTPError must come first, otherwise except URLError will also catch an 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 and geturl

The response returned by urlopen (or the HTTPError instance) has two useful methods info() and geturl() and is defined in the module urllib.response..

geturl - 这会传回所撷取网页的实际网址。这很有用,因为urlopen(或使用的开启者对象)可能跟随重定向。获取的网页的网址可能与请求的网址不同。

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

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

Openers and Handlers

当你获取一个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

Basic Authentication

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

当需要认证时,服务器发送请求认证的头(以及401错误代码)。这指定了认证方案和“领域”。标题如下:WWW验证: 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实际上是完整网址(包括“http:”计划组件和主机名以及可选的端口号)。http://example.com/“授权”(即主机名,可选地包括端口号)。“example.com”或“example.com:8080”(后一示例包括端口号)。权限(如果存在)不能包含“userinfo”组件,例如“joe:[email protected]”不正确。

Proxies

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()的文档。

Sockets and Layers

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)

Footnotes

本文件由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请求标头的详细信息,请参阅HTTP标头快速参考
[5]在我的情况下,我必须使用代理访问互联网在工作。如果您尝试通过此代理获取localhost网址,则会阻止它们。IE设置为使用代理,urllib选择。为了使用localhost服务器测试脚本,我必须防止urllib使用代理。
[6]用于SSL代理的URL urllib(CONNECT方法):ASPN Cookbook Recipe