Lo mejor de ambos mundos: combinar XPath con XmlReader
Atrévese a Obasanjo y Howard Hao
Microsoft Corporation
5 de mayo de 2004
Descargue el archivo de ejemplo XPathReader.exe.
Resumen: Dare Obasanjo describe el XPathReader, que proporciona la capacidad de filtrar y procesar documentos XML grandes de forma eficaz mediante xmlReader compatible con XPath. Con XPathReader, uno puede procesar secuencialmente un documento grande y extraer un subárbol identificado coincidente con una expresión XPath.
(11 páginas impresas)
Introducción
Hace aproximadamente un año, leí un artículo de Tim Bray titulado XML Is Too Hard For Programmers, en el que se quejó de la naturaleza complicada de las API del modelo de inserción, como SAX, para tratar grandes secuencias de XML. Tim Bray describe un modelo de programación ideal para XML como uno similar a trabajar con texto en Perl, donde uno puede procesar secuencias de texto mediante la coincidencia de elementos de interés mediante expresiones regulares. A continuación se muestra un extracto del artículo de Tim Bray que muestra su modelo de programación idealizado para secuencias 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 no es el único que añoró este modelo de procesamiento XML. Durante los últimos años, varias personas con las que trabajo han estado trabajando para crear un modelo de programación para procesar secuencias de documentos XML de manera análoga al procesamiento de secuencias de texto con expresiones regulares. En este artículo se describe la culminación de este trabajo: XPathReader.
Buscar libros prestados: Solución XmlTextReader
Para indicar claramente las ganancias de productividad de XPathReader en comparación con las técnicas de procesamiento XML existentes con XmlReader, he creado un programa de ejemplo que realiza tareas básicas de procesamiento XML. En el siguiente documento de ejemplo se describen varios libros que posee y si se les presta actualmente a amigos.
<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>
En el ejemplo de código siguiente se muestran los nombres de las personas a las que he prestado libros, así como los libros que he prestado a ellos. Los ejemplos de código deben generar la siguiente salida.
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 de XPath como expresiones regulares para XML
Lo primero que necesitamos es una manera de realizar la coincidencia de patrones para los nodos de interés en una secuencia XML de la misma manera que podemos con expresiones regulares para las cadenas de una secuencia de texto. XML ya tiene un lenguaje para los nodos coincidentes denominados XPath, que pueden servir como un buen punto de partida. Hay un problema con XPath que impide que se use sin modificaciones como mecanismo para buscar coincidencias de nodos en documentos XML de gran tamaño de forma de streaming. XPath supone que todo el documento XML se almacena en memoria y permite operaciones que requerirían varios pasos sobre el documento, o al menos requeriría que se almacenen grandes partes del documento XML en memoria. La siguiente expresión XPath es un ejemplo de esta consulta:
/books/book[author='Frederick Brooks']/@publisher
La consulta devuelve el atributo publisher de un elemento book si tiene un elemento de autor secundario cuyo valor es "Frederick Brooks". Esta consulta no se puede ejecutar sin almacenar en caché más datos de lo habitual para un analizador de streaming porque el atributo publisher debe almacenarse en caché cuando se ve en el elemento book hasta que se ha visto el elemento de autor secundario y su valor examinado. Según el tamaño del documento y la consulta, la cantidad de datos que se deben almacenar en caché en la memoria podría ser bastante grande y averiguar qué almacenar en caché podría ser bastante complejo. Para evitar tener que lidiar con estos problemas, un compañero de trabajo, Arpan Desai, se presentó con una propuesta para un subconjunto de XPath que es adecuado para el procesamiento de solo avance de XML. Este subconjunto de XPath se describe en su artículo Introducción a XPath secuencial.
Hay varios cambios en la gramática XPath estándar en XPath secuencial, pero el mayor cambio es la restricción en el uso de ejes. Ahora, ciertos ejes son válidos en el predicado, mientras que otros ejes solo son válidos en la parte no predicada de la expresión XPath secuencial. Hemos clasificado los ejes en tres grupos diferentes:
- Ejes comunes: proporcione información sobre el contexto del nodo actual. Se pueden aplicar en cualquier parte de la expresión XPath secuencial.
- Ejes de reenvío: proporcione información sobre los nodos delante del nodo de contexto en la secuencia. Solo se pueden aplicar en el contexto de ruta de acceso de ubicación porque buscan nodos "futuros". Un ejemplo es "child". Podemos seleccionar correctamente los nodos secundarios de una ruta de acceso determinada si "child" está en la ruta de acceso. Sin embargo, si "child" estuviera en el predicado, no podremos seleccionar el nodo actual porque no podemos mirar hacia delante a sus elementos secundarios para probar la expresión de predicado y, a continuación, volver a activar el lector para seleccionar el nodo.
- Eje inverso: son esencialmente lo opuesto a los ejes hacia delante. Un ejemplo sería "primario". Si el elemento primario estuviera en la ruta de acceso de ubicación, queremos devolver el elemento primario de un nodo específico. Una vez más, dado que no podemos retroceder, no podemos admitir estos ejes en la ruta de acceso de ubicación o en predicados.
Esta es una tabla que muestra los ejes XPath admitidos por XPathReader:
Tipo | Ejes | Dónde se admite |
---|---|---|
Ejes comunes | atributo, espacio de nombres, self | Cualquier lugar de la expresión XPath |
Ejes hacia delante | child, descendant, descendant-or-self, following, following-sibling | Cualquier lugar de la expresión XPath, excepto predicados |
Eje inverso | antecesor, antecesor o propio, primario, anterior, relacionado anterior | No compatible |
Hay algunas funciones XPath no compatibles con XPathReader debido al hecho de que también requieren almacenamiento en caché de grandes partes del documento XML en memoria o la capacidad de retroceder el analizador XML. Las funciones como count() y sum() no se admiten en absoluto, mientras que las funciones como local-name() y namespace-uri() solo funcionan cuando no se especifica ningún argumento (es decir, solo cuando se solicitan estas propiedades en el nodo de contexto). En la tabla siguiente se enumeran las funciones XPath que no son compatibles o que han tenido alguna de sus funcionalidades limitadas en XPathReader.
Función XPath | Subconjunto admitido | Descripción |
---|---|---|
number last() | No compatible | No se puede trabajar sin almacenamiento en búfer |
number count(node-set) | No compatible | No se puede trabajar sin almacenamiento en búfer |
string local-name(node-set?) | string local-name() | No se puede usar un conjunto de nodos como parámetro |
string namespace-uri(node-set?) | string namespace-uri() | No se puede usar un conjunto de nodos como parámetro |
string name(node-set?) | string name() | No se puede usar un conjunto de nodos como parámetro |
number sum(node-set) | No compatible | No se puede trabajar sin almacenamiento en búfer |
La restricción principal final realizada a XPath en XPathReader es no permitir pruebas para los valores de elementos o nodos de texto. XPathReader no admite la siguiente expresión XPath:
/books/book[contains(.,'Frederick Brooks')]
La consulta anterior selecciona el elemento book si su cadena contiene el texto "Frederick Brooks". Para poder admitir estas consultas, es posible que haya que almacenar en caché grandes partes del documento y XPathReader tendría que poder rebobinar su estado. Sin embargo, se admiten las pruebas de valores de atributos, comentarios o instrucciones de procesamiento. La expresión XPath siguiente es compatible con XPathReader:
/books/book[contains(@publisher,'WROX')]
El subconjunto de XPath descrito anteriormente se reduce lo suficiente como para permitir que uno proporcione un analizador XML basado en XPath de streaming eficaz en memoria que sea análogo a las expresiones regulares que coinciden con las secuencias de texto.
Un primer vistazo a XPathReader
XPathReader es una subclase de XmlReader que admite el subconjunto de XPath descrito en la sección anterior. XPathReader se puede usar para procesar archivos cargados desde una dirección URL o se pueden colocar en capas en otras instancias de XmlReader. En la tabla siguiente se muestran los métodos agregados al objeto XmlReader por XPathReader.
Método | Descripción |
---|---|
Match(XPathExpression) | Comprueba si el nodo en el que el lector está situado actualmente coincide con XPathExpression. |
Match(string) | Comprueba si el nodo en el que el lector está situado actualmente coincide con la cadena XPath. |
Match(int) | Comprueba si el nodo en el que el lector está situado actualmente coincide con la expresión XPath en el índice especificado en XPathCollection del lector. |
MatchesAny(ArrayList) | Comprueba si el nodo en el que el lector está situado actualmente coincide con cualquiera de los XPathExpressions de la lista. |
ReadUntilMatch() | Continúa leyendo la secuencia XML hasta que el nodo actual coincida con una de las expresiones XPath especificadas. |
En el ejemplo siguiente se usa XPathReader para imprimir el título de cada libro de mi biblioteca:
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);
}
}
}
Una ventaja obvia de XPathReader sobre el procesamiento XML convencional con XmlTextReader es que la aplicación no tiene que realizar un seguimiento del contexto del nodo actual mientras se procesa la secuencia XML. En el ejemplo anterior, el código de la aplicación no tiene que preocuparse de si el elemento title cuyo contenido se muestra e impresión es un elemento secundario de un elemento de libro o no mediante el seguimiento explícito del estado porque ya lo ha hecho XPath.
La otra pieza del rompecabezas es la clase XPathCollection . XPathCollection es la colección de expresiones XPath con las que se supone que XPathReader debe coincidir. Un XPathReader solo coincide con los nodos contenidos en su objeto XPathCollection . Esta coincidencia es dinámica, lo que significa que las expresiones XPath se pueden agregar y quitar de XPathCollection durante el proceso de análisis según sea necesario. Esto permite optimizaciones de rendimiento en las que las pruebas no se realizan en expresiones XPath hasta que se necesiten. XPathCollection también se usa para especificar enlaces de espacio de nombres de prefijo<> utilizados por XPathReader al buscar coincidencias de nodos con expresiones XPath. En el fragmento de código siguiente se muestra cómo se logra esto:
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);
Buscar libros prestados: solución XPathReader
Ahora que hemos visto XPathReader, es el momento de ver la cantidad de archivos XML de procesamiento mejorado que se pueden comparar con el uso de XmlTextReader. En el ejemplo de código siguiente se usa el archivo XML de la sección titulada Finding Loaned Books: XmlTextReader Solution y se debe producir la siguiente salida:
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);
}
}
}
Esta salida se simplifica en gran medida del bloque de código original, es casi tan eficaz para la memoria y muy análoga al procesamiento de secuencias de texto con expresiones regulares. Parece que hemos llegado al ideal de Tim Bray para un modelo de programación XML para procesar flujos XML grandes.
Cómo funciona XPathReader
XPathReader coincide con los nodos XML mediante la creación de una colección de expresiones XPath compiladas en un árbol de sintaxis abstracta (AST) y, a continuación, recorre este árbol de sintaxis mientras recibe nodos entrantes del xmlReader subyacente. Al recorrer el árbol de AST, se genera un árbol de consulta y se inserta en una pila. La profundidad de los nodos que debe coincidir la consulta se calcula y se compara con la propiedad Depth de XmlReader , ya que los nodos se encuentran en la secuencia XML. El código para generar el AST para una expresión XPath se obtiene del código subyacente para las clases de la System.Xml. Xpath, que está disponible como parte del código fuente en la versión de Common Language Infrastructure 1.0 de origen compartido.
Cada nodo del AST implementa la interfaz IQuery que define los tres métodos siguientes:
internal virtual object GetValue(XPathReader reader);
internal virtual bool MatchNode(XPathReader reader);
internal abstract XPathResultType ReturnType()
El método GetValue devuelve el valor del nodo de entrada en relación con el aspecto actual de la expresión de consulta. El método MatchNode comprueba si el nodo de entrada coincide con el contexto de consulta analizado, mientras que la propiedad ReturnType especifica qué tipo XPath evalúa la expresión de consulta.
Planes futuros para XPathReader
En función de lo útil que han encontrado varias personas de Microsoft, incluido el BizTalk Server que se incluye con una variación de esta implementación, he decidido crear un área de trabajo de GotDotNet para el proyecto. Hay algunas características que me gustaría ver agregadas, como la integración de algunas de las funciones del proyecto de EXSLT.NET en XPathReader y la compatibilidad con una gama más amplia de XPath. Los desarrolladores que tendrían ganas de trabajar en el desarrollo adicional de XPathReader pueden unirse al área de trabajo GotDotNet.
Conclusión
XPathReader proporciona una forma potente de procesar flujos XML mediante el aprovechamiento de la potencia de XPath y su combinación con la flexibilidad del modelo de analizador XML basado en extracción del XmlReader. El diseño compositivo de System.Xml permite que una supere XPathReader sobre otras implementaciones de XmlReader y viceversa. El uso de XPathReader para procesar secuencias XML es casi tan rápido como usar XmlTextReader, pero al mismo tiempo es tan utilizable como XPath con XmlDocument. Realmente es lo mejor de ambos mundos.
Dare Obasanjo es miembro del equipo de WebData de Microsoft, que, entre otras cosas, desarrolla los componentes dentro del espacio de nombres System.Xml y System.Data de .NET Framework, Microsoft XML Core Services (MSXML) y Microsoft Data Access Components (MDAC).
Howard Hao es ingeniero de diseño de software en pruebas en el equipo XML de WebData y es el desarrollador principal del XPathReader.
No dude en publicar cualquier pregunta o comentario sobre este artículo en el panel de mensajes Extreme XML en GotDotNet.