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 меняет управление: парсер сам руководит, вызывая методы вашего обработчика при встрече каждого фрагмента разметки. Вы храните только то состояние, которое вам нужно. Компромисс заключается в том, что нельзя двигаться назад или смотреть вперёд — каждое событие вы видите ровно один раз, в порядке документа.
| Аспект | SAX | DOM |
|---|---|---|
| Память | Константная, не зависит от размера файла | Пропорциональна размеру документа |
| Модель | 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.
Что следует вынести из запуска:
- Три строки
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() и прерывает поток. Проверяйте и защищайте значения атрибутов внутри колбэка, а не рассчитывайте на то, что входные данные правильно сформированы.