Freigeben über


Das Beste aus beiden Welten: Kombinieren von XPath mit dem XmlReader

 

Dare Obasanjo und Howard Hao
Microsoft Corporation

5. Mai 2004

Laden Sie die XPathReader.exe Beispieldatei herunter.

Zusammenfassung: Dare Obasanjo erläutert den XPathReader, der die Möglichkeit bietet, große XML-Dokumente effizient mit einem XPath-fähigen XmlReader zu filtern und zu verarbeiten. Mit dem XPathReader kann man ein großes Dokument sequenziell verarbeiten und eine identifizierte Unterstruktur extrahieren, die mit einem XPath-Ausdruck übereinstimmt. (11 gedruckte Seiten)

Einführung

Vor etwa einem Jahr las ich einen Artikel von Tim Bray mit dem Titel XML ist zu hart für Programmierer, in dem er sich über die umständliche Natur von Pushmodell-APIs wie SAX für den Umgang mit großen XML-Datenströmen beschwerte. Tim Bray beschreibt ein ideales Programmiermodell für XML als ein Modell, das dem Arbeiten mit Text in Perl ähnelt, bei dem Textströme verarbeitet werden können, indem man interessante Elemente mit regulären Ausdrücken abgleicht. Im Folgenden finden Sie einen Auszug aus tim Brays Artikel, der sein idealisiertes Programmiermodell für XML-Streams zeigt.

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 ist nicht der Einzige, der sich nach diesem XML-Verarbeitungsmodell sehnt. In den letzten Jahren haben verschiedene Personen, mit denen ich zusammenarbeite, daran gearbeitet, ein Programmiermodell für die Verarbeitung von Datenströmen von XML-Dokumenten zu erstellen, die der Verarbeitung von Textstreams mit regulären Ausdrücken entspricht. In diesem Artikel wird der Höhepunkt dieser Arbeit beschrieben– der XPathReader.

Suchen nach ausgeliehenen Büchern: XmlTextReader-Lösung

Um einen deutlichen Hinweis auf die Produktivitätssteigerungen des XPathReader im Vergleich zu vorhandenen XML-Verarbeitungstechniken mit dem XmlReader zu geben, habe ich ein Beispielprogramm erstellt, das grundlegende XML-Verarbeitungsaufgaben ausführt. Das folgende Beispieldokument beschreibt eine Reihe von Büchern, die ich besitze und ob sie derzeit an Freunde ausgeliehen werden.

 <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>
   

Im folgenden Codebeispiel werden die Namen der Personen angezeigt, denen ich Bücher ausgeliehen habe, sowie die Bücher, die ich ihnen ausgeliehen habe. Die Codebeispiele sollten die folgende Ausgabe erzeugen.

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();
    }
}       

Verwenden von XPath als reguläre Ausdrücke für XML

Als Erstes benötigen wir eine Möglichkeit, Musterabgleiche für Knoten von Interesse in einem XML-Stream auf die gleiche Weise durchzuführen wie bei regulären Ausdrücken für Zeichenfolgen in einem Textstream. XML verfügt bereits über eine Sprache für übereinstimmende Knoten namens XPath, die als guter Ausgangspunkt dienen kann. Es liegt ein Problem mit XPath vor, das verhindert, dass es ohne Änderungen als Mechanismus für den Abgleich von Knoten in großen XML-Dokumenten auf Streaming-Weise verwendet wird. XPath geht davon aus, dass das gesamte XML-Dokument im Arbeitsspeicher gespeichert ist und Vorgänge zulässt, die mehrere Durchläufe des Dokuments erfordern oder zumindest große Teile des XML-Dokuments im Arbeitsspeicher speichern müssen. Der folgende XPath-Ausdruck ist ein Beispiel für eine solche Abfrage:

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

