IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Plongez au coeur de Python ,De débutant à expert


précédentsommairesuivant

XI. Services Web HTTP

XI-A. Plonger

Nous avons vu le traitement du HTML et le traitement du XML et, au cours de ces chapitres, comment télécharger une page Web et comment analyser du XML à partir d'une URL. Nous allons maintenant plonger dans le sujet plus général des services Web HTTP.

Pour parler simplement, les services Web HTTP sont une manière d'envoyer et de recevoir des données vers et depuis des serveurs distant par la programmation à l'aide des opérations HTTP. Si nous voulons recevoir des données d'un serveur distant, nous utilisons une simple instruction HTTP GET, si nous voulons envoyer des données au serveur, nous utilisons HTTP POST (certaines API de services Web HTTP plus sophistiquées définissent aussi des manières de modifier ou de supprimer des données avec HTTP PUT et HTTP DELETE). En d'autres termes, les «verbes» du protocole HTTP (GET, POST, PUT et DELETE) correspondent directement à des opérations au niveau de l'application pour recevoir, envoyer, modifier et supprimer des données.

L'avantage principale de cette approche est sa simplicité et cette simplicité a rencontré un succès certain sur de nombreux sites. Les données, en général au format XML, peuvent être construites et stockées de manière statique ou générées dynamiquement par un script côté serveur et tous les principaux langages ont une bibliothèque HTTP pour les télécharger. Le débogage est également plus simple car on peut voir les données brutes à l'aide de n'importe quel navigateur Web. Les navigateur récents affichent même le XML formaté et en couleur pour faciliter sa lecture.

Exemples de services Web XML pur par HTTP :

Dans des chapitres suivants, nous explorerons des API qui utilisent HTTP comme moyen de transport pour envoyer et recevoir des données, mais qui ne font pas correspondre la sémantique de l'application et celle du protocole HTTP (elles communiquent tout par un HTTP POST). Mais ce chapitre est centré sur l'utilisation de HTTP GET pour obtenir des données d'un serveur distant et nous verrons plusieurs fonctionnalités du protocole HTTP que nous pouvons utiliser pour obtenir le maximum des services Web HTTP.

Voici une version plus perfectionnée du module openanything que nous avons vu au chapitre précédent:

Exemple 11.1. openanything.py

Si vous ne l'avez pas déjà fait,vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.

 
Sélectionnez
import urllib2, urlparse, gzip
from StringIO import StringIO

USER_AGENT = 'OpenAnything/1.0 +http://diveintopython.org/http_web_services/'

class SmartRedirectHandler(urllib2.HTTPRedirectHandler):    
    def http_error_301(self, req, fp, code, msg, headers):  
        result = urllib2.HTTPRedirectHandler.http_error_301(
            self, req, fp, code, msg, headers)              
        result.status = code                                
        return result                                       

    def http_error_302(self, req, fp, code, msg, headers):  
        result = urllib2.HTTPRedirectHandler.http_error_302(
            self, req, fp, code, msg, headers)              
        result.status = code                                
        return result                                       

class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):   
    def http_error_default(self, req, fp, code, msg, headers):
        result = urllib2.HTTPError(                           
            req.get_full_url(), code, msg, headers, fp)       
        result.status = code                                  
        return result                                         

def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT):
    '''URL, filename, or string --> stream

    This function lets you define parsers that take any input source
    (URL, pathname to local or network file, or actual data as a string)
    and deal with it in a uniform manner.  Returned object is guaranteed
    to have all the basic stdio read methods (read, readline, readlines).
    Just .close() the object when you're done with it.

    If the etag argument is supplied, it will be used as the value of an
    If-None-Match request header.

    If the lastmodified argument is supplied, it must be a formatted
    date/time string in GMT (as returned in the Last-Modified header of
    a previous request).  The formatted date/time will be used
    as the value of an If-Modified-Since request header.

    If the agent argument is supplied, it will be used as the value of a
    User-Agent request header.
    '''

    if hasattr(source, 'read'):
        return source

    if source == '-':
        return sys.stdin

    if urlparse.urlparse(source)[0] == 'http':                                      
        # open URL with urllib2                                                     
        request = urllib2.Request(source)                                           
        request.add_header('User-Agent', agent)                                     
        if etag:                                                                    
            request.add_header('If-None-Match', etag)                               
        if lastmodified:                                                            
            request.add_header('If-Modified-Since', lastmodified)                   
        request.add_header('Accept-encoding', 'gzip')                               
        opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler())
        return opener.open(request)                                                 
    
    # try to open with native open function (if source is a filename)
    try:
        return open(source)
    except (IOError, OSError):
        pass

    # treat source as string
    return StringIO(str(source))

def fetch(source, etag=None, last_modified=None, agent=USER_AGENT):  
    '''Fetch data and metadata from a URL, file, stream, or string'''
    result = {}                                                      
    f = openAnything(source, etag, last_modified, agent)             
    result['data'] = f.read()                                        
    if hasattr(f, 'headers'):                                        
        # save ETag, if the server sent one                          
        result['etag'] = f.headers.get('ETag')                       
        # save Last-Modified header, if the server sent one          
        result['lastmodified'] = f.headers.get('Last-Modified')      
        if f.headers.get('content-encoding', '') == 'gzip':          
            # data came back gzip-compressed, decompress it          
            result['data'] = gzip.GzipFile(fileobj=StringIO(result['data']])).read()
    if hasattr(f, 'url'):                                            
        result['url'] = f.url                                        
        result['status'] = 200                                       
    if hasattr(f, 'status'):                                         
        result['status'] = f.status                                  
    f.close()                                                        
    return result

