Parser un fichier XML volumineux en C avec la Libxml2
Dans le cadre d’un projet, j’ai plusieurs fichiers XML volumineux (plusieurs Go) que je dois parser pour en extraire des données à insérer en base de données. En fait, ces fichiers XML contiennent des articles d’un site de commerce électronique. Par un programme d’affiliation, je dois faire apparaître certains de ces articles sur un site web.
Lorsque que l’on souhaite parser du XML, on a principalement le choix entre 2 méthodes différentes. On a DOM (Document Object Model) qui va charger l’intégralité d’un document XML en mémoire sous forme d’un arbre, ce qui rend aisé la manipulation du document. Cependant avec un fichier XML volumineux cette méthode n’est pas possible. Dans ce cas, on va utiliser la méthode SAX (Simple API for XML). Avec SAX on a une analyse événementielle, cela veut dire que le document XML est analysé au fur et à mesure et selon le type de l’élément rencontré (balise, commentaire ou texte), une fonction de rappel (callback) est appelée pour traiter l’élément concerné. Donc SAX en utilisant moins de mémoire est parfait pour extraire des données de grands fichiers XML.
Par le passé, j’utilisais Java avec Xerces mais pour un souci de performance (le plus vite possible en utilisant le moins de mémoire) je me suis orienté sur des implémentations en C. J’ai découvert deux librairies open source : Expat et Libxml2. Comme il faut faire un choix, j’ai opté pour la Libxml2. Il s’agit d’une puissante boîte à outils XML développée dans le cadre du projet Gnome, mais elle est tout à fait utilisable en dehors de celui-ci, elle n’a pas de dépendance et elle se compile simplement avec l’API ANSI C.
Ci-dessous je vais présenter un cas concret à l’aide de la Libxml2 (SAX) d’un programme qui permet de parser un fichier XML pour en afficher le contenu souhaité. En parcourant le code source de ce programme, j’expliquerai au fur et à mesure les fonctions utilisées de l’API Libxml2.
Vous pouvez télécharger une archive contenant le fichier source c, le Makefile ainsi que le fichier XML exemple : xmlsaxparser.tar.gz.
– Fichier ‘articles.xml’ –
Voici comment est structuré le fichier XML des articles :
<?xml version="1.0" encoding="iso-8859-1"?> <articles> <article> <id>173968</id> <name><![CDATA[The Rock]]></name> <description><![CDATA[a film of the year 1996]]></description> <category><![CDATA[DVD Movie]]></category> <price>11.95</price> <url><![CDATA[http://abc.domain.ext/showArticle?id=173968]]></url> <availability>141</availability> <condition>used</condition> </article> [...] </articles>
Remarquez l’encodage du fichier en ISO-8859-1 (encodage par défaut des fichiers que je reçois dans le cadre du projet), mais de préférence utilisez si possible UFT-8 (unicode) car la Libxml2 travaille dans cet encodage internement.
– Fichier ‘xmlsaxparser.c’ –
(Seulement les lignes qui me semblent importantes sont présentées ci-dessous, pour l’intégralité se référer au fichier fourni dans l’archive téléchargeable ci-dessus.)
Inclure la ligne suivante afin de pouvoir accéder aux fonctions de la Libxml2 :
#include <libxml2/libxml/parser.h>
Créer une structure pour stocker temporairement les champs d’un article :
struct article {
char id[ID_SIZE + 1];
char name[NAME_SIZE + 1];
char category[CATEGORY_SIZE + 1];
char price[PRICE_SIZE + 1];
char url[URL_SIZE + 1];
char availability[AVAILABILITY_SIZE + 1];
char condition[CONDITION_SIZE + 1];
} article;
Créer une fonction (qui nous sera utile plus tard) permettant de copier dans le champ correspondant de la structure article, le texte renvoyé par le parseur :
void writeFieldArticle(char *articleField, int articleSize, const xmlChar *ch,
int len) {
int nbCharsConsumed = len;
/* I want text is encoded in ISO-8859-1 */
UTF8Toisolat1(articleField, &articleSize, ch, &nbCharsConsumed);
/* Detect, no enough space in articleField */
if(nbCharsConsumed != len) {
fprintf(stderr,"ERROR: Not enough space to write %d characters\n\n",
len);
fprintf(stderr, "Content to write (first %d characters) : \n\n%s\n",
len, ch);
exit (EXIT_FAILURE);
}
articleField[articleSize] = '\0';
xmlTag = IGNORE;
}
Cette fonction a comme paramètres : un pointeur vers la chaîne de caractères où il faut écrire, le nombre de caractères effectifs (le caractère NULL de terminaison est déjà pris en compte) que peut contenir cette chaîne de caractères, la chaîne de caractères contenant le texte renvoyé par le parseur, la longueur de cette chaîne de caractères. Notez le type ‘xmlChar’, il s’agit d’un simple char non-signé et la chaîne de caractères est encodée en UTF-8. Donc, même si l’API fournit des fonctions spécialisées (ex: xmlStrcmp) pour la manipulation des chaînes de caractères, on peut tout à fait utiliser aussi les fonctions standard string (string.h).
La fonction ‘UTF8Toisolat1′ permet de convertir la string en ISO-8859-1 et par la même occasion de la copier dans le champ correspondant de notre structure article. Après l’exécution, l’argument deux contiendra le nombre d’octets écrit et l’argument quatre, le nombre d’octets consommé. A la ligne suivante, un test permet de détecter les cas où il n’y a pas assez de place pour accueillir le texte du parseur. Finalement, on n’oublie pas d’écrire le caractère NULL de terminaison. (Si vous souhaitez travailler en UTF-8, utilisez la fonction ’strncpy’ pour copier ‘ch’ dans ‘articleField’.)
La fonction suivante est appelée à chaque ouverture d’une balise XML :
void saxHandlerStartElement(void *ctx, const xmlChar *fullname,
const xmlChar **atts) {
/* New product to parse therefore reset fields */
if (!strcmp(fullname, "article")) {
article.id[0] = '\0';
article.name[0] = '\0';
article.category[0] = '\0';
article.price[0] = '\0';
article.url[0] = '\0';
article.availability[0] = '\0';
article.condition[0] = '\0';
xmlTag = IGNORE;
} else if(!strcmp(fullname, "id")) {
xmlTag = ID;
} [...]
Le premier argument de la fonction est un pointeur vers des données « utilisateur », ce n’est pas utilisé dans cet exemple, mais on aurait très bien pu passer de cette façon la structure article. Les deux autres arguments sont le nom de la balise ouvrante rencontrée et ses attributs.
Dans cette fonction, on va simplement tester où on est au niveau de la structure du fichier XML et attribuer à ‘xmlTag’, la valeur correspondante. Remarquez qu’à chaque ouverture de la balise article, on « reset » les champs de notre structure étant donné que l’on procède à l’analyse d’un nouvel article.
La fonction suivante est appelée lorsque du texte est disponible entre une balise ouvrante et fermante :
void saxHandlerCharacters(void *ctx, const xmlChar *ch, int len) {
switch (xmlTag) {
case IGNORE:
break;
case ID:
writeFieldArticle(article.id, ID_SIZE, ch, len);
break;
[...]
L’argument ‘*ch’ est un pointeur vers le texte renvoyé par le parseur. Pour les balises dont on souhaite récupérer leur contenu, on appelle la fonction ‘writeFieldArticle’ que l’on a créée précédemment.
La fonction suivante est appelée lorsque du texte est disponible entre une balise ouvrante et fermante, mais cette fois-ci lorsque l’on a à faire à des blocks CDATA :
void saxHandlerCdataBlock(void *ctx, const xmlChar *ch, int len) {
[...]
Au niveau du fonctionnement, c’est identique à la fonction précédente.
La fonction suivante est appelée lorsque une fermeture de balise XML est rencontrée :
void saxHandlerEndElement(void *ctx, const xmlChar *name) {
/* An article was parsed. */
if (!xmlStrcmp(name, "article")) {
/* Write to standard output */
printf("%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
article.id, article.name, article.category, article.price,
article.url, article.availability, article.condition);
}
}
Si la balise fermante est article, cela veut dire que l’on a fini le parsing d’un article. Il est simplement afficher sur la sortie standard sur une ligne, les champs séparés par des tabulations. Le code peut être adapté, par exemple pour envoyer la structure article à une fonction responsable de l’insérer en base de données.
Voici maintenant comment initialiser le parseur pour ensuite le lancer :
xmlSAXHandler saxHandler = { 0 };
saxHandler.startElement = saxHandlerStartElement;
saxHandler.endElement = saxHandlerEndElement;
saxHandler.characters = saxHandlerCharacters;
saxHandler.cdataBlock = saxHandlerCdataBlock;
/* Start the parser */
if (xmlSAXUserParseFile(&saxHandler, NULL, xmlFilePath) != 0) {
fprintf(stderr,
"ERROR : Error encountered during parsing of the XML file.\n");
return (EXIT_FAILURE);
}
La structure ‘xmlSAXHandler’ contient des pointeurs sur des fonctions de rappel responsables de traiter les événements qui surviennent durant le parsing du document XML. On commence par définir à NULL tous les pointeurs de fonction pour éviter des appels indéfinis se soldant par « segmentation fault ». Puis on attribue pour les quatre fonctions de rappel (callback) ci-dessus, la correspondance avec le pointeur de fonction de la structure ‘xmlSAXHandler’. Et finalement on démarre le parseur en passant comme arguments : l’adresse de notre structure ‘xmlSAXHandler’, NULL pour préciser que l’on n’a pas de données « utilisateur » à transmettre aux fonctions de rappel et le dernier argument est le nom du fichier XML.
– Fichier ‘Makefile’ –
Voici le fichier Makefile que l’on peut utiliser pour compiler notre petit programme :
CC = gcc
CFLAGS = `xml2-config --cflags`
LDFLAGS = `xml2-config --libs`
EXEC = xmlsaxparser
all: $(EXEC)
xmlsaxparser: xmlsaxparser.o
{TAB}$(CC) -o xmlsaxparser xmlsaxparser.o $(LDFLAGS)
xmlsaxparser.o: xmlsaxparser.c
{TAB}$(CC) -o xmlsaxparser.o -c xmlsaxparser.c $(CFLAGS)
clean:
{TAB}rm -rf *.o
mrproper: clean
{TAB}rm -rf $(EXEC)
(Remplacez ci-dessus les {TAB} par des tabulations.)
Ce Makefile permet de compiler les sources sur un environnement de type UNIX comme Linux par exemple. Pour les autres systèmes d’exploitation (Windows, …), il faudra certainement faire quelques petites adaptations. Cependant il faut remarquer que la Libxml2 est extrêmement portable et donc cet exemple devrait compiler « partout ».
Remarquez dans ce Makefile, les deux appels à ‘xml2-config’, une fois pour optenir le chemin vers les fichiers d’inclusion et l’autre fois pour obtenir la librairie (libxml2) devant être liée lors de l’édition des liens.
– Installation des outils, compilation et exécution du programme –
Ayant développé ce petit exemple sur Ubuntu, l’installation des outils et la compilation sont basés sur cet OS mais tout ceci devrait être adaptable facilement à d’autres systèmes.
Sur Ubuntu, lancez un terminal et tapez ces lignes pour installer le kit de compilation et la librairie Libxml2 avec ces fichiers nécessaires pour le développement :
sudo apt-get install build-essential libxml2 libxml2-dev
Ensuite pour compiler notre programme, il suffit d’aller dans le répertoire où se trouvent les sources et saisir les 2 commandes suivantes :
make clean
make
La première commande nettoyera les éventuels restes d’anciennes compilations et la deuxième lancera la compilation.
Il reste plus qu’à exécuter le programme :
./xmlsaxparser articles.xml
Si tout se passe bien, les quatre articles du fichier XML devraient s’afficher.
(Attention le texte en sortie est encodé en ISO-8859-1, donc il faut éventuellement changer le paramètre d’encodage du terminal pour avoir un affichage correcte des caractères caractéristiques de la langue française.)
– Ressources et suite … –
Pour tout ce qui concerne la Libxml2 (documentation de l’API, download, …), le site officiel est : http://xmlsoft.org.
Si vous cherchez un très bon tutoriel en français sur l’ensemble de l’API Libxml2 SAX et autres, c’est ici : http://julp.developpez.com/c/libxml2.
Dans un prochain billet, je vais présenter une solution simple pour insérer à grande vitesse le contenu extrait du fichier XML dans MySQL.


Merci.
C’est la première fois que je manipule XML avec C.
J’utilise C sous Visual Studio 2008.
- Quelles sont les étapes d’installation et de compilation à suivre sous windows?
- Quel est le meilleur parseur XMl en C qui n’est pas couteux coté mémoire et performant au niveau temps d’exécution?
@Bouali Asma
Il m’est difficile de répondre à tes questions, je n’ai pas d’expérience dans le développement C sous Windows.
Sous Windows, il faut utiliser le système interne à Visual Studio pour la compilation, à la place du Makefile que l’on utilise dans un système de type Unix.
La Libxml2 utilisée dans cet exemple est compatible avec Windows. Il faut installer ses headers (.h) ainsi que son code objet correspondant (libs).
Concernant ta dernière question, la réponse dépend de l’utilisation…
Il y a deux principales techniques de manipulation de documents XML : DOM et SAX.
C’est la taille de ton document XML et la manipulation que tu souhaites effectuée qui te guidera vers une ou l’autre des deux techniques. (Voir le deuxième paragraphe de cette article.)
Pour Windows en C/C++, il y a ces parseurs :
– Libxml2 (http://xmlsoft.org) (SAX & DOM)
– Expat (http://expat.sf.net) (SAX style)
– Apache Xerces C++ (http://xerces.apache.org) (SAX & DOM)
– …
– Il existe peut-être quelque chose par Microsoft en C/C++
Bonjour,
trés bon explicatif,
dans mon cas je dois manipuler des gros XML et isoler certaines parties.