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

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


précédentsommairesuivant

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

X-A. 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

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

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

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

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

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

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

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

X-B. Entrée, sortie et erreur standard

Les utilisateurs d'UNIX sont déjà familiers avec les concepts d'entrée standard, de sortie standard et d'erreur standard. Cette section s'adresse aux autres.

La sortie standard et l'erreur standard (communément abrégé en stdout et en stderr) sont des canaux de communication (pipes) intégrés à chaque système UNIX. Lorsque vous affichez (fonction print) quelque chose, il est dirigé vers le canal de communication stdout; quand votre programme plante et affiche des informations de débogage (comme un traceback en Python), elles sont envoyées vers le canal de communication stderr. Chacun de ces deux canaux sont d'ordinaire simplement connectés à la fenêtre du terminal avec laquelle vous travaillez et de cette façon vous voyez s'afficher la sortie du programme ou l'information de débogage s'il plante. (Si vous travaillez sur un système pourvu d'un IDE Python fenêtré, stdout et stderr sont redirigés par défaut vers la «Fenêtre Interactive».)

Exemple 10.8. Introduction à stdout et à stderr

 
Sélectionnez
>>> for i in range(3):
...     print 'Dive in'             ***1***
Dive in
Dive in
Dive in
>>> import sys
>>> for i in range(3):
...     sys.stdout.write('Dive in') ***2***
Dive inDive inDive in
>>> for i in range(3):
...     sys.stderr.write('Dive in') ***3***
Dive inDive inDive in

***1*** Comme vous l'avez vu dans l'Exemple 6.9, «Compteurs simples», vous pouvez utiliser la fonction prédéfinie de Python range pour construire un simple compteur de boucles qui répète une instruction un nombre déterminé de fois.

***2*** stdout est un pseudo objet-fichier; appeler sa fonction write affichera toutes les chaînes que vous lui donnez. En fait, c'est bien ce que fait la fonction print; elle ajoute un retour chariot à la fin de la chaîne que vous affichez et appelle sys.stdout.write.

***3*** Dans le cas le plus simple, stdout et stderr envoient leur sortie au même endroit : l'IDE Python (si vous en utilisez un), ou la console (si vous avez lancé Python à partir de la ligne de commande). Comme stdout, stderr n'ajoute pas de retour chariot pour vous; si vous en avez besoin, ajoutez-les vous-même.

stdout et stderr sont toutes les deux des pseudo objet-fichiers, comme ceux dont il a été question dans la Section 10.1, «Extraire les sources de données en entrée», mais ils sont tous les deux en écriture seule. Ils n'ont pas de méthode read, seulement une méthode write. Ils n'en restent pas moins des pseudo objet-fichiers auxquels vous pouvez assigner n'importe quel autre fichier - ou pseudo objet-fichier afin d'en rediriger la sortie.

Exemple 10.9. Rediriger la sortie standard

 
Sélectionnez
[you@localhost kgp]$ python stdout.py
Dive in
[you@localhost kgp]$ cat out.log
This message will be logged instead of displayed