Pour en savoir plus

XI-B. Obtenir des données par HTTP : la mauvaise méthode

Imaginons que nous souhaitons télécharger une ressource par HTTP, par exemple un fil Atom. Seulement, nous ne voulons pas le télécharger une seule fois, nous voulons le télécharger toutes les heures, pour obtenir les dernières nouvelles sur le site qui fournit le fil. Nous allons le faire de la manière la plus simple, puis nous verrons comment faire mieux.

Exemple 11.2. Télécharger un fil de la manière la plus simple

 
Sélectionnez
>>> import urllib
>>> data = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read()    ***1***
>>> print data
<?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 -->

***1*** Télécharger quoi que ce soit par HTTP est incroyablement simple en Python, en fait cela se fait en une ligne. Le module urllib a une fonction urlopen qui prend l'adresse de la page que nous voulons télécharger et retourne un objet-fichier que vous pouvez simplement lire par read() pour obtenir le contenu de la page. Il n'y a pas plus simple.

Alors, qu'est-ce qui ne va pas dans cette méthode ? Et bien, si c'est un petit test en cours de test ou de développement, il n'y a aucun problème. J'utilise cette méthode tout le temps, je voulais le contenu du fil et c'est ce que j'ai obtenu. La même technique marche pour n'importe quelle page Web. Mais si nous pensons en terme de service Web auquel nous voulons accéder régulièrement (rappelez-vous que nous voulons le relever toutes les heures) alors c'est non seulement inefficace, mais en plus impoli.

Nous allons examiner certaines fonctionnalités de base du protocole HTTP.

XI-C. Fonctionnalités de HTTP

Il y a cinq fonctionnalités importantes de HTTP que nous devons supporter dans notre programme.

XI-C-1. User-Agent

La chaîne d'identification User-Agent est simplement un moyen pour le client de déclarer au serveur qui il est lorsqu'il demande une page, un fil ou tout autre service Web par HTTP. Quand le client demande une ressource, il doit toujours annoncer qui il est, de la manière la plus spécifique possible. Cela permet à l'administrateur du serveur de contacter le développeur du client si les choses se passent mal.

Par défaut, Python envoi une chaîne User-Agent générique : Python-urllib/1.15. Dans la section suivante, nous verrons comment la changer pour quelque chose de plus spécifique.

XI-C-2. Les redirections

Parfois, les ressources changent d'emplacement. Les sites Web sont réorganisés, les pages sont déplacées à une nouvelle adresse. Les services Web aussi peuvent être réorganisés. Un fil de syndication à l'adresse http://example.com/index.xml peut être déplacé vers http://example.com/xml/atom.xml. Un domaine complet peut être déplacé, si une organisation s'élargit et se réorganise. Par exemple, http://www.example.com/index.xml peut être redirigé vers http://server-farm-1.example.com/index.xml.

A chaque fois que nous demandons une ressource quelle qu'elle soit à un serveur HTTP, le serveur inclut un code de statut dans sa réponse. Le code de statut 200 signifie «tout est normal, voici la page demandée». Le code de statut 404 signifie «page non trouvée» (vous avez sans doute déjà rencontré des erreurs 404 en navigant sur le Web).

Le protocole HTTP a deux manières différentes de signaler qu'une ressources a été déplacée. Le code de statut 302 est une redirection temporaire, il signifie «attention, cette page a été déplacée ici temporairement» (il est suivit d'une adresse temporaire dans un en-tête Location:). Si nous recevons un code de statut 302, la spécification HTTP dit que nous devons utiliser la nouvelle adresse pour obtenir la ressource, mais que nous devons réessayer l'ancienne adresse la prochaine fois que nous voulons y accéder. Par contre, si nous recevons un code de statut 301 et une nouvelle adresse, nous devons dorénavant utiliser la nouvelle adresse.

urllib.urlopen «suit» automatiquement les redirections lorsqu'il reçoit le code de statut approprié du serveur, mais malheureusement, il ne nous le signale pas. Nous obtenons ainsi les données que nous voulions, mais nous ignorons que la bibliothèque a suivit une redirection «pour vous aider». Donc, nous risquons de continuer d'utiliser l'ancienne adresse et d'être à chaque fois redirigé sur la nouvelle. Cela fait deux aller-retours au lieu d'un seul, ce qui n'est pas très efficace. Nous verrons plus loin dans ce chapitre comment contourner cette difficulté pour pouvoir prendre en compte les redirections permanentes correctement.

XI-C-3. Last-Modified/If-Modified-Since

Certaines données changent constamment. La page d'accueil de CNN.com est mise à jour au bout de quelques minutes. Par contre, la page d'accueil de Google.com ne change qu'au bout de plusieurs semaines. Les services Web ne sont pas différents, en général le serveur sait quand les données que nous demandons ont été modifiées pour la dernière fois et HTTP fournit un moyen pour le serveur d'inclure cette date de modification avec les données que nous demandons.

Si nous demandons ces mêmes données une deuxième fois (ou une troisième, une quatrième), nous pouvons dire au serveur la date de dernière modification que nous avons obtenu la dernière fois : nous envoyons un en-tête If-Modified-Since avec notre requête avec la date que nous avions obtenue. Si les données n'ont pas été modifiées depuis la dernière fois, le serveur renvoi un code de statut 304, qui signifie «ces données n'ont pas changé depuis la dernière fois». Qu'est-ce que cela apporte ? Quand le serveur renvoi un code 304, il ne renvoi pas les données. Nous ne recevons que le code de statut. Nous n'avons donc pas besoin de télécharger les mêmes données encore et encore si elles n'ont pas changé, le serveur considère que nous les avons en cache.

