2 Rutas y ejes

En el capítulo anterior vimos como leer un documento XML completo. Sin embargo, este modo de procesar documentos XML puede que no siempre sea el más eficaz en cada caso.

Por ejemplo, podemos querer leer sólo el valor de un elemento, o alguno de sus atributos, pero sin necesidad de procesar el árbol secuencialmente hasta encontrarlo.

Para eso se pueden usar las rutas (paths).

Las rutas funcionan de forma similar a los nombres de fichero en un sistema de archivos con carpetas, o a las direcciones URL de internet. Podemos formar la ruta a un elemento concreto mediante los nombres de sus elementos padre, separados con el carácter '/'.

Por ejemplo, en nuestro documento anterior, la ruta "/cursos/curso[1]/capitulo[2]/nombre" se refiere a elemento nombre del segundo capítulo del primer curso, es decir "Variables I".

Las rutas nos proporcionan una forma sencilla y a la vez muy potente de recuperar valores de un documento XML.

Definiciones

Las especificaciones de XML están definidas por W3C. Estas especificaciones definen algunos conceptos que conviene conocer para entender cualquier documentación sobre XML.

Conceptos

Nodos

Desde el punto de vista de las rutas (XPath), podemos clasificar los nodos en varios tipos:

  • Raíz: el elemento superior del árbol que forma el documento XML.
  • Comentario: nodos con comentarios adicionales que no forman parte de los datos, y se suelen incluir como información complementaria para las personas que lean o procesen documentos XML.
  • Nodos de elemento: cada uno de los nodos formados por las etiquetas de inicio y final y su contenido.
  • Atributos: nodos correspondientes a los valores definidos en el interior de la etiqueta de inicio.
  • Texto: nodos que únicamente contienen texto. Corresponden a los contenidos entre dos etiquetas cuando el nodo no contiene otros nodos y no se trate de un nodo vacío.
  • Espacio con nombre: nodos de declaración de espacios con nombre.
  • Instrucción de procesamiento: nodos que contienen instrucciones para la aplicación que debe representar los datos contenidos en el documento XML. (Por ejemplo, las hojas de estilo CSS para documentos XHTML).
Valores atómicos
Se refiere a nodos que no contienen hijos.
Items
Término general para referirse a nodos o valores atómicos.

Relaciones

Padre (parent)
Dado un nodo, padre es el nodo elemento en el que está contenido. Todos los nodos tienen un nodo padre, excepto el nodo raíz.
Hijos (children)
Cada uno de los nodos contenidos en un nodo elemento. Cualquier elemento puede tener cero, uno o más nodos hijos.
Hermanos (siblings)
Nodos con el mismo padre.
Antepasados (ancestors)
Cualquiera de los nodos en los que está contenido un nodo: el padre, el padre del padre, etc.
Descendientes (descendants)
Cualquiera de los nodos contenidos en un nodo elemento: sus hijos, los hijos de sus hijos, etc.

Expresiones de ruta

Las rutas XML tienen muchas similitudes con las rutas de un sistema de archivos, aunque tienen algunas diferencias importantes.

Una expresión de ruta se compone de diferentes elementos:

  • Una cadena de texto identifica un nodo por su nombre, esto es, su etiqueta.
  • Un asterisco '*' identifica al nodo actual y a todos los sus nodos descendientes.
  • Mediante 'text()' selecciona el texto asociado a una etiqueta.
  • Los corchetes'[]' sirven para especificar predicados, que se usan para seleccionar ciertos nodos hermanos que cumplan determinada condición.
  • Una barra '/' se usa de separador entre identificadores de nodo. Si se encuentra al principio de la expresión, indica que la ruta comienza en el nodo raíz, es decir, indica una ruta absoluta.
  • Una doble barra '//' sirve para seleccionar nodos en cualquier parte del documento del nodo actual, independientemente de dónde se encuentren.
  • El punto '.' selecciona el nodo actual, igual que en el sistema de archivos se refiere al directorio actual.
  • El doble punto '..' selecciona el nodo padre del nodo actual.
  • El carácter '@' se usa para seleccionar atributos.
  • El carácter '|' permite combinar varias rutas.

