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 是一个 行为类似于字典的对象,它允许您获取从 HTTP 服务器返回的任何单个头部。
2 在第二个请求中,您添加了 If-Modified-Since 头部,其中包含第一个请求中的最后修改日期。如果数据没有更改,服务器应该返回 304 状态码。
3 果然,数据没有改变。您可以从回溯中看到,urllib2 针对 304 状态码抛出了一个特殊的异常,即 HTTPError。这有点不寻常,而且不是很有帮助。毕竟,这不是一个错误;您明确要求服务器在数据没有更改的情况下不要向您发送任何数据,而数据没有更改,所以服务器告诉您它没有向您发送任何数据。这不是错误;这正是您所希望的。

urllib2 还会针对您认为是错误的情况引发 HTTPError 异常,例如 404(未找到页面)。事实上,对于除 200(OK)、301(永久重定向)或 302(临时重定向)之外的 任何 状态码,它都会引发 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 urllib2 从服务器遇到 304 状态码时,它会在已定义的处理程序中搜索并调用 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 资源的过程分为三个步骤,并且有充分的理由吗?这就是构建 URL 打开器本身就是一个步骤的原因,因为您可以使用自己的自定义 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 好的,您获得了数据。
3 现在,通过将 If-None-Match 头部设置为从第一次调用中获得的 ETag 来设置第二次调用。
4 第二次调用安静地成功了(没有抛出异常),您再次看到服务器返回了 304 状态码。根据您第二次发送的 ETag,它知道数据没有更改。
5 无论 304 是由 Last-Modified 日期检查还是 ETag 哈希匹配触发的,您都不会在 304 中获得数据。这就是重点。
Note
在这些示例中,HTTP 服务器同时支持 Last-ModifiedETag 头部,但并非所有服务器都支持。作为 Web 服务客户端,您应该准备好同时支持两者,但您必须进行防御性编码,以防服务器只支持其中之一或两者都不支持。