介绍
改教程翻译自python官网的一篇文档。
urllib.request是一个用于访问URL(统一资源定位符)的Python模块。它以urlopen函数的形式提供了一个非常简单的接口,可以访问使用多种不同协议的URL。它也提供了一个稍微复杂一些的接口,用来处理常用的情况——如基本的认证,cookies,代理等等。这些服务由叫做handlers和openers的对象提供。
urllib.request支持访问多种“URL模式”(模式由URL中“:”前面的字符串确定——比如“ftp”就是“ftp://python.org/”的URL模式),使用的是它们对应的网络协议(如FTP,HTTP)。这个教程集中于最常用的的类型,HTTP。
对于直截了当的情况,urlopen很容易使用。但是,一旦你在打开HTTP URL的时候遇到错误或者一些不平常的情况,你就需要对超文本转换协议的一些理解。关于HTTP最全面最权威的参考是RFC 2616。这是一个技术文档,不太容易阅读。这篇文章的目标就是说明如何使用urllib,包含足够的HTTP协议细节帮助你理解。这篇文章不是要代替urllib.request的文档,而是对它的补充。
访问URL
使用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://pythpn.org/\')
html = open(local_filename)
下图是演示效果:
uellib的很多用法就是这么简单(注意除了‘http:’类型的url,我们也可以使用以‘ftp:’、‘file:’等开头的url)。然而,这个教程的目的是解释HTTP中一些更复杂的情形。
HTTP是基于请求和响应的——客户端发起请求,服务端返回响应。urllib.request用Request类来表示你做出的HTTP请求。它的最简形式就是只指定你需要访问的url。Request对象调用urlopen方法会为这个请求返回一个响应对象。这个响应是一个类文件对象,意味着你可以对其调用.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.urlopen(\'ftp://example.com/\')
在HTTP的情况下,Request对象还允许你做两件事:第一,你可以传递要发送到服务端的数据;第二,你可以发送关于数据或请求自身的额外信息(元数据)给服务器——这个信息会作为HTTP的“请求头”进行发送。让我们分别看一下。
数据
有时你想使用HTTP发送数据到一个url(通常这个url会指向一个CGI(通用网关接口,Common Gateway Interface)脚本或其它网络应用),这通常使用一个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()
如果不传递data参数,那urllib就会使用GET请求方式。GET方式和POST方式的其中一个区别在于POST请求经常有副作用:它们会以某种方式改变系统的状态(比如,在网上下订单,会有一英担的午餐肉罐头送到你家门口)。尽管HTTP标准明确说POST方式总是会造成副作用,而GET方式从来不会,但是并没有保证措施。数据也可以用GET方式传递,只要把它编码在url中。
做法如下:
>>> 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)
注意full_url 是通过在url后面加一个?,然后再加上编码后的数据进行创建的。
头信息
我们在这里讨论一个特殊的HTTP头信息,来说明如何在你的HTTP请求中添加头信息。
一些网站不喜欢被程序访问,或者会给不同的浏览器发送不同的版本。默认情况下,urllib会把自身标记为Python-urllib/x.y(其中x和y表示Python的版本号,如Python-urllib/2.5),这可能会迷惑网站,或者干脆不起作用。浏览器标识自己的方式就是通过User-agent头信息。当你创建一个Request对象时,你可以传递一个头信息的字典。下面的例子发起的是跟上面一样的请求,但是把自己标识为一个IE浏览器的版本。
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](#info 和 geturl) 这一节。
处理异常
urlopen在不能处理某个响应的时候会抛出URLError(虽然一般使用Python API时,ValueError、TypeError等内建异常也可能被抛出)。
HTTPError是URLError的子类,在遇到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’(请求认证)。
查看RFC 2616的第10节,作为对所有HTTP错误码的参考。
抛出的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.info())
print (e.geturl())
print (e.read())
上面程序的效果如下,
简单起见,上图中后面的页面内容没有全部截图。
包装一下
如果你希望程序对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
注意:
except HTTPError必须放在第一个,否则except URLError也将捕获一个HTTPError。
第二种
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.reuest.OpenerDirector的一个实例)。一般来说我们使用的是默认的opener—通过urlopen—但是你可以创建自定义的opener。opener会使用handler。所有的“重活”都是由handler完成的。每一个handler知道如何去处理某种类型的URL(http,ftp等等),或者是如何处理访问URL的某一方面,如HTTP重定向或HTTP cookies。
如果你想要用特定的handler来访问URL,你可以创建一个opener。比如,得到一个处理cookies的opener或者是一个不处理重定向的opener。
要创建一个opener,可以实例化一个OpenerDirector,然后重复地调用.add_handler(some_handler_instance)。
或者,你可以使用build_opener方法,这是一个很方便的函数,可以通过一次函数调用创建opener对象。build_opener默认添加了一些handler,但是提供了一个简单的方法添加或者覆写默认的handler。
你可能需要其它类型的handler,比如可以处理代理,认证,以及其它常见但是特殊的情形。
install_opener可以用来把一个opener对象设定为(全局)默认opener。这意味着调用urlopen的时候会使用你安装的opener。
opener对象有一个open方法,可以直接调用来获取URL资源,方式与urlopen相同:除非为了方便,否则不需要调用install_opener。
基本认证
为了说明创建和安装一个handler,我们使用HTTPBasicAuthHandler为例。关于这个话题的更多细节—包括关于基本认证如何工作的说明—请参看基本认证教程。
当需要认证(或授权)时,服务器会发送一个头信息(还有一个401错误码)请求认证。它指定了认证模式和一个“realm(领域)”。这个头信息的形式看起来是:WWW-Authenticate:SCHEME realm="REALM"。
例如,WWW-Authenticate: Basic realm="cPanel Users"。
如何客户端应该重新发起请求,并把对应realm的用户名与密码加入请求的头信息中。这就是“基础认证”。为了简化这个过程,我们创建一个HTTPBasicAuthHandler实例,再有一个opener来使用这个handler。
HTTPBasicAuthHandler使用一个叫做密码管理器的对象处理url和用户名密码域的映射。如果你知道领域是什么(根据服务器发送的认证头信息得知),那么你可以使用HTTPPasswordMgr。人们往往不在乎领域是什么。在那种情况下,最方便的就是使用HTTPPasswordMgrWithDefaultRealm。它允许你为一个url指定一个默认的用户名和密码。如果你没有为某个领域提供可选的组合,anemia就会使用这个。我们通过把None作为add_password方法的realm参数来表示它。
顶层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)
注意:在上面的例子中,我们
build_opener时只使用了我们的HTTPBasicAuthHandler。默认情况下,opener包含处理常见情形的handler—ProxyHandler( 如果设置了代理,比如http_proxy变量 ),UnknownHandler,HTTPHandler,HTTPDefaultErrorHandler,HTTPRedirectHandler,FTPHandler,FileHandler,DataHandler,HTTPErrorProcessor.
事实上,顶层url要么是一个完整的url(包括“http:”模式和主机名以及可选的端口号),例如“http://example.com/” ,或者是一个“授权机构”(即主机名,可以再加一个端口号)如“example.com” 或“example.com:8080” 。如果使用“授权机构”的话,一定不能包含“userinfo”元素—比如“joe:password@example.com”就是不正确的。
代理
urllib会自动地检测并使用你的代理设置。这是通过ProxyHandler完成的,它是在检测到一个代理设置时的正常处理程序链的一部分。通常这是个好事,但是有些时候它可能没有用。还有一个方法就是设置我们自己的ProxyHandler,不定义代理。做法与设置一个Basic Authentication的步骤相同:
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
Note:现在urllib.request不支持通过代理访问一个https网址。不过,可以通过这篇教程实现。
如果设置一个变量
REQUEST_METHOD,那么HTTP_PROXY就会被忽略,具体可以查看getproxies()的文档
套接字层
Python对于获取网络数据的支持是有层次的。urllib使用的是http.client库,而他又转而使用socket库。
在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)