(Avec Windows, il faut utiliser type au lieu de cat pour afficher le contenu d'un fichier.)

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
#stdout.py
import sys

print 'Dive in'                                          ***1***
saveout = sys.stdout                                     ***2***
fsock = open('out.log', 'w')                             ***3***
sys.stdout = fsock                                       ***4***
print 'This message will be logged instead of displayed' ***5***
sys.stdout = saveout                                     ***6***
fsock.close()                                            ***7***

***1*** Ce message s'affichera dans la «Fenêtre Interactive» de l'IDE (ou sur la console, si le script est lancé à la ligne de commande).

***2*** Sauvegarder toujours stdout avant de le rediriger, ainsi vous pourrez revenir à la normale plus tard.

***3*** Ouvre un nouveau fichier en écriture. Si le fichier n'existe pas, il sera créé. Si le fichier existe, il sera écrasé

***4*** Redirige toutes les sorties supplémentaires dans le fichier que vous venez d'ouvrir.

***5*** Ce message «s'affichera» seulement dans le fichier journal; il ne sera pas visible ni dans la fenêtre de l'IDE ni à l'écran.

***6*** Restaure stdout dans l'état où il était avant que vous n'y mettiez la pagaïe.

***7*** Ferme fichier journal.

Rediriger stderr fonctionne exactement de la même manière, en utilisant sys.stderr au lieu de sys.stdout.

Exemple 10.10. Rediriger un message d'erreur

 
Sélectionnez
[you@localhost kgp]$ python stderr.py
[you@localhost kgp]$ cat error.log
Traceback (most recent line last):
  File "stderr.py", line 5, in ?
    raise Exception, 'this error will be logged'
Exception: this error will be logged
 
Sélectionnez
#stderr.py
import sys

fsock = open('error.log', 'w')               ***1***
sys.stderr = fsock                           ***2***
raise Exception, 'this error will be logged' ***3*** ***4***

***1*** Ouvre le fichier journal où vous voulez enregistrer l'information de débogage.

***2*** Redirige l'erreur standard en affectant à stderr l'objet-fichier correspondant au fichier journal nouvellement créé.

***3*** Déclenche une exception. Notez que sur l'écran de sortie rien ne s'affiche. Toute l'information de traceback a été écrite dans error.log.

***4*** Remarquez également que vous ne fermez pas explicitement votre fichier journal, ni ne restaurez stderr dans son état d'origine. Il n'y a pas d'erreur, puisqu'une fois le programme planté (à cause de l'exception), Python nettoiera et fermera le fichier pour nous et cela n'a pas d'importance que stderr soit restauré, puisque, comme je l'ai signalé, le programme plante et Python se termine. Un retour à l'état antérieur est plus important pour stdout, si vous souhaiter continuer à travailler avec le même script ultérieurement.

Puisqu'il est si trivial d'écrire des messages d'erreurs sur le canal d'erreur standard, il existe une syntaxe abrégée qui peut être utilisée plutôt que de s'embêter à effectuer une redirection complète.

Exemple 10.11. Afficher un message sur stderr

 
Sélectionnez
>>> print 'entering function'
entering function
>>> import sys
>>> print >> sys.stderr, 'entering function' ***1***
entering function

***1*** Cette syntaxe abrégée de l'expression print peut être utilisée pour écrire dans tout fichier ou pseudo objet-fichier. Dans cet exemple, vous pouvez rediriger une seule expression print vers stderr sans affecter les expressions print ultérieures.

L'entrée standard, de l'autre côté, est un objet-fichier en lecture seule et représente les données circulant entre un programme et un programme exécuté antérieurement. Cela n'a probablement pas grand sens pour les utilisateurs chevronnés de Mac OS, ou même pour les utilisateurs de Windows à moins que vous ne soyez coutumier de la ligne de commande MS-DOS. Son principe de fonctionnement vous permet de construire une chaîne de commandes sur une seule ligne, de telle sorte que la sortie d'un premier programme devienne l'entrée du programme suivant dans la chaîne. Le premier programme retourne simplement un résultat vers la sortie standard (sans effectuer lui-même une redirection spéciale, sinon le renvoi en sortie de quelques instructions print ou que sais-je encore), le programme suivant lit l'entrée standard, et le système d'exploitation se charge de connecter la sortie d'un programme à l'entrée du programme suivant.

Exemple 10.12. Chaîner les commandes

 
Sélectionnez
[you@localhost kgp]$ python kgp.py -g binary.xml         ***1***
01100111
[you@localhost kgp]$ cat binary.xml                      ***2***
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<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>
[you@localhost kgp]$ cat binary.xml | python kgp.py -g - ***3*** ***4***
10110001

***1*** Comme vous l'aviez vu dans la Section 9.1, «Plonger», cette commande affiche une chaîne de huit bits aléatoires, 0 ou 1.

***2*** Cette commande affiche simplement la totalité du contenu de binary.xml. (les utilisateurs de Windows doivent utiliser type au lieu de cat.)

***3*** Cette commande affiche le contenu de binary.xml, mais le caractère «|», appelé «pipe», signifie que le contenu ne sera pas affiché à l'écran. A la place, il deviendra l'entrée standard de la prochaine commande qui dans ce cas appelle votre script Python.

***4*** Plutôt que de spécifier un module (comme binary.xml), vous spécifiez «-», ce qui oblige votre script à charger la grammaire à partir de l'entrée standard au lieu d'un fichier particulier sur le disque. (Vous en saurez plus à ce propos dans le prochain exemple.) Ainsi le résultat est le même qu'avec la syntaxe précédente, où vous spécifiiez directement le nom du fichier de grammaire, mais pensez en plus aux nombreuses possibilités qui s'offrent à vous. Plutôt que de simplement exécuter cat binary.xml, vous pourriez lancer un premier script qui générerait dynamiquement une grammaire que vous redirigeriez vers votre script. Les données pourraient provenir de n'importe où : une base de données, un méta-script générateur de grammaire, ou que sais-je encore. L'important est que vous n'avez pas besoin de modifier votre script kgp.py pour tenir compte de cette fonctionnalité. Tout ce dont vous avez besoin, c'est de pouvoir récupérer le fichier de grammaire à partir de l'entrée standard et alors vous pouvez confier toute la logique restante à un autre programme.

Comment donc notre script «sait»-il qu'il doit lire à partir de l'entrée standard quand le fichier de grammaire correspond à «-» ? Cela n'a rien de magique; juste logique.

Exemple 10.13. Lire à partir de l'entrée standard dans kgp.py

 
Sélectionnez
def openAnything(source):
    if source == "-":    ***1***
        import sys
        return sys.stdin

    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib
    try:

[... snip ...]

***1*** Il s'agit de la fonction openAnything de toolbox.py, que vous aviez précédemment examinée dans la Section 10.1, «Extraire les sources de données en entrée». Tout ce que vous avez fait est d'ajouter trois lignes de code au début de cette fonction pour tester si la source correspond à «-»; si c'est le cas, vous retournez sys.stdin. Rien de plus ! Souvenez-vous que stdin est un pseudo objet-fichier pourvu d'une méthode read, si bien que le reste du code (dans kgp.py où vous appelez openAnything) ne change pas d'un pouce.

X-C. Mettre en cache la consultation de noeuds

kgp.py recourt à différentes astuces qui peuvent ou non se révéler également utiles dans votre traitement XML. La première tire avantage de la structure logique des documents en entrée pour construire un cache de noeuds.

Un fichier de grammaire définit une série d'éléments ref. Chaque élément ref contient un ou plusieurs éléments p, qui peuvent contenir à leur tour un bon nombre de choses, y compris des éléments xref. Chaque fois que vous rencontrez un élément xref, vous cherchez un élément ref correspondant avec le même attribut id et choisissez l'un des enfants de l'élément ref pour l'analyser. (Vous verrez comment est effectué ce choix aléatoire dans la prochaine section.)

Voici comment construire la grammaire : définissez des éléments ref pour les plus petites parties, puis définissez des éléments ref qui "incluent" les premiers éléments ref au moyen de xref et ainsi de suite. Ensuite vous analysez la référence "la plus large" et suivez chaque xref, et au besoin vous récupérez le texte brut associé. Le texte que vous produisez dépend des décisions (aléatoires) que vous faites chaque fois que vous renseignez un élément xref, ainsi le résultat est à chaque fois différent.

Tout cela fonctionne d'une manière très souple, mais il y a un revers : la performance. Lorsque vous trouvez un élément xref et avez besoin de retrouver l'élément ref correspondant, un problème se pose. L'élément xref a un attribut id et vous désirez trouver l'élément ref qui a le même attribut id, mais il n'y a pas de moyen simple de le faire. La façon lente de procéder serait de récupérer à chaque fois la liste complète des éléments ref, et de les parcourir en recherchant chaque attribut id. La manière rapide est de ne faire qu'une fois ce travail en construisant un cache sous la forme d'un dictionnaire.

Exemple 10.14. loadGrammar

 
Sélectionnez
 def loadGrammar(self, grammar):                         
        self.grammar = self._load(grammar)                  
        self.refs = {}                                       ***1***
        for ref in self.grammar.getElementsByTagName("ref"): ***2***
            self.refs[ref.attributes["id"].value] = ref      ***3*** ***4***

***1*** Commencez par créer un dictionnaire vide, self.refs.

***2*** Comme vous l'avez vu dans la Section 9.5, «Rechercher des éléments», getElementsByTagName retourne une liste de tous les éléments portant le même nom. Vous pouvez obtenir facilement une liste de tous les éléments ref, puis simplement la parcourir.

***3*** Comme vous l'avez vu dans la Section 9.6, «Accéder aux attributs d'un élément», vous pouvez accéder aux attributs individuels d'un élément par leur nom, en utilisant la syntaxe d'un dictionnaire standard. Ainsi les clés du dictionnaire self.refs seront les valeurs de l'attribut id de chaque élément ref.

***4*** Les valeurs du dictionnaire self.refs seront les éléments ref eux-mêmes. Comme vous l'avez vu dans la Section 9.3, «Analyser un document XML», chaque élément, chaque noeud, chaque commentaire, chaque fragment de texte d'un document XML après analyse devient un objet.

Une fois le cache construit, il vous suffit simplement de consulter self.refs lorsque vous tombez sur un élément xref et qu'il vous faut retrouver l'élément ref avec l'attribut id correspondant.

Exemple 10.15. Utiliser le cache de noeuds ref

 
Sélectionnez
def do_xref(self, node):
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id]))

