Il meglio di entrambi i mondi: combinare XPath con XmlReader
Dare Obasanjo e Howard Hao
Microsoft Corporation
5 maggio 2004
Scaricare il file di esempio XPathReader.exe.
Riepilogo: Dare Obasanjo illustra XPathReader, che offre la possibilità di filtrare ed elaborare documenti XML di grandi dimensioni in modo efficiente usando un XmlReader compatibile con XPath. Con XPathReader, è possibile elaborare in sequenza un documento di grandi dimensioni ed estrarre un sotto-albero identificato corrispondente a un'espressione XPath.
(11 pagine stampate)
Introduzione
Circa un anno fa, ho letto un articolo di Tim Bray intitolato XML Is Too Hard For Programmers, in cui si lamentava della natura complessa delle API del modello push, come SAX, per la gestione di grandi flussi di XML. Tim Bray descrive un modello di programmazione ideale per XML come uno simile all'uso del testo in Perl, in cui è possibile elaborare i flussi di testo associando elementi di interesse usando espressioni regolari. Di seguito è riportato un estratto dell'articolo di Tim Bray che mostra il suo modello di programmazione idealizzato per i flussi 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 non è l'unico che ha annonato per questo modello di elaborazione XML. Negli ultimi anni, varie persone che lavoro con hanno lavorato per creare un modello di programmazione per l'elaborazione dei flussi di documenti XML in modo analogo all'elaborazione dei flussi di testo con espressioni regolari. Questo articolo descrive il culmine di questo lavoro, ovvero XPathReader.
Ricerca di libri in prestito: soluzione XmlTextReader
Per dare un'indicazione chiara dei guadagni di produttività dalle tecniche di elaborazione XPathReader rispetto alle tecniche di elaborazione XML esistenti con XmlReader, ho creato un programma di esempio che esegue attività di elaborazione XML di base. Il documento di esempio seguente descrive un numero di libri che sono proprietari e se sono attualmente prestati agli amici.
<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>
Nell'esempio di codice seguente vengono visualizzati i nomi delle persone a cui ho prestato libri, nonché i libri che ho prestato a loro. Gli esempi di codice devono produrre l'output seguente.
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();
}
}
Uso di XPath come espressioni regolari per XML
La prima cosa necessaria è un modo per eseguire la corrispondenza dei criteri per i nodi di interesse in un flusso XML nello stesso modo in cui è possibile usare espressioni regolari per le stringhe in un flusso di testo. XML dispone già di un linguaggio per i nodi corrispondenti denominati XPath, che possono fungere da buon punto di partenza. Si verifica un problema con XPath che impedisce l'uso senza modifica come meccanismo per la corrispondenza dei nodi in documenti XML di grandi dimensioni in modo di streaming. XPath presuppone che l'intero documento XML sia archiviato in memoria e consenta operazioni che richiedono più passaggi nel documento o che richiedono almeno parti di grandi dimensioni del documento XML vengano archiviate in memoria. L'espressione XPath seguente è un esempio di tale query:
/books/book[author='Frederick Brooks']/@publisher
La query restituisce l'attributo publisher di un elemento libro se ha un elemento autore figlio il cui valore è "Frederick Brooks". Questa query non può essere eseguita senza memorizzare nella cache più dati di quanto sia tipico per un parser di streaming perché l'attributo publisher deve essere memorizzato nella cache quando viene visualizzato nell'elemento book fino a quando l'elemento autore figlio non è stato visto e il relativo valore esaminato. A seconda delle dimensioni del documento e della query, la quantità di dati che deve essere memorizzata nella cache in memoria potrebbe essere abbastanza grande e capire cosa potrebbe essere abbastanza complesso. Per evitare di dover affrontare questi problemi, un co-worker, Arpan Desai, è arrivato con una proposta per un subset di XPath adatto per l'elaborazione forward-only di XML. Questo subset di XPath è descritto nel suo documento Introduzione alla XPath sequenziale.
Esistono diverse modifiche alla grammatica XPath standard in Sequenziale XPath, ma la modifica più grande è la restrizione nell'utilizzo degli assi. Ora, alcuni assi sono validi nel predicato, mentre altri assi sono validi solo nella parte non predicata dell'espressione XPath sequenziale. Sono stati classificati gli assi in tre gruppi diversi:
- Assi comuni: fornire informazioni sul contesto del nodo corrente. Possono essere applicati ovunque nell'espressione Sequenziale XPath.
- Assi di inoltro: fornire informazioni sui nodi in anticipo sul nodo di contesto nel flusso. Possono essere applicati solo nel contesto del percorso del percorso perché cercano nodi "futuri". Un esempio è "figlio". È possibile selezionare correttamente i nodi figlio di un determinato percorso se "figlio" si trova nel percorso. Tuttavia, se "figlio" si trovavano nel predicato, non saremmo in grado di selezionare il nodo corrente perché non è possibile guardare avanti ai relativi figli per testare l'espressione predicato e quindi riavvolgere il lettore per selezionare il nodo.
- Asse inverso: sono essenzialmente l'opposto degli assi avanti. Un esempio sarebbe "padre". Se l'elemento padre si trova nel percorso del percorso, si vuole restituire l'elemento padre di un nodo specifico. Ancora una volta, perché non è possibile tornare indietro, non è possibile supportare questi assi nel percorso della posizione o nei predicati.
Ecco una tabella che mostra gli assi XPath supportati da XPathReader:
Tipo | Assi | Dove supportato |
---|---|---|
Assi comuni | attributo, spazio dei nomi, self | Ovunque nell'espressione XPath |
Assi avanti | figlio, discendente, discendente-o-self, seguente, fratello seguente | Ovunque nell'espressione XPath, ad eccezione dei predicati |
Asse inverso | predecessore, predecessore o self, padre, precedente, fratello precedente | Non supportato |
Esistono alcune funzioni XPath non supportate da XPathReader a causa del fatto che richiedono anche la memorizzazione nella cache di grandi parti del documento XML in memoria o la possibilità di eseguire il backtracking del parser XML. Le funzioni come count() e sum() non sono supportate, mentre le funzioni, ad esempio local-name() e namespace-uri() funzionano solo quando non vengono specificati argomenti (ovvero, solo quando si chiede queste proprietà nel nodo di contesto). Nella tabella seguente sono elencate le funzioni XPath che non sono supportate o hanno avuto alcune delle relative funzionalità limitate in XPathReader.
Funzione XPath | Subset supportato | Descrizione |
---|---|---|
numero last() | Non supportato | Non è possibile funzionare senza buffering |
numero(node-set) | Non supportato | Non è possibile funzionare senza buffering |
string local-name(node-set?) | string local-name() | Impossibile usare un set di nodi come parametro |
string namespace-uri(node-set?) | string namespace-uri() | Impossibile usare un set di nodi come parametro |
nome stringa(node-set?) | nome stringa() | Impossibile usare un set di nodi come parametro |
number sum(node-set) | Non supportato | Non è possibile funzionare senza buffering |
La restrizione principale finale apportata a XPath in XPathReader consiste nel impedire il test per i valori di elementi o nodi di testo. XPathReader non supporta l'espressione XPath seguente:
/books/book[contains(.,'Frederick Brooks')]
La query precedente seleziona l'elemento libro se la stringa contiene il testo "Frederick Brooks". Per supportare tali query, le grandi parti del documento potrebbero essere memorizzate nella cache e XPathReader dovrebbe essere in grado di riavvolgere lo stato. È tuttavia supportato il test dei valori di attributi, commenti o istruzioni di elaborazione. L'espressione XPath seguente è supportata da XPathReader:
/books/book[contains(@publisher,'WROX')]
Il subset di XPath descritto in precedenza è sufficientemente ridotto in modo da consentire a uno di fornire un parser XML basato su XPath in streaming analogo alle espressioni regolari corrispondenti ai flussi di testo.
Una prima occhiata al XPathReader
XPathReader è una sottoclasse di XmlReader che supporta il subset di XPath descritto nella sezione precedente. XPathReader può essere usato per elaborare i file caricati da un URL o in altre istanze di XmlReader. Nella tabella seguente vengono illustrati i metodi aggiunti a XmlReader da XPathReader.
Metodo | Descrizione |
---|---|
Match(XPathExpression) | Verifica se il nodo in cui il lettore è attualmente posizionato è corrispondente a XPathExpression. |
Match(string) | Verifica se il nodo in cui il lettore è attualmente posizionato è corrispondente alla stringa XPath. |
Match(int) | Verifica se il nodo in cui il lettore è attualmente posizionato è corrispondente all'espressione XPath in corrispondenza dell'indice specificato in XPathCollection del lettore. |
MatchesAny(ArrayList) | Verifica se il nodo in cui il lettore è attualmente posizionato su corrisponde a uno qualsiasi degli XPathExpressions nell'elenco. |
ReadUntilMatch() | Continua la lettura del flusso XML fino a quando il nodo corrente corrisponde a una delle espressioni XPath specificate. |
L'esempio seguente usa XPathReader per stampare il titolo di ogni libro nella mia raccolta:
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 vantaggio ovvio di XPathReader rispetto all'elaborazione XML convenzionale con XmlTextReader è che l'applicazione non deve tenere traccia del contesto del nodo corrente durante l'elaborazione del flusso XML. Nell'esempio precedente, il codice dell'applicazione non deve preoccuparsi se l'elemento titolo il cui contenuto viene visualizzato e la stampa è un elemento figlio di un elemento del libro o non monitora in modo esplicito lo stato perché questa operazione è già eseguita da XPath.
L'altro pezzo del puzzle è la classe XPathCollection . XPathCollection è l'insieme di espressioni XPath rispetto a cui deve corrispondere XPathReader. Un XPathReader corrisponde solo ai nodi contenuti nell'oggetto XPathCollection . Questa corrispondenza è dinamica, ovvero le espressioni XPath possono essere aggiunte e rimosse da XPathCollection durante il processo di analisi in base alle esigenze. Ciò consente ottimizzazioni delle prestazioni in cui i test non vengono eseguiti su espressioni XPath fino a quando non sono necessari. L'oggetto XPathCollection viene usato anche per specificare associazioni di prefissi-spazio<> dei nomi utilizzate da XPathReader durante la corrispondenza dei nodi rispetto alle espressioni XPath. Il frammento di codice seguente mostra come viene eseguita questa operazione:
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);
Ricerca di libri in prestito: soluzione XPathReader
Ora che è stato esaminato XPathReader, è il momento di vedere quanto è possibile confrontare i file XML di elaborazione migliorati con XmlTextReader. L'esempio di codice seguente usa il file XML nella sezione intitolata Ricerca di libri in prestito: Soluzione XmlTextReader e deve produrre l'output seguente:
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);
}
}
}
Questo output è notevolmente semplificato dal blocco di codice originale, è quasi altrettanto efficiente per la memoria e molto simile all'elaborazione di flussi di testo con espressioni regolari. Sembra che abbiamo raggiunto l'ideale di Tim Bray per un modello di programmazione XML per l'elaborazione di flussi XML di grandi dimensioni.
Funzionamento di XPathReader
XPathReader corrisponde ai nodi XML creando una raccolta di espressioni XPath compilate in un albero della sintassi astratta (AST) e quindi passando a questo albero della sintassi durante la ricezione di nodi in ingresso dal XmlReader sottostante. Passando attraverso l'albero AST, viene generato e inserito in uno stack un albero di query. La profondità dei nodi da confrontare con la query viene calcolata e confrontata con la proprietà Depth di XmlReader quando vengono rilevati nodi nel flusso XML. Il codice per la generazione dell'AST per un'espressione XPath viene ottenuto dal codice sottostante per le classi nel System.Xml. Xpath, disponibile come parte del codice sorgente nella versione Shared Source Common Language Infrastructure 1.0.
Ogni nodo nell'AST implementa l'interfaccia IQuery che definisce i tre metodi seguenti:
internal virtual object GetValue(XPathReader reader);
internal virtual bool MatchNode(XPathReader reader);
internal abstract XPathResultType ReturnType()
Il metodo GetValue restituisce il valore del nodo di input rispetto all'aspetto corrente dell'espressione di query. Il metodo MatchNode verifica se il nodo di input corrisponde al contesto di query analizzato, mentre la proprietà ReturnType specifica quale tipo XPath valuta l'espressione di query.
Piani futuri per XPathReader
In base all'utilità di vari utenti di Microsoft, XPathReader, tra cui la BizTalk Server fornita con una variante di questa implementazione, ho deciso di creare un'area di lavoro GotDotNet per il progetto. Ci sono alcune funzionalità che vorrei aggiungere, ad esempio l'integrazione di alcune delle funzioni del progetto EXSLT.NET in XPathReader e il supporto per una gamma più ampia di XPath. Gli sviluppatori che vogliono lavorare per un ulteriore sviluppo di XPathReader possono partecipare all'area di lavoro GotDotNet.
Conclusione
XPathReader offre un potente modo per elaborare i flussi XML sfruttando la potenza di XPath e combinandola con la flessibilità del modello di parser XML basato sul pull di XmlReader. La progettazione composizione di System.Xml consente di eseguire il layer XPathReader su altre implementazioni di XmlReader e viceversa. L'uso di XPathReader per l'elaborazione di flussi XML è quasi veloce rispetto all'uso di XmlTextReader, ma allo stesso tempo è utilizzabile come XPath con XmlDocument. Veramente è il meglio di entrambi i mondi.
Dare Obasanjo è membro del team WebData di Microsoft, che tra le altre cose sviluppa i componenti all'interno del System.Xml e dello spazio dei nomi System.Data di .NET Framework, Microsoft XML Core Services (MSXML) e Microsoft Data Access Components (MDAC).
Howard Hao è un Software Design Engineer in Test nel team WebData XML ed è lo sviluppatore principale di XPathReader.
È possibile pubblicare eventuali domande o commenti su questo articolo nella bacheca dei messaggi Extreme XML su GotDotNet.