11. HTTP Web 服务
出处: http://www.woodpecker.org.cn/diveintopython/http_web_services/index.html
在讲解如何下载 web 页和如何从 URL 解析 XML时,你已经学习了关于 HTML 处理和 XML 处理,接下来让我们来更全面地探讨有关 HTTP web 服务的主题。
简单地讲,HTTP web 服务是指以编程的方式直接使用 HTTP 操作从远程服务器发送和接收数据。如果你要从服务器获取数据,直接使用 HTTP GET;如果您想发送新数据到服务器,使用 HTTP POST。(一些较高级的 HTTP web 服务 API 也定义了使用 HTTP PUT 和 HTTP DELETE 修改和删除现有数据的方法。) 换句话说,构建在 HTTP 协议中的 “verbs (动作)” (GET, POST, PUT 和 DELETE) 直接映射为接收、发送、修改和删除等应用级别的操作。
这种方法的主要优点是简单,并且许多不同的站点充分印证了这样的简单性是受欢迎的。数据 (通常是 XML 数据) 能静态创建和存储,或通过服务器端脚本和所有主流计算机语言 (包括用于下载数据的 HTTP 库) 动态生成。调试也很简单,因为您可以在任意浏览器中调用网络服务来查看这些原始数据。现代浏览器甚至可以为您进行良好的格式化并漂亮地打印这些 XML 数据,以便让您快速地浏览。
HTTP web 服务上的纯 XML 应用举例:
- Amazon API 允许您从 Amazon.com 在线商店获取产品信息。
- National Weather Service (美国) 和 Hong Kong Observatory (香港) 通过 web 服务提供天气警报。
- Atom API 用来管理基于 web 的内容。
- Syndicated feeds 应用于 weblogs 和新闻站点中带给您来自众多站点的最新消息。
在后面的几章里,我们将探索使用 HTTP 进行数据发送和接收传输的 API,但是不会将应用语义映射到潜在的 HTTP 语义。(所有这些都是通过 HTTP POST 这个管道完成的。) 但是本章将关注使用 HTTP GET 从远程服务器获取数据,并且将探索几个由纯 HTTP web 服务带来最大利益的 HTTP 特性。
如下所示为上一章曾经看到过的 openanything 模块的更高级版本:
例 11.1. openanything.py
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
import urllib2, urlparse, gzip from StringIO import StringIO USER_AGENT = 'OpenAnything/1.0 +http://diveintopython.org/http_web_services/' class SmartRedirectHandler(urllib2.HTTPRedirectHandler): def http_error_301(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_301( self, req, fp, code, msg, headers) result.status = code return result def http_error_302(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_302( self, req, fp, code, msg, headers) result.status = code return result class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): def http_error_default(self, req, fp, code, msg, headers): result = urllib2.HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT): '''URL, filename, or string --> stream This function lets you define parsers that take any input source (URL, pathname to local or network file, or actual data as a string) and deal with it in a uniform manner. Returned object is guaranteed to have all the basic stdio read methods (read, readline, readlines). Just .close() the object when you're done with it. If the etag argument is supplied, it will be used as the value of an If-None-Match request header. If the lastmodified argument is supplied, it must be a formatted date/time string in GMT (as returned in the Last-Modified header of a previous request). The formatted date/time will be used as the value of an If-Modified-Since request header. If the agent argument is supplied, it will be used as the value of a User-Agent request header. ''' if hasattr(source, 'read'): return source if source == '-': return sys.stdin if urlparse.urlparse(source)[0] == 'http': # open URL with urllib2 request = urllib2.Request(source) request.add_header('User-Agent', agent) if etag: request.add_header('If-None-Match', etag) if lastmodified: request.add_header('If-Modified-Since', lastmodified) request.add_header('Accept-encoding', 'gzip') opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler()) return opener.open(request) # try to open with native open function (if source is a filename) try: return open(source) except (IOError, OSError): pass # treat source as string return StringIO(str(source)) def fetch(source, etag=None, last_modified=None, agent=USER_AGENT): '''Fetch data and metadata from a URL, file, stream, or string''' result = {} f = openAnything(source, etag, last_modified, agent) result['data'] = f.read() if hasattr(f, 'headers'): # save ETag, if the server sent one result['etag'] = f.headers.get('ETag') # save Last-Modified header, if the server sent one result['lastmodified'] = f.headers.get('Last-Modified') if f.headers.get('content-encoding', '') == 'gzip': # data came back gzip-compressed, decompress it result['data'] = gzip.GzipFile(fileobj=StringIO(result['data']])).read() if hasattr(f, 'url'): result['url'] = f.url result['status'] = 200 if hasattr(f, 'status'): result['status'] = f.status f.close() return result进一步阅读
- Paul Prescod 认为纯 HTTP web 服务是 Internet 的未来。
假如说你想用 HTTP 下载资源,例如一个 Atom feed 汇聚。你不仅仅想下载一次;而是想一次又一次地下载它,如每小时一次,从提供 news feed 的站点获得最新的消息。让我们首先用一种直接而原始的方法来实现它,然后看看如何改进它。
例 11.2. 用直接而原始的方法下载 feed
>>> import urllib >>> data = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read()>>> print data <?xml version="1.0" encoding="iso-8859-1"?> <feed version="0.3" xmlns="http://purl.org/atom/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en"> <title mode="escaped">dive into mark</title> <link rel="alternate" type="text/html" href="http://diveintomark.org/"/> <-- rest of feed omitted for brevity -->这里有五个你必须关注的 HTTP 重要特性。
User-Agent 是一种客户端告知服务器谁在什么时候通过 HTTP 请求了一个 web 页、feed 汇聚或其他类型的 web 服务的简单途径。当客户端请求一个资源时,应该尽可能明确发起请求的是谁,以便当产生异常错误时,允许服务器端的管理员与客户端的开发者取得联系。
默认情况下 Python 发送一个通用的 User-Agent:Python-urllib/1.15。下一节,您将看到更加有针对性的 User-Agent。
有时资源移来移去。Web 站点重组内容,页面移动到了新的地址。甚至是 web 服务重组。原来位于 http://example.com/index.xml 的 feed 汇聚可能被移动到 http://example.com/xml/atom.xml。或者因为一个机构的扩展或重组,整个域被迁移。例如,http://www.example.com/index.xml 可能被重定向到 http://server-farm-1.example.com/index.xml。
您每次从 HTTP 服务器请求任何类型的资源时,服务器的响应中均包含一个状态代码。状态代码 200 的意思是 “一切正常,这就是您请求的页面”。状态代码 404 的意思是 “页面没找到”。 (当浏览 web 时,你可能看到过 404 errors。)
HTTP 有两种不同的方法表示资源已经被移动。状态代码 302 表示临时重定向;这意味着 “哎呀,访问内容被临时移动” (然后在 Location: 头信息中给出临时地址)。状态代码 301 表示永久重定向;这意味着 “哎呀,访问内容被永久移动” (然后在 Location: 头信息中给出新地址)。如果您获得了一个 302 状态代码和一个新地址,HTTP 规范说您应该使用新地址获取您的请求,但是下次您要访问同一资源时,应该使用原地址重试。但是如果您获得了一个 301 状态代码和一个新地址,您应该从此使用新地址。
当从 HTTP 服务器接受到一个适当的状态代码时,urllib.urlopen 将自动 “跟踪” 重定向,但不幸的是,当它做了重定向时不会告诉你。 你将最终获得所请求的数据,却丝毫不会察觉到在这个过程中一个潜在的库 “帮助” 你做了一次重定向操作。因此你将继续不断地使用旧地址,并且每次都将获得被重定向的新地址。这一过程要往返两次而不是一次:太没效率了!本章的后面,您将看到如何改进这一点,从而适当地且有效率地处理永久重定向。
有些数据随时都在变化。CNN.com 的主页经常几分钟就更新。另一方面,Google.com 的主页几个星期才更新一次 (当他们上传特殊的假日 logo,或为一个新服务作广告时)。 Web 服务是不变的:通常服务器知道你所请求的数据的最后修改时间,并且 HTTP 为服务器提供了一种将最近修改数据连同你请求的数据一同发送的方法。
如果你第二次 (或第三次,或第四次) 请求相同的数据,你可以告诉服务器你上一次获得的最后修改日期:在你的请求中发送一个 If-Modified-Since 头信息,它包含了上一次从服务器连同数据所获得的日期。如果数据从那时起没有改变,服务器将返回一个特殊的 HTTP 状态代码 304,这意味着 “从上一次请求后这个数据没有改变”。这一点有何进步呢?当服务器发送状态编码 304 时,不再重新发送数据。您仅仅获得了这个状态代码。所以当数据没有更新时,你不需要一次又一次地下载相同的数据;服务器假定你有本地的缓存数据。
所有现代的浏览器都支持最近修改 (last-modified) 的数据检查。如果你曾经访问过某页,一天后重新访问相同的页时发现它没有变化,并奇怪第二次访问时页面加载得如此之快——这就是原因所在。你的浏览器首次 访问时会在本地缓存页面内容,当你第二次访问,浏览器自动发送首次访问时从服务器获得的最近修改日期。服务器简单地返回 304: Not Modified (没有修改),因此浏览器就会知道从本地缓存加载页面。在这一点上,Web 服务也如此智能。
Python 的 URL 库没有提供内置的最近修改数据检查支持,但是你可以为每一个请求添加任意的头信息并在每一个响应中读取任意头信息,从而自己添加这种支持。
ETag 是实现与最近修改数据检查同样的功能的另一种方法:没有变化时不重新下载数据。其工作方式是:服务器发送你所请求的数据的同时,发送某种数据的 hash (在 ETag 头信息中给出)。hash 的确定完全取决于服务器。当第二次请求相同的数据时,你需要在 If-None-Match: 头信息中包含 ETag hash,如果数据没有改变,服务器将返回 304 状态代码。与最近修改数据检查相同,服务器仅仅 发送 304 状态代码;第二次将不为你发送相同的数据。在第二次请求时,通过包含 ETag hash,你告诉服务器:如果 hash 仍旧匹配就没有必要重新发送相同的数据,因为你还有上一次访问过的数据。
Python 的 URL 库没有对 ETag 的内置支持,但是在本章后面你将看到如何添加这种支持。
最后一个重要的 HTTP 特性是 gzip 压缩。 关于 HTTP web 服务的主题几乎总是会涉及在网络线路上传输的 XML。XML 是文本,而且还是相当冗长的文本,而文本通常可以被很好地压缩。当你通过 HTTP 请求一个资源时,可以告诉服务器,如果它有任何新数据要发送给我时,请以压缩的格式发送。在你的请求中包含 Accept-encoding: gzip 头信息,如果服务器支持压缩,它将返回由 gzip 压缩的数据并且使用 Content-encoding: gzip 头信息标记。
Python 的 URL 库本身没有内置对 gzip 压缩的支持,但是你能为请求添加任意的头信息。Python 还提供了一个独立的 gzip 模块,它提供了对数据进行解压缩的功能。
注意我们用于下载 feed 汇聚的小单行脚本并不支持任何这些 HTTP 特性。让我们来看看如何改善它。
首先,让我们开启 Python HTTP 库的调试特性并查看网络线路上的传输过程。这对本章的全部内容都很有用,因为你将添加越来越多的特性。
例 11.3. 调试 HTTP
>>> import httplib >>> httplib.HTTPConnection.debuglevel = 1>>> import urllib >>> feeddata = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read() connect: (diveintomark.org, 80)send: ' GET /xml/atom.xml HTTP/1.0Host: diveintomark.org
User-agent: Python-urllib/1.15
' reply: 'HTTP/1.1 200 OKrn'header: Date: Wed, 14 Apr 2004 22:27:30 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMTheader: ETag: "e8284-68e0-4de30f80"
header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close改善你的 HTTP web 服务客户端的第一步就是用 User-Agent 适当地鉴别你自己。为了做到这一点,你需要远离基本的 urllib 而深入到 urllib2。
例 11.4. urllib2 介绍
>>> import httplib >>> httplib.HTTPConnection.debuglevel = 1>>> import urllib2 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml')>>> opener = urllib2.build_opener()
>>> feeddata = opener.open(request).read()
connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OKrn' header: Date: Wed, 14 Apr 2004 23:23:12 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT header: ETag: "e8284-68e0-4de30f80" header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close既然你知道如何在你的 web 服务请求中添加自定义的 HTTP 头信息,接下来看看如何添加 Last-Modified 和 ETag 头信息的支持。
下面的这些例子将以调试标记置为关闭的状态来显示输出结果。如果你还停留在上一部分的开启状态,可以使用 httplib.HTTPConnection.debuglevel = 0 将其设置为关闭状态。或者,如果你认为有帮助也可以保持为开启状态。
例 11.6. 测试 Last-Modified
>>> import urllib2 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> opener = urllib2.build_opener() >>> firstdatastream = opener.open(request) >>> firstdatastream.headers.dict{'date': 'Thu, 15 Apr 2004 20:42:41 GMT', 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 'content-type': 'application/atom+xml', 'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'etag': '"e842a-3e53-55d97640"', 'content-length': '15955', 'accept-ranges': 'bytes', 'connection': 'close'} >>> request.add_header('If-Modified-Since', ... firstdatastream.headers.get('Last-Modified'))>>> seconddatastream = opener.open(request)
Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:python23liburllib2.py", line 326, in open '_open', req) File "c:python23liburllib2.py", line 306, in _call_chain result = func(*args) File "c:python23liburllib2.py", line 901, in http_open return self.do_open(httplib.HTTP, req) File "c:python23liburllib2.py", line 895, in do_open return self.parent.error('http', req, fp, code, msg, hdrs) File "c:python23liburllib2.py", line 352, in error return self._call_chain(*args) File "c:python23liburllib2.py", line 306, in _call_chain result = func(*args) File "c:python23liburllib2.py", line 412, in http_error_default raise HTTPError(req.get_full_url(), code, msg, hdrs, fp) urllib2.HTTPError: HTTP Error 304: Not Modified
你可以使用两种不同的自定义 URL 处理器来处理永久重定向和临时重定向。
首先,让我们来看看重定向处理的必要性。
例 11.10. 没有重定向处理的情况下,访问 web 服务
>>> import urllib2, httplib >>> httplib.HTTPConnection.debuglevel = 1>>> request = urllib2.Request( ... 'http://diveintomark.org/redir/example301.xml')>>> opener = urllib2.build_opener() >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /redir/example301.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 301 Moved Permanentlyrn'header: Date: Thu, 15 Apr 2004 22:06:25 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 338 header: Connection: close header: Content-Type: text/html; charset=iso-8859-1 connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OKrn' header: Date: Thu, 15 Apr 2004 22:06:25 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Content-Length: 15955 header: Connection: close header: Content-Type: application/atom+xml >>> f.url'http://diveintomark.org/xml/atom.xml' >>> f.headers.dict {'content-length': '15955', 'accept-ranges': 'bytes', 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'connection': 'close', 'etag': '"e842a-3e53-55d97640"', 'date': 'Thu, 15 Apr 2004 22:06:25 GMT', 'content-type': 'application/atom+xml'} >>> f.status Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: addinfourl instance has no attribute 'status'你要支持的最后一个重要的 HTTP 特性是压缩。许多 web 服务具有发送压缩数据的能力,这可以将网络线路上传输的大量数据消减 60% 以上。这尤其适用于 XML web 服务,因为 XML 数据 的压缩率可以很高。
服务器不会为你发送压缩数据,除非你告诉服务器你可以处理压缩数据。
例 11.14. 告诉服务器你想获得压缩数据
>>> import urllib2, httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> request.add_header('Accept-encoding', 'gzip')>>> opener = urllib2.build_opener() >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 Accept-encoding: gzip' reply: 'HTTP/1.1 200 OKrn' header: Date: Thu, 15 Apr 2004 22:24:39 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Vary: Accept-Encoding header: Content-Encoding: gzip
header: Content-Length: 6289
header: Connection: close header: Content-Type: application/atom+xml
你已经看到了构造一个智能的 HTTP web 客户端的所有片断。现在让我们看看如何将它们整合到一起。
例 11.17. openanything 函数
这个函数定义在 openanything.py 中。
def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT): # non-HTTP code omitted for brevity if urlparse.urlparse(source)[0] == 'http':# open URL with urllib2 request = urllib2.Request(source) request.add_header('User-Agent', agent)if etag: request.add_header('If-None-Match', etag)if lastmodified: request.add_header('If-Modified-Since', lastmodified)request.add_header('Accept-encoding', 'gzip')opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler())return opener.open(request)
openanything.py 及其函数现在可以完美地工作了。
每个客户端都应该支持 HTTP web 服务的以下 5 个重要特性:
- 通过设置适当的 User-Agent 识别你的应用。
- 适当地处理永久重定向。
- 支持 Last-Modified 日期检查从而避免在数据未改变的情况下重新下载数据。
- 支持 ETag hash 从而避免在数据未改变的情况下重新下载数据。
- 支持 gzip 压缩从而在数据已经 改变的情况下尽可能地减少传输带宽。