11.8. Prise en charge des données compressées.

La dernière fonctionnalité importante du protocole HTTP que nous voulons supporter est la compression. Beaucoup de services Web ont la capacité d'envoyer les données compressées, ce qui qui peut réduire le volume de données envoyées de 60 % ou plus. C'est particulièrement vrai des services Web XML puisque les données XML se compressent très bien.

Les serveurs n'envoient de données compressées que si on déclare les prendre en charge.

Exemple 11.14. Déclarer au serveur que nous voulons des données compressées.

>>> 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 C'est l'étape-clé : une fois que nous avons créé notre objet Request, nous ajoutons un en-tête Accept-encoding pour déclarer au serveur que nous acceptons les données encodées gzip. gzip est le nom de l'algorithme de compression que nous utilisons. En théorie il pourrait y en avoir d'autres, mais gzip est l'algorithme de compression utilisé par 99 % des serveurs Web.
2 Voici l'en-tête envoyé au serveur.
3 Et voici la réponse envoyée par le serveur : l'en-tête Content-Encoding: gzip signale que les données que nous allons recevoir sont compressées par gzip.
4 L'en-tête Content-Length indique la longueur des données compressées, pas leur longueur décompressées. Comme nous allons le voir, la taille réelle des données décompressées est ici 15955, la compression gzip nous a donc permis de réduire la bande passante utilisée de plus de 60 % !

Exemple 11.15. Decompression des données

>>> 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 Nous continuons l'exemple précédent, f est l'objet-fichier retourné par l'opener d'URL. Appeler sa méthode read() nous permettrait d'habitude d'obtenir les données non compressées, mais ici ce n'est que la première étape puisque les données sont compressées par gzip.
2 Cette étape est un peu du bidouillage. Python a un module gzip qui lit (et peut aussi écrire) des fichiers compressés par gzip sur le disque. Mais ici, nous n'avons pas de fichier sur le disque, nous avons un tampon de données compressées en mémoire et nous n'allons pas l'écrire dans un fichier temporaire uniquement pour le décompresser. Donc nous créons un objet-fichier à partir de ces données en mémoire (compresseddata) à l'aide du module StringIO. Nous avons vu le module StringIO au chapitre précédent, mais nous avons maintenant un autre emploi pour lui.
3 Maintenant nous pouvons créer une instance de GzipFile et lui indiquer que son «fichier» est l'objet-fichier compressedstream.
4 Voici la ligne qui effectue le véritable travail : «lire» GzipFile décompresse les données. C'est étrange, mais en fait il y a une logique. gzipper est un objet-fichier qui repreésente un fichier compressé par gzip. Mais ce «fichier» n'est pas un vrai fichier sur le disque, gzipper ne «lit» que l'objet-fichier que nous avons créé avec StringIO pour contenir les données compressées, qui sont elles-mêmes en mémoire dans la variable compresseddata. Et d'où viennent les données compressées ? Nous les avons téléchargées d'un serveur HTTP distant en «lisant» l'objet-fichier que nous avions construit avec urllib2.build_opener. Et tout cela fonctionne, chaque étape dans la chaîne n'a aucune idée que l'étape précédente ne produit pas un vrai fichier.
5 Et voilà, de véritables données (15955 octets, plus précisément).

«Mais attendez !», vous exclamez-vous. «Cela pourrait être simplifié !» Je sais ce que vous pensez, vous vous dite que opener.open retourne un objet-fichier, alors pourquoi ne pas se débarasser de l'intermédiaire StringIO et passer f directement à GzipFile ? Bon, peut-être que vous ne pensiez pas ça, mais de toute manière ça ne marche pas.

Exemple 11.16. Decompression directe des données du serveur.

>>> 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 En poursuivant l'exemple précédent, nous avons déjà un objet Request avec un en-tête Accept-encoding: gzip.
2 L'ouverture de la requête nous donne les en-têtes (mais ne télécharge pas encore les données). Comme vous pouvez le voir, les données qui ont été envoyées sont compressées par gzip.
3 Puisque opener.open retourne un objet-fichier et que nous savons par les en-têtes que nous obtiendrons des données compressées par gzip en le lisant, pourquoi ne pas passer cet objet-fichier directement à GzipFile ? Comme nous «lisons» l'instance de GzipFile, elle «lira» les données compressées du serveur HTTP distant et les décompressera à la volée. C'est une bonne idée, mais malheureusement ça ne marche pas. A cause de la manière dont la compression gzip fonctionne, GzipFile doit sauvegarder sa position et se déplacer vers l'avant et l'arrière dans le fichier compressé. Cela ne marche pas lorsque le «file» est un flux d'octets provenant d'un serveur distant, tout ce que nous pouvons faire est de le recevoir un octet après l'autre, il est impossible de se déplacer d'avant en arrière dans le flux de données. Donc la bidouille inélégante consistant à utiliser StringIO est la meilleure solution : télécharger les données compressées, en faire un objet-fichier avec StringIO et décompresser les données depuis cet objet-fichier.