«1. Обзор

В этой статье мы рассмотрим основы XPath с поддержкой стандартного Java JDK.

Мы собираемся использовать простой XML-документ, обработать его и посмотреть, как просмотреть документ, чтобы извлечь из него нужную нам информацию.

XPath — это стандартный синтаксис, рекомендованный W3C, это набор выражений для навигации по XML-документам. Полный справочник по XPath можно найти здесь.

2. Простой синтаксический анализатор XPath

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

Теперь давайте более подробно рассмотрим элементы, которые вы найдете в 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);

Давайте разберем это:

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();

Мы используйте этот объект для создания дерева объектов DOM из нашего XML-документа:

DocumentBuilder builder = builderFactory.newDocumentBuilder();

Имея экземпляр этого класса, мы можем анализировать XML-документы из множества различных источников ввода, таких как InputStream, File, URL и SAX:

Document xmlDocument = builder.parse(fileIS);

~ ~~ Документ (org.w3c.dom.Document) представляет весь XML-документ, является корнем дерева документов, обеспечивает наш первый доступ к данным:

XPath xPath = XPathFactory.newInstance().newXPath();

Из объекта XPath мы получим доступ к выражениям и выполнить их над нашим документом, чтобы извлечь из него то, что нам нужно:

xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

Мы можем скомпилировать выражение XPath, переданное в виде строки, и определить, какие данные мы ожидаем получить, например, NODESET, NODE или String.

3. Начнем

Теперь, когда мы рассмотрели базовые компоненты, которые будем использовать, давайте начнем с кода, использующего простой XML для целей тестирования:

<?xml version="1.0"?>
<Tutorials>
    <Tutorial tutId="01" type="java">
        <title>Guava</title>
  <description>Introduction to Guava</description>
  <date>04/04/2016</date>
  <author>GuavaAuthor</author>
    </Tutorial>
    <Tutorial tutId="02" type="java">
        <title>XML</title>
  <description>Introduction to XPath</description>
  <date>04/05/2016</date>
  <author>XMLAuthor</author>
    </Tutorial>
</Tutorials>

3.1. Получение базового списка элементов

Первый метод — это простое использование выражения XPath для получения списка узлов из 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);

Мы можем получить учебный список, содержащийся в корневом узле, с помощью метода выражение выше, или с помощью выражения «//Tutorial», но это будет извлекать все узлы \u003cTutorial\u003e в документе из текущего узла, независимо от того, где они расположены в документе, это означает, на любом уровне дерева начиная с текущего узла.

NodeList, который он возвращает, указав NODESET в инструкции компиляции в качестве возвращаемого типа, представляет собой упорядоченную коллекцию узлов, доступ к которым можно получить, передав индекс в качестве параметра.

3.2. Получение определенного узла по его идентификатору

Мы можем искать элемент на основе любого заданного идентификатора, просто фильтруя:

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

Используя такого рода выражения, мы можем фильтровать любой элемент, который нам нужно искать, просто используя правильный синтаксис. Такие выражения называются предикатами, и они позволяют легко найти определенные данные в документе, например:

/Tutorials/Tutorial[1]

/Tutorials/Tutorial[first()]

/ Tutorials/Tutorial[position()\u003c4]

Полный справочник предикатов можно найти здесь

3.3. Получение узлов по определенному имени тега

Теперь мы идем дальше, вводя оси, давайте посмотрим, как это работает, используя его в выражении 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);

С помощью выражения, использованного выше, мы ищем каждый Элемент \u003cTutorial\u003e, у которого есть потомок \u003ctitle\u003e с текстом, переданным в качестве параметра в переменной «name».

Следуя примеру XML, предоставленному для этой статьи, мы могли бы найти \u003ctitle\u003e, содержащий текст «Guava» или «XML», и получить весь элемент \u003cTutorial\u003e со всеми его данными.

Оси обеспечивают очень гибкий способ навигации по XML-документу, и вы можете найти полную документацию на официальном сайте.

3.4. Манипулирование данными в выражениях

XPath позволяет нам также манипулировать данными в выражениях, если это необходимо.

XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "//Tutorial[number(translate(date, '/', '')) > " + date + "]";
nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

В этом выражении мы передаем нашему методу простую строку в виде даты, которая выглядит как «ддммгггг», но XML хранит эти данные в формате «дд/мм/гггг», чтобы соответствовать В результате мы манипулируем строкой, чтобы преобразовать ее в правильный формат данных, используемый нашим документом, и мы делаем это с помощью одной из функций, предоставляемых XPath

3.5. Извлечение элементов из документа с определенным пространством имен

Если в нашем xml-документе пространство имен определено так, как оно указано в используемом здесь example_namespace.xml, правила извлечения необходимых нам данных изменятся, поскольку наш xml начинается так: ~ ~~

<?xml version="1.0"?>
<Tutorials xmlns="/full_archive">

</Tutorials>

«Теперь, когда мы используем выражение, похожее на «//Tutorial», мы не получим никакого результата. Это выражение XPath вернет все элементы \u003cTutorial\u003e, которые не находятся ни в одном пространстве имен, а в нашем новом example_namespace.xml все элементы \u003cTutorial\u003e определены в пространстве имен /full_archive.

Давайте посмотрим, как обращаться с пространствами имен.

Прежде всего нам нужно установить контекст пространства имен, чтобы XPath мог знать, где мы ищем наши данные:

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

В приведенном выше методе мы определяем «bdn» как имя для наше пространство имен «/full_archive», и с этого момента нам нужно добавить «bdn» к выражениям XPath, используемым для поиска элементов:

String expression = "/bdn:Tutorials/bdn:Tutorial";
nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

Используя приведенное выше выражение, мы можем получить все \u003cTutorial \u003e элементы в пространстве имен «bdn».

3.6. Как избежать проблем с пустыми текстовыми узлами

Как вы могли заметить, в коде в разделе 3.3 этой статьи сразу после синтаксического анализа нашего XML в объект Document вызывается новая функция, this.clean(xmlDocument);

Иногда, когда мы перебираем элементы, дочерние узлы и т. д., если в нашем документе есть пустые текстовые узлы, мы можем обнаружить неожиданное поведение результатов, которые хотим получить.

Мы вызвали node.getFirstChild(), когда перебираем все элементы \u003cTutorial\u003e в поисках информации \u003ctitle\u003e, но вместо того, что мы ищем, мы имеем просто «#Text» как пустой узел.

Чтобы устранить проблему, мы можем перемещаться по нашему документу и удалять эти пустые узлы, например:

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

Сделав это, мы можем проверить каждый тип найденных узлов и удалить те, которые нам не нужны.

4. Выводы

Здесь мы только что представили стандартную поддержку XPath, но теперь есть много популярных библиотек, таких как JDOM, Saxon, XQuery, JAXP, Jaxen или даже Jackson. Существуют библиотеки для специального анализа HTML, такие как JSoup.

Это не ограничивается java, выражения XPath могут использоваться языком XSLT для навигации по XML-документам.

Как видите, существует широкий спектр возможностей обработки таких файлов.

По умолчанию существует отличная стандартная поддержка разбора, чтения и обработки документов XML/HTML. Вы можете найти полный рабочий образец здесь.