En nuestro ejemplo, las rutas "/cursos/curso/capitulo/nombre", "//nombre", //capitulo/nombre" o "/cursos//nombre" son equivalentes.

La ruta "*" selecciona todos los nodos de un documento. Por ejemplo, "/cursos/curso/*" selecciona todos los nodos curso y sus descendientes.

En general, las rutas seleccionan nodos elemento y sus textos asociados, mediante "text()" podemos seleccionar solo el texto asociado. Por ejemplo "/cursos/curso/capitulo/nombre/text()" selecciona los nodos de texto asociados a los nombres de capítulo de todos los cursos.

A diferencia de un sistema de archivos, en un documento XML pueden existir varios nodos hermanos con el mismo nombre. Para poder referirse a uno de los nodos hermanos en concreto podemos usar los corchetes y el número de orden del nodo, empezando en 1, o (como veremos más abajo) una expresión, denominada predicado. Por ejemplo, "/cursos/curso[2]/capitulo/nombre" nos devolverá los nodos con los nombres de los capítulos del segundo curso.

Para seleccionar nodos de tipo atributo usaremos el carácter '@', por ejemplo, "//@lang" seleccionará todos los nodos de tipo atributo con el nombre 'lang'.

Para combinar varias rutas usaremos el carácter '|', por ejemplo "//@lang|//nombre" seleccionará todos los nodos de tipo atributo con el nombre 'lang' y todos los nodos 'nombre'.

Contexto

En XML se entiende por contexto el nodo actualmente seleccionado. Las rutas siempre se refieren a un contexto determinado, que es el nodo a partir del cual se seguirá esa ruta.

Si en nuestro ejemplo seleccionamos como nodo de partida el correspondiente al capítulo 1 del curso de C++, podremos acceder al título del curso mediante la ruta "../titulo" o a sus atributos mediante la ruta "@*".

En la librería libxml2 primero crearemos un contexto mediante la función xmlXPathNewContext, indicando como parámetro un puntero al documento, que nos devuelve un puntero a una estructura xmlXPathContext que contiene un contexto para el nodo raíz del documento.

Para seleccionar un contexto determinado usaremos la función xmlXPathSetContextNode, indicando en el primer parámetro el nodo en el que queremos definir el contexto, mediante un puntero a una estructura xmlNode, y como segundo parámetro un puntero a una estructura xmlXPathContext, que recibirá el valor del nuevo contexto.

Cuando un contexto ya no sea necesario es responsabilidad de la aplicación liberar su memoria usando la función xmlXPathFreeContext, pasando como parámetro el puntero al contexto a liberar.

Por ejemplo:

    doc = xmlReadFile("ejemplo.xml", NULL, XML_PARSE_NOBLANKS);
    xpathCtx = xmlXPathNewContext(doc);
...
    // Contexto para el nodo de comentario:
    xmlXPathSetContextNode(doc->children->children, xpathCtx);
...
    xmlXPathFreeContext(xpathCtx);
    xmlFreeDoc(doc);
    xmlCleanupParser();

Predicados

Los predicados permiten usar expresiones para seleccionar grupos de nodos que cumplan determinadas condiciones.

Podemos restringir los valores para un nodo hijo. Por ejemplo, la ruta "//capitulo[numero='0']/nombre", seleccionará los nodos hijo 'nombre' para todos los capítulos cuyo nodo hijo 'numero' tenga el valor '0'.

Disponemos de varias funciones básicas que se pueden usar para construir las expresiones de los predicados.

Funciones de conjuntos de nodos

La forma más simple de predicado es el número de orden, empezando en 1, pero existen muchas otras formas. Disponemos de varias funciones numéricas, como son:

numero last()
Devuelve un número igual al tamaño del contexto en el que se evalúa la expresión. Es decir, se aplica al predicado de un elemento, devolverá el número de hijos con ese valor de elemento.
numero position()
Devuelve un número igual a la posición del contexto en el que se evalúa la expresión.
numero count(nodeset)
Devuelve el número de elementos en el conjunto de nodos pasado como parámetro.

Por ejemplo, la ruta "//capitulo[last()]/nombre" seleccionará los nodos 'nombre' del último capítulo de cada curso.

Dado que last() devuelve un número, se puede aplicar aritmética, por ejemplo, para obtener el nombre del penúltimo capítulo del curso de "MySQL en C++", podemos usar la ruta "/cursos/curso[2]/capitulo[last()-1]/nombre".

La función position() nos permite seleccionar nodos que ocupen determinadas posiciones. Por ejemplo, para seleccionar el nombre del segundo capítulo del "Curso de C++", podemos usar la ruta "/cursos/curso[1]/capitulo[position()=2]/nombre". También se puede usar cualquier otra comparación, para obtener los dos primeros, "position()<=2", o los nodos a partir del cuarto ""position()>4".

La función count() nos permite seleccionar nodos con un determinado número de nodos hijo. Por ejemplo, la ruta "/cursos/curso[count(capitulo)=2]//nombre" seleccionará los nodos 'nombre' de los cursos con dos capítulos. O la ruta "/cursos/curso[count(*)>2]//nombre", seleccionará los nodos 'nombre' con los cursos con más de dos nodos hijos.

Funciones de cadenas

Funciones que devuelven una cadena:

cadena string(objeto)
Es una función de conversión de tipo, convierte un objeto a cadena.
cadena contat(cadena, cadena, cadena...)
Devuelve la cadena resultante de concatenar los argumentos en el mismo orden en que se indican. Se pueden incluir tantos argumentos como se desee.
booleano starts-with(cadena, cadena)
Devuelve el valor true si la cadena del primer argumento empieza con la cadena del segundo.
booleano contains(cadena, cadena)
Devuelve el valor true si la cadena del primer argumento contiene a la cadena del segundo.
cadena substring-before(cadena, cadena)
Devuelve la subcadena del primer argumento que precede a la primera aparición del segundo argumento. Si la primera cadena no contiene a la segunda, se devuelve una cadena vacía.
cadena substring-after(cadena, cadena)
Devuelve la subcadena del primer argumento que sigue a la primera aparición del segundo argumento. Si la primera cadena no contiene a la segunda, se devuelve una cadena vacía.
cadena substring(cadena, numero, numero)
Devuelve la subcadena del primer argumento que empieza en la posición del segundo y con el número de caracteres especificado en el tercero. El tercer parámetro es opcional, si no se especifica se devuelve la subcadena desde la posición del segundo parámetro hasta el final de la cadena. Los valores de posición empiezan en 1.
numero string-length(cadena)
Devuelve el número de caracteres en la cadena pasada como argumento. El argumento es opcional, si se omite se usará la conversión a cadena del nodo actual.
cadena translate(cadena, cadena, cadena)
Devuelve la cadena del primer argumento sustituyendo los caracteres del segundo argumento por los caracteres en su posición del tercer argumento.

La función string() convierte el parámetro a cadena según estas reglas:

  • Si el objeto es un conjunto de nodos, devolverá el valor de la cadena del nodo que esté más próximo al nodo raíz. Si el conjunto está vacío devolverá una cadena vacía.
  • Si el objeto es un número hay varios posibles casos:
    • NaN se convierte en la cadena 'NaN'.
    • +0 se convierte a '0'.
    • -0 se convierte a '0'.
    • +infinito se convierte a 'Infinite'.
    • -infinito se convierte a '-Infinite'.
    • Los números enteros se representan en formato decimal, sin el punto ni ceros a la izquierda, y con el signo menos si son negativos.
    • El resto de números se representan en formato decimal, añadiendo el punto decimal y el signo menos si es necesario.
  • Un valor booleano false se devuelve como 'false' y un valor booleano true como 'true'.
  • Para el resto de objetos, la conversión depende del tipo.