Tous les navigateurs récents implémentent la vérification de date de dernière modification. Si nous retournons voir une page qui n'a pas été modifiée de puis votre dernière visite, elle se charge très rapidement. Notre navigateur a mis le contenu de la page dans son cache local à la première visite et à la deuxième il a automatiquement envoyé la date de dernière modification qu'il avait obtenu la première fois. Le serveur a répondu simplement 304: Not Modified, donc notre navigateur sait qu'il doit recharger la page à partir de son cache. Les services Web peuvent suivre la même procédure.

La bibliothèque URL de Python n'implémente pas la vérification de date de dernière modification, mais comme nous pouvons ajouter les en-têtes que nous voulons à chaque requête et lire les en-têtes que nous voulons à chaque réponse, nous pouvons l'implémenter nous même.

XI-C-4. ETag/If-None-Match

Les ETags sont une manière alternative d'accomplir la même chose que la vérification de date de dernière modification : ne pas télécharger des données qui n'ont pas été modifiées. Leur fonctionnement est le suivant : le serveur envoi un code de hachage des données (dans un en-tête ETag) avec les données que nous avons demandé. La manière dont ce code est généré est entièrement à la discrétion du serveur. La deuxième fois que nous demandons les données, nous incluons le code de hachage ETag dans un en-tête If-None-Match: et si les données n'ont pas été modifiées, le serveur nous renvoi un code de statut 304. Comme pour la vérification de date de dernière modification, le serveur n'envoi que le code 304, il n'envoi pas les données une nouvelle fois. En incluant le code de hachage ETag dans notre seconde requête, nous disons au serveur qu'il n'est pas nécessaire de renvoyer les mêmes données si elles correspondent à ce code de hachage, puisque nous avons encore les données de la dernière fois.

La bibliothèque URL de Python n'implémente pas les Etags, mais nous verrons plus loin comment les ajouter.

XI-C-5. La compression

La dernière fonctionnalité important de HTTP est la compression gzip. Quand on parle de services Web HTTP, il s'agit presque tout le temps d'envoyer et de recevoir du XML. Le XML est du texte et c'est un format verbeux qui se compresse bien. Lorsque nous demandons une ressource par HTTP, nous pouvons demander au serveur, si il a des nouvelles données à nous envoyer, de les envoyer en format compressé. Il suffit d'inclure l'en-tête Accept-encoding: gzip dans notre requête et le serveur, si il implémente la compression, nous enverra des données compressées par gzip en les signalant par un en-tête Content-encoding: gzip.

La bibliothèque URL de Python n'implémente pas la compression gzip en tant que telle, mais nous pouvons ajouter les en-têtes que nous voulons à la requête. Python fournit un module gzip qui a des fonctions nous permettant de décompresser les données nous même.

Notez que notre petit script d'une ligne pour télécharger un fil de syndication n'implémentait aucune de ces fonctionnalités du protocole HTTP. Voyons comment améliorer cela.

XI-D. débogage de services Web HTTP

Pour commencer, nous allons activer les fonctionnalités de débogage de la bibliothèque HTTP de Python pour voir tout ce qui est échangé. Cela nous servira tout au long de ce chapitre, au fur et à mesure que nous rajouterons des fonctionnalités.

Exemple 11.3. débogage de HTTP

 
Sélectionnez
>>> import httplib
>>> httplib.HTTPConnection.debuglevel = 1             ***1***
>>> import urllib
>>> feeddata = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read()
connect: (diveintomark.org, 80)                       ***2***
send: '
GET /xml/atom.xml HTTP/1.0                            ***3***
Host: diveintomark.org                                ***4***
User-agent: Python-urllib/1.15                        ***5***
'
reply: 'HTTP/1.1 200 OK\r\n'                          ***6***
header: Date: Wed, 14 Apr 2004 22:27:30 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT  ***7***
header: ETag: "e8284-68e0-4de30f80"                   ***8***
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close

***1*** urllib utilise une autre bibliothèque standard de Python, httplib. Normalement, il n'est pas nécessaire de faire directement un import httplib (urllib le fait automatiquement), mais nous le faisons ici pour activer le drapeau de débogage de la classe HTTPConnection qu'urllib utilise en interne pour se connecter au serveur HTTP. C'est une technique extrêmement utile. D'autres bibliothèques de Python ont des drapeaux de déboguage similaires, mais il n'y a pas de standard pour les nommer ou les activer, il faut consulter la documentation de chaque bibliothèque pour voir si une telle option est disponible.

***2*** Maintenant que le drapeau de débogage est mis, les informations sur la requête et la réponse HTTP sont affichées en temps réel. La première chose que cela nous dit est que nous nous connectons au serveur diveintomark.org sur le port 80, qui est le port standard du protocole HTTP.

***3*** Lorsque nous demandons le fil Atom, urllib envoi trois lignes au serveur. La première ligne spécifie le verbe HTTP que nous utilisons et le chemin de la ressource (sans le nom de domaine). Toutes les requêtes de ce chapitre utiliseront GET, mais dans le chapitre suivant sur SOAP, nous verrons qu'il utilise POST pour toutes ses requêtes. La syntaxe de base est la même, indépendamment du verbe.

***4*** La deuxième ligne est l'en-tête Host, qui spécifie le nom de domaine du service auquel nous accédons. C'est important, parce qu'un serveur HTTP unique peut héberger plusieurs domaines différents. Mon serveur héberge actuellement 12 domaines, d'autres serveurs peuvent en héberger des centaines, voir des milliers.

