W3docs

Java XML SAX Parser

Потоковый разбор больших XML-документов в Java с помощью событийного SAX парсера.

SAX (Simple API for XML) — это событийный, потоковый XML-парсер из JDK. Вместо того чтобы строить дерево в памяти, как это делает DOM, SAX читает документ однократно от начала до конца и отправляет вам события — «элемент открыт», «встречен текст», «элемент закрыт» — которые вы обрабатываете по мере их поступления. Поскольку парсер никогда не держит весь документ целиком, SAX разбирает файлы любого размера, используя постоянный минимальный объём памяти. Он находится в org.xml.sax и создаётся через javax.xml.parsers.SAXParserFactory — оба класса входят в стандартный JDK и не требуют установки дополнительных библиотек.

На этой странице рассматриваются: чем push-разбор отличается от построения дерева, настройка фабрики и обработчика, переопределяемые колбэки, отслеживание состояния между событиями, обработка ошибок и полный запускаемый пример. Если вы только начинаете работать с XML в Java, начните со введения в XML; если вам нужен произвольный доступ или редактирование документа, обратитесь к DOM парсеру.

Push-разбор vs. построение дерева

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

АспектSAXDOM
ПамятьКонстантная, не зависит от размера файлаПропорциональна размеру документа
МодельPush: парсер вызывает ваши колбэкиPull/дерево: вы обходите загруженное дерево
НавигацияТолько вперёд, один проходПроизвольный доступ в любом направлении
ИзменениеТолько чтениеЧтение и запись
Лучше всего дляОгромных файлов, извлечения подмножества данныхНебольших/средних документов, которые нужно редактировать

Фабрика и обработчик

Почти всю работу выполняют два типа. SAXParserFactory создаёт SAXParser, а вы создаёте подкласс DefaultHandler для получения событий. DefaultHandler реализует все колбэки как пустые операции, поэтому вы переопределяете только те, которые вам нужны:

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);          // optional: report namespace URIs
SAXParser parser = factory.newSAXParser();

DefaultHandler handler = new DefaultHandler() {
  @Override
  public void startElement(String uri, String localName, String qName, Attributes attr) {
    System.out.println("start <" + qName + ">");
  }
};
parser.parse(new File("data.xml"), handler);

Основные колбэки

Это методы ContentHandler, которые вы чаще всего переопределяете (DefaultHandler предоставляет их все):

КолбэкВызывается, когда
startDocument() / endDocument()Разбор начинается / заканчивается
startElement(uri, localName, qName, attr)Читается открывающий тег; attr содержит его атрибуты
endElement(uri, localName, qName)Читается закрывающий тег
characters(ch, start, length)Читается текстовое содержимое — возможно, несколькими фрагментами
error() / fatalError()Документ некорректен или невалиден

Два момента, на которых часто спотыкаются новички. Во-первых, characters не гарантирует доставку всего текста элемента за один вызов — парсер может разбить его, поэтому нужно накапливать текст в StringBuilder и читать его в endElement. Во-вторых, значения атрибутов доступны только внутри startElement, через аргумент Attributes:

@Override
public void startElement(String uri, String localName, String qName, Attributes attr) {
  String id = attr.getValue("id");           // by name
  for (int i = 0; i < attr.getLength(); i++) // or by index
    System.out.println(attr.getQName(i) + "=" + attr.getValue(i));
}

Отслеживание состояния между событиями

Поскольку SAX не предоставляет дерева, вы сами ведёте контекст. Распространённый паттерн — флаг, устанавливаемый в startElement и сбрасываемый в endElement, плюс текстовый буфер, который обнуляется при старте каждого элемента и потребляется при его завершении:

private final StringBuilder text = new StringBuilder();

@Override public void startElement(String u, String l, String q, Attributes a) {
  text.setLength(0);            // begin collecting fresh text
}
@Override public void characters(char[] ch, int start, int len) {
  text.append(ch, start, len);  // text may arrive in pieces
}
@Override public void endElement(String u, String l, String q) {
  if (q.equals("title")) System.out.println("title = " + text.toString().trim());
}

Практический пример: подсчёт каталога без дерева

Эта программа разбирает небольшой каталог книг, хранящийся в текстовом блоке. Обработчик считает книги, считает, сколько из них есть в наличии (читая атрибут stock), и суммирует все цены — всё это происходит, пока парсер потоково обрабатывает документ однократно. Используются только классы JDK.

java— editable, runs on the server

Что следует вынести из запуска:

  • Три строки parsed: выводятся в порядке документа — Effective Java, Clean Code, Java Concurrency in Practice — доказывая, что SAX выполняет единственный прямой проход: каждый endElement для price срабатывает ровно один раз, в том порядке, в котором книги встречаются в документе, и никогда — не по порядку.
  • books seen : 3 получается путём увеличения счётчика в startElement для каждого тега <book>. Счётчик живёт в вашем обработчике, а не в каком-либо дереве — SAX не сохранял никаких узлов, только целое число, которое вы решили отслеживать.
  • in stock : 2 читается из атрибута stock через attr.getValue("stock"), доступного только внутри startElement. Книга b2 имеет stock="0" и исключается, поэтому из трёх подходят две.
  • total price : 135.50 — это сумма 45.00 + 38.50 + 52.00, накопленная путём чтения текста каждого элемента <price> в его endElement. Чтение текста в конце элемента (а не в characters) — безопасный паттерн, поскольку characters может доставлять текст несколькими фрагментами.
  • Весь документ был передан через ByteArrayInputStream и обработан однократно; программа ни разу не держала DOM-дерево. Именно поэтому SAX масштабируется на многогигабайтные файлы там, где DOM исчерпал бы кучу.

Обработка некорректного XML

SAX сообщает о проблемах через три колбэка ErrorHandler, все они переопределяемы в DefaultHandler:

КолбэкЗначениеРазбор продолжается?
warning(SAXParseException e)Незначительная проблема (например, восстанавливаемое предупреждение DTD)Да
error(SAXParseException e)Ошибка валидности против DTD/схемыДа, если не перебросить исключение
fatalError(SAXParseException e)Нарушение правильности формирования (сломанная разметка)Нет — разбор останавливается

По умолчанию parse() бросает SAXParseException при фатальной ошибке, поэтому оборачивания вызова в try/catch достаточно для большинства кода. Исключение предоставляет getLineNumber() и getColumnNumber(), что позволяет легко указать на проблемную разметку:

try {
  parser.parse(new File("data.xml"), handler);
} catch (SAXParseException e) {
  System.err.println("bad XML at line " + e.getLineNumber()
      + ", column " + e.getColumnNumber() + ": " + e.getMessage());
}
Внимание

Если ваш обработчик бросает непроверяемое исключение (например, NumberFormatException при разборе значения атрибута), оно распространяется прямо из parse() и прерывает поток. Проверяйте и защищайте значения атрибутов внутри колбэка, а не рассчитывайте на то, что входные данные правильно сформированы.

Практика

Практика
В SAX обработчике, почему обычно накапливают текст в StringBuilder внутри characters() и читают его в endElement(), а не используют текст напрямую внутри characters()?
В SAX обработчике, почему обычно накапливают текст в StringBuilder внутри characters() и читают его в endElement(), а не используют текст напрямую внутри characters()?
Was this page helpful?