Vous explorerez la fonction randomChildElement dans la prochaine section.

X-D. Trouver les descendants directs d'un noeud

Une autre technique bien utile lorsqu'il s'agit d'analyser un document XML consiste à retrouver tous les descendants directs d'un élément particulier. Par exemple, dans les fichiers de grammaire, un élément ref peut contenir plusieurs éléments p, qui à leur tour peuvent contenir un certain nombre de choses, y compris d'autres éléments p. Mais vous ne voulez retrouver que les éléments p qui sont les enfants d'un élément ref et non les éléments p qui sont les enfants d'un autre élément p.

Vous pourriez penser qu'il suffit pour cela d'utiliser simplement la fonction getElementsByTagName, mais il n'en est rien. La fonction getElementsByTagName cherche récursivement et retourne une liste unique de tous les éléments qu'elle trouve. Et puisqu'un élément p peut contenir d'autres éléments p, vous ne pouvez pas utiliser getElementsByTagName, parce qu'elle retournerait alors des éléments p imbriqués dont vous n'avez pas besoin. Retrouver uniquement les descendants directs est une tâche qui vous incombe.

Exemple 10.16. Trouver les descendants directs de type élément

 
Sélectionnez
 def randomChildElement(self, node):
        choices = [e for e in node.childNodes
                   if e.nodeType == e.ELEMENT_NODE] ***1*** ***2*** ***3***
        chosen = random.choice(choices)             ***4***
        return chosen