***5*** La troisième ligne est l'en-tête User-Agent. Ce qui s'affiche ici est la chaîne User-Agent standard que la bibliothèque urllib ajoute par défaut. Dans la section suivante, nous verrons comment la modifier pour la rendre plus spécifique.

***6*** Le serveur répond par un code de statut et une série d'en-têtes (et peut-être des données, qui ont été stockées dans la variable feeddata). Le code de statut est 200, ce qui signifie «tout est normal, voici les données demandées». Le serveur nous donne également la date à laquelle il a répondu à la requête, des informations sur le serveur lui-même et le type de contenu des données qu'il nous envoi. En fonction de l'application, ces informations peuvent être utile ou non. Ici, nous sommes rassurés, nous avions demandé un fil Atom et le serveur nous renvoi un fil Atom (application/atom+xml, qui est le type de contenu déclaré pour les fils Atom).

***7*** Le serveur annonce la date de dernière modification de ce fil Atom (dans le cas présent il y a environ 13 minutes). Nous pouvons envoyer cette information au serveur la prochaine fois que nous demandons le même fil pour que le serveur vérifie s'il a été modifié entre temps.

***8*** Le serveur annonce que ce fil Atom a un code de hachage ETag de "e8284-68e0-4de30f80". Le code de hachage ne signifie rien par lui-même, nous ne pouvons rien en faire, sauf l'envoyer au serveur lors de notre prochaine requête. Le serveur pourra l'utiliser pour dire si les données ont changé ou non.

XI-E. Changer la chaîne User-Agent

La première chose à faire pour améliorer notre client de services Web HTTP est de faire en sorte qu'il s'identifie correctement avec une chaîne User-Agent. Pour cela, nous devons aller plus loin qu'urllib et plonger dans urllib2.

Exemple 11.4. Présentation de urllib2

 
Sélectionnez
>>> import httplib
>>> httplib.HTTPConnection.debuglevel = 1                             ***1***
>>> import urllib2
>>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') ***2***
>>> opener = urllib2.build_opener()                                   ***3***
>>> feeddata = opener.open(request).read()                            ***4***
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Wed, 14 Apr 2004 23:23:12 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT
header: ETag: "e8284-68e0-4de30f80"
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close

***1*** Si vous avez gardé votre IDE Python ouverte avec l'exemple de la section précédente, vous pouvez sauter cette étape, cette instruction active le débogage HTTP pour que nous puissions voir ce qui est exactement envoyé et ce qui est reçu.

***2*** Obtenir une ressource HTTP avec urllib2 se fait en trois étape, pour des raisons qui seront bientôt claires. La première étape est la création d'un objet Request, qui prend en paramètre l'URL de la ressource que nous voulons obtenir. Notez que cette étape n'effectue encore aucune requête.

***3*** La seconde étape est de construire un opener d'URL. Celui-ci peut prendre en paramètre un nombre quelconque de gestionnaires (handlers) qui contrôleront la gestion des réponses. Nous pouvons aussi construire un opener sans gestionnaire particulier, ce que nous faisons ici. Nous verrons comment définir et utiliser des gestionnaires spécialisés plus loin dans ce ce chapitre lorsque nous traiterons des redirections.

***4*** La dernière étape est de dire à l'opener d'ouvrir l'URL avec l'objet Request que nous avons créé. Comme vous pouvez le voir à l'affichage des informations de débogage, cette étape effectue véritablement la requête et stocke les données obtenues dans feeddata.

Exemple 11.5. Ajout d'en-têtes avec l'objet Request

 
Sélectionnez
>>> request                                                ***1***
<urllib2.Request instance at 0x00250AA8>
>>> request.get_full_url()
http://diveintomark.org/xml/atom.xml
>>> request.add_header('User-Agent',
...     'OpenAnything/1.0 +http://diveintopython.org/')    ***2***
>>> feeddata = opener.open(request).read()                 ***3***
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: OpenAnything/1.0 +http://diveintopython.org/   ***4***
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Wed, 14 Apr 2004 23:45:17 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT
header: ETag: "e8284-68e0-4de30f80"
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close

***1*** C'est la suite de l'exemple précédent, nous avons déjà créé un objet Request avec l'URL à laquelle nous voulons accéder.

***2*** En utilisant la méthode add_header de l'objet Request, nous pouvons ajouter les en-têtes HTTP de notre choix à la requête. Le premier paramètre est l'en-tête, le deuxième est la valeur fournie pour cet en-tête. Par convention, une chaîne User-Agent a le format suivant : un nom d'application, suivi d'une barre oblique, suivi d'un numéro de version. Le reste est de forme libre, on en voit de nombreuses variations, mais il doit contenir l'URL de l'application. La chaîne User-Agent est en général enregistrée par le serveur dans son journal avec d'autres détails de la requête, inclure une URL de l'application permet aux administrateurs de serveurs de contacter l'auteur en cas de problème.

***3*** L'objet opener que nous avons créé précédemment peut être réutilisé lui aussi, il ouvrira à nouveau le même fil, mais avec l'en-tête User-Agent que nous avons défini.

***4*** Voici la chaîne User-Agent spécialisée que nous avons définie à la place de la chaîne générique envoyée par défaut par Python. Si vous regardez attentivement, vous verrez que nous avons défini un en-tête User-Agent, mais que ce qui a été envoyé est un en-tête User-agent. Vous voyez la différence ? urllib2 a modifié la casse de manière à ce que seule la première lettre soit en majuscule. Cela n'a aucune importance, le protocole HTTP spécifie que les noms de champs d'en-têtes sont insensibles à la casse.

