1. Información general
En este artículo vamos a repasar los conceptos básicos de XPath con el soporte del estándar Java JDK .
Vamos a utilizar un documento XML simple, procesarlo y ver cómo repasar el documento para extraer la información que necesitamos de él.
XPath es una sintaxis estándar recomendada por el W3C, es un conjunto de expresiones para navegar por documentos XML. Puede encontrar una referencia completa de XPath aquí.
2. Un analizador XPath simple
import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; public class DefaultParser { private File file; public DefaultParser(File file) { this.file = file; } }
Ahora echemos un vistazo más de cerca a los elementos que encontrará en DefaultParser :
FileInputStream fileIS = new FileInputStream(this.getFile()); DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = builderFactory.newDocumentBuilder(); Document xmlDocument = builder.parse(fileIS); XPath xPath = XPathFactory.newInstance().newXPath(); String expression = "/Tutorials/Tutorial"; nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
Analicemos eso:
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
Usaremos este objeto para producir un árbol de objetos DOM a partir de nuestro documento xml:
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Al tener una instancia de esta clase, podemos analizar documentos XML de muchas fuentes de entrada diferentes como InputStream , File , URL y SAX :
Document xmlDocument = builder.parse(fileIS);
Un documento ( org.w3c.dom.Document ) representa el documento XML completo, es la raíz del árbol del documento, proporciona nuestro primer acceso a los datos:
XPath xPath = XPathFactory.newInstance().newXPath();
Desde el objeto XPath accederemos a las expresiones y las ejecutaremos sobre nuestro documento para extraer lo que necesitemos de él:
xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
Podemos compilar una expresión XPath pasado como cadena y definir qué tipo de datos que estamos esperando para recibir un tal nodeset , NODO o cuerda , por ejemplo.
3. Empecemos
Ahora que echamos un vistazo a los componentes base que usaremos, comencemos con algo de código usando un XML simple, para propósitos de prueba:
Guava Introduction to Guava 04/04/2016 GuavaAuthor XML Introduction to XPath 04/05/2016 XMLAuthor
3.1. Recuperar una lista básica de elementos
El primer método es un uso simple de una expresión XPath para recuperar una lista de nodos del XML:
FileInputStream fileIS = new FileInputStream(this.getFile()); DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = builderFactory.newDocumentBuilder(); Document xmlDocument = builder.parse(fileIS); XPath xPath = XPathFactory.newInstance().newXPath(); String expression = "/Tutorials/Tutorial"; nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
Podemos recuperar la lista de tutoriales contenida en el nodo raíz usando la expresión anterior, o usando la expresión " // Tutorial " pero esta recuperará todas nodos en el documento desde el nodo actual sin importar dónde estén ubicados en el documento, esto significa en cualquier nivel del árbol comenzando desde el nodo actual.
La NodeList que devuelve especificando NODESET a la instrucción de compilación como tipo de retorno, es una colección ordenada de nodos a la que se puede acceder pasando un índice como parámetro.
3.2. Recuperar un nodo específico por su ID
Podemos buscar un elemento basado en cualquier ID dado simplemente filtrando:
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = builderFactory.newDocumentBuilder(); Document xmlDocument = builder.parse(this.getFile()); XPath xPath = XPathFactory.newInstance().newXPath(); String expression = "/Tutorials/Tutorial[@tutId=" + "'" + id + "'" + "]"; node = (Node) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODE);
Al usar este tipo de expresiones, podemos filtrar por cualquier elemento que necesitemos buscar simplemente usando la sintaxis correcta. Este tipo de expresiones se denominan predicados y son una forma sencilla de localizar datos específicos en un documento, por ejemplo:
/ Tutoriales / Tutorial [1]
/ Tutoriales / Tutorial [first ()]
/ Tutoriales / Tutorial [posición () <4]
Puede encontrar una referencia completa de predicados aquí
3.3. Recuperación de nodos por un nombre de etiqueta específico
Ahora vamos más allá al introducir ejes, veamos cómo funciona usándolo en una expresión XPath:
Document xmlDocument = builder.parse(this.getFile()); this.clean(xmlDocument); XPath xPath = XPathFactory.newInstance().newXPath(); String expression = "//Tutorial[descendant::title[text()=" + "'" + name + "'" + "]]"; nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
Con la expresión usada arriba, buscamos cada elemento que tiene un descendiente con el texto pasado como parámetro en la variable "nombre".
Siguiendo el xml de muestra proporcionado para este artículo, podríamos buscar un que contiene el texto "Guayaba" o "XML" y recuperaremos el elemento con todos sus datos.
Axes proporciona una forma muy flexible de navegar por un documento XML y puede encontrar una documentación completa en el sitio oficial.
3.4. Manipular datos en expresiones
XPath también nos permite manipular datos en las expresiones si es necesario.
XPath xPath = XPathFactory.newInstance().newXPath(); String expression = "//Tutorial[number(translate(date, '/', '')) > " + date + "]"; nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
En esta expresión le estamos pasando a nuestro método una cadena simple como una fecha que se parece a "ddmmyyyy" pero el XML almacena estos datos con el formato " dd / mm / aaaa ", por lo que para que coincida con un resultado, manipulamos la cadena para convertirlo al formato de datos correcto utilizado por nuestro documento y lo hacemos utilizando una de las funciones proporcionadas por XPath
3.5. Recuperar elementos de un documento con espacio de nombres definido
If our xml document has a namespace defined as it is in the example_namespace.xml used here, the rules to retrieve the data we need are going to change since our xml starts like this:
Now when we use an expression similar to “//Tutorial”, we are not going to get any result. That XPath expression is going to return all elements that aren't under any namespace, and in our new example_namespace.xml, all elements are defined in the namespace /full_archive.
Lets see how to handle namespaces.
First of all we need to set the namespace context so XPath will be able to know where are we looking for our data:
xPath.setNamespaceContext(new NamespaceContext() { @Override public Iterator getPrefixes(String arg0) { return null; } @Override public String getPrefix(String arg0) { return null; } @Override public String getNamespaceURI(String arg0) { if ("bdn".equals(arg0)) { return "/full_archive"; } return null; } });
In the method above, we are defining “bdn” as the name for our namespace “/full_archive“, and from now on, we need to add “bdn” to the XPath expressions used to locate elements:
String expression = "/bdn:Tutorials/bdn:Tutorial"; nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
Using the expression above we are able to retrieve all elements under “bdn” namespace.
3.6. Avoiding Empty Text Nodes Troubles
As you could notice, in the code at the 3.3 section of this article a new function is called just right after parsing our XML to a Document object, this .clean( xmlDocument );
Sometimes when we iterate through elements, childnodes and so on, if our document has empty text nodes we can find an unexpected behavior in the results we want to get.
We called node .getFirstChild() when we are iterating over all elements looking for the information, but instead of what we are looking for we just have “#Text” as an empty node.
To fix the problem we can navigate through our document and remove those empty nodes, like this:
NodeList childs = node.getChildNodes(); for (int n = childs.getLength() - 1; n >= 0; n--) { Node child = childs.item(n); short nodeType = child.getNodeType(); if (nodeType == Node.ELEMENT_NODE) { clean(child); } else if (nodeType == Node.TEXT_NODE) { String trimmedNodeVal = child.getNodeValue().trim(); if (trimmedNodeVal.length() == 0){ node.removeChild(child); } else { child.setNodeValue(trimmedNodeVal); } } else if (nodeType == Node.COMMENT_NODE) { node.removeChild(child); } }
By doing this we can check each type of node we find and remove those ones we don't need.
4. Conclusions
Here we just introduced the default XPath provided support, but there are many popular libraries as JDOM, Saxon, XQuery, JAXP, Jaxen or even Jackson now. There are libraries for specific HTML parsing too like JSoup.
It's not limited to java, XPath expressions can be used by XSLT language to navigate XML documents.
As you can see, there is a wide range of possibilities on how to handle these kind of files.
Existe un gran soporte estándar por defecto para el análisis, lectura y procesamiento de documentos XML / HTML. Puede encontrar la muestra de trabajo completa aquí.