11.8. Tratamiento de datos comprimidos

La última característica importante de HTTP que queremos tratar es la compresión. Muchos servicios web tienen la capacidad de enviar los datos comprimidos, lo que puede rebajar en un 60% o más la cantidad de datos a enviar. Esto se aplica especialmente a los servicios web XML, ya que los datos XML se comprimen bastante bien.

Los servidores no nos van a dar comprimidos los datos a menos que le digamos que lo aceptamos así.

Ejemplo 11.14. Le decimos al servidor que queremos datos comprimidos

>>> 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 Ésta es la clave: una vez creado el objeto Request object, añada una cabecera Accept-encoding para decirle al servidor que podemos aceptar datos gzip-encoded. gzip es el nombre del algoritmo de compresión que usamos. En teoría podría haber otros algoritmos de compresión, pero el que usa el 99% de los servidores es gzip.
2 Ahí va nuestra cabecera camino al cable.
3 Y aquí está lo que devuelve el servidor: la cabecera Content-Encoding: gzip significa que los datos que estamos a punto de recibir están comprimidos con gzip.
4 La cabecera Content-Length es la longitud de los datos comprimidos, no de la versión sin comprimir. Como veremos en un momento, la longitud real de los datos sin comprimir era 15955, ¡así que la compresión gzip minimizó la descarga en más del 60%!

Ejemplo 11.15. Descompresión de los datos

>>> 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 Continuamos el ejemplo anterior, y f es el objeto de tipo fichero devuelto por el abridor de URL. Normalmente obtendríamos datos sin comprimir usando su método read(), pero dado que estos datos han sido comprimidos, éste sólo es el primer paso para obtener los datos que realmente queremos.
2 Bien, este paso es un rodeo un poco sucio. Python tiene un módulo gzip que lee (y escribe) ficheros comprimidos en el disco. Pero no tenemos un fichero sino un búfer en memoria comprimido con gzip, y no queremos escribirlo en un fichero temporal sólo para poder descomprimirlo. Así que lo que haremos es crear un objeto de tipo fichero partiendo de los datos en memoria (compresseddata), haciendo uso del módulo StringIO. Conocimos este módulo en el capítulo anterior, pero ahora le hemos encontrado un nuevo uso.
3 Ahora podemos crear una instancia de GzipFile y decirle que su “fichero” es el objeto tipo fichero compressedstream.
4 Ésta es la línea que hace todo el trabajo: “leer” de GzipFile descomprimirá los datos. ¿Extraño? Sí, pero tiene sentido de una manera retorcida. gzipper es un objeto de tipo fichero que representa un fichero comprimido con gzip. Ese “fichero” no es real, sin embargo; en realidad gzipper sólo está “leyendo” del objeto tipo fichero que creamos con StringIO para obtener los datos comprimidos, que están en memoria en la variable compresseddata. ¿Y de dónde vinieron esos datos comprimidos? Originalmente los descargamos de un servidor HTTP remoto “leyendo” el objeto tipo fichero que construimos con urllib2.build_opener. Y sorprendentemente, todo funciona. Cada paso en la cadena ignora que el paso anterior está engañándole.
5 ¡Mira mamá!, datos de verdad (15955 bytes, para ser exactos).

¡Pero espere!” puedo oírle gritar. “¡Esto podría ser incluso más sencillo!” Sé lo que está pensando. Está pensando que opener.open devuelve un fichero de tipo objeto así que, ¿por qué no eliminar el paso intermedio por StringIO y limitarnos a pasar f directamente a GzipFile? Bueno vale, quizá no estuviera pensando eso, pero no se preocupe que tampoco hubiera funcionado.

Ejemplo 11.16. Descompresión de datos directamente del servidor

>>> 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 Siguiendo con el ejemplo anterior, ya tenemos un objeto Request preparado con la cabecera Accept-encoding: gzip.
2 Abrir la petición nos dará las cabeceras (aunque aún no descargará datos). Como puede ver de la cabecera Content-Encoding devuelta, estos datos se han enviado comprimidos con gzip.
3 Como opener.open devuelve un objeto tipo fichero y sabemos por las cabeceras que cuando lo leamos vamos a obtener datos comprimidos con gzip, ¿por qué no pasarle directamente ese objeto tipo fichero a GzipFile? A medida que lea de la instancia de GzipFile, leerá datos comprimidos del servidor remoto HTTP y los descomprimirá al vuelo. Es una buena idea, pero desafortunadamente no funciona. Debido a la manera en que funciona la compresión gzip, GzipFile necesita guardar su posición y retroceder y avanzar por el fichero comprimido. Esto no funciona cuando el “fichero” es un flujo de bytes que viene de un servidor remoto; todo lo que podemos hacer es recoger bytes uno tras otro, y no movernos adelante y atrás por el flujo de datos. Por ello la mejor solución es el poco elegante uso de StringIO: descargar los datos comprimidos, crear un objeto tipo fichero partiendo de ellos con StringIO, y luego descomprimir los datos con eso.