Para la función substring, y en general, en XML las posiciones de los caracteres en las cadenas se empiezan a contar desde 1. substring("cadena", 3, 3) devolverá la cadena "den".

Si en la función translate, las cadenas del segundo y tercer parámetro tienen la misma longitud, la sustitución no afecta a la longitud de la cadena indicada como primer parámetro. Por ejemplo translate("cadena", "aeiou", "AEIOU") devolverá la cadena "cAdEnA". Sin embargo, si la tercera cadena es más corta que la segunda, los caracteres de la segunda sin correspondencia en la tercera serán suprimidos. Por ejemplo translate("cadena", "aeioud", "AEIOU") devolverá "cAEnA".

Funciones booleanas

booleano boolean(objeto)
Es una función de conversión de tipo, convierte un objeto a booleano.
booleano not(booleano)
Devuelve true si el argumento es false, y viceversa.
booleano true()
Devuelve true.
booleano false()
Devuelve false.
booleano lang(cadena)
Devuelve true o false dependiendo de si el idioma del nodo de contexto especificado por los atributos xml:lang es el mismo o es un sublenguaje del idioma especificado por la cadena del argumento.

La conversión de tipo a booleano se rige por las siguientes reglas:

  • Un número es verdadero si y solo si no es cero ni NaN.
  • Un conjunto de nodos es verdadero si y solo si no es un conjunto vacío.
  • Una cadena es verdadera si y sólo si su longitud es distinta de cero.
  • Cualquier otro tipo de objeto se convierte a booleano dependiendo de su tipo.

Funciones numéricas

numero number(objeto)
Es una función de conversión de tipo, convierte un objeto a número. El argumento es opcional, si se omite se toma el conjunto de nodos que contiene solo el nodo del conexto.
numero sum(nodeset)
Devuelve la suma del resultado de convertir cada nodo del conjunto a número.
numero floor(numero)
Devuelve el número entero más grande que no es mayor que el argumento.
numero ceiling(numero)
Devuelve el número entero más pequeño que no es menor que el argumento.
numero round(numero)
Devuelve el número entero más cercano al argumento.

La conversión a número sigue las siguientes reglas:

  • Cualquier cadena que consista en espacios seguidos por un signo (opcional), por un número y seguido por espacios se convierte a su valor numérico. Cualquier otra cadena se convierte a NaN.
  • El valor booleano true se convierte a 1 y el false a 0.
  • Uno conjunto de nodos se convierte primero a cadena y a continuación esa cadena a número.
  • Cualquier objeto de otro tipo se convierte a número dependiendo de su tipo.

Operadores

Se pueden usar operadores para crear predicados más elaborados. Por ejemplo, la ruta "//capitulo[contains(nombre, ' ') or contains(nombre, '/')]/nombre" seleccionará los nodos hijo 'nombre' para los capítulos en los que el nombre contenga un espacio o un '/'.

Disponemos de varios operadores para crear estas condiciones:

  • Operadores lógicos, que se pueden aplicar a expresiones booleanas: and, or, not().
  • Operadores aritméticos, aplicables a expresiones numéricas: +, -, *, div, mod.
  • Operadores de comparación, que se pueden aplicar a expresiones numéricas o a textos: =, !=, <, >, <=, >=.

También podemos seleccionar nodos en función de los valores de atributos. La ruta "//capitulo[@orden='1']/nombre", seleccionará los nodos hijo 'nombre' para los capítulos en los que su atributo 'orden' tenga el valor '1'.

Ejemplo 2

En el programa de ejemplo 2 se muestran algunos ejemplos de rutas.

Hemos creado una función para evaluar una ruta y mostrar el conjunto de nodos que selecciona:

void Evalua(const xmlChar *ruta, xmlXPathContextPtr xpathCtx) {
    xmlXPathObjectPtr xpathObj;

    xpathObj = xmlXPathEvalExpression(ruta, xpathCtx);
    std::cout << "Path: " << ruta << std::endl;
    MostrarNodos(xpathObj->nodesetval);
    xmlXPathFreeObject(xpathObj);
}

La función xmlXPathEvalExpression sirve para evaluar una expresión de ruta, que pasaremos como una cadena en el primer argumento, y como segundo argumento pasaremos un puntero al contexto al que aplicar la ruta.

El resultado es un objeto XPath, que recibiremos en un puntero a una estructura xmlXPathObject. Esta estructura contiene, entre otras cosas, un puntero a un conjunto de nodos xmlNodeSet, el el miembro nodesetval.

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 2 xml002.zip 2024-01-23 2336 bytes 26

Ejes

Un eje representa una relación con el nodo de contexto, por lo tanto, se utiliza para localizar nodos relativos a ese nodo dentro de un árbol.

XPath proporciona varios tipos de ejes:

  • El eje 'child' contiene los hijos del nodo de contexto.
  • El eje 'descendant' contiene los nodos descendientes del nodo de contexto, esto incluye los hijos de ese nodo, los hijos de esos hijos, etc. Pero no incluye nodos de atributos o espacios con nombre.
  • El eje 'parent' contiene el padre del nodo de contexto, si existe.
  • El eje 'ancestor' contiene los antepasados del nodo de contexto, es decir, su nodo padre, el padre de su padre, etc. Siempre incluirá el nodo raíz, salvo que el nodo de contexto sea el nodo raíz.
  • El eje 'following-sibling' contiene los siguientes hermanos del nodo de contexto; si el nodo de contexto es uno de atributo o de espacio con nombre, se tratará de un eje vacío.
  • El eje "preceding-sibling" contiene los hermanos anteriores al nodo de contexto; si el nodo de contexto es uno de atributo o de espacio con nombre, se tratará de un eje vacío.
  • El eje "following" contiene todos los nodos en el mismo documento que el nodo de contexto que están después del nodo de contexto dentro del documento, excluyendo cualquier descendiente, atributo o nodos de espacio con nombre.
  • El eje "preceding" contiene todos los nodos en el mismo documento que el nodo de contexto que están antes del nodo de contexto dentro del documento, excluyendo cualquier ascendiente, atributo o nodos de espacio con nombre.
  • El eje "attribute" contiene los atributos del nodo de contexto. El eje estará vacío salvo que el nodo de contexto sea un elemento.
  • El eje "namespace" contiene los nodos de espacios con nombre del nodo de contexto. El eje estará vacío salvo que el nodo de contexto sea un elemento.
  • El eje "self" contiene solo el propio nodo de contexto.
  • El eje "descendant-or-self" contiene el nodo de contexto y sus descendientes.
  • El eje "ancestor-or-self" contiene el nodo de contexto y sus ascendientes.

Si en nuestro ejemplo colocamos el contexto en el nodo correspondiente al primer capítulo del curso de C++, podemos crear algunas rutas basadas en ejes:

"self::node()" devolverá un conjunto de nodos consistente en un único nodo, el del contexto.

"self::capitulo/nombre" devolverá el conjunto de nodos con un único nodo, el del nombre del capítulo.

"child::*" devolverá el conjunto de nodos correspondiente a los nodos hijos del nodo d contexto.

"parent::node()/titulo" obtiene el conjunto de nodos correspondiente al nodo "titulo" del nodo curso padre del nodo de contexto.

Ejemplo 3

En el programa de ejemplo 3 se muestran algunos ejemplos de rutas basadas en ejes, usando las mismas funciones auxiliares que en el ejemplo 2.

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 3 xml003.zip 2024-01-23 2140 bytes 25