XI-F. Prise en charge de Last-Modified et ETag

Maintenant que nous savons comment ajouter des en-têtes HTTP à nos requêtes de services Web, voyons comment prendre en charge les en-têtes Last-Modified et ETag.

Ces exemples montrent la sortie avec le mode débogage désactivé. Si il est toujours activé, vous pouvez le désactiver en tapant httplib.HTTPConnection.debuglevel = 0. Vous pouvez aussi le laisser activé, si cela vous aide.

Exemple 11.6. Test de Last-Modified

 
Sélectionnez
>>> 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*** Vous vous rappelez de tous les en-têtes HTTP qui s'affichaient lorsque nous avions activé le débogage ? Voici la manière d'y accéder par la programmation : firstdatastream.headers est un objet qui se comporte comme un dictionnaire et permet d'accéder à chacun des en-têtes retournés par le serveur HTTP.

***2*** A la seconde requête, nous ajoutons l'en-tête If-Modified-Since avec la date de dernière modification de la première requête. Si les données n'ont pas changé, le serveur devrait retourner un code de status 304.

***3*** Les données n'ont pas changé. Nous pouvons voir dans la trace de pile que urllib2 déclenche une exception spécifique, HTTPError, en réponse au code de statut 304. C'est assez inhabituel et pas forcément pratique. Après tout, ce n'est pas une erreur, nous avons spécifiquement demandé au serveur de ne pas renvoyer les données si elles n'avaient pas changé, ce qu'il a fait, puisqu'elles n'avaient pas changé. Ce n'est pas une erreur, c'est exactement le résultat que nous recherchions.

La bibliothèque urllib2 déclenche également une exception HTTPError pour des situations que nous considérerions sans doute comme des erreurs, comme le code 404 (page non trouvée). En fait, elle déclenche HTTPError pour n'importe quel code de statut autre que 200 (OK), 301 (redirection permanente) ou 302 (redirection temporaire). Il serait plus utile pour notre programme qu'elle capture le code de statut et qu'elle le retourne simplement, sans déclencher d'exception. Pour cela, nous devons définir un gestionnaire d'URL spécialisé.

Exemple 11.7. Définition de gestionnaires d'URL

Ce gestionnaire d'URL spécialisé fait partie de openanything.py.

 
Sélectionnez
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*** La conception d'urllib2 est centrée sur les gestionnaires d'URL. Chaque gestionnaire est simplement une classe qui peut définir un nombre quelconque de méthodes. Lorsque quelque chose se passe, comme une erreur HTTP ou même un code 304, urllib2 recherche par introspection dans la liste des gestionnaires définis une méthode qui puisse le prendre en charge. Nous avons utilisé une technique semblable d'introspection au Chapitre 9, Traitement de données XML pour définir des gestionnaires pour différents types de noeuds, mais urllib2 est plus flexible et recherche par introspection dans tous les gestionnaires définis pour la requête en cours.

***2*** urllib2 recherche parmis les gestionnaires définis et appelle la méthode http_error_default lorsqu'il reçoit un code de statut 304 du serveur. En définissant un gestionnaire d'erreur spécialisé, nous pouvons empêcher urllib2 de déclencher une exception. Nous créons plutôt un objet HTTPError et le retournons au lieu de le déclencher.

***3*** C'est l'étape-clé : avant de retourner de la fonction, nous sauvegardons le code de statut retourné par le serveur HTTP. Cela permettra d'y accéder facilement à partir du programme appelant.

Exemple 11.8. Utilisation de gestionnaires d'URL spécialisés

 
Sélectionnez
>>> 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*** Nous continuons l'exemple précédent, donc l'objet Request est déjà défini et nous avons déjà ajouté l'en-tête If-Modified-Since.

***2*** C'est l'étape-clé : maintenant que nous avons défini notre gestionnaire d'URL spécialisé, nous devons dire à urllib2 de l'utiliser. Vous vous rappelez que j'ai dit qu'urllib2 décompose l'accès à une ressource en trois étapes et qu'il y avait de bonnes raisons à cela ? Voila pourquoi la construction de l'opener d'URL est une étape séparée, pour que nous puissions le construire avec notre propre gestionnaire d'URL redéfinissant le comportement par défaut d'urllib2.

***3*** Maintenant nous pouvons tranquillement ouvrir la ressource et ce que nous obtenons est un objet qui, en plus des en-têtes habituels (accessibles par seconddatastream.headers.dict), contient aussi le code de statut HTTP. Dans ce cas, comme on peut s'y attendre, le code est 304, ce qui signifie que les données n'ont pas changé depuis la dernière requête.

***4*** Notez que lorsque le serveur retourne un code de statut 304, il ne renvoi pas les données. C'est tout là l'intérêt : préserver de la bande passante en ne retéléchargeant pas ce qui n'a pas été modifié. Nous devons donc mettre ces données en cache la première fois que nous les recevons si nous voulons les utiliser.

La gestion de ETag fonctionne de la même manière, mais au lieu de vérifier Last-Modified et d'envoyer If-Modified-Since, on vérifie ETag et on envoiIf-None-Match. Commençons une nouvelle session dans l'IDE

Exemple 11.9. Prise en charge de ETag/If-None-Match

 
Sélectionnez
>>> 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*** A l'aide du pseudo-dictionnaire firstdatastream.headers, nous pouvons obtenir l'ETag retourné par le serveur (si le serveur n'a pas retourné d'ETag cette ligne retournera None).

***2*** Voilà, nous avons les données.

***3*** Maintenant, nous préparons le deuxième appel en assignant à l'en-tête If-None-Match l'ETag obtenu à la première requête.

***4*** La deuxième requête réussit silencieusement (sans déclencher d'exception) et nous voyons là encore que le serveur a renvoyé un code de statut 304. En se basant sur le ETag que nous avons envoyé la deuxième fois, il sait que les données n'ont pas changé.

***5*** Qu'il soit produit par la vérification de date avec Last-Modified ou la correspondance de code de hachage avec ETag, les données ne sont jamais renvoyé avec le code de statut 304. C'est tout l'intérêt.

Dans ces exemples, le serveur HTTP supporte à la fois les en-têtes Last-Modified et ETag, mais ce n'est pas le cas de tous les serveurs. Pour vos clients de services Web, vous devez prévoir de supporter les deux et programmer de manière défensive au cas ou un serveur ne supporterais que l'un des deux, ou aucun.

XI-G. Prise en charge des redirections

La prise en charge des redirections temporaires et permanentes se fait avec un autre type de gestionnaire d'URL spécialisé.

D'abord, voyons pourquoi un gestionnaire de redirection est nécessaire.

Exemple 11.10. Accéder à des services Web sans gestionnaire de redirection

 
Sélectionnez
>>> import urllib2, httplib
>>> httplib.HTTPConnection.debuglevel = 1           ***1***
>>> request = urllib2.Request(
...     'http://diveintomark.org/redir/example301.xml') ***2***
>>> opener = urllib2.build_opener()
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: '
GET /redir/example301.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 301 Moved Permanently\r\n'             ***3***
header: Date: Thu, 15 Apr 2004 22:06:25 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml  ***4***
header: Content-Length: 338
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0                              ***5***
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:06:25 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: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.url                                               ***6***
'http://diveintomark.org/xml/atom.xml'
>>> f.headers.dict
{'content-length': '15955', 
'accept-ranges': 'bytes', 
'server': 'Apache/2.0.49 (Debian GNU/Linux)', 
'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 
'connection': 'close', 
'etag': '"e842a-3e53-55d97640"', 
'date': 'Thu, 15 Apr 2004 22:06:25 GMT', 
'content-type': 'application/atom+xml'}
>>> f.status
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: addinfourl instance has no attribute 'status'

***1*** Pour mieux comprendre ce qu'il se passe, nous activons le débogage.

***2*** Voici un URL que j'ai mise en place pour rediriger de manière permanent vers mon fil Atom à l'adresse http://diveintomark.org/xml/atom.xml.

***3*** Evidemment, lorsque nous essayons de télécharger les données à cette adresse, le serveur renvoi un code de statut 301, signalant que la ressource a été déplacée de manière permanente.

***4*** Le serveur renvoi également un en-tête Location: avec la nouvelle adresse de ces données.

***5*** urllib2 remarque le code de redirection et tente automatiquement d'obtenir les données à la nouvelle adresse spécifiée dans l'en-tête Location :.

***6*** L'objet obtenu de opener contient la nouvelle adresse permanente et tous les en-têtes retourné à la seconde requête (faite sur la nouvelle adresse permanente). Mais le code de statut manque, nous n'avons donc aucun moyen de savoir par la programmation si cette redirection est temporaire ou permanente. Or, cette information est très importante. Si c'est une redirection temporaire, nous devons continuer de demander les données à l'ancienne adresse. Si c'est une redirection permanente, nous devons désormais demander les données à la nouvelle adresse.

C'est loin d'être parfait, mais c'est facile à corriger. urllib2 ne se comporte pas exactement comme nous le souhaitons dans la gestion des codes 301 et 302, nous allons donc redéfinir ce comportement. Comment ? Avec un gestionnaire d'URL spécialisé, comme nous l'avons fait pour prendre en charge les codes 304.

Exemple 11.11. Definition du gestionnaire de redirection

Cette classe est définie dans openanything.py.

 
Sélectionnez
class SmartRedirectHandler(urllib2.HTTPRedirectHandler):     ***1***
    def http_error_301(self, req, fp, code, msg, headers):  
        result = urllib2.HTTPRedirectHandler.http_error_301( ***2***
            self, req, fp, code, msg, headers)              
        result.status = code                                 ***3***
        return result                                       

    def http_error_302(self, req, fp, code, msg, headers):   ***4***
        result = urllib2.HTTPRedirectHandler.http_error_302(
            self, req, fp, code, msg, headers)              
        result.status = code                                
        return result

***1*** La gestion des redirections est définie dans urllib2 dans une classe appelée HTTPRedirectHandler. Nous ne voulons pas redéfinir entièrement son comportement, nous voulons simplement l'étendre un peu, nous dérivons donc HTTPRedirectHandler de manière à pouvoir appeler la classe ancêtre pour faire le gros du travail.

***2*** Quand il reçoit un code de statut 301 du serveur, urllib2 recherche parmi ses gestionnaires et appelle la méthode http_error_301. La première chose que la notre fait est d'appeler la méthode http_error_301 de la classe ancêtre, qui s'occupe du travail de base consistant à chercher l'en-tête Location: et à suivre la redirection à la nouvelle adresse.

***3*** Voici l'étape-clé : avant le retour de fonction, nous stockons le code de statut (301) pour que le programme appelant puisse y accéder plus tard.

***4*** Les redirections temporaires (codes de statut 302) fonctionnent de la même manière : réécriture de la méthode http_error_302, appel de l'ancêtre et sauvegarde du code de statut avant retour.

A quoi cela nous avance-t-il ? Nous pouvons maintenant construire un opener d'URL avec notre gestionnaire de redirection spécialisé et il suivra toujours les redirections automatiquement, mais maintenant il exposera également le code de statut de redirection.

Exemple 11.12. Utilisation du gestionnaire de redirection pour détecter les redirections permanentes
 
Sélectionnez
>>> request = urllib2.Request('http://diveintomark.org/redir/example301.xml')
>>> import openanything, httplib
>>> httplib.HTTPConnection.debuglevel = 1
>>> opener = urllib2.build_opener(
...     openanything.SmartRedirectHandler())           ***1***
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: 'GET /redir/example301.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 301 Moved Permanently\r\n'            ***2***
header: Date: Thu, 15 Apr 2004 22:13:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 338
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:13:21 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: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml

>>> f.status                                           ***3***
301
>>> f.url
'http://diveintomark.org/xml/atom.xml'

***1*** D'abord, nous construisons un opener d'URL avec le gestionnaire de redirection que nous venons de définir.

***2*** Nous avons envoyé une requête et nous avons reçu un code de statut 301 en réponse. A ce moment, la méthode http_error_301 est appelée. Nous appelons la méthode ancêtre qui suit la redirection et envoi une requête à la nouvelle adresse (http://diveintomark.org/xml/atom.xml).

***3*** Voici le bénéfice de notre travail : maintenant, nous n'avons pas seulement accès à la nouvelle URL, mais également au code de statut de redirection, nous pouvons donc voir qu'il s'agit d'une redirection permanente. La prochaine fois que nous demanderont ces données, nous devrons les demander à la nouvelle adresse (http://diveintomark.org/xml/atom.xml, comme spécifié dans f.url). Si nous avons stocké l'adresse dans un fichier de configuration ou une base de données, nous devons la mettre à jour pour ne pas continuer à envoyer des requêtes à l'ancienne adresse. Il faut mettre à jour notre carnet d'adresse.

Le gestionnaire de redirection peut aussi nous apprendre quand nous ne devons pas mettre à jour notre carnet d'adresse.

Exemple 11.13. Utilisation du gestionnaire de redirection pour détecter les redirections temporaires.
 
Sélectionnez
>>> request = urllib2.Request(
...     'http://diveintomark.org/redir/example302.xml')   ***1***
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: '
GET /redir/example302.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 302 Found\r\n'                           ***2***
header: Date: Thu, 15 Apr 2004 22:18:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 314
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0                                ***3***
Host: diveintomark.org
User-agent: Python-urllib/2.1
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 15 Apr 2004 22:18:21 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: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.status                                              ***4***
302
>>> f.url
http://diveintomark.org/xml/atom.xml

***1*** C'est une URL d'exemple que j'ai configurée pour signaler aux clients de se rediriger temporairement sur http://diveintomark.org/xml/atom.xml.

***2*** Le serveur renvoi un code de statut 302, ce qui indique une redirection temporaire. Le nouvel emplacement temporaire des données est donné dans l'en-tête Location:.

***3*** urllib2 appelle notre méthode http_error_302, qui appelle la méthode ancêtre du même nom dans urllib2.HTTPRedirectHandler, qui suit la redirection vers le nouvel emplacement. Puis notre méthode http_error_302 stocke le code de statut (302) pour que l'application appelante puisse l'utiliser plus tard.

***4*** Nous y voilà, après avoir suivi la redirection vers http://diveintomark.org/xml/atom.xml. f.status nous dit que c'était une redirection temporaire, ce qui signifie que nous devons continuer à chercher les données à l'adresse originelle (http://diveintomark.org/redir/example302.xml). Peut-être que nous serons redirigés encore la prochaine fois, mais peut-être que non. Peut-être que nous seront redirigés vers une adresse différente, nous ne pouvons pas le dire. Le serveur nous a indiqué que cette redirection était temporaire, nous devons suivre cette indication et maintenant que nous exposons assez d'information pour le faire, l'application appelante peut suivre cette indication.

XI-H. 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.

 
Sélectionnez
>>> 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

 
Sélectionnez
>>> 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.

 
Sélectionnez
>>> 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.

XI-I. Assembler les pièces

Nous avons vu toutes les pièces nécessaires à la construction d'un client de services Web intelligent. Maintenat, voyons comment tout cela s'assemble.

Exemple 11.17. La fonction openanything

Cette fonction est définie dans openanything.py.

 
Sélectionnez
def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT):
    # non-HTTP code omitted for brevity
    if urlparse.urlparse(source)[0] == 'http':                                       ***1***
        # open URL with urllib2                                                     
        request = urllib2.Request(source)                                           
        request.add_header('User-Agent', agent)                                      ***2***
        if etag:                                                                    
            request.add_header('If-None-Match', etag)                                ***3***
        if lastmodified:                                                            
            request.add_header('If-Modified-Since', lastmodified)                    ***4***
        request.add_header('Accept-encoding', 'gzip')                                ***5***
        opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler()) ***6***
        return opener.open(request)                                                  ***7***

***1*** urlparse est un module utile pour, vous l'avez devinez, analyser des URL. Sa fonction principale, appelée également urlparse, prend une URL en paramètre et la découpe en un tuple composé de (schème, domaine, chemin, paramètres, paramètres de la chaîne de requête et identificateur de fragment). Seul le schème nous intéresse ici, pour nous assurer qu'il s'agit bien d'une URL HTTP (que urllib2 peut prendre en charge).

***2*** Nous nous identifions auprès du serveur HTTP avec la chaîne User-Agent passée par la fonction appelante. Si aucune chaîne User-Agent n'a été passée en paramètre, nous utilisons la valeur par défaut définie plus haut dans le module openanything.py. Nous n'utilisons jamais la valeur par défaut définie par urllib2.

***3*** Si nous avons une code de hachage ETag, nous l'envoyons dans l'en-tête If-None-Match.

***4*** Si nous avons une date de dernière modification, nous l'envoyons dans l'en-tête If-Modified-Since.

***5*** Nous indiquons au serveur que nous souhaitons des données compressées.

***6*** Nous construisons un opener d'URL qui utilise les deux gestionnaires d'URL spécialisés : SmartRedirectHandler pour gérer les redirections 301 et 302 et DefaultErrorHandler pour gérer les codes 304, 404 et les autres erreurs.

***7*** Et voilà ! Nous ouvrons l'URL et retournons un objet-fichier à l'appelant.

Exemple 11.18. La fonction fetch

Cette fonction est définie dans openanything.py.

 
Sélectionnez
def fetch(source, etag=None, last_modified=None, agent=USER_AGENT):  
    '''Fetch data and metadata from a URL, file, stream, or string'''
    result = {}                                                      
    f = openAnything(source, etag, last_modified, agent)              ***1***
    result['data'] = f.read()                                         ***2***
    if hasattr(f, 'headers'):                                        
        # save ETag, if the server sent one                          
        result['etag'] = f.headers.get('ETag')                        ***3***
        # save Last-Modified header, if the server sent one          
        result['lastmodified'] = f.headers.get('Last-Modified')       ***4***
        if f.headers.get('content-encoding', '') == 'gzip':           ***5***
            # data came back gzip-compressed, decompress it          
            result['data'] = gzip.GzipFile(fileobj=StringIO(result['data']])).read()
    if hasattr(f, 'url'):                                             ***6***
        result['url'] = f.url                                        
        result['status'] = 200                                       
    if hasattr(f, 'status'):                                          ***7***
        result['status'] = f.status                                  
    f.close()                                                        
    return result

***1*** D'abord, nous appelons la fonction openAnything avec une URL, un code de hachage ETag, une date Last-Modified et une chaîne User-Agent.

***2*** Nous lisons les données retournées par le serveur. Si elles sont compressées, nous les décompresserons plus tard.

***3*** Nous sauvegardons le code de hachage ETag retourné par le serveur pour que l'application appelante puisse nous la passer à nouveau la prochaine fois et que nous la passions à openAnything, qui la mettra dans l'en-tête If-None-Match et l'enverra au serveur distant.

***4*** Nous sauvegardons aussi la date Last-Modified.

***5*** Si le serveur indique qu'il a envoyé des données compressées, nous les décompressons.

***6*** Si le serveur nous envoi une URL, nous la sauvegardons et considérons que le code de statut est 200 jusqu'à preuve du contraire.

***7*** Si un des gestionnaires d'URL spécialisés a obtenu un code de statut, nous le sauvegardons également.

Exemple 11.19. Utilisation de openanything.py

 
Sélectionnez
>>> import openanything
>>> useragent = 'MyHTTPWebServicesApp/1.0'
>>> url = 'http://diveintopython.org/redir/example301.xml'
>>> params = openanything.fetch(url, agent=useragent)              ***1***
>>> params                                                         ***2***
{'url': 'http://diveintomark.org/xml/atom.xml', 
'lastmodified': 'Thu, 15 Apr 2004 19:45:21 GMT', 
'etag': '"e842a-3e53-55d97640"', 
'status': 301,
'data': '<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
<-- rest of data omitted for brevity -->'}
>>> if params['status'] == 301:                                    ***3***
...     url = params['url']
>>> newparams = openanything.fetch(
...     url, params['etag'], params['lastmodified'], useragent)    ***4***
>>> newparams
{'url': 'http://diveintomark.org/xml/atom.xml', 
'lastmodified': None, 
'etag': '"e842a-3e53-55d97640"', 
'status': 304,
'data': ''}                                                        ***5***

***1*** La toute première fois que nous allons chercher une ressource, nous n'avons pas de code de hachage ETag ni de date Last-Modified, donc nous laissons ces paramètres vides (ce sont des paramètres optionnels).

***2*** Ce qui nous est retourné est un dictionnaire contenant les en-têtes, le code de statut HTTP et les données retournées par le serveur. le module openanything s'occupe de la compression gzip en internet, nous n'avons donc pas à nous en occuper à ce niveau.

***3*** Si nous recevons un code de statut 301, c'est une redirection permanente et nous devons mettre à jour notre URL à la nouvelle adresse.

***4*** La deuxième fois que nous allons chercher la même ressource, nous avons toutes sortes d'information a passer en argument : une URL (éventuellement mise à jour), le ETag et la date Last-Modified de la dernière requête et bien sûr la chaîne User-Agent.

***5*** Ce qui nous est retourné est à nouveau un dictionnaire, mais les données n'ayant pas changé, nous n'obtenons qu'un code de statut 304 et aucune données.

XI-J. Résumé

openanything.py et ses fonctions devraient être tout à fait clairs maintenant.

Il y a 5 fonctionnalités importantes des services Web HTTP que chaque client devrait supporter :

  • Identifier l'application en définissant une chaîne User-Agent appropriée.
  • Prendre en charge les redirections permanentes correctement.
  • Supporter la vérification de date Last-Modified pour éviter de télécharger à nouveau des données non modifiées.
  • Supporter les codes de hachage ETag pour éviter de télécharger à nouveau des données non modifiées.
  • Supporter la compression gzip pour réduire la consommation de bande passante lorsque les données ont été modifiées.

précédentsommairesuivant