W3docs

Java XML DOM парсер

Разбор, навигация, изменение и сериализация XML в Java с помощью встроенного DOM парсера, с полным примером чтения и записи.

DOM (Document Object Model) парсер считывает XML-документ целиком в память и предоставляет дерево узлов, по которому можно перемещаться, выполнять запросы и вносить изменения. Он поставляется вместе с JDK в пакетах javax.xml.parsers и org.w3c.dom, поэтому ничего не нужно добавлять в classpath. DOM — подходящий инструмент, когда документы достаточно малы, чтобы поместиться в памяти, и вам нужен произвольный доступ к любой части дерева — для чтения конфигурационного файла, преобразования данных или программного создания XML.

В этой главе рассматривается полный жизненный цикл: как DOM моделирует документ, как разобрать источник в дерево, как читать и изменять узлы, и как записать дерево обратно в XML. Если вы только начинаете работать с XML в Java, начните с введения в XML; для больших документов, где важна память, сравните DOM с потоковым SAX парсером.

Как DOM моделирует документ

DOM преобразует разметку в дерево объектов Node. Каждый элемент, атрибут, текст и комментарий являются узлами, и весь документ выстраивается от единственного корня Document. Вы читаете дерево, запрашивая у узлов их дочерние элементы, и изменяете его, создавая, перемещая или удаляя узлы.

ПонятиеИнтерфейсЧто представляет
ДокументDocumentВесь разобранный файл; точка входа в дерево
ЭлементElementТег, например <book>, с атрибутами и дочерними элементами
АтрибутAttrПара имя/значение у элемента
ТекстTextСимвольные данные внутри элемента
Список узловNodeListУпорядоченная, адресуемая по индексу коллекция узлов

Ключевой компромисс: DOM удобен, поскольку всё дерево адресуемо, но загружает всё в память сразу. Для многогигабайтных потоков данных лучше использовать SAX или StAX, которые обрабатывают документ потоково, не строя дерево. А если вы отображаете XML на Java-объекты и обратно вместо обхода сырых узлов, JAXB обычно требует меньше кода, чем ручной DOM.

Разбор документа

Парсер никогда не конструируется напрямую. Вы запрашиваете DocumentBuilder у DocumentBuilderFactory, затем вызываете parse для потока, файла или URI. Настройте фабрику перед созданием — поддержка пространств имён и валидация включаются на уровне фабрики.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);

DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("library.xml"));

// Collapse adjacent text nodes and drop empty ones so getTextContent is clean.
doc.getDocumentElement().normalize();

parse бросает SAXException для некорректного XML и IOException, если источник не удаётся прочитать — оба являются проверяемыми исключениями, которые необходимо обрабатывать. Вызов normalize() один раз после разбора объединяет разделённые текстовые узлы — распространённый источник неожиданностей при чтении текста элементов.

Навигация по дереву

Два метода покрывают большинство операций чтения: getElementsByTagName находит всех потомков с заданным тегом, а getChildNodes возвращает прямые дочерние элементы узла. Помните, что getChildNodes включает текстовые узлы с пробелами, поэтому фильтруйте по типу узла, когда вам нужны только элементы.

Element root = doc.getDocumentElement();          // <library>
NodeList books = doc.getElementsByTagName("book"); // every <book> in the tree

for (int i = 0; i < books.getLength(); i++) {
    Element book = (Element) books.item(i);
    String id = book.getAttribute("id");           // attribute by name
    String title = book.getElementsByTagName("title")
                       .item(0).getTextContent();   // first child <title> text
    System.out.println(id + " -> " + title);
}

NodeList основан на индексах и не является итерируемым, поэтому вы перебираете элементы с помощью getLength() и item(i). getAttribute возвращает пустую строку (никогда null), когда атрибут отсутствует — это стоит знать, прежде чем писать проверку на null, которая никогда не сработает.

Изменение и создание узлов

Дерево DOM является изменяемым. Вы меняете текст с помощью setTextContent, меняете атрибуты с помощью setAttribute, и расширяете дерево, создавая узлы через фабричные методы Document и добавляя их. Узлы должны быть созданы тем же документом, в который они вставляются.

// Update existing content.
Element price = (Element) book.getElementsByTagName("price").item(0);
price.setTextContent("49.50");
price.setAttribute("currency", "USD");

// Build a new subtree and attach it.
Element added = doc.createElement("book");
added.setAttribute("id", "b3");
Element title = doc.createElement("title");
title.setTextContent("The Pragmatic Programmer");
added.appendChild(title);
doc.getDocumentElement().appendChild(added);

createElement создаёт отсоединённый узел; ничего не появляется в документе, пока вы не вызовете appendChild где-нибудь. Чтобы удалить узел, вызовите parent.removeChild(child).

Запись дерева обратно

У DOM нет метода toString(), который производит XML. Для сериализации передайте документ в Transformer через DOMSource и StreamResult. Тот же пакет javax.xml.transform позволяет записывать в файл, строку или любой поток, и задавать параметры форматирования.

Transformer tr = TransformerFactory.newInstance().newTransformer();
tr.setOutputProperty(OutputKeys.INDENT, "yes");
tr.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");

tr.transform(new DOMSource(doc), new StreamResult(new File("out.xml")));

Для недоверенных входных данных усильте защиту фабрики перед разбором — отключите объявления DOCTYPE с помощью factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true), чтобы заблокировать XXE (XML External Entity) атаки.

Полный практический пример

Эта программа разбирает документ с библиотекой книг в памяти, читает каждую книгу, повышает каждую цену на 10%, вставляет новую книгу и сериализует первую обновлённую строку с ценой обратно в XML — выполняя полный цикл чтения-изменения-записи на одном дереве.

java— editable, runs on the server

Что следует понять из выполнения:

  • Корневой элемент выводится как library, потому что getDocumentElement() возвращает единственный верхний узел, от которого зависит всё остальное.
  • getElementsByTagName("book") показывает количество 2 до вставки, подтверждая, что были собраны оба потомка <book> корня.
  • Цены читаются с помощью getTextContent() и разбираются через Double.parseDouble, поэтому 45.00 и 38.50 дают в сумме напечатанный итог 83.50.
  • После appendChild тот же запрос getElementsByTagName("book") возвращает 3, показывая, что живое дерево приняло узел, созданный через doc.createElement.
  • Сериализованная первая строка с ценой читается как 49.50, доказывая, что setTextContent изменил узел в памяти, а Transformer записал обновлённое значение (45.00, повышенное на 10%) обратно в XML.

Практика

Практика
В DOM API, почему необходимо вызвать doc.createElement() перед appendChild() для добавления нового узла?
В DOM API, почему необходимо вызвать doc.createElement() перед appendChild() для добавления нового узла?
Was this page helpful?