11.6. 处理 Last-ModifiedETag

既然你知道如何在你的 web 服务请求中添加自定义的 HTTP 头信息,接下来看看如何添加 Last-ModifiedETag 头信息的支持。

下面的这些例子将以调试标记置为关闭的状态来显示输出结果。如果你还停留在上一部分的开启状态,可以使用 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                       1
{'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'))  2
>>> seconddatastream = opener.open(request)            3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "c:\python23\lib\urllib2.py", line 326, in open
    '_open', req)
  File "c:\python23\lib\urllib2.py", line 306, in _call_chain
    result = func(*args)
  File "c:\python23\lib\urllib2.py", line 901, in http_open
    return self.do_open(httplib.HTTP, req)
  File "c:\python23\lib\urllib2.py", line 895, in do_open
    return self.parent.error('http', req, fp, code, msg, hdrs)
  File "c:\python23\lib\urllib2.py", line 352, in error
    return self._call_chain(*args)
  File "c:\python23\lib\urllib2.py", line 306, in _call_chain
    result = func(*args)
  File "c:\python23\lib\urllib2.py", line 412, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 304: Not Modified
1 还记得当调试标记设置为开启时所有那些你看到的 HTTP 头信息打印输出吗? 这里便是用编程方式访问它们的方法: firstdatastream.headers一个类似 dictionary 行为的对象并且允许你获得任何个别的从 HTTP 服务器返回的头信息。
2 在第二次请求时,你用第一次请求获得的最近修改时间添加了 If-Modified-Since 头信息。如果数据没被改变,服务器应该返回一个 304 状态代码。
3 毫无疑问,数据没被改变。你可以从跟踪返回结果看到 urllib2 抛出了一个特殊异常,HTTPError,以响应 304 状态代码。这有点不寻常,并且完全没有任何帮助。毕竟,它不是个错误;你明确地询问服务器如果没有变化就不要发送任何数据,并且数据没有变化,所以服务器告诉你它没有为你发送任何数据。那不是个错误;实际上也正是你所期望的。

urllib2 也为你认为是错误的其他条件引发 HTTPError 异常,比如 404 (page not found)。实际上,它将为任何 除了状态代码 200 (OK)、301 (permanent redirect)或 302 (temporary redirect) 之外的状态引发 HTTPError。捕获状态代码并简单返回它,而不是抛出异常,这应该对你很有帮助。为了实现它,你将需要自定义一个 URL 处理器。

例 11.7. 定义 URL 处理器

这个自定义的 URL 处理器是 openanything.py 的一部分。


class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):    1
    def http_error_default(self, req, fp, code, msg, headers): 2
        result = urllib2.HTTPError(                           
            req.get_full_url(), code, msg, headers, fp)       
        result.status = code                                   3
        return result                                         
1 urllib2 是围绕 URL 处理器而设计的。每一个处理器就是一个能定义任意数量方法的类。当某事件发生时——比如一个 HTTP 错误,甚至是 304 代码——urllib2 审视用于处理它的 一系列已定义的处理器方法。在此要用到自省,与 第 9 章 XML 处理中为不同节点类型定义不同处理器类似。但是 urllib2 是很灵活的,还可以内省为当前请求所定义的所有处理器。
2 当从服务器接收到一个 304 状态代码时,urllib2 查找定义的操作并调用 http_error_default 方法。通过定义一个自定义的错误处理,你可以阻止 urllib2 引发异常。取而代之的是,你创建 HTTPError 对象,返回它而不是引发异常。
3 这是关键部分:返回之前,你保存从 HTTP 服务器返回的状态代码。这将使你从主调程序轻而易举地访问它。

例 11.8. 使用自定义 URL 处理器

>>> request.headers                           1
{'If-modified-since': 'Thu, 15 Apr 2004 19:45:21 GMT'}
>>> import openanything
>>> opener = urllib2.build_opener(
...     openanything.DefaultErrorHandler())   2
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status                   3
304
>>> seconddatastream.read()                   4
''
1 继续前面的例子,Request 对象已经被设置,并且你已经添加了 If-Modified-Since 头信息。
2 这是关键所在:既然已经定义了你的自定义 URL 处理器,你需要告诉 urllib2 来使用它。还记得我怎么说 urllib2 将一个 HTTP 资源的访问过程分解为三个步骤的正当理由吗?这便是为什么构建 HTTP 开启器是其步骤之一,因为你能用你自定义的 URL 操作覆盖 urllib2 的默认行为来创建它。
3 现在你可以快速地打开一个资源,返回给你的对象既包括常规头信息 (使用 seconddatastream.headers.dict 访问它们),也包括 HTTP 状态代码。在此,正如你所期望的,状态代码是 304,意味着此数据自从上次请求后没有被修改。
4 注意当服务器返回 304 状态代码时,并没有重新发送数据。这就是全部的关键:没有重新下载未修改的数据,从而节省了带宽。因此若你确实想要那个数据,你需要在首次获得它时在本地缓存数据。

处理 ETag 的工作也非常相似,只不过不是检查 Last-Modified 并发送 If-Modified-Since,而是检查 ETag 并发送 If-None-Match。让我们打开一个新的 IDE 会话。

例 11.9. 支持 ETag/If-None-Match

>>> import urllib2, openanything
>>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml')
>>> opener = urllib2.build_opener(
...     openanything.DefaultErrorHandler())
>>> firstdatastream = opener.open(request)
>>> firstdatastream.headers.get('ETag')        1
'"e842a-3e53-55d97640"'
>>> firstdata = firstdatastream.read()
>>> print firstdata                            2
<?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 -->
>>> request.add_header('If-None-Match',
...     firstdatastream.headers.get('ETag'))   3
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status                    4
304
>>> seconddatastream.read()                    5
''
1 使用 firstdatastream.headers 伪字典,你可以获得从服务器返回的 ETag (如果服务器没有返回 ETag 会发生什么?答案是,这一行代码将返回 None。)
2 OK,你获得了数据。
3 现在进行第二次调用,将 If-None-Match 头信息设置为你第一次调用获得的 ETag
4 第二次调用静静地成功了 (没有出现任何的异常),并且你又一次看到了从服务器返回的 304 状态代码。你第二次基于 ETag 发送请求,服务器知道数据没有被改变。
5 无论 304 是被 Last-Modified 数据检查还是 ETag hash 匹配触发的,获得 304 的同时都不会下载数据。这就是重点所在。
注意
在这些例子中,HTTP 服务器同时支持 Last-ModifiedETag 头信息,但并非所有的服务器皆如此。作为一个 web 服务的客户端,你应该为支持两种头信息做准备,但是你的程序也应该为服务器仅支持其中一种头信息或两种头信息都不支持而做准备。