Partager via


Le meilleur des deux mondes : combinaison de XPath avec xmlReader

 

Dare Obasanjo et Howard Hao
Microsoft Corporation

5 mai 2004

Téléchargez l’exemple de fichier XPathReader.exe.

Résumé: Dare Obasanjo aborde le XPathReader, qui offre la possibilité de filtrer et de traiter des documents XML volumineux de manière efficace à l’aide d’un XmlReader prenant en charge XPath. Avec XPathReader, vous pouvez traiter séquentiellement un document volumineux et extraire une sous-arborescence identifiée correspondant à une expression XPath. (11 pages imprimées)

Introduction

Il y a environ un an, j’ai lu un article de Tim Bray intitulé XML Is Too Hard For Programmers, dans lequel il se plaignait de la lourdeur des API de modèle push, comme SAX, pour le traitement de grands flux de XML. Tim Bray décrit un modèle de programmation idéal pour XML comme un modèle similaire à l’utilisation de texte dans Perl, où l’on peut traiter des flux de texte en faisant correspondre des éléments intéressants à l’aide d’expressions régulières. Voici un extrait de l’article de Tim Bray montrant son modèle de programmation idéalisé pour les flux XML.

while (<STDIN>) {
  next if (X<meta>X);
  if    (X<h1>|<h2>|<h3>|<h4>X)
  { $divert = 'head'; }
  elsif (X<img src="/^(.*\.jpg)$/i>X)
  { &proc_jpeg($1); }
  # and so on...
}

Tim Bray n’est pas le seul à avoir aspiré à ce modèle de traitement XML. Au cours des dernières années, diverses personnes avec lesquelles je travaille ont travaillé à la création d’un modèle de programmation pour le traitement de flux de documents XML d’une manière analogue au traitement de flux de texte avec des expressions régulières. Cet article décrit l’aboutissement de ce travail: le XPathReader.

Recherche de livres prêtés : solution XmlTextReader

Pour donner une indication claire des gains de productivité du XPathReader par rapport aux techniques de traitement XML existantes avec XmlReader, j’ai créé un exemple de programme qui effectue des tâches de traitement XML de base. L’exemple de document suivant décrit un certain nombre de livres que je possède et s’ils sont actuellement prêtés à des amis.

 <books>
  <book publisher="IDG books" on-loan="Sanjay">
    <title>XML Bible</title>
    <author>Elliotte Rusty Harold</author>
  </book>
  <book publisher="Addison-Wesley">
    <title>The Mythical Man Month</title>
    <author>Frederick Brooks</author>
  </book>
  <book publisher="WROX">
    <title>Professional XSLT 2nd Edition</title>
    <author>Michael Kay</author>
  </book>
  <book publisher="Prentice Hall" on-loan="Sander" >
   <title>Definitive XML Schema</title>
   <author>Priscilla Walmsley</author>
  </book>
  <book publisher="APress">
   <title>A Programmer's Introduction to C#</title>
   <author>Eric Gunnerson</author>
  </book>
</books>
   

L’exemple de code suivant affiche les noms des personnes auxquelles j’ai prêté des livres, ainsi que les livres que je leur ai prêtés. Les exemples de code doivent produire la sortie suivante.

Sanjay was loaned XML Bible by Elliotte Rusty Harold 
Sander was loaned Definitive XML Schema by Priscilla Walmsley

XmlTextReader Sample: 
using System; 
using System.IO; 
using System.Xml;

public class Test{


    static void Main(string[] args) {

      try{ 
      XmlTextReader reader = new XmlTextReader("books.xml");
      ProcessBooks(reader);

      }catch(XmlException xe){
        Console.WriteLine("XML Parsing Error: " + xe);
      }catch(IOException ioe){
        Console.WriteLine("File I/O Error: " + ioe);
      }
    }  

    static void ProcessBooks(XmlTextReader reader) {
      
      while(reader.Read()){
      
        //keep reading until we see a book element 
        if(reader.Name.Equals("book") && 
      (reader.NodeType == XmlNodeType.Element)){ 
          
     if(reader.GetAttribute("on-loan") != null){ 
            ProcessBorrowedBook(reader);
          }else {
            reader.Skip();
          }
        }
      }
    }


   static void ProcessBorrowedBook(XmlTextReader reader){

 Console.Write("{0} was loaned ", 
                             reader.GetAttribute("on-loan"));
      
      
      while(reader.NodeType != XmlNodeType.EndElement && 
                                            reader.Read()){
       
       if (reader.NodeType == XmlNodeType.Element) {
          
     switch (reader.Name) {
            case "title":              
              Console.Write(reader.ReadString());
              reader.Read(); // consume end tag
              break;
            case "author":
              Console.Write(" by ");
              Console.Write(reader.ReadString());
              reader.Read(); // consume end tag
              break;
          }
        }
      }
      Console.WriteLine();
    }
}       

Utilisation de XPath comme expressions régulières pour XML

La première chose dont nous avons besoin est un moyen d’effectuer une correspondance de modèle pour les nœuds d’intérêt dans un flux XML de la même façon que nous le pouvons avec des expressions régulières pour les chaînes dans un flux de texte. XML a déjà un langage pour la correspondance des nœuds appelé XPath, qui peut servir de bon point de départ. Il existe un problème avec XPath qui l’empêche d’être utilisé sans modification comme mécanisme de mise en correspondance des nœuds dans des documents XML volumineux en continu. XPath suppose que l’intégralité du document XML est stockée en mémoire et autorise les opérations qui nécessiteraient plusieurs passages sur le document, ou au moins nécessiteraient que de grandes parties du document XML soient stockées en mémoire. L’expression XPath suivante est un exemple de requête de ce type :

/books/book[author='Frederick Brooks']/@publisher

La requête retourne l’attribut publisher d’un élément book s’il a un élément auteur enfant dont la valeur est « Frederick Brooks ». Cette requête ne peut pas être exécutée sans mettre en cache plus de données que ce qui est normal pour un analyseur de streaming, car l’attribut d’éditeur doit être mis en cache lorsqu’il apparaît sur l’élément book jusqu’à ce que l’élément d’auteur enfant ait été vu et sa valeur examinée. Selon la taille du document et de la requête, la quantité de données qui doivent être mises en cache en mémoire peut être assez importante et déterminer ce qu’il faut mettre en cache peut être assez complexe. Pour éviter d’avoir à gérer ces problèmes, un collègue, Arpan Desai, a proposé un sous-ensemble de XPath adapté au traitement avant uniquement de XML. Ce sous-ensemble de XPath est décrit dans son article An Introduction to Sequential XPath.

Plusieurs modifications ont été apportées à la grammaire XPath standard dans Le XPath séquentiel, mais la plus grande modification est la restriction de l’utilisation des axes. À présent, certains axes sont valides dans le prédicat, tandis que d’autres sont valides uniquement dans la partie non prédicat de l’expression XPath séquentielle. Nous avons classé les axes en trois groupes différents :

  • Axes communs : fournissent des informations sur le contexte du nœud actuel. Ils peuvent être appliqués n’importe où dans l’expression XPath séquentielle.
  • Axes de transfert : fournissez des informations sur les nœuds avant le nœud de contexte dans le flux. Ils ne peuvent être appliqués que dans le contexte du chemin d’accès d’emplacement, car ils recherchent des nœuds « futurs ». Par exemple, « child ». Nous pouvons sélectionner correctement les nœuds enfants d’un chemin donné si « child » se trouve dans le chemin. Toutefois, si « enfant » se trouve dans le prédicat, nous ne pourrions pas sélectionner le nœud actuel, car nous ne pouvons pas regarder à l’avance ses enfants pour tester l’expression de prédicat, puis rembobiner le lecteur pour sélectionner le nœud.
  • Axe inverse : sont essentiellement à l’opposé des axes avant. Par exemple, « parent ». Si le parent se trouve dans le chemin d’accès de l’emplacement, nous souhaiterions retourner le parent d’un nœud spécifique. Encore une fois, comme nous ne pouvons pas revenir en arrière, nous ne pouvons pas prendre en charge ces axes dans le chemin d’emplacement ou dans les prédicats.

Voici un tableau montrant les axes XPath pris en charge par le XPathReader :

Type Axes Là où pris en charge
Axes communs attribut, espace de noms, self N’importe où dans l’expression XPath
Axes avant child, descendant, descendant-or-self, suivant, suivant-frère N’importe où dans l’expression XPath, à l’exception des prédicats
Axe inverse ancêtre, ancêtre-ou-soi, parent, précédent, frère-précédent Non prise en charge

Certaines fonctions XPath ne sont pas prises en charge par XPathReader en raison du fait qu’elles nécessitent également la mise en cache de grandes parties du document XML en mémoire ou la possibilité de revenir en arrière de l’analyseur XML. Les fonctions telles que count() et sum() ne sont pas du tout prises en charge, tandis que les fonctions telles que local-name() et namespace-uri() fonctionnent uniquement quand aucun argument n’est spécifié (c’est-à-dire uniquement lors de la demande de ces propriétés sur le nœud de contexte). Le tableau suivant répertorie les fonctions XPath qui ne sont pas prises en charge ou dont certaines fonctionnalités ont été limitées dans le XPathReader.

Fonction XPath Sous-ensemble pris en charge Description
number last() Non pris en charge Impossible de travailler sans mise en mémoire tampon
nombre de nombres(node-set) Non pris en charge Impossible de travailler sans mise en mémoire tampon
string local-name(node-set?) string local-name() Impossible d’utiliser un jeu de nœuds comme paramètre
string namespace-uri(node-set?) string namespace-uri() Impossible d’utiliser un jeu de nœuds comme paramètre
string name(node-set?) string name() Impossible d’utiliser un jeu de nœuds comme paramètre
nombre sum(node-set) Non pris en charge Impossible de travailler sans mise en mémoire tampon

La dernière restriction majeure apportée à XPath dans XPathReader consiste à interdire le test des valeurs d’éléments ou de nœuds de texte. XPathReader ne prend pas en charge l’expression XPath suivante :

 /books/book[contains(.,'Frederick Brooks')]

La requête ci-dessus sélectionne l’élément book si sa chaîne contient le texte « Frederick Brooks ». Pour pouvoir prendre en charge ces requêtes, de grandes parties du document doivent peut-être être mises en cache et le XPathReader doit être en mesure de rembobiner son état. Toutefois, le test des valeurs d’attributs, de commentaires ou d’instructions de traitement est pris en charge. L’expression XPath suivante est prise en charge par XPathReader :

/books/book[contains(@publisher,'WROX')]

Le sous-ensemble de XPath décrit ci-dessus est suffisamment réduit pour permettre à l’un d’entre eux de fournir un analyseur XML de diffusion en continu basé sur XPath efficace en mémoire qui est analogue à la correspondance d’expressions régulières pour les flux de texte.

Première vue d’ensemble du XPathReader

XPathReader est une sous-classe du XmlReader qui prend en charge le sous-ensemble de XPath décrit dans la section précédente. XPathReader peut être utilisé pour traiter des fichiers chargés à partir d’une URL ou peut être superposé sur d’autres instances de XmlReader. Le tableau suivant présente les méthodes ajoutées au XmlReader par le XPathReader.

Méthode Description
Match(XPathExpression) Teste si le nœud dans lequel le lecteur est actuellement positionné correspond à la XPathExpression.
Match(string) Teste si le nœud dans lequel le lecteur est actuellement positionné correspond à la chaîne XPath.
Match(int) Teste si le nœud dans lequel le lecteur est actuellement positionné correspond à l’expression XPath à l’index spécifié dans la collection XPathCollection du lecteur.
CorrespondAny(ArrayList) Teste si le nœud sur lequel le lecteur est actuellement positionné est mis en correspondance par l’une des XPathExpressions de la liste.
ReadUntilMatch() Continue la lecture du flux XML jusqu’à ce que le nœud actuel corresponde à l’une des expressions XPath spécifiées.

L’exemple suivant utilise XPathReader pour imprimer le titre de chaque livre de ma bibliothèque :

using System; 
using System.Xml;
using System.Xml.XPath;
using GotDotNet.XPath;

public class Test{
static void Main(string[] args) {

      try{ 
XPathReader xpr  = new XPathReader("books.xml", "//book/title"); 

            while (xpr.ReadUntilMatch()) {
               Console.WriteLine(xpr.ReadString()); 
             }      
            Console.ReadLine(); 
   }catch(XPathReaderException xpre){
      Console.WriteLine("XPath Error: " + xpre);
      }catch(XmlException xe){
         Console.WriteLine("XML Parsing Error: " + xe);
      }catch(IOException ioe){
         Console.WriteLine("File I/O Error: " + ioe);
      }
   }  
}

Un avantage évident de XPathReader par rapport au traitement XML classique avec XmlTextReader est que l’application n’a pas besoin de suivre le contexte de nœud actuel lors du traitement du flux XML. Dans l’exemple ci-dessus, le code d’application n’a pas à se soucier de savoir si l’élément title dont il affiche et imprime le contenu est un enfant d’un élément book ou non par un état de suivi explicite, car cela est déjà effectué par le XPath.

L’autre pièce du puzzle est la classe XPathCollection . XPathCollection est la collection d’expressions XPath à laquelle le XPathReader est censé correspondre. Un XPathReader correspond uniquement aux nœuds contenus dans son objet XPathCollection . Cette correspondance est dynamique, ce qui signifie que les expressions XPath peuvent être ajoutées et supprimées de la XPathCollection pendant le processus d’analyse si nécessaire. Cela permet d’optimiser les performances lorsque les tests ne sont pas effectués sur les expressions XPath tant qu’ils ne sont pas nécessaires. XPathCollection est également utilisé pour spécifier les liaisons préfixe-espace<> de noms utilisées par le XPathReader lors de la mise en correspondance de nœuds avec des expressions XPath. Le fragment de code suivant montre comment cela est effectué :

XPathCollection xc  = new XPathCollection();
xc.NamespaceManager = new XmlNamespaceManager(new NameTable()); 
xc.NamespaceManager.AddNamespace("ex", "http://www.example.com"); 
xc.Add("//ex:book/ex:title"); 

XPathReader xpr  = new XPathReader("books.xml", xc); 

Recherche de livres prêtés : solution XPathReader

Maintenant que nous avons examiné le XPathReader, il est temps de voir à quel point les fichiers XML de traitement améliorés peuvent être comparés à l’utilisation de XmlTextReader. L’exemple de code suivant utilise le fichier XML de la section intitulée Recherche de livres prêts : XmlTextReader Solution et doit produire la sortie suivante :

Sanjay was loaned XML Bible by Elliotte Rusty Harold 
Sander was loaned Definitive XML Schema by Priscilla Walmsley

XPathReader Sample: 
using System; 
using System.IO; 
using System.Xml;
using System.Xml.XPath;
using GotDotNet.XPath;

public class Test{
static void Main(string[] args) {

      try{ 
         XmlTextReader xtr = new XmlTextReader("books.xml"); 
         
         XPathCollection xc = new XPathCollection();
         int onloanQuery = xc.Add("/books/book[@on-loan]");
         int titleQuery  = xc.Add("/books/book[@on-loan]/title");
         int authorQuery = xc.Add("/books/book[@on-loan]/author");

         XPathReader xpr  = new XPathReader(xtr, xc); 

         while (xpr.ReadUntilMatch()) {

            if(xpr.Match(onloanQuery)){
               Console.Write("{0} was loaned ", xpr.GetAttribute("on-loan"));
            }else if(xpr.Match(titleQuery)){
               Console.Write(xpr.ReadString());
            }else if(xpr.Match(authorQuery)){
               Console.WriteLine(" by {0}", xpr.ReadString());
            }

         }         

         Console.ReadLine(); 

   }catch(XPathReaderException xpre){
      Console.WriteLine("XPath Error: " + xpre);   
   }catch(XmlException xe){
         Console.WriteLine("XML Parsing Error: " + xe);
      }catch(IOException ioe){
         Console.WriteLine("File I/O Error: " + ioe);
      }
   }  
}

Cette sortie est grandement simplifiée par rapport au bloc de code d’origine, est presque aussi efficace en mémoire et très analogue au traitement des flux de texte avec des expressions régulières. Il semble que nous ayons atteint l’idéal de Tim Bray pour un modèle de programmation XML pour le traitement de flux XML volumineux.

Fonctionnement du XPathReader

Le XPathReader correspond aux nœuds XML en créant une collection d’expressions XPath compilées dans une arborescence de syntaxe abstraite (AST), puis en parcourant cette arborescence de syntaxe tout en recevant les nœuds entrants du XmlReader sous-jacent. En parcourant l’arborescence AST, une arborescence de requêtes est générée et poussée sur une pile. La profondeur des nœuds à mettre en correspondance par la requête est calculée et comparée à la propriété Depth du XmlReader , car des nœuds sont rencontrés dans le flux XML. Le code permettant de générer l’AST pour une expression XPath est obtenu à partir du code sous-jacent des classes du System.Xml. Xpath, qui est disponible dans le code source dans la version 1.0 de l’infrastructure common language de source partagée.

Chaque nœud de l’AST implémente l’interface IQuery qui définit les trois méthodes suivantes :

        internal virtual object GetValue(XPathReader reader);
        internal virtual bool MatchNode(XPathReader reader);
        internal abstract XPathResultType ReturnType()

La méthode GetValue retourne la valeur du nœud d’entrée par rapport à l’aspect actuel de l’expression de requête. La méthode MatchNode vérifie si le nœud d’entrée correspond au contexte de requête analysé, tandis que la propriété ReturnType spécifie le type XPath que l’expression de requête évalue.

Plans futurs pour XPathReader

En fonction de l’utilité des différentes personnes de Microsoft qui ont trouvé le XPathReader, y compris le BizTalk Server fourni avec une variante de cette implémentation, j’ai décidé de créer un espace de travail GotDotNet pour le projet. J’aimerais que certaines fonctionnalités soient ajoutées, telles que l’intégration de certaines fonctions du projet EXSLT.NET dans le XPathReader et la prise en charge d’une plus large gamme de XPath. Les développeurs qui souhaitent travailler sur le développement ultérieur de XPathReader peuvent rejoindre l’espace de travail GotDotNet.

Conclusion

Le XPathReader offre un moyen efficace de traiter les flux XML en exploitant la puissance de XPath et en la combinant avec la flexibilité du modèle d’analyseur XML basé sur l’extraction du xmlReader. La conception de composition de System.Xml permet de superposer le XPathReader sur d’autres implémentations du XmlReader et vice versa. L’utilisation de XPathReader pour le traitement des flux XML est presque aussi rapide que l’utilisation de XmlTextReader, mais en même temps est aussi utilisable que XPath avec xmlDocument. C’est vraiment le meilleur des deux mondes.

Dare Obasanjo est membre de l’équipe WebData de Microsoft, qui développe, entre autres, les composants de l’espace de noms System.Xml et System.Data du .NET Framework, Microsoft XML Core Services® (MSXML) et Microsoft Data Access Components (MDAC).

Howard Hao est un ingénieur de conception de logiciels dans l’équipe WebData XML et est le développeur main du XPathReader.

N’hésitez pas à publier des questions ou des commentaires sur cet article sur le tableau de messages Extreme XML sur GotDotNet.