***1*** Comme vous l'avez vu dans l'Exemple 9.9, «Obtenir les noeuds enfants», l'attribut childNodes retourne une liste de tous les noeuds enfants d'un élément.

***2*** Cependant, comme vous l'avez vu dans l'Exemple 9.11, «Les noeuds enfants peuvent être de type texte», la liste retournée par childNodes contient tous les différents types de noeuds, y compris les noeuds texte. Mais ce n'est pas ce que vous recherchez ici. Vous voulez seulement les enfants qui sont des éléments.

***3*** Chaque noeud possède un attribut nodeType, dont la valeur peut être ELEMENT_NODE, TEXT_NODE, COMMENT_NODE, ou une quelque autre valeur. La liste complète des valeurs possibles se trouve dans le fichier __init__.py du paquetage xml.dom. (Voir la Section 9.2, «Les paquetages» pour en savoir plus sur les paquetages.) Mais comme vous n'êtes intéressés que par les noeuds de type élément, vous pouvez filtrer la liste pour ne tenir compte que des noeuds dont le nodeType est ELEMENT_NODE.

***4*** Une fois établie la liste des éléments disponibles, il est aisé d'en choisir un au hasard. Python dispose d'un module appelé random qui inclut plusieurs fonctions bien utiles. La fonction random.choice retourne un élément au hasard pris dans une liste d'éléments quelconques. Par exemple, si les éléments ref contiennent plusieurs éléments p, alors choices sera une liste d'éléments p et chosen se verra assigner au final l'un d'entre eux, pris au hasard.

X-E. Créer des gestionnaires distincts pour chaque type de noeud

Une troisième astuce bien utile au traitement XML implique la séparation de votre code en fonctions logiques, sur la base des types de noeud et des noms d'élément. Les documents XML analysés sont constitués de divers types de noeud, chacun représenté par un objet Python. La racine d'un document est elle-même représentée par un objet Document. L'objet Document contient alors un ou plusieurs objets Element (pour les balises XML courantes), dont chacun peut contenir d'autres objets Element, Text (pour les fragments de texte), ou Comment (pour les commentaires imbriqués). Python facilite l'écriture d'un sélecteur pour séparer la logique de chaque type de noeud.

Exemple 10.17. Les noms de classe des objets XML analysés

 
Sélectionnez
>>> from xml.dom import minidom

>>> xmldoc = minidom.parse('kant.xml') ***1***
>>> xmldoc
<xml.dom.minidom.Document instance at 0x01359DE8>
>>> xmldoc.__class__                   ***2***
<class xml.dom.minidom.Document at 0x01105D40>
>>> xmldoc.__class__.__name__          ***3***
'Document'

***1*** Supposez pour le moment que kant.xml se trouve dans le répertoire courant. ***2*** Comme vous l'avez vu dans la Section 9.2, «Les paquetages», l'objet retourné par l'analyse d'un document XML est un objet Document, tel que défini dans le module minidom.py du paquetage xml.dom. Comme vous l'avez vu dans la Section 5.4, «Instantiation de classes», __class__ est un attribut prédéfini de chaque objet Python. ***3*** De plus, __name__ est un attribut prédéfini de chaque classe Python et c'est une chaîne. Cette chaîne n'a rien de mystérieux; elle correspond au nom de la classe que vous inscrivez lorsque vous définissez vous-même une classe. (Voir la Section 5.3, «Définition de classes».)

Parfait, vous pouvez désormais obtenir le nom de la classe de n'importe quel noeud XML particulier (puisque chaque noeud XML est représenté par un objet Python). Comment pouvez-vous mettre cet avantage à profit pour séparer la logique de traitement de chaque type de noeud ? La réponse est getattr, ce que vous aviez vu précédemment dans la Section 4.4, «Obtenir des références objet avec getattr».

Exemple 10.18. La fonction parse, un sélecteur de noeuds XML générique

 
Sélectionnez
   def parse(self, node):          
        parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) ***1*** ***2***
        parseMethod(node) ***3***

***1*** Tout d'abord, remarquez que vous construisez une longue chaîne basée sur le nom de la classe du noeud que vous passez à la fonction (dans l'argument node). Ainsi, si vous passez un noeud Document, vous constituez la chaîne 'parse_Document' et ainsi de suite.

***2*** Maintenant vous pouvez traiter cette chaîne comme un nom de fonction et obtenir une référence de la fonction elle-même en utilisant getattr

***3*** Enfin, vous pouvez appeler cette fonction et lui passer le noeud comme argument. L'exemple suivant présente les définitions de chacune de ces fonctions.

Exemple 10.19. Les fonctions appelées par le sélecteur de méthodes parse

 
Sélectionnez
def parse_Document(self, node): ***1***
        self.parse(node.documentElement)

    def parse_Text(self, node):    ***2***
        text = node.data
        if self.capitalizeNextWord:
            self.pieces.append(text[0].upper())
            self.pieces.append(text[1:])
            self.capitalizeNextWord = 0
        else:
            self.pieces.append(text)

    def parse_Comment(self, node): ***3***
        pass

    def parse_Element(self, node): ***4***
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)

***1*** La fonction parse_Document n'est jamais appelée qu'une fois, puisqu'il n'y a qu'un unique noeud Document dans un document XML et un unique objet Document dans la représentation du document XML analysé. Elle ne tient compte que de l'élément racine du fichier de grammaire et l'analyse.

***2*** La fonction parse_Text est appelée pour les noeuds de type texte. Elle commence par mettre automatiquement en majuscule le premier mot de chaque phrase et ajoute ensuite le texte considéré à une liste.

***3*** La fonction parse_Comment ne contient que l'instruction pass, puisque vous n'avez que faire des commentaires imbriqués dans les fichiers de grammaire. Notez, cependant, que vous avez tout de même besoin de définir cette fonction et, explicitement, de ne lui faire jouer aucun rôle. Si cette fonction n'existait pas, la fonction générique parse échouerait sitôt qu'elle rencontrerait un commentaire, faute de pouvoir trouver la fonction parse_Comment. Définir une fonction distincte pour chaque type de noeud, même si elle n'est d'aucun usage, permet à la fonction générique parse de rester simple et silencieuse.

***4*** La méthode parse_Element est en réalité elle-même un sélecteur, basé sur le nom de la balise de l'élément. L'idée de départ est la même : retenir le caractère discriminant de chacun de ces éléments (le nom de leur balise) et l'affecter à une fonction distincte. Vous construisez une chaîne comme 'do_xref' (pour une balise <xref>), trouvez la fonction qui porte ce nom et l'appelez. Et ainsi de suite pour chacun des autres noms de balise que vous pourriez trouver au cours de l'analyse d'un fichier de grammaire (les balises <p>, les balises <choice>).

Dans cet exemple, les fonctions de sélection parse et parse_Element trouvent simplement les autres méthodes dans la même classe. Si votre traitement est très complexe (ou si vous avez de nombreux noms de balise), vous pourriez diviser votre code en modules séparés et utiliser l'importation dynamique pour importer chaque module et appeler les fonctions dont vous avez besoin. L'importation dynamique sera discuté dans le Chapitre 16, Programmation fonctionnelle.

X-F. Manipuler les arguments de la ligne de commande

Python supporte complètement la création de programmes qui peuvent être lancés en ligne de commande, à l'aide d'arguments et de drapeaux longs ou cours pour spécifier diverses options. Cela n'est nullement spécifique à XML, mais comme ce script fait grand usage du traitement en ligne de commande, il est très à propos d'y faire ici mention.

Il est difficile de parler du traitement en ligne de commande sans aborder la façon dont les arguments sont passés au programme Python, commencez donc par un petit programme en guise d'introduction.

Exemple 10.20. Introduction à sys.argv

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
#argecho.py
import sys

for arg in sys.argv: ***1***
    print arg

***1*** Chaque argument de ligne de commande passé au programme est ajouté à sys.argv, qui est un objet liste. Ici le script affiche chaque argument sur une ligne séparée.

Exemple 10.21. Les caractéristiques de sys.argv

 
Sélectionnez
[you@localhost py]$ python argecho.py             ***1***
argecho.py
[you@localhost py]$ python argecho.py abc def     ***2***
argecho.py
abc
def
[you@localhost py]$ python argecho.py --help      ***3***
argecho.py
--help
[you@localhost py]$ python argecho.py -m kant.xml ***4***
argecho.py
-m
kant.xml

***1*** Ce qu'il faut d'abord retenir de l'objet sys.argv est qu'il contient le nom du script que vous appelez. Vous en tirerez avantage plus tard, dans le Chapitre 16, Programmation fonctionnelle. Ne vous en souciez pas pour le moment.

***2*** Les arguments de la ligne de commande sont séparés par des espaces et chacun se présente comme un élément distinct dans la liste sys.argv.

***3*** Les drapeaux de la ligne de commande, comme --help, se présentent également comme des éléments propres dans la liste sys.argv.

***4*** Pour corser le tout, certains drapeaux de la ligne de commande prennent eux-mêmes des arguments. Par exemple, vous avez ici un drapeau (-m) qui prend un argument (kant.xml). Aussi bien le drapeau que son argument sont présentés comme des éléments distincts dans la liste sys.argv. Rien n'est fait pour les associer; vous n'obtenez rien de plus qu'une liste.

Comme vous pouvez le voir maintenant, vous disposez indiscutablement de toutes les informations passées à la ligne de commande, mais, de nouveau, il apparaît que tout ne sera pas forcément facile à utiliser. Pour des programme simples qui ne nécessite qu'un seul argument et pas de drapeau, vous pouvez simplement utiliser sys.argv[1] pour accéder à l'argument. Aucune honte à avoir; je fais ça tout le temps. Pour des programmes plus complexes, il vous faut recourir au module getopt.

Exemple 10.22. Introduction à getopt

 
Sélectionnez
def main(argv):                         
    grammar = "kant.xml"                 ***1***
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) ***2***
    except getopt.GetoptError:           ***3***
        usage()                          ***4***
        sys.exit(2)                     

...

if __name__ == "__main__":
    main(sys.argv[1:])

***1*** Tout d'abord, regardez au bas de l'exemple et remarquez que vous appelez la fonction main avec sys.argv[1:]. Rappelez-vous, sys.argv[0] est le nom du script en cours; vous n'avez pas à vous en soucier pour le traitement en ligne de commande, aussi le supprimez-vous et envoyez-vous le reste de la liste.

***2*** C'est ici que se trouve la partie intéressante du traitement. La fonction getopt du module getopt prend trois paramètres : la liste des arguments (que vous obtenez à partir de sys.argv[1:]), une chaîne contenant tous les drapeaux courts possibles acceptés par le programme et une liste des drapeaux plus longs qui correspondent aux versions courtes. C'est bien confus à première vue, mais l'explication détaillée vient plus bas.

***3*** Si un dysfonctionnement survient au moment d'analyser les drapeaux de ligne de commande, getopt déclenche une exception, que vous récupérez ensuite. Comme vous avez indiqué à getopt tous les drapeaux que vous connaissiez, il y a fort à parier que l'utilisateur final a passé des drapeaux de ligne de commande qui vous sont inconnus.

***4*** Comme il est de coutume dans le monde UNIX, quand le script reçoit des drapeaux qu'il ne connaît pas, vous mettez fin au programme de la manière la plus élégante qui soit, en fournissant un résumé des règles de bon usage. Remarquez que je n'ai pas présenté ici la fonction usage. Vous aurez encore besoin d'ajouter dans un coin quelques lignes de code pour afficher le résumé approprié; ce n'est pas automatique.

Quels sont donc tous ces paramètres que vous passez à la fonction getopt ? Et bien, le premier est simplement la liste brute des arguments et des drapeaux de ligne de commande (à l'exception du premier élément, le nom du script, que vous avez éliminé avant d'appeler la fonction main). Le deuxième est la liste des drapeaux courts acceptés par le script.

 
Sélectionnez
"hg:d"

-h
    affiche les règles d'usage
-g ...
    utilise le fichier de grammaire ou l'URL spécifié
-d
    montre l'information de débogage au cours du traitement

Le premier et le troisième drapeaux fonctionnent de manière autonome; vous les spécifiez ou non et le cas échéant ils exécutent une action (affichage l'aide) ou changent un état (actionnement du débogage). Au contraire, le deuxième drapeau (-g) doit être suivi par un argument, qui est le nom du fichier de grammaire à analyser. En fait, ce peut être un nom de fichier ou une adresse web, et vous ne savez pas encore lequel (vous l'apprendrez plus tard); mais vous êtes certains qu'il doit bien y avoir quelque chose. Aussi le signalez-vous à getopt en ajoutant deux points après g, le second paramètre de la fonction getopt.

Pour compliquer davantage les choses, le script accepte soit des drapeaux courts (comme -h) soit des drapeaux longs (comme --help) et vous voulez que ces derniers effectuent la même chose. D'où l'utilité du troisième paramètre de getopt : spécifier une liste de drapeaux longs qui correspondent aux drapeaux courts du second paramètre.

 
Sélectionnez
["help", "grammar="]

--help
    affiche les règles d'usage
--grammar ...
    utilise le fichier de grammaire ou l'URL spécifié

Trois choses à remarquer ici :

  1. Tous les drapeaux longs sont précédés par deux tirets sur la ligne de commande, mais vous n'avez pas besoin d'inclure ces tirets quand vous appelez getopt. Ils sont implicites.
  2. Le drapeau --grammar doit toujours être suivi par un argument additionnel, comme pour le drapeau -g. C'est indiqué par un signe égal, "grammar=".
  3. La liste des drapeaux longs est plus courte que la liste des drapeaux courts, parce que le drapeau -d n'a pas de version longue correspondante. Très bien; seul -d activera le débogage. Mais l'ordre des drapeaux courts et des drapeaux longs a besoin d'être le même, aussi vous devez d'abord spécifier tous les drapeaux courts qui possèdent un drapeau long correspondant, puis tout le reste des drapeaux courts.

Encore confus ? Tournez-vous vers le code en question et voyez si, dans ce contexte, cela fait sens.

Exemple 10.23. Manipuler les arguments de la ligne de commande dans kgp.py

 
Sélectionnez
def main(argv):                          ***1***
    grammar = "kant.xml"                
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
        usage()                         
        sys.exit(2)                     
    for opt, arg in opts:                ***2***
        if opt in ("-h", "--help"):      ***3***
            usage()                     
            sys.exit()                  
        elif opt == '-d':                ***4***
            global _debug               
            _debug = 1                  
        elif opt in ("-g", "--grammar"): ***5***
            grammar = arg               

    source = "".join(args)               ***6***

    k = KantGenerator(grammar, source)
    print k.output()

***1*** La variable grammar gardera une trace du fichier de grammaire utilisé. Vous l'initialisez ici au cas où elle ne serait pas spécifiée à la ligne de commande (en utilisant les drapeaux -g ou --grammar).

***2*** La variable opts que vous récupérez à partir de getopt contient une liste de tuples : flag et argument. Si le drapeau ne prend pas d'argument, alors arg vaudra simplement None. Cela facilite le parcours des drapeaux.

***3*** La fonction getopt valide les drapeaux de ligne de commande qui sont acceptables, mais elle ne fait aucune sorte de conversion entre les drapeaux courts et les drapeaux longs. Si vous spécifiez le drapeau -h, opt contiendra "-h"; si vous spécifiez le drapeau --help, opt contiendra "--help". Aussi avez-vous besoin de tester les deux.

***4*** Rappelez-vous, le drapeau -d n'avait pas de drapeau long correspondant; aussi n'avez-vous besoin que de tester la forme courte. Si vous le détectez, vous déclarez une variable globale à laquelle vous vous référerez plus tard pour afficher les informations de débogage. (Je l'ai utilisé au cours du développement de ce script. Vous ne pensiez tout de même pas que ces exemples ont fonctionné du premier coup ?)

***5*** Si vous trouvez un fichier de grammaire, indiqué par les drapeaux -g ou --grammar, vous conservez l'argument qui le suit (stocké dans arg) dans la variable grammar, écrasant alors la valeur par défaut que vous aviez initialisée au début de la fonction main.
***6*** C'est tout. Vous avez parcouru et traité les drapeaux de la ligne de commande. Ce qui signifie qu'il ne peut rester alors que des arguments de ligne de commande. Ils sont retournés par la fonction getopt et placés dans la variable args. Dans ce cas, vous les traitez comme une source de données pour l'analyseur. Si aucun argument de ligne de commande n'est spécifié, args sera une liste vide et source contiendra au final une chaîne vide.

X-G. Assembler les pièces

Vous avez déjà parcouru un long chemin. Portez votre regard en arrière et voyez comment rassembler toutes ces étapes.

Pour commencer, il s'agit d'un script qui prend ses arguments sur la ligne de commande, en utilisant le module getopt.

 
Sélectionnez
def main(argv):                         
...
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
...
    for opt, arg in opts:               
...

Vous créez une nouvelle instance de la classe KantGenerator et vous lui passez un fichier de grammaire et une source de données qui peuvent ou non avoir été spécifiés à la ligne de commande.

 
Sélectionnez
    k = KantGenerator(grammar, source)

L'instance KantGenerator charge automatiquement la grammaire, qui est un fichier XML. Vous utilisez la fonction taillée sur-mesure, openAnything, pour ouvrir le fichier (qui pourrait être stocké dans un fichier local ou sur un serveur web distant), puis vous utilisez les fonctions de traitement intégrées du module minidom pour analyser le document XML en un arbre d'objets Python.

 
Sélectionnez
def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()

Tiens, au passage, vous tirez avantage de votre connaissance de la structure d'un document XML pour mettre en place un petit cache de références, constitué simplement des éléments du document XML.

 
Sélectionnez
 def loadGrammar(self, grammar):                         
        for ref in self.grammar.getElementsByTagName("ref"):
            self.refs[ref.attributes["id"].value] = ref

Si vous avez spécifié une source de données à la ligne de commande, vous l'utilisez; autrement vous décortiquez la grammaire en recherchant la référence de plus haut niveau (celle qui n'est référencée par aucune autre) et vous l'utilisez comme point de départ.

 
Sélectionnez
 def getDefaultSource(self):
        xrefs = {}
        for xref in self.grammar.getElementsByTagName("xref"):
            xrefs[xref.attributes["id"].value] = 1
        xrefs = xrefs.keys()
        standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
        return '<xref id="%s"/>' % random.choice(standaloneXrefs)

Maintenant vous décortiquez la source de données. La source est aussi du XML et vous analysez ses noeuds un par un. Pour conserver un code séparé et plus maintenable, vous utilisez des gestionnaires distincts pour chaque type de noeud.

 
Sélectionnez
  def parse_Element(self, node): 
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)

Vous parcourez la grammaire, en analysant tous les enfants de chaque élément p,

 
Sélectionnez
    def do_p(self, node):
...
        if doit:
            for child in node.childNodes: self.parse(child)

en remplaçant les éléments choice par un enfant choisi aléatoirement,

 
Sélectionnez
def do_choice(self, node):
        self.parse(self.randomChildElement(node))

et en remplaçant les éléments xref par l'un des enfants, choisi aléatoirement, de l'élément ref correspondant, que vous avez préalablement mis en cache.

 
Sélectionnez
 def do_xref(self, node):
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id]))

Finalement, vous poussez l'analyse jusqu'au texte brut,

 
Sélectionnez
  def parse_Text(self, node):    
        text = node.data
...
            self.pieces.append(text)

qu'il ne vous reste plus qu'à afficher.

 
Sélectionnez
def main(argv):                         
...
    k = KantGenerator(grammar, source)
    print k.output()

X-H. Résumé

Python est accompagné de puissantes bibliothèques pour analyser et manipuler des documents XML. Le module minidom prend un fichier XML et l'analyse en objets Python, fournissant un accès aléatoire à des éléments arbitraires. Ce chapitre montre encore comment Python peut servir à créer un "véritable" script autonome exécutable en ligne de commande, pourvu de drapeaux et d'arguments de ligne de commande, d'une gestion d'erreur et même de la capacité de récupérer en entrée la redirection du résultat d'un programme antérieur.

Avant de passer au prochain chapitre, prenez le temps de vous familiariser avec les points suivants :

  • Chaîner des programmes au moyen de l'entrée standard et de la sortie standard
  • Définir des sélecteurs dynamiques avec getattr.
  • Utiliser les drapeaux de ligne de commande et les valider avec getopt

précédentsommairesuivant