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 — выполняя полный цикл чтения-изменения-записи на одном дереве.
Что следует понять из выполнения:
- Корневой элемент выводится как
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.