11.8. 处理压缩数据

您想要支持的最后一个重要的 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')        1
>>> 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                                    2
'
reply: 'HTTP/1.1 200 OK\r\n'
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                           3
header: Content-Length: 6289                             4
header: Connection: close
header: Content-Type: application/atom+xml
1 关键在于:一旦您创建了 Request 对象,就添加一个 Accept-encoding 标头,告诉服务器您可以接受 gzip 编码的数据。gzip 是您正在使用的压缩算法的名称。理论上,可能还有其他压缩算法,但 gzip 是 99% 的 Web 服务器使用的压缩算法。
2 这就是通过网络发送的标头。
3 以下是服务器返回的内容:Content-Encoding: gzip 标头表示您将要接收的数据已使用 gzip 压缩。
4 Content-Length 标头是压缩数据的长度,而不是未压缩数据的长度。您稍后会看到,未压缩数据的实际长度为 15955,因此 gzip 压缩将您的带宽减少了 60% 以上!

示例 11.15. 解压缩数据

>>> compresseddata = f.read()                              1
>>> len(compresseddata)
6289
>>> import StringIO
>>> compressedstream = StringIO.StringIO(compresseddata)   2
>>> import gzip
>>> gzipper = gzip.GzipFile(fileobj=compressedstream)      3
>>> data = gzipper.read()                                  4
>>> print data                                             5
<?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 -->
>>> len(data)
15955
1 继续前面的示例,f 是从 URL 打开器返回的类文件对象。使用其 read() 方法通常会获取未压缩的数据,但由于此数据已使用 gzip 压缩,因此这只是获取您真正想要的数据的第一步。
2 好的,这一步需要一些繁琐的变通方法。Python 有一个 gzip 模块,可以读取(实际上是写入)磁盘上的 gzip 压缩文件。但您在磁盘上没有文件,而是在内存中有一个 gzip 压缩缓冲区,并且您不想仅仅为了解压缩它而写出一个临时文件。因此,您要做的是使用 StringIO 模块从内存中的数据 (compresseddata) 创建一个类文件对象。您在 上一章 中第一次看到了 StringIO 模块,但现在您找到了它的另一个用途。
3 现在您可以创建一个 GzipFile 的实例,并告诉它它的“文件”是类文件对象 compressedstream
4 这是完成所有实际工作的代码行:从 GzipFile“读取”将解压缩数据。奇怪吗?是的,但这在某种程度上是说得通的。gzipper 是一个类文件对象,它表示一个 gzip 压缩文件。但是,该“文件”不是磁盘上的真实文件;gzipper 实际上只是从您使用 StringIO 创建的类文件对象中“读取”,以包装压缩数据,而这些数据只在变量 compresseddata 的内存中。那么这些压缩数据是从哪里来的呢?您最初是通过从使用 urllib2.build_opener 构建的类文件对象中“读取”数据,从远程 HTTP 服务器下载的。令人惊讶的是,这一切都正常工作。链中的每一步都不知道前一步是伪造的。
5 看,是真实的数据。(实际上是 15955 字节。)

“等等!”我听到你哭了。“这可以更简单!”我知道你在想什么。您在想 opener.open 返回一个类文件对象,那么为什么不删除 StringIO 中间人,直接将 f 传递给 GzipFile 呢?好吧,也许您没有这么想,但不用担心,因为它不起作用。

示例 11.16. 直接从服务器解压缩数据

>>> f = opener.open(request)                  1
>>> f.headers.get('Content-Encoding')         2
'gzip'
>>> data = gzip.GzipFile(fileobj=f).read()    3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "c:\python23\lib\gzip.py", line 217, in read
    self._read(readsize)
  File "c:\python23\lib\gzip.py", line 252, in _read
    pos = self.fileobj.tell()   # Save current position
AttributeError: addinfourl instance has no attribute 'tell'
1 继续前面的示例,您已经设置了一个带有 Accept-encoding: gzip 标头的 Request 对象。
2 只需打开请求即可获取标头(但尚未下载任何数据)。从返回的 Content-Encoding 标头中可以看出,此数据已使用 gzip 压缩发送。
3 由于 opener.open 返回一个类文件对象,并且您从标头中知道,当您读取它时,您将获得 gzip 压缩的数据,那么为什么不直接将该类文件对象传递给 GzipFile 呢?当您从 GzipFile 实例中“读取”时,它将从远程 HTTP 服务器“读取”压缩数据并动态解压缩。这是一个好主意,但不幸的是它不起作用。由于 gzip 压缩的工作方式,GzipFile 需要保存其位置并在压缩文件中前后移动。当“文件”是从远程服务器传入的字节流时,这不起作用;您所能做的就是一次检索一个字节,而不是在数据流中来回移动。因此,使用 StringIO 的不雅观的技巧是最好的解决方案:下载压缩数据,使用 StringIO 从中创建一个类文件对象,然后从该对象解压缩数据。