Die Abfrage gibt das publisher-Attribut eines Book-Elements zurück, wenn es über ein untergeordnetes author-Element mit dem Wert "Frederick Brooks" verfügt. Diese Abfrage kann nicht ausgeführt werden, ohne mehr Daten zwischenzuspeichern, als für einen Streamingparser typisch ist, da das publisher-Attribut zwischengespeichert werden muss, wenn es für das book-Element angezeigt wird, bis das untergeordnete author-Element erkannt und sein Wert untersucht wurde. Abhängig von der Größe des Dokuments und der Abfrage kann die Datenmenge, die im Arbeitsspeicher zwischengespeichert werden muss, ziemlich groß sein, und die Ermittlung, was zwischengespeichert werden soll, kann ziemlich komplex sein. Um diese Probleme nicht bewältigen zu müssen, hat ein Mitarbeiter, Arpan Desai, einen Vorschlag für eine Teilmenge von XPath erstellt, die für die vorwärtsgerichtete Verarbeitung von XML geeignet ist. Diese Teilmenge von XPath wird in seinem Artikel An Introduction to Sequential XPath beschrieben.

Es gibt mehrere Änderungen an der XPath-Standardgrammatik in Sequenziellem XPath, aber die größte Änderung ist die Einschränkung bei der Verwendung von Achsen. Jetzt sind bestimmte Achsen im Prädikat gültig, während andere Achsen nur im Nicht-Prädikatteil des Sequenziellen XPath-Ausdrucks gültig sind. Wir haben die Achsen in drei verschiedene Gruppen eingeteilt:

  • Allgemeine Achsen: Stellen Informationen zum Kontext des aktuellen Knotens bereit. Sie können an einer beliebigen Stelle im Sequenziellen XPath-Ausdruck angewendet werden.
  • Vorwärtsachsen: Stellen Sie Informationen zu Knoten vor dem Kontextknoten im Stream bereit. Sie können nur im Kontext des Standortpfads angewendet werden, da sie nach "zukünftigen" Knoten suchen. Ein Beispiel ist "child". Wir können die untergeordneten Knoten eines angegebenen Pfads erfolgreich auswählen, wenn "child" im Pfad enthalten ist. Wenn sich jedoch "child" im Prädikat befindet, könnten wir den aktuellen Knoten nicht auswählen, da wir nicht voraus auf seine untergeordneten Elemente blicken können, um den Prädikatausdruck zu testen, und dann den Reader zurückspulen, um den Knoten auszuwählen.
  • Umgekehrte Achse: sind im Wesentlichen das Gegenteil von Vorwärtsachsen. Ein Beispiel wäre "parent". Wenn sich parent im Standortpfad befindet, möchten wir das übergeordnete Element eines bestimmten Knotens zurückgeben. Da wir nicht rückwärts gehen können, können wir diese Achsen nicht im Positionspfad oder in Prädikaten unterstützen.

Hier ist eine Tabelle mit den XPath-Achsen, die vom XPathReader unterstützt werden:

Typ Achsen Wo unterstützt
Allgemeine Achsen Attribut, Namespace, Selbst Beliebiger Ort im XPath-Ausdruck
Vorwärtsachsen child, descendant, descendant-or-self, following, following-sibling Beliebiger XPath-Ausdruck mit Ausnahme von Prädikaten
Umgekehrte Achse Vorgänger, Vorfahre oder Selbst, übergeordnete, vorangehende, gleichgeordnete Vorgänger Nicht unterstützt

Einige XPath-Funktionen werden vom XPathReader nicht unterstützt, da sie auch große Teile des XML-Dokuments im Arbeitsspeicher zwischenspeichern oder den XML-Parser zurückverfolgen müssen. Funktionen wie count() und sum() werden überhaupt nicht unterstützt, während Funktionen wie local-name() und namespace-uri() nur funktionieren, wenn keine Argumente angegeben sind (d. a. nur, wenn Sie nach diesen Eigenschaften auf dem Kontextknoten fragen). In der folgenden Tabelle sind die XPath-Funktionen aufgeführt, die entweder nicht unterstützt werden oder die in XPathReader teilweise eingeschränkt sind.

XPath-Funktion Unterstützte Teilmenge BESCHREIBUNG
number last() Nicht unterstützt Kann nicht ohne Pufferung funktionieren
number count(node-set) Nicht unterstützt Kann nicht ohne Pufferung funktionieren
string local-name(node-set?) string local-name() Ein Knotensatz kann nicht als Parameter verwendet werden.
string namespace-uri(node-set?) string namespace-uri() Ein Knotensatz kann nicht als Parameter verwendet werden.
string name(node-set?) string name() Ein Knotensatz kann nicht als Parameter verwendet werden.
number sum(node-set) Nicht unterstützt Kann nicht ohne Pufferung funktionieren

