Utilisation de l'API Stax de libXML2


Pour charger un fichier XML, il y a traditionnellement deux API : Sax et DOM

* Sax oblige a écrire plein de callbacks. Ca fait tout ce dont on peut avoir besoin, mais c'est assez lourd à coder.
* DOM chargeant tout en mémoire, ce n'est pas adapté pour les gros fichiers.
;

Introduction

Le XML c'est génial, le XML c'est beau, le XML vous fait le cacao le matin.

Pour charger un fichier XML, il y a traditionnellement deux API : Sax et DOM

  • Sax oblige a écrire plein de callbacks. Ca fait tout ce dont on peut avoir besoin, mais c'est assez lourd à coder.
  • DOM chargeant tout en mémoire, ce n'est pas adapté pour les gros fichiers.

Stax

L'API de type Stax est un juste milieu entre des deux approches. L'idée, c'est qu'au lieu de laisser le parseur tout gérer, on va lui demander un élément, l'analyser nous-même, puis demander l'élément suivant.

La libxml2, du projet Gnome, fournit une API de ce type. Elle est plutôt bien documentée, mais les exemples, mêmes s'ils sont fonctionnels, ne sont pas très utiles tels quels : la plupart accèdent à un élément connu d'avance et listent ses propriétés. Généralement, quand on a un fichier XML tellement gros qu'on ne peut pas utiliser DOM, la problématique est plutôt : découvrir à la volée ce qu'on a dans le fichier.

Fichier d'exemple

Pour ce tutoriel, j'utiliserai des fichiers de dump OpenStreetMap, qui peuvent atteindre plusieurs centaines de Go. Un bon test pour les fuites mémoires...

En simplifié , ça ressemble à ça (ici sur un bout de granite près de Lannion) :

<?xml  version="1.0" encoding="UTF-8" ?>
<osm version="0.6"  generator="CGImap 0.0.2">
    <bounds minlat="48.7640630"  minlon="-3.5936820" maxlat="48.7670190" maxlon="-3.5882580"  />
    <node id="28204783"  lat="48.7661740" lon="-3.5946960">
        <tag k="created_by"  v="almien_coastlines" />
    </node>
    ... encore beaucoup de texte

Donc, une racine "osm" avec un noeud "bounds" et une liste de noeuds "node", tous avec des attributs.

Initialisation

L'initialisation est très simple, et bien expliquée dans les tutoriaux officiels :

xmlTextReaderPtr reader = xmlReaderForFile("machin.xml", NULL, 0);
if (reader == NULL) return;
// 1 if the node was read successfully, 0 if there is no more nodes to read, or -1 in case of error :
if (xmlTextReaderRead(reader) == 0) return;
readRoot(policy); // C'est ici qu'on va lire la racine du fichier, et tout les enfants
xmlFreeTextReader(reader);

Lecture

Voyons voir cette fonction readRoot :

Tout d'abord, un petit test pour être sûr que l'on a pas fait de bêtise :

if (xmlTextReaderDepth(reader) != 0) return error("Le noeud racine devrait être OSM");

Ensuite, on demande au parser le nom de l'élément suivant. Il va donc lire juste ce qu'il faut du fichier pour savoir ça, et s'arrêter :

xmlChar *name;
name = xmlTextReaderName(reader);

On vérifie alors qu'on a bien un noeud "osm" comme racine:

if (name == NULL) return error("pas de noeud racine");
if (xmlStrcmp(name, (const xmlChar *) "osm")) return error("Ceci n'est pas un fichier OpenStreetMap");

Maintenant qu'on est sûrs qu'on est dans l'élément qu'on veut, on n'a plus besoin de name, donc on le libère :

xmlFree(name);

Ensuite, on va lire tous les attributs, par exemple pour vérifier qu'on est bien en OSM 0.6 :

while(xmlTextReaderMoveToNextAttribute(reader)){
    xmlChar *name_attrib, *value_attrib;
    name_attrib = xmlTextReaderName(reader);
    value_attrib = xmlTextReaderValue(reader);
    if (!xmlStrcmp(name_attrib, (const xmlChar *) "version") &amp;&amp; xmlStrcmp(value_attrib, (const xmlChar *) "0.6")){
        std::cout << "Attention, la version du fichier n'est pas 0.6" << std::endl;
    }
    xmlFree(name_attrib);
    xmlFree(value_attrib);
}

Remarquez que les attributs ne sont absolument pas ordonnés, ni accessibles tous d'un coup. Si c'est le comportement que vous souhaitez, utilisez une map (ou map )
On veut maintenant lire les enfants : on demande le chunk suivant, on regarde son type, et on avise. Et on répète jusqu'à ce que le fichier soit vide, càd que xmlTextReaderRead retourne 0 (fin) ou -1 (erreur).

L'astuce, c'est que dans le "on avise", on va également pouvoir appeler xmlTextReaderRead, et lire ainsi récursivement tous les enfants :

int ret = xmlTextReaderRead(reader);
while (ret == 1) {
    xmlChar * name = xmlTextReaderName(reader);
    if (!xmlStrcmp(name, (const xmlChar *) "bounds")){ ret = readBounds();}
    else if (!xmlStrcmp(name, (const xmlChar *) "node"  )){ ret = readNode();    }
    else if (!xmlStrcmp(name, (const xmlChar *) "way"   )){ ret = readWay();}
    else                                                  { ret = xmlTextReaderRead(reader);} // Ne devrait pas arriver, mais peut-être que le fichier est mal formé ?
    xmlFree(name);
}

Et voilà ! readBounds n'a pas de surprises, il n'y a que des attributs, mais voyons readNode pour l'exemple :

if (xmlTextReaderDepth(reader) != 1) return error("un Node doit être au niveau 1");
while(xmlTextReaderMoveToNextAttribute(reader)){
    // Faites ce que vous voulez avec les attributs
}
int ret = xmlTextReaderRead(reader);
while (ret == 1) {
 
    // un noeud frère ou parent arrive : on redonne le contrôle au parent pour qu'il le gère.
    // Si c'est de nouveau un noeud, readRoot rappellera tout naturellement readNode.
	if (xmlTextReaderDepth(reader) <= 1){
		return 1; // 1 = lire la suite (cf readRoot)
	}else{ // child node
	xmlChar * name = xmlTextReaderName(reader);
        // faire quelque chose en fonction de name. Ici, on lit en boucle en ignorant les tags
        ret = xmlTextReaderRead(reader);
        xmlFree(name);
    }
}
return 0; // Ne devrait pas arriver

Lecture de très gros fichiers

Quand on lit des fichiers de 200Go, il est souvent utile de prévenir l'utilisateur de là où on en est, histoire d'éviter le ctrl-c furieux. On sort alors un peu d'une utilisation "simple" de la libxml2, mais c'est possible.

Tout d'abord, on abandonne xmlReaderForFile au profit de xmlNewTextReader, qui prend en paramètre un xmlParserInputBufferPtr. Cet objet implémentera les callbacks de lecture de fichier (i.e. fopen, fread, etc), tout comme xmlReaderForFile le fait en interne, à la différence près qu'on va retenir là où on en est dans le fichier.

Tout d'abord, on veut trouver la taille du fichier, en octets :

FILE * file = fopen(path,"r");
if (!m_file) return -1;
 _fseeki64(m_file,0, SEEK_END);
 __int64 size = _ftelli64(m_file); // la taille
 _fseeki64(m_file,0, SEEK_SET);

Remarquez que l'on n'utilise pas fseek, qui sont limités à des fichiers de 3Go (adressable en 32 bits). Les streams C++ ont le même problème sur les OS 32 bits, d'où l'utilisation de ces fonction assez incongrues en C++.

Maintenant, créons notre parseur :

xmlParserInputBufferPtr input = xmlParserInputBufferCreateIO(
    xmlInputReadCallback_Function,
    xmlInputCloseCallback_Function,
    this, // Ou n'importe quel objet, ou même NULL si vous mettez vos données en global
    XML_CHAR_ENCODING_NONE
 );
reader = xmlNewTextReader(input, "foobar file"); // Le second paramètre est obligatoire et inutile

Nous avons donc besoin de deux fonctions : xmlInputReadCallback_Function et xmlInputCloseCallback_Function. La seconde est assez évidente (fclose), intéressons-nous à la première :

int xmlInputReadCallback_Function (void * c, char * buffer, int length ) {
    // length est la taille du buffer demandé.
    // Sauf si vous avez recompilé la lib avec des options particulières, ce sera 4096, soit 4Ko
    UpdateProgress(length);
    // Lit 4096 octets du fichier dans le buffer
    return (int)fread(buffer,1,length,file);
}

Voilà, le callback est fini. Il ne vous reste plus qu'à implémenter UpdateProgress à votre convenance. Par exemple, pour une application en ligne de commande :

pos+=length; // On retient la position actuelle
if (m_size &amp;&amp; (100*(m_pos-length)/m_size) != 100*m_pos/m_size) // Si la position a changé de plus de 1%
 std::cout << (int(100*m_pos/m_size)) << std::endl; // on affiche

ce qui fera un compteur de 1 à 100. Avec Qt, un signal en Qt::QueuedConnection (pour éviter de bloquer l'UI) fera également très bien l'affaire.

Conclusion

Nous avons donc vu comment lire un très gros fichier XML avec l'API Stax de la libxml2. Maintenant, il ne vous reste plus qu'à encapsuler tout ça, vu que pour des raisons de simplicité, j'ai fait comme si toutes les variables étaient globales.

Donc pour résumer :

  • DOM si vous pouvez
  • Sax si vous êtes obligés (pas d'autre API sur votre plate-forme, ...)
  • Stax sinon !
  1. No comments yet.
(will not be published)

  1. No trackbacks yet.