Python3使用urllib访问网页
介绍
改教程翻译自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)