Die letzte Haupteinschränkung für XPath im XPathReader besteht darin, das Testen der Werte von Elementen oder Textknoten nicht zuzulassen. Der XPathReader unterstützt den folgenden XPath-Ausdruck nicht:

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

Die obige Abfrage wählt das book-Element aus, wenn seine Zeichenfolge den Text "Frederick Brooks" enthält. Um solche Abfragen unterstützen zu können, müssen möglicherweise große Teile des Dokuments zwischengespeichert werden, und der XPathReader muss seinen Zustand zurückspulen können. Das Testen von Werten von Attributen, Kommentaren oder Verarbeitungsanweisungen wird jedoch unterstützt. Der folgende XPath-Ausdruck wird vom XPathReader unterstützt:

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

Die oben beschriebene Teilmenge von XPath ist ausreichend reduziert, um es zu ermöglichen, einen speichereffizienten XPath-basierten XML-Parser bereitzustellen, der den regulären Ausdrücken entspricht, die für Textströme übereinstimmen.

Ein erster Blick auf den XPathReader

Der XPathReader ist eine Unterklasse von XmlReader , die die im vorherigen Abschnitt beschriebene Teilmenge von XPath unterstützt. Der XPathReader kann verwendet werden, um Dateien zu verarbeiten, die von einer URL geladen wurden, oder kann auf anderen Instanzen von XmlReader überlappen. Die folgende Tabelle zeigt die Methoden, die dem XmlReader vom XPathReader hinzugefügt wurden.

Methode BESCHREIBUNG
Match(XPathExpression) Testet, ob der Knoten, in dem sich der Reader derzeit befindet, von XPathExpression abgeglichen wird.
Match(string) Testet, ob der Knoten, in dem sich der Reader derzeit befindet, von der XPath-Zeichenfolge abgeglichen wird.
Match(int) Testet, ob der Knoten, in dem sich der Reader derzeit befindet, vom XPath-Ausdruck am angegebenen Index in der XPathCollection des Lesers abgeglichen wird.
MatchesAny(ArrayList) Testet, ob der Knoten, auf dem sich der Leser derzeit befindet, mit einem der XPathExpressions in der Liste übereinstimmt.
ReadUntilMatch() Setzt das Lesen des XML-Datenstroms fort, bis der aktuelle Knoten mit einem der angegebenen XPath-Ausdrücke übereinstimmt.

Im folgenden Beispiel wird der XPathReader verwendet, um den Titel jedes Buches in meiner Bibliothek zu drucken:

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);
      }
   }  
}

Ein offensichtlicher Vorteil von XPathReader gegenüber der herkömmlichen XML-Verarbeitung mit dem XmlTextReader ist, dass die Anwendung den aktuellen Knotenkontext während der Verarbeitung des XML-Datenstroms nicht nachverfolgen muss. Im obigen Beispiel muss sich der Anwendungscode nicht darum kümmern, ob das title-Element , dessen Inhalt angezeigt und gedruckt wird, ein untergeordnetes Element eines Buchelements ist oder nicht, indem der Zustand explizit nachverfolgt wird, da dies bereits vom XPath erfolgt.

Das andere Puzzleteil ist die XPathCollection-Klasse . Die XPathCollection ist die Auflistung von XPath-Ausdrücken, mit denen der XPathReader übereinstimmen soll. Ein XPathReader entspricht nur Knoten, die im XPathCollection-Objekt enthalten sind. Dieser Abgleich ist dynamisch, was bedeutet, dass XPath-Ausdrücke während des Analyseprozesses bei Bedarf hinzugefügt und aus der XPathCollection entfernt werden können. Dies ermöglicht Leistungsoptimierungen, bei denen Tests für XPath-Ausdrücke erst durchgeführt werden, wenn sie benötigt werden. Die XPathCollection wird auch zum Angeben von Präfix-Namespace-Bindungen< verwendet, die vom XPathReader beim Abgleich von Knoten mit XPath-Ausdrücken> verwendet werden. Das folgende Codefragment zeigt, wie dies erreicht wird:

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); 

Suchen nach ausgeliehenen Büchern: XPathReader-Lösung

