Читаем на Java XML-файл с помощью SAX-парсера

User Rating: 5 / 5

В этой статье мы посмотрим, как прочитать XML-файл при помощи SAX-парсера на Java. Вкратце объясню, что означает аббревиатура SAX - она взята по первым буквам от Simple API for XML, т.е. фактически это можно перевести как Простой программный интерфейс для работы с XML. Этот программный интерфейс, или по-другому API, и используется для того, чтобы читать (парсить) XML-документы.

Стоит иметь в виду, что помимо SAX существует также и другой термин DOM, который расшифровывается как Document Object Model, или Объектная модель документа. Для DOM также существует парсер, но алгоритм чтения данных из XML-файла существенно отличается между SAX и DOM. В первую очередь эти отличия касаются загрузки XML-документа в память программы - в то время как SAX парсер построен на основе событий, которые вызываются в процессе чтения документа, DOM загружает весь XML-документ в память программы, чтобы его распарсить (т.е. прочитать). Поэтому если перед вами стоит задача прочитать огромный по объему XML-файл, то учтите, что DOM может привести к тому, что память может переполниться, а SAX будет последовательно читать документ, несмотря на его объем.

Итак, перейдем к примеру реализации на Java. Пусть у нас есть задача считывания из XML-файла информации о книгах и авторах книг, которые хранятся в некоторой библиотеке. В процессе чтения из XML-файла мы бы хотели загрузить информацию обо всех книгах и авторах из соответствующих элементов XML-документа и инициализировать соответствующие этим сущностям Java-классы для более удобного доступа в нашей программе и управления этими данными.

Давайте возьмём следующий тестовый XML-файл, который мы будем впоследствии читать с помощью SAX-парсера на Java:

<book_library>
    <books>
        <book>
            <author_name>Лев Николаевич Толстой</author_name>
            <title>Война и мир</title>
            <date_published>1869</date_published>
        </book>
        <book>
            <author_name>Фёдор Михайлович Достоевский</author_name>
            <title>Преступление и наказание</title>
            <date_published>1866</date_published>
        </book>
        <book>
            <author_name>Александр Сергеевич Пушкин</author_name>
            <title>Евгений Онегин</title>
            <date_published>1833</date_published>
        </book>
    </books>
    <authors>
        <author>
            <name>Лев Николаевич Толстой</name>
            <date_born>1828</date_born>
        </author>
        <author>
            <name>Александр Сергеевич Пушкин</name>
            <date_born>1799</date_born>
        </author>
        <author>
            <name>Фёдор Михайлович Достоевский</name>
            <date_born>1821</date_born>
        </author>
    </authors>
</book_library>

Как видите, в файле есть два основных блока элементов - books и authors с данными о книгах и авторах.

Давайте теперь приготовим простые Java-классы, которые соответствуют структуре этого XML-файла: 

  • Book (класс книги) - будет хранить ФИО автора, название книги и дату первой публикации книги
  • Author (автор книги) - будет хранить ФИО автора и дату его рождения

Ниже представлен код этих классов:

Класс Book:

package ru.allineed.samples.xml;

public class Book {
    private String title;
    private String authorName;
    private String datePublished;

    public void setTitle(String title) {
        this.title = title;
    }

    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }

    public void setDatePublished(String datePublished) {
        this.datePublished = datePublished;
    }

    @Override
    public String toString() {
        return "Book{" +
                "title='" + title + '\'' +
                ", authorName='" + authorName + '\'' +
                ", datePublished='" + datePublished + '\'' +
                '}';
    }
}

Класс Author:

package ru.allineed.samples.xml;

public class Author {
    private String name;
    private String dateBorn;

    public void setName(String name) {
        this.name = name;
    }

    public void setDateBorn(String dateBorn) {
        this.dateBorn = dateBorn;
    }

    @Override
    public String toString() {
        return "Author{" +
                "name='" + name + '\'' +
                ", dateBorn='" + dateBorn + '\'' +
                '}';
    }
}

Отлично, теперь у нас готовы простые классы (или POJO-классы от Plain Old Java Object - "старый добрый Java-объект") для хранения информации о книге и авторе. Но сами по себе классы не столь интересны, т.к. дают лишь способ получить контейнер для наших данных, которые будем считывать из XML.

Давайте ещё создадим класс BookLibrary ("книжная библиотека"), который будет управлять всеми данными о книгах и авторах, храня их в двух отдельных списках. Код этого класса представлен ниже:

package ru.allineed.samples.xml;

import java.util.ArrayList;
import java.util.List;

public class BookLibrary {
    private List<Author> authors;
    private List<Book> books;

    public void createAuthorsList() {
        authors = new ArrayList<>();
    }

    public void createAuthor() {
        authors.add(new Author());
    }

    public void createBooksList() {
        books = new ArrayList<>();
    }

    public void createBook() {
        books.add(new Book());
    }

    private void validateBooks() {
        if (books == null || books.isEmpty()) {
            throw new IllegalStateException("We don't have any books in the library!");
        }
    }
    private Book getCurrentBook() {
        validateBooks();
        return books.get(books.size() - 1);
    }

    private void validateAuthors() {
        if (authors == null || authors.isEmpty()) {
            throw new IllegalStateException("We don't have any authors in the library!");
        }
    }

    private Author getCurrentAuthor() {
        validateAuthors();
        return authors.get(authors.size() - 1);
    }

    public void setCurrentBookTitle(String title) {
        getCurrentBook().setTitle(title);
    }
    public void setCurrentBookAuthorName(String authorName) {
        getCurrentBook().setAuthorName(authorName);
    }

    public void setCurrentBookDatePublished(String datePublished) {
        getCurrentBook().setDatePublished(datePublished);
    }

    public void setCurrentAuthorName(String name) {
        getCurrentAuthor().setName(name);
    }

    public void setCurrentAuthorDateBorn(String dateBorn) {
        getCurrentAuthor().setDateBorn(dateBorn);
    }

    public void printAllBooks(String title) {
        validateBooks();
        System.out.println(title);
        books.forEach(System.out::println);
    }

    public void printAllAuthors(String title) {
        validateAuthors();
        System.out.println(title);
        authors.forEach(System.out::println);
    }
}

Разберём этот класс и его особенности (если по коду всё понятно - можете пропустить эту часть со списком):

  • внутри класса определены всего два списковых поля - authors и books, для хранения авторов и книг 
  • методы createAuthorsList() и createBooksList() инициализируют список авторов и книг, соответственно. Они будут вызываться далее перед добавлением очередного автора или книги в нашу библиотеку
  • методы createAuthor() и createBook() вызываются для уже инициализированного списка и просто добавляют новый экземпляр автора или книги в конец соответствующего списка
  • методы validateBooks() и validateAuthors - проверяют корректность состояния каждого списка, убеждаясь, что список книг и авторов непустой. Если же один из списков пустой, то выбрасывается исключение, а программа завершит работу.
  • также в классе есть соответствующие сеттеры, которые устанавливают значения полей для авторов и книг. Все они работают с последним добавленным в список элементом (книгой или автором). Например, сеттер setCurrentBookTitle(String title) берёт последнюю книгу из списка и устанавливает ей название, переданное во входном аргументе.
  • наконец, в классе есть два метода по выводу информации о всех книгах и авторах на консоль - printAllBooks(String title) и printAllAuthors(String title)

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

Для этого нужно создать класс обработчика событий при чтении парсером XML-файла, унаследовать его от стандартного Java-класса org.xml.sax.helpers.DefaultHandler и переопределить следующие  его методы:

  • public void startDocument() throws SAXException
  • public void characters(char[] ch, int start, int length) throws SAXException
  • public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
  • public void endElement(String uri, String localName, String qName) throws SAXException

Давайте назовём наш новый класс обработчика XmlBookLibraryHandler. Ниже код класса:

package ru.allineed.samples.xml;

import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class XmlBookLibraryHandler extends DefaultHandler {
    public static final String BOOKS_NODE = "books";
    public static final String BOOK_NODE = "book";
    public static final String AUTHOR_NAME_NODE = "author_name";
    public static final String TITLE_NODE = "title";
    public static final String DATE_PUBLISHED_NODE = "date_published";
    public static final String AUTHORS_NODE = "authors";
    public static final String AUTHOR_NODE = "author";
    public static final String NAME_NODE = "name";
    public static final String DATE_BORN_NODE = "date_born";
    private StringBuilder currentElementText;
    private BookLibrary library;
    public BookLibrary getLibrary() {
        return library;
    }

    @Override
    public void startDocument() throws SAXException {
        library = new BookLibrary();
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (currentElementText == null) {
            return;
        }
        currentElementText.append(ch, start, length);
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        switch (qName) {
            case BOOKS_NODE:
                library.createBooksList();
                break;
            case BOOK_NODE:
                library.createBook();
                break;
            case AUTHORS_NODE:
                library.createAuthorsList();
                break;
            case AUTHOR_NODE:
                library.createAuthor();
                break;
            case AUTHOR_NAME_NODE:
            case TITLE_NODE:
            case DATE_PUBLISHED_NODE:
            case NAME_NODE:
            case DATE_BORN_NODE:
                currentElementText = new StringBuilder();
                break;
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        switch (qName) {
            case AUTHOR_NAME_NODE:
                library.setCurrentBookAuthorName(currentElementText.toString());
                break;
            case TITLE_NODE:
                library.setCurrentBookTitle(currentElementText.toString());
                break;
            case DATE_PUBLISHED_NODE:
                library.setCurrentBookDatePublished(currentElementText.toString());
                break;
            case NAME_NODE:
                library.setCurrentAuthorName(currentElementText.toString());
                break;
            case DATE_BORN_NODE:
                library.setCurrentAuthorDateBorn(currentElementText.toString());
                break;
        }
    }
}

Далее будет разбор кода, который мы написали внутри этого обработчика (если вам и так всё понятно, можете пропустить эту часть):

  • во-первых, вы заметили константы вида BOOKS_NODE, BOOK_NODE и т. д., которые по сути представляют все уникальные теги, которые встречаются в нашем тестовом XML-документе. Вынос строк с названиями тегов в константы позволяет нам удобно обращаться к ним из разных участков класса класса обработчика
  • метод startDocument() вызовется один раз при начале парсинга XML-документа. Как видим, в нём мы создаём экземпляр нашей библиотеки
  • метод characters(char[] ch, int start, int length) вызывается каждый раз при чтении парсером содержимого конкретного тега. Внутри него мы накапливаем в поле currentElementText текущий текст, хранящийся в определенном считываемом теге файла.
  • метод startElement(String uri, String localName, String qName, Attributes attributes) вызывается парсером при разборе очередного элемента (тега) из файла. Именно в нём мы должны проверить, какой сейчас элемент считывается и предпринять соответствующие действия. Можно видеть, что когда читаем элементы books и authors из XML-файла, то мы в этот момент инициализируем списки книг и авторов. Когда читаем элементы book и author, то добавляем в списки новую книгу и нового автора. Наконец, когда читаем какой-то из внутренних тегов, связанных с атрибутами книги или автора, мы сбрасываем строковый буфер currentElementText, чтобы он "забыл" о своём предыдущем состоянии и начал накапливать новый текст при ближайшем вызове метода characters.
  • метод endElement(String uri, String localName, String qName) - является схожим с предыдущим, но выполняет, как вы догадались, противоположную задачу. Он вызывается парсером в тот момент, когда парсер выходит или "покидает" ранее встреченный элемент. Именно тут-то мы и будем устанавливать данные в наш текущий экземпляр книги или автора, как раз к моменту вызова этого метода поле currentElementText хранит соответствующий текст нужного тега из файла.

Почти всё готово! Мы добрались до финальной части, теперь мы должны лишь создать какую-то тестовую программу для проверки нашего парсера, где мы создадим экземпляр парсера и передадим ему наш обработчик. Я назову новый класс  XmlParserExample, а ниже его код:

package ru.allineed.samples.xml;

import org.xml.sax.SAXException;
import ru.allineed.samples.common.OutputUtils;
import ru.allineed.samples.config.Localization;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.InputStream;

public class XmlParserExample {
    public static void main(String[] args) {
        OutputUtils.printSampleTitle(
                "Читаем на Java файл XML с помощью SAX-парсера",
                "Reading an XML file with Java using the SAX parser",
                "https://allineed.ru/development/java-development/66-java-reading-xml-via-sax-parser");

        SAXParserFactory parserFactory = SAXParserFactory.newInstance();
        try {
            SAXParser parser = parserFactory.newSAXParser();
            XmlBookLibraryHandler xmlBookLibraryHandler = new XmlBookLibraryHandler();

            try (InputStream ins = XmlParserExample.class.getClassLoader().getResourceAsStream("xml/xml_parser_example.xml")) {
                parser.parse(ins, xmlBookLibraryHandler);
                BookLibrary library = xmlBookLibraryHandler.getLibrary();

                String authorsTitle = Localization.getLocalized(
                        "Зарегистрированные в библиотеке авторы:",
                        "Authors registered in the Library:");

                library.printAllAuthors(authorsTitle);
                System.out.println();

                String booksTitle = Localization.getLocalized(
                        "Зарегистрированные в библиотеке книги:",
                        "Books registered in the Library:");
                library.printAllBooks(booksTitle);

            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        } catch (ParserConfigurationException | SAXException e) {
            e.printStackTrace();
        }
    }
}

Разберём ключевую логику этого класса.

В следующей строке мы создаём так называемую фабрику по производству SAX-парсеров.

SAXParserFactory parserFactory = SAXParserFactory.newInstance();

Далее, в блоке try (поскольку может произойти какое-то исключение связанное с конфигурацией парсера или с самим парсингом XML-файла), мы создаём сам SAX-парсер, а также экземпляр нашего класса-обработчика для парсера:

SAXParser parser = parserFactory.newSAXParser();
XmlBookLibraryHandler xmlBookLibraryHandler = new XmlBookLibraryHandler();

В этой строке с try мы обращаемся к ресурсу (это наш тестовый XML-файл), который должен располагаться в директории /src/main/resources/xml/, через загрузчик классов для текущего класса:

try (InputStream ins = XmlParserExample.class.getClassLoader().getResourceAsStream("xml/xml_parser_example.xml")) {
 // ...
}

Когда получен входной поток (InputStream), можно начинать парсинг XML-файла, вызывая метод parse у парсера и передавая ему входной поток и экземпляр xmlBookLibraryHandler нашего класса-обработчика:

parser.parse(ins, xmlBookLibraryHandler);

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

BookLibrary library = xmlBookLibraryHandler.getLibrary();

String authorsTitle = Localization.getLocalized(
		"Зарегистрированные в библиотеке авторы:",
		"Authors registered in the Library:");

library.printAllAuthors(authorsTitle);
System.out.println();

String booksTitle = Localization.getLocalized(
		"Зарегистрированные в библиотеке книги:",
		"Books registered in the Library:");
library.printAllBooks(booksTitle);

Если запустить пример, то вы увидите следующий вывод в консоли:

========================================================================================================================
>> Запуск примера для статьи "Читаем на Java файл XML с помощью SAX-парсера"
>> Ссылка на статью: https://allineed.ru/development/java-development/66-java-reading-xml-via-sax-parser
========================================================================================================================
Зарегистрированные в библиотеке авторы:
Author{name='Лев Николаевич Толстой', dateBorn='1828'}
Author{name='Александр Сергеевич Пушкин', dateBorn='1799'}
Author{name='Фёдор Михайлович Достоевский', dateBorn='1821'}

Зарегистрированные в библиотеке книги:
Book{title='Война и мир', authorName='Лев Николаевич Толстой', datePublished='1869'}
Book{title='Преступление и наказание', authorName='Фёдор Михайлович Достоевский', datePublished='1866'}
Book{title='Евгений Онегин', authorName='Александр Сергеевич Пушкин', datePublished='1833'}

Process finished with exit code 0

Как видим, все наши авторы и книги были считаны корректно из файла и выведены на консоль.

Внизу вы найдете ссылку на пример со всеми классами, который есть в нашем Git-репозитории, а также инструкцию по запуску всех примеров из статей сайта.

На этом всё, спасибо за внимание и удачи. Напишите в комментариях впечатления о статье, о своём опыте работы с SAX-парсерами. Насколько часто используете их в своих программах? Какому виду парсеров больше отдаёте предпочтение - SAX или DOM?

Яндекс.Метрика