Blog ENI : Toute la veille numérique !
-25€ dès 75€ sur les livres en ligne, vidéos... avec le code FUSEE25. J'en profite !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

XML avec libxml2

Introduction

XML est un excellent langage de description de documents utilisable dans de nombreux domaines. Il est donc naturel que les implémentations d’analyseurs syntaxiques fleurissent dans tous les langages, y compris le C. Parmi les plus connus, expat est utilisé entre autres dans des projets de Mozilla. Mais nous retenons libxml2, qui a été préféré par le projet GNOME et qui porte d’ailleurs le surnom de gnome-xml même s’il en est indépendant. libxml2 présente plusieurs avantages par rapport à d’autres projets similaires. Cette bibliothèque implémente à la fois les méthodes SAX et DOM pour la lecture et l’interprétation de documents XML. Elle accompagne une autre bibliothèque du même auteur, libxslt, qui permet de travailler sur les transformations XSLT comme nous le verrons dans la dernière recette de ce chapitre. Cette bibliothèque propose de plus une API très fournie. Cet avantage devient en contrepartie son principal défaut pour démarrer avec, car son approche prend beaucoup de temps.

Pour utiliser libxml2, outre les indispensables bases de XML, il faut avoir quelques notions sur DOM et SAX. Ce sont deux méthodes de lecture et de traitement de documents XML. Le principe de DOM consiste à lire le document en entier et à le mettre en mémoire sous forme d’un...

Lire un document XML

Problème

Vous souhaitez lire un document XML afin d’en mettre le contenu en mémoire.

Solution

Utilisez l’API DOM de libxml2 pour le charger en mémoire et accéder à son contenu.

Discussion

Le chargement en mémoire d’un document XML s’effectue avec xmlParseFile() si le document se trouve dans un fichier. S’il se trouve en mémoire, par exemple dans une chaîne de caractères, utilisez xmlParseMemory(). Dans le cas d’un flux, assurez-vous que vous souhaitez bien le mettre en mémoire. 


int 
main (int argc, char **argv) 
{ 
  xmlDocPtr xmldoc; 
  xmlNodePtr xmlroot; 
  char *filename = argv[1]; 
 
/* Lecture et analyse du document XML contenu dans filename. */ 
  if (NULL == (xmldoc = xmlParseFile (filename))) 
    { 
      fprintf (stderr, "xmlParseFile a échoué\n"); 
      exit (EXIT_FAILURE); 
    } 
 
/* Obtention du noeud racine. */ 
  if (NULL == (xmlroot = xmlDocGetRootElement (xmldoc))) 
    { 
      fprintf (stderr, "Le document est vide\n"); 
      xmlFreeDoc (xmldoc); 
      exit (EXIT_FAILURE); 
    } 
  exit (EXIT_SUCCESS); 
}
 

Ce programme charge le document XML en mémoire et fait pointer xmldoc dessus. Le nœud de l’arbre DOM correspondant à la racine XML du document s’obtient avec...

Transformer un arbre DOM en XML

Problème

Vous voulez afficher le contenu d’un arbre DOM sur la sortie standard ou l’écrire dans un fichier.

Solution

Utilisez xmlDocDump(), xmlDocFormatDump(), xmlSaveFile() ou xmlSaveFormatFile().

Discussion

La fonction xmlDocDump() prend en arguments un pointeur sur un descripteur de flux de type FILE* et l’arbre DOM de type xmlDocPtr. Le flux doit avoir été ouvert au préalable, comme c’est le cas pour stdout ou avec fopen() comme expliqué au chapitre "Contenu des fichiers". Lorsqu’il s’agit de sauvegarder dans un fichier, vous pouvez économiser l’appel aux fonctions d’ouverture et de fermeture des fichiers en utilisant xmlSaveFile() qui prend en argument le nom du fichier à écrire, et l’arbre DOM.

Par ailleurs, la sortie peut être formatée, ce qui modifie en particulier certains nœuds de texte (ceux qui contiennent des retours à la ligne et des caractères pour l’indentation). Suivant le besoin, vous pouvez préférer cela pour une meilleure lisibilité. Dans ce cas, au lieu d’utiliser les deux fonctions précédentes, appelez respectivement xmlDocFormatDump() et xmlSaveFormatFile().

En guise d’exemple, essayez celui de la recette précédente, puis intercalez ceci pour afficher l’arbre DOM : xmlDocFormatDump(stdout...

Ajouter un nœud à un arbre DOM

Problème

Vous avez un arbre DOM en mémoire et voulez ajouter un nœud à cet arbre.

Solution

Créez votre nouveau nœud avec xmlNewNode(), puis accrochez-le à l’arbre avec xmlAddChild(), xmlAddSibling(), xmlAddPrevSibling() ou xmlAddNextSibling(). Dans le cas d’un nœud texte, ces deux opérations sont possibles en une seule fois via xmlNewTextChild().

Discussion

L’ajout d’un nœud s’effectue en deux étapes. La première est la création du nœud avec xmlNewNode(). Vous pouvez préciser des attributs avec xmlSetProp(). Pour qu’il contienne du texte, nous utilisons xmlNodeSetContent(). La seconde étape consiste à rattacher le nœud ainsi créé à l’arbre par rapport à un autre nœud. Les fonctions xmlAddSibling() et xmlAddChild() accrochent le nouveau nœud à l’arbre en l’ajoutant à la liste, respectivement, des nœuds frères ou des nœuds fils du nœud indiqué. Les fonctions xmlAddPrevSibling() et xmlAddNextSibling() sont plus précises en accrochant le nœud juste à côté du nœud spécifié, respectivement avant ou après ce nœud. En guise d’exemple, ajoutons la distance de la galaxie d’Andromède par rapport à...

Modifier un nœud d’un arbre DOM

Problème

Vous avez un arbre DOM en mémoire et voulez modifier un nœud de cet arbre.

Solution

En fonction de ce que vous souhaitez modifier, utilisez xmlNodeSetName() pour modifier le nom du nœud, xmlSetProp() pour changer ou ajouter un attribut, xmlUnsetProp() pour supprimer un attribut et xmlSetContent() pour modifier le contenu.

Discussion

Soit un nœud n de type xmlNodePtr. L’exemple suivant montre comment changer le nom du nœud, lui ajouter un attribut, le changer, le supprimer et enfin changer le contenu du nœud :


xmlNodeSetName (n, "nom"); 
xmlSetProp (n, "attribut", "valeur"); 
xmlSetProp (n, "attribut", "valeur2"); 
xmlUnsetProp (n, "attribut"); 
xmlNodeSetContent (n, "Le contenu\nsur plusieurs\nlignes");
 

L’exemple montre que la valeur d’un attribut peut être modifiée. Changer son nom est impossible, car en réalité, cela revient à créer un nouvel attribut avec le nouveau nom et la valeur de l’ancien attribut, puis à supprimer l’ancien attribut. Dans ce cas, nous nous intéresserons à la fonction xmlGetProp() qui permet d’obtenir la valeur de l’attribut spécifié en argument :


xmlChar *valeur; 
valeur = xmlGetProp (n, "attribut"); 
if (valeur) ...

Supprimer un nœud d’un arbre DOM

Problème

Vous avez un arbre DOM en mémoire et voulez supprimer un nœud de cet arbre.

Solution

La suppression d’un nœud s’effectue en deux étapes. La première consiste à détacher le nœud de l’arbre avec xmlUnlinkNode(). La seconde, optionnelle, est de libérer la mémoire de ce nœud avec xmlFreeNode().

Discussion

La suppression d’une feuille de l’arbre (un nœud sans descendants) est chose évidente pour un nœud n :


xmlUnlinkNode (n); 
xmlFreeNode (n);
 

Cependant, il faut savoir que ces deux opérations concernent à la fois le nœud spécifié en argument, mais aussi toute sa descendance. Ainsi, si vous souhaitez supprimer un nœud de l’arbre mais aussi tous ses fils et petits-fils, la méthode précédente est celle à suivre. Par contre, si vous ne souhaitez pas supprimer les nœuds fils, il vous faudra les rattacher un par un à l’un ou à plusieurs autres nœuds de l’arbre avec xmlNodeAddChild() avant de détacher le nœud que vous voulez supprimer.

Prototypes


void xmlUnlinkNode (xmlNodePtr cur); 
void xmlFreeNode (xmlNodePtr cur);
 

Voir aussi l’article Libxml2 et petits fichiers XML paru dans Linux Magazine France, numéro 51, juin 2003.

Parcourir un arbre DOM

Problème

Vous voulez appliquer une opération sur tous les nœuds d’un arbre DOM.

Solution

Appliquez un algorithme de parcours d’arbre et effectuez l’action sur chaque nœud parcouru.

Discussion

Voici un algorithme de parcours, dit méthode d’itération post-ordre, qui travaille d’abord sur la descendance d’un nœud puis regarde les frères de ce nœud. L’action n’a lieu que quand tous les nœuds fils ont été traités.


void 
traverse (xmlNodePtr node) 
{ 
  xmlNodePtr n; 
  for (n = node; n; n = n->next) 
    { 
      if ((n->type == XML_ELEMENT_NODE) && (n->children)) 
        traverse (n->children); 
      proceed (n); 
    } 
}
 

Nous pouvons appeler cette fonction sur le nœud racine, afin de parcourir l’arbre en entier. L’action est effectuée via la fonction proceed(). Par exemple, pour afficher le contenu des nœuds textes (ou CDATA), utilisez une fonction comme celle-ci :


void 
proceed (xmlNodePtr node) 
{ 
  if ((node->type == XML_CDATA_SECTION_NODE) 
      || (node->type == XML_TEXT_NODE)) 
    { 
      xmlChar *path = xmlGetNodePath (node); 
      printf ("%s -> '%s'\n", path, 
              node->content ? (char *) node->content : "(null)"); 
      xmlFree (path); 
    } ...

Rechercher un nœud ou un ensemble de nœuds avec XPath dans un arbre DOM

Problème

Vous avez un arbre DOM en mémoire et voulez rechercher un nœud ou un ensemble de nœuds correspondant à un critère.

Solution

Utilisez xmlXPathNewContext() pour créer un contexte XPath et utilisez ce contexte avec xmlXPathEval() pour effectuer les requêtes.

Discussion

Avant d’utiliser les fonctions XPath, il est nécessaire d’effectuer un appel à xmlXPathInit() qui ne prend pas d’argument et ne renvoie rien. Cet appel initialise l’environnement XPath.

Ensuite, il faut initialiser le contexte XPath avec xmlXPathNewContext() qui est propre à chaque arbre DOM. Puis nous pouvons effectuer des requêtes XPath avec xmlXPathEval() en fournissant le chemin XPath et le contexte à cette fonction. Cette dernière fonction renvoie un objet de type xmlXPathObjectPtr, dont le champ type est XPATH_NODESET et qui contient un tableau dont les éléments sont les nœuds trouvés. L’exemple suivant, qui recherche le nom de l’objet numéro 31 du catalogue de Messier, illustre comment utiliser ce tableau. La mémoire utilisée par l’objet xmlXPathObjectPtr est à libérer avec xmlXPathFreeObject() et le contexte avec xmlXPathFreeContext().


xmlXPathInit (); 
  xmlcontext = xmlXPathNewContext (xmldoc); 
 
/* Construction d'une liste de noeuds à partir de la requête XPATH. */ 
  xmlobject = 
    xmlXPathEval ("/messier/objet[@numero='31']/nom/text()", 
                  xmlcontext); 
/* Nous avons une liste de noeuds si xmlobject->type contient bien 
 * XPATH_NODESET. 
 */ 
  if (xmlobject->type == XPATH_NODESET) 
    { 
      int i; 
      /* Parcours de la liste des noeuds. */ 
      for (i = 0; i < xmlobject->nodesetval->nodeNr; i++) 
        { 
 /* n est le noeud courant. */ 
          n = xmlobject->nodesetval->nodeTab[i]; 
          if ((n->type == XML_TEXT_NODE) ...

Créer et utiliser un fichier de configuration en XML

Problème

Vous voulez utiliser XML pour stocker la configuration de votre application dans un fichier afin de pouvoir lire, écrire et modifier facilement ce fichier de configuration.

Solution

Le principe d’un fichier de configuration XML est d’être mis en mémoire sous forme d’arbre DOM, et chaque élément de la configuration est recherché à l’aide d’une requête XPath, comme vu dans les recettes "Lire un document XML" et "Rechercher un nœud ou un ensemble de nœuds avec XPath dans un arbre DOM".

Discussion

Avant de choisir le format XML pour son fichier de configuration, le programmeur doit s’interroger si le format nécessite d’être XML. Dans le cas d’un simple fichier contenant des lignes clé=’valeur’, orientez-vous plutôt vers un fichier simple, comme décrit dans la recette "Lire un fichier de configuration simple" (chapitre "Contenu des fichiers"). Pour un fichier de configuration de structure plus complexe, XML peut se révéler la solution la plus simple et la plus appropriée.

L’utilisation d’un fichier de configuration commence par son chargement. Nous écrirons une fonction config_load() pour cela, qui initialisera certaines variables en plus de mettre le fichier en mémoire...

Parcourir un document XML avec SAX

Problème

Vous voulez lire un fichier XML sans le mettre en mémoire.

Solution

Créez un contexte avec xmlFileCreateParserCtxt() et renseignez ce contexte avec des fonctions de rappel. Ces fonctions dites callback seront appelées au fur et à mesure de la lecture en fonction de la nature de ce que l’analyseur syntaxique lit. Puis faites appel à ce dernier avec xmlParseDocument().

Discussion

La création et l’initialisation d’un contexte s’effectue ainsi :


  xmlParserCtxtPtr ctxt; 
  xmlSAXHandler *my_sax_handler; 
 
  if (NULL == (my_sax_handler = malloc (sizeof *my_sax_handler))) 
    exit (EXIT_FAILURE); 
  memset (my_sax_handler, 0, sizeof *my_sax_handler); 
 
/* Attribution des fonctions de rappel. */ 
  my_sax_handler->getEntity = cb_sax_get_entity; 
  my_sax_handler->startDocument = cb_sax_start_document; 
  my_sax_handler->endDocument = cb_sax_end_document; 
  my_sax_handler->characters = cb_sax_characters; 
  my_sax_handler->cdataBlock = cb_sax_cdata; 
  my_sax_handler->startElement = cb_sax_start_element; 
  my_sax_handler->endElement = cb_sax_end_element; 
  my_sax_handler->comment = cb_sax_comment; 
  my_sax_handler->warning = cb_sax_warning; 
  my_sax_handler->error = cb_sax_error; 
  my_sax_handler->fatalError = cb_sax_fatal_error; 
 
/* Création du contexte. */ 
  if (NULL == (ctxt = xmlCreateFileParserCtxt (filename))) 
    exit (EXIT_FAILURE); 
 
/* Insertion du jeu de fonctions de rappel dans le contexte. */ 
  ctxt->sax = my_sax_handler; 
 
/* Insertion de données utilisateur dans le contexte. */ 
  ctxt->userData = user_data;
 

La première partie consiste à initialiser une structure xmlSAXHandler en lui indiquant des fonctions de rappel. La seconde partie crée le contexte avec la fonction xmlCreateFileParserCtxt() dans lequel nous renseignons le champ sax avec la structure contenant les pointeurs...

Rechercher une donnée dans un document XML (SAX)

Problème

Vous voulez trouver une donnée dans un fichier XML sans pour autant le charger intégralement en mémoire.

Solution

Codez les tests adéquats dans les fonctions de rappel en utilisant certaines informations fournies par libxml2 via le contexte.

Discussion

Libxml2 ne propose pas une fonction de recherche de nœuds avec XPath. Il serait possible de le faire via les fonctions de rappel et l’API de la bibliothèque spécifique à XPath, mais cela se limiterait à des expressions simples. En effet, il ne faut pas perdre de vue que l’analyseur syntaxique ne place rien en mémoire de l’arborescence de l’arbre XML, ce qui empêche toute recherche contenant des axes autres que celui du nœud père vers le nœud fils.

Par conséquent, il est plus simple de coder soi-même les tests de recherche dans les fonctions de rappel. Pour cela, nous disposons du nœud courant fourni en argument à la fonction de rappel de début de balise ; idem pour celle de fin de balise. La liste des attributs est aussi fournie à la fonction en début de balise. À tout moment, via le contexte, nous pouvons connaître le chemin du nœud courant grâce aux champs nameNr et nameTab. Voici quelques exemples de tests :


/* Test sur le nom d'une balise. */ ...

Lire un flux de données XML

Problème

Vous voulez lire un flux de données XML provenant d’un fichier qui croît, un tube, une socket...

Solution

Utilisez xmlCreateIOParserCtxt() au lieu de xmlCreateFileParserCtxt() lors de la création du contexte.

Discussion

Dans la recette "Parcourir un document XML avec SAX", nous lisons les données XML depuis un fichier car le contexte a été créé avec xmlCreateFileParserCtxt(). En utilisant à la place xmlCreateIOParserCtxt(), nous pouvons lire le flux XML depuis n’importe quel descripteur de fichiers ouvert. Ainsi, les deux exemples suivants permettent d’ouvrir un fichier en vue de lire son contenu :


ctxt = xmlCreateFileParserCtxt (filename); 
  ctxt->sax = my_sax_handler; 
  ctxt->userData = user_data;
 

et


int fd = open (filename, O_RDONLY); 
  if (-1 == fd) exit (EXIT_FAILURE); 
  ctxt = 
    xmlCreateIOParserCtxt (my_sax_handler, user_data, cb_sax_read, 
                           cb_sax_close, &fd, XML_CHAR_ENCODING_NONE);
 

L’intérêt de cette fonction réside surtout dans le fait que nous pouvons non seulement lire depuis un fichier ouvert au préalable, mais aussi depuis une socket, un tube et toute source de données identifiée par un descripteur de fichier.  Nous pouvons de plus spécifier nos propres fonctions de rappel pour la lecture et la fermeture du fichier :...

Transformer un document XML avec XSLT

Problème

Vous voulez écrire votre propre générateur de documents en prenant des fichiers XML comme modèles.

Solution

Utilisez libxslt. Créez une feuille de style XSL que vous chargez en mémoire avec xsltParseStylesheetFile(). Chargez en mémoire le document XML que vous voulez transformer, à l’aide d’une des méthodes vues dans la première recette, par exemple xmlParseFile(). Vous obtiendrez le document en appliquant la fonction xsltApplyStylesheet(). Le document peut ensuite être sauvegardé dans un fichier avec xsltSaveResultToFile().

Discussion

La création d’un générateur de documents s’effectue facilement à l’aide des transformations XSLT et de la bibliothèque libxslt. Nous utilisons en effet une feuille de style XSL que nous appliquons sur des documents XML chargés comme nous avons pu voir dans la recette "Lire un document XML". Nous allons pour illustrer cela utiliser notre exemple de document XML sur les objets de Messier, et le transformer en un fichier HTML, à l’aide de la feuille de style suivante (à mettre dans un fichier, messier.xsl par exemple) :


<?xml version="1.0" encoding="ISO-8859-1"?> 
 
<xsl:stylesheet version="1.0" 
                    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> ...