Nachdem wir nun einen Blick auf den XPathReader geholt haben, ist es an der Zeit zu sehen, wie viel verbesserte Verarbeitung von XML-Dateien mit der Verwendung von XmlTextReader verglichen werden kann. Im folgenden Codebeispiel wird die XML-Datei im Abschnitt Finding Loaned Books: XmlTextReader Solution verwendet und sollte die folgende Ausgabe erzeugen:

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);
      }
   }  
}

Diese Ausgabe ist gegenüber dem ursprünglichen Codeblock stark vereinfacht, ist fast genauso effizient im Speicher und sehr analog zur Verarbeitung von Textstreams mit regulären Ausdrücken. Es sieht so aus, als hätten wir Tim Brays Ideal für ein XML-Programmiermodell für die Verarbeitung großer XML-Datenströme erreicht.

Funktionsweise von XPathReader

Der XPathReader entspricht XML-Knoten, indem eine Auflistung von XPath-Ausdrücken erstellt wird, die in eine abstrakte Syntaxstruktur (AST) kompiliert wurden, und dann diese Syntaxstruktur durchlaufen, während eingehende Knoten vom zugrunde liegenden XmlReader empfangen werden. Beim Durchlaufen der AST-Struktur wird eine Abfragestruktur generiert und auf einen Stapel gepusht. Die Tiefe der Knoten, die von der Abfrage abgeglichen werden sollen, wird berechnet und mit der Depth-Eigenschaft des XmlReader verglichen, da Knoten im XML-Stream gefunden werden. Der Code zum Generieren des AST für einen XPath-Ausdruck wird aus dem zugrunde liegenden Code für die Klassen im System.Xml abgerufen. Xpath, das als Teil des Quellcodes im Release Shared Source Common Language Infrastructure 1.0 verfügbar ist.

Jeder Knoten in der AST implementiert die IQuery-Schnittstelle , die die folgenden drei Methoden definiert:

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

Die GetValue-Methode gibt den Wert des Eingabeknotens relativ zum aktuellen Aspekt des Abfrageausdrucks zurück. Die MatchNode-Methode testet, ob der Eingabeknoten mit dem analysierten Abfragekontext übereinstimmt, während die ReturnType-Eigenschaft angibt, welchen XPath-Typ der Abfrageausdruck auswertet.

Zukunftspläne für XPathReader

Basierend darauf, wie nützlich verschiedene Personen bei Microsoft den XPathReader gefunden haben, einschließlich der BizTalk Server, die mit einer Variante dieser Implementierung ausgeliefert werden, habe ich beschlossen, einen GotDotNet-Arbeitsbereich für das Projekt zu erstellen. Es gibt einige Features, die ich hinzufügen möchte, z. B. die Integration einiger Funktionen aus dem EXSLT.NET-Projekts in den XPathReader und die Unterstützung für einen größeren Bereich von XPath. Entwickler, die an der Weiterentwicklung von XPathReader arbeiten möchten, können dem GotDotNet-Arbeitsbereich beitreten.

Zusammenfassung

Der XPathReader bietet eine leistungsstarke Möglichkeit zum Verarbeiten von XML-Datenströmen, indem die Leistungsfähigkeit von XPath genutzt und mit der Flexibilität des pullbasierten XML-Parsermodells des XmlReader kombiniert wird. Der kompositorische Entwurf von System.Xml ermöglicht es, den XPathReader über andere Implementierungen des XmlReader zu übertragen und umgekehrt. Die Verwendung von XPathReader für die Verarbeitung von XML-Datenströmen ist fast so schnell wie die Verwendung von XmlTextReader, ist aber gleichzeitig genauso nutzbar wie XPath mit dem XmlDocument. Wirklich ist es das Beste aus beiden Welten.

Dare Obasanjo ist Mitglied des WebData-Teams von Microsoft, das unter anderem die Komponenten innerhalb des System.Xml- und System.Data-Namespaces der .NET Framework, Microsoft XML Core Services (MSXML) und Microsoft Data Access Components (MDAC) entwickelt.

Howard Hao ist Software design Engineer in Test für das WebData XML-Team und ist der Standard Entwickler des XPathReader.

Sie können alle Fragen oder Kommentare zu diesem Artikel auf dem Extreme XML-Nachrichtenboard in GotDotNet posten.