Chapitre 10. Des scripts et des flots de données (streams)

10.1. Extraire les sources de données en entrée

L'une des grandes forces de Python repose sur son principe de liaison dynamique, dont un puissant usage est le pseudo objet-fichier (file-like objecti).

De nombreuses fonctions qui nécessitent une source de données en entrée pourraient simplement prendre un nom de fichier, ouvrir le fichier en lecture, le lire et le fermer après lecture. Mais elles ne le font pas. A la place, elles utilisent un pseudo objet-fichier.

Dans les cas les plus simples, un pseudo objet-fichier est tout objet pourvu d'une méthode read accompagnée d'un paramètre size optionnel et qui retourne une chaîne. Quand elle est appelée sans le paramètre size, elle lit l'ensemble du contenu de la source en entrée et retourne l'ensemble des données comme une seule chaîne. Lorsqu'elle est appelée avec le paramètre size, elle ne parcourt que la longueur indiquée et retourne les données correspondantes; Lorsqu'elle est de nouveau appelée, elle poursuit sa lecture là où elle s'était interrompue, et renvoie le paquet de données suivant.

Vous aviez vu comment la lecture de véritables fichiers fonctionne; La différence tient à ce que vous n'êtes pas restreints à utiliser de réels fichiers. La source en entrée peut être n'importe quoi : un fichier sur le disque, une page web, voire une chaîne codée en dur. Tant que vous passez un pseudo objet-fichier à la fonction et qu'elle appelle simplement la méthode read de cet objet, la fonction peut manipuler toute sorte de source en entrée sans avoir besoin de recourir à un code spécifique pour chacune.

Au cas où vous vous demanderiez en quoi cela concerne le traitement des données XML, minidom.parse est justement une fonction qui peut recevoir ce type d'objet.

Exemple 10.1. Analyser un document XML à partir d'un fichier

>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    1
>>> xmldoc = minidom.parse(fsock) 2
>>> fsock.close()                 3
>>> print xmldoc.toxml()          4
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
1 D'abord, vous ouvrez le fichier sur le disque. Vous obtenez alors un objet-fichier.
2 Vous passez l'objet-fichier à minidom.parse, qui appelle la méthode read de fsock et lit le document XML à partir du fichier sur le disque.
3 Assurez-vous d'appeler la méthode close de l'objet-fichier, une fois la lecture terminée. minidom.parse ne s'en charge pas.
4 Appeler la méthode toxml() du document XML retourné affiche la totalité de son contenu.

Et bien, tout cela ressemble à une colossale perte de temps. Après tout, vous aviez déjà vu que la fonction minidom.parse peut simplement prendre en argument le nom du fichier et effectuer automatiquement les opérations d'ouverture et de fermeture. Et il est vrai que si vous savez que vous devez analyser un fichier local, vous pouvez lui passer le nom du fichier et la fonction minidom.parse est suffisamment intelligente pour avoir le bon réflexe (Do The Right Thing™). Mais remarquez maintenant combien l'analyse d'un document XML en provenance d'Internet est semblable -- et tout aussi aisée.

Exemple 10.2. Analyser XML à partir d'un URL

>>> import urllib
>>> usock = urllib.urlopen('http://slashdot.org/slashdot.rdf') 1
>>> xmldoc = minidom.parse(usock)                              2
>>> usock.close()                                              3
>>> print xmldoc.toxml()                                       4
<?xml version="1.0" ?>
<rdf:RDF xmlns="http://my.netscape.com/rdf/simple/0.9/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

<channel>
<title>Slashdot</title>
<link>http://slashdot.org/</link>
<description>News for nerds, stuff that matters</description>
</channel>

<image>
<title>Slashdot</title>
<url>http://images.slashdot.org/topics/topicslashdot.gif</url>
<link>http://slashdot.org/</link>
</image>

<item>
<title>To HDTV or Not to HDTV?</title>
<link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link>
</item>

[...snip...]
1 Comme vous l'avez vu au chapitre précédent, urlopen prend l'URL d'une page web et retourne un pseudo objet-fichier. De plus, cet objet dispose d'une méthode read qui retourne la source HTML d'une page web.
2 Maintenant vous passez le pseudo objet-fichier à la fonction minidom.parse, qui, très obéissante, appelle la méthode read de l'objet et analyse les données XML retournées par cette méthode. Le fait que ces données XML proviennent directement d'une page web n'a aucune pertinence. La fonction minidom.parse ne sait pas ce qu'est une page web et ne s'en soucie guère; elle ne connaît que les pseudo objet-fichiers.
3 Dès que vous en avez terminé, assurez-vous de fermer le pseudo objet-fichier fourni par urlopen.
4 Signalons au passage qu'il s'agit là d'un URL qui propose un véritable contenu XML. C'est la version XML des titres à la une du site Slashdot, un site consacré aux nouveautés et aux potins de l'actualité technologique .

Exemple 10.3. Analyser XML à partir d'une chaîne (voie facile mais rigide)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> xmldoc = minidom.parseString(contents) 1
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
1 minidom possède une méthode, parseString, qui récupère un document XML entier sous forme de chaîne et l'analyse. Vous pouvez l'utiliser à la place de minidom.parse si vous savez que votre document XML est sous la forme d'une chaîne.

D'accord, vous pouvez ainsi utiliser la fonction minidom.parse pour analyser à la fois des fichiers locaux et des URLs distantes, mais pour analyser des chaînes, vous utilisez... une fonction différente. Cela signifie que si vous voulez être capable de recevoir en entrée un fichier, un URL, ou une chaîne, vous avez besoin de mettre en place une logique particulière pour contrôler s'il s'agit d'une chaîne et appeler le cas échéant la fonction parseString. Quelle déception !

S'il y avait un moyen de transformer une chaîne en un pseudo objet-fichier, vous pourriez alors simplement passer cet objet à minidom.parse. En fait, un module spécifiquement conçu à cet effet existe : il s'agit de StringIO.

Exemple 10.4. Introduction à StringIO

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> import StringIO
>>> ssock = StringIO.StringIO(contents)   1
>>> ssock.read()                          2
"<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock.read()                          3
''
>>> ssock.seek(0)                         4
>>> ssock.read(15)                        5
'<grammar><ref i'
>>> ssock.read(15)
"d='bit'><p>0</p"
>>> ssock.read()
'><p>1</p></ref></grammar>'
>>> ssock.close()                         6
1 Le module StringIO ne contient qu'une seule classe qui a pour nom StringIO, laquelle vous permet de transformer une chaîne en un pseudo objet-fichier. La classe StringIO prend la chaîne en paramètre au moment de créer une instance.
2 Vous avez désormais un pseudo objet-fichier et vous pouvez le manipuler comme s'il s'agissait d'un fichier. En utilisant, par exemple, la méthode read, qui retourne la chaîne originale.
3 Appeler read une seconde fois retourne une chaîne vide. Les véritables objets-fichier fonctionnent également de cette façon; une fois que la totalité du fichier est lue, vous ne pouvez lire rien de plus à moins de revenir explicitement au début du fichier. L'objet StringIO fonctionne pareillement.
4 Vous pouvez revenir explicitement au début de la chaîne de la même façon que pour un fichier, en utilisant la méthode seek de l'objet StringIO.
5 Vous pouvez aussi lire la chaîne par morceaux, en passant un paramètre size à la méthode read.
6 A tout moment, read retournera le reste de la chaîne qui n'a pas encore été lu. Le fonctionnement est exactement le même que pour les objets-fichier; d'où le terme pseudo objet-fichier.

Exemple 10.5. Analyser XML à partir d'une chaîne (la voie du pseudo objet-fichier)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) 1
>>> ssock.close()
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
1 Désormais vous pouvez passer le pseudo objet-fichier (une instance de StringIO) à minidom.parse, qui appelle la méthode read de l'objet et l'analyse en retour sans se soucier du fait qu'il s'agit en entrée d'une chaîne codée en dur.

Ainsi, vous savez comment utiliser une fonction unique, minidom.parse, pour analyser un document XML stocké sur une page web, dans un fichier local, ou dans une chaîne codée en dur. Pour une page web, vous utilisez urlopen pour obtenir un pseudo objet-fichier; pour un fichier local, vous utilisez open; et pour une chaîne, vous utilisez StringIO. Passez maintenant à l'étape suivante et généralisez toutes ces différences.

Exemple 10.6. openAnything


def openAnything(source):                  1
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)      2
    except (IOError, OSError):            
        pass                              

    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)                3
    except (IOError, OSError):            
        pass                              

    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source))  4
1 La fonction openAnything prend un seul argument, source et retourne un pseudo objet-fichier. source est une chaîne quelconque; ce peut être ou bien un URL (comme 'http://slashdot.org/slashdot.rdf'), ou bien un chemin d'accès absolu ou relatif à un fichier local (comme 'binary.xml'), ou encore une chaîne qui contient les données XML à analyser.
2 Premièrement, vous testez si source est un URL. La méthode est brutale : vous essayez de l'ouvrir comme un URL et vous ignorez les erreurs survenues s'il ne s'agit pas d'un URL. Le procédé n'est cependant pas sans élégance dans la mesure où, si urllib supporte à l'avenir de nouveaux types d'URLs, ils seront pris en compte sans avoir besoin de reprogrammer.
3 Si urllib se plaint que source n'est pas un URL valide, vous supposez alors que c'est le chemin d'un fichier sur le disque et vous essayez de l'ouvrir. De nouveau, rien de très sophistiqué pour tester si source est ou non un nom de fichier valide (les règles de validation d'un nom de fichier variant grandement d'un système à l'autre, vous vous égareriez certainement en procédant différemment). A la place, vous ouvrez à l'aveugle le fichier et interceptez silencieusement les erreurs éventuelles.
4 A ce stade, vous devez supposer que source est une chaîne codée en dur (puisque rien d'autre n'a fonctionné), aussi utilisez-vous StringIO pour la convertir en un pseudo objet-fichier et le retourner. (En fait, puisque vous utilisez la fonction str, source n'a pas besoin d'être une chaîne; ce pourrait être un objet quelconque et vous utiliseriez sa représentation sous forme de chaîne, telle qu'elle est définie par la méthode spéciale __str__.)

Vous pouvez à présent utiliser la fonction openAnything en conjonction avec minidom.parse pour écrire une fonction qui prend un argument source en référence à un document XML quelconque (un URL, un fichier local, ou encore un document XML sous la forme d'une chaîne codée en dur) et l'analyse.

Exemple 10.7. Utiliser openAnything dans le fichier kgp.py


class KantGenerator:
    def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()
        return xmldoc