JavaRush /Java блог /Random /Основы XML для Java программиста – Часть 3.1 из 3 - SAX
Ярослав
40 уровень
Днепр

Основы XML для Java программиста – Часть 3.1 из 3 - SAX

Статья из группы Random
Вступление Привет всем читателям моей еще не последней статьи и хочу поздравить: сложное про XML осталось позади. В данной статье будет уже код на Java. Будет немного теории, а далее практика. Из-за того, что одного материала по SAX у меня получилось на 10 страничек в ворде, я понял, что в лимиты не помещусь. Потому, 3 статья будет разделена на 3 отдельные статьи, как бы это странно не звучало. Будет все в таком порядке: SAX -> DOM -> JAXB. Данная статья будет посвящена только SAX. P.S. Там где-то в курсе была задача, где надо было в HTML файле вывести все внутренние элементы. После данной статьи, вы сможете это сделать без считывания построчно обычным BufferedReader и сложными алгоритмами обработки, а, так же, близкое решение будет дано в последнем практическом примере. Давайте приступать :) SAX (Simple API for XML) — ТЕОРИЯ SAX-обработчик устроен так, что он просто считывает последовательно XML файлы и реагирует на разные события, после чего передает информацию специальному обработчику событий. У него есть немало событий, однако самые частые и полезные следующие:
  1. startDocument — начало документа
  2. endDocument — конец документа
  3. startElement — открытие элемента
  4. endElement — закрытие элемента
  5. characters — текстовая информация внутри элементов.
Все события обрабатываются в обработчике событий, который нужно создать и переопределить методы. Преимущества: высокая производительность благодаря "прямому" способу считывания данных, низкие затраты памяти. Недостатки: ограниченная функциональность, а, значит, в нелинейных задачах дорабатывать её надо будет уже нам. SAX (Simple API for XML) – ПРАКТИКА Сразу список импортов, чтобы вы не искали и ничего не спутали:

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
Теперь, для начала, нам нужно создать SAXParser:

public class SAXExample {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        // Создание фабрики и образца парсера
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
    }
}
Как вы видите, сначала нужно создать фабрику, а потом в фабрике создать уже сам парсер. Теперь, когда у нас есть сам парсер, нам нужен обработчик его событий. Для этого нам нужен отдельный класс ради нашего же удобства:

public class SAXExample {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
    }
    
    private static class XMLHandler extends DefaultHandler {
        @Override
        public void startDocument() throws SAXException {
            // Тут будет логика реакции на начало документа
        }

        @Override
        public void endDocument() throws SAXException {
            // Тут будет логика реакции на конец документа
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            // Тут будет логика реакции на начало элемента
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            // Тут будет логика реакции на конец элемента
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            // Тут будет логика реакции на текст между элементами
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            // Тут будет логика реакции на пустое пространство внутри элементов (пробелы, переносы строчек и так далее).
        }
    }
}
Мы создали класс со всеми нужными нам методами для обработки событий, которые были перечислены в теории. Еще немного дополнительной теории: Немного про characters: если в элементе будет текст, например, «hello», то, теоретически, метод способен вызваться 5 раз подряд на каждый отдельный символ, однако это не страшно, так как все равно все будет работать. О методах startElement и endElement: uri — это пространство, в котором находится элемент, localName — это имя элемента без префикса, qName — это имя элемента с префиксом (если он есть, иначе просто имя элемента). uri и localName всегда пустые, если мы не подключили в фабрике обработку пространств. Это делается методом фабрики setNamespaceAware(true). Тогда мы сможем получать пространство (uri) и элементы с префиксами перед ними (localName). Задача №1 — у нас есть следующий XML

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<company>
    <name>IT-Heaven</name>
    <offices>
        <office floor="1" room="1">
            <employees>
                <employee name="Maksim" job="Middle Software Developer" />
                <employee name="Ivan" job="Junior Software Developer" />
                <employee name="Franklin" job="Junior Software Developer" />
            </employees>
        </office>
        <office floor="1" room="2">
            <employees>
                <employee name="Herald" job="Middle Software Developer" />
                <employee name="Adam" job="Middle Software Developer" />
                <employee name="Leroy" job="Junior Software Developer" />
            </employees>
        </office>
    </offices>
</company>
Наша цель: достать всю информацию про всех сотрудников из данного файла. Для начала, нам нужно создать класс Employee:

public class Employee {
    private String name, job;

    public Employee(String name, String job) {
        this.name = name;
        this.job = job;
    }

    public String getName() {
        return name;
    }

    public String getJob() {
        return job;
    }
}
А в нашем основном классе SAXExample нам нужен список со всеми сотрудниками:

private static ArrayList<Employee> employees = new ArrayList<>();
Теперь давайте внимательно смотреть, где нужная нам информация находится в XML файле. И, как мы можем видеть, вся нужная нам информация — это атрибуты элементов employee. А так, как startElement у нас обладает таким полезным параметром, как attributes, то у нас довольно простая задача. Для начала, давайте уберем ненужные методы, чтобы не захламлять наш код. Нам нужен только метод startElement. А в самом методе мы должны собрать информацию с атрибутов тега employee. Внимание:

public class SAXExample {
    private static ArrayList<Employee> employees = new ArrayList<>();

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
    }

    private static class XMLHandler extends DefaultHandler {
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            if (qName.equals("employee")) {
                String name = attributes.getValue("name");
                String job = attributes.getValue("job");
                employees.add(new Employee(name, job));
            }
        }
    }
}
Логика простая: если имя элемента — employee, мы просто будем получать информацию про его атрибуты. В attributes есть полезный метод, где, зная название атрибута, можно получить его значение. Именно его мы и использовали. Теперь, когда мы создали обрабатывание события на начало элемента, нам нужно запарсить наш XML файл. Для этого достаточно сделать так:

public class SAXExample {
    private static ArrayList<Employee> employees = new ArrayList<>();

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();

        XMLHandler handler = new XMLHandler();
        parser.parse(new File("resource/xml_file1.xml"), handler);

        for (Employee employee : employees)
            System.out.println(String.format("Имя сотрудника: %s, его должность: %s", employee.getName(), employee.getJob()));
    }

    private static class XMLHandler extends DefaultHandler {
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            if (qName.equals("employee")) {
                String name = attributes.getValue("name");
                String job = attributes.getValue("job");
                employees.add(new Employee(name, job));
            }
        }
    }
}
В методе parse вы должны передать путь к xml файлу и обработчик, который вы создали. И так, с помощью данного кода мы достали информацию из этого XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<company>
    <name>IT-Heaven</name>
    <offices>
        <office floor="1" room="1">
            <employees>
                <employee name="Maksim" job="Middle Software Developer" />
                <employee name="Ivan" job="Junior Software Developer" />
                <employee name="Franklin" job="Junior Software Developer" />
            </employees>
        </office>
        <office floor="1" room="2">
            <employees>
                <employee name="Herald" job="Middle Software Developer" />
                <employee name="Adam" job="Middle Software Developer" />
                <employee name="Leroy" job="Junior Software Developer" />
            </employees>
        </office>
    </offices>
</company>
А выходные данные мы получили такие:

Имя сотрудника: Maksim, его должность: Middle Software Developer
Имя сотрудника: Ivan, его должность: Junior Software Developer
Имя сотрудника: Franklin, его должность: Junior Software Developer
Имя сотрудника: Herald, его должность: Middle Software Developer
Имя сотрудника: Adam, его должность: Middle Software Developer
Имя сотрудника: Leroy, его должность: Junior Software Developer
Задача выполнена! Задача №2 — у нас есть следующий XML:

<?xml version="1.0" encoding="UTF-8"?>
<company>
    <name>IT-Heaven</name>
    <offices>
        <office floor="1" room="1">
            <employees>
                <employee>
                    <name>Maksim</name>
                    <job>Middle Software Developer</job>
                </employee>
                <employee>
                    <name>Ivan</name>
                    <job>Junior Software Developer</job>
                </employee>
                <employee>
                    <name>Franklin</name>
                    <job>Junior Software Developer</job>
                </employee>
            </employees>
        </office>
        <office floor="1" room="2">
            <employees>
                <employee>
                    <name>Herald</name>
                    <job>Middle Software Developer</job>
                </employee>
                <employee>
                    <name>Adam</name>
                    <job>Middle Software Developer</job>
                </employee>
                <employee>
                    <name>Leroy</name>
                    <job>Junior Software Developer</job>
                </employee>
            </employees>
        </office>
    </offices>
</company>
Наша цель: достать всю информацию про всех сотрудников из данного файла. Задача хорошо продемонстрирует, каким образом плохо структурированный XML файл может приводить к усложнению написания кода. Как вы видите, информация про имя и должность теперь хранится как текстовая информация внутри элементов name и job. Для считывания текста внутри элементов у нас есть метод characters. Для этого, нам нужно создать новый класс-обработчик с улучшенной логикой. Не забывайте, что обработчики – полноценные классы, способные хранить в себе логику любой сложности. Потому, сейчас мы будем тюнинговать наш обработчик. На самом деле, достаточно заметить, что у нас всегда name и job идут по очереди, и не важно, в каком порядке, мы можем спокойно сохранить имя и профессию в отдельные переменные, и когда обе переменные сохранены – создать нашего сотрудника. Только вот вместе с началом элемента у нас нет параметра для текста внутри элемента. Нам нужно использовать методы для текста. Но как нам получить текстовую информацию внутри элемента, если это совершенно разные методы? Мое решение: нам достаточно запомнить имя последнего элемента, а в characters проверять, в каком элементе мы считываем информацию. Так же нужно помнить, что <codee>characters считывает все символы внутри элементов, а это значит, что будут считываться все пробелы и даже переносы строчек. А они нам не нужны. Нам нужно игнорировать эти данные, так как они неправильные.</codee> Код:

public class SAXExample {
    private static ArrayList<Employee> employees = new ArrayList<>();

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();

        AdvancedXMLHandler handler = new AdvancedXMLHandler();
        parser.parse(new File("resource/xml_file2.xml"), handler);

        for (Employee employee : employees)
            System.out.println(String.format("Имя сотрудника: %s, его должность: %s", employee.getName(), employee.getJob()));
    }

    private static class AdvancedXMLHandler extends DefaultHandler {
        private String name, job, lastElementName;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            lastElementName = qName;
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            String information = new String(ch, start, length);

            information = information.replace("\n", "").trim();

            if (!information.isEmpty()) {
                if (lastElementName.equals("name"))
                    name = information;
                if (lastElementName.equals("job"))
                    job = information;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if ( (name != null && !name.isEmpty()) && (job != null && !job.isEmpty()) ) {
                employees.add(new Employee(name, job));
                name = null;
                job = null;
            }
        }
    }
}
Как вы видите, из-за банального усложнения структуры XML файла у нас значительно усложнился код. Однако, код не сложный. Описание: мы создали переменные для хранения данных про сотрудника (name, job), а так же переменную lastElementName, чтобы фиксировать, внутри какого элемента мы находимся. После этого, в методе characters мы фильтруем информацию, и если там еще осталась информация, то, значит, это нужный нам текст, а далее мы определяем, имя это или профессия, используя lastElementName. В методе endElement мы проверяем, считана ли вся информация, и если считана, то мы создаем сотрудника и сбрасываем информацию. Выходные данные решения эквивалентны первому примеру:

Имя сотрудника: Maksim, его должность: Middle Software Developer
Имя сотрудника: Ivan, его должность: Junior Software Developer
Имя сотрудника: Franklin, его должность: Junior Software Developer
Имя сотрудника: Herald, его должность: Middle Software Developer
Имя сотрудника: Adam, его должность: Middle Software Developer
Имя сотрудника: Leroy, его должность: Junior Software Developer
Таким образом, данная задача была решена, но вы можете заметить то, что сложность выше. Потому можно сделать вывод, что хранить текстовую информацию в атрибутах чаще всего будет правильней, чем в отдельных элементах. И еще одна сладкая задача, которая будет частично решать задачу на JavaRush про вывод информации об элементе в HTML, только её надо будет немного подредактировать, тут мы будем просто перечислять все элементы внутри какого-то элемента :) Задача №3 — дан элемент element, вывести имена и атрибуты всех внутренних элементов, если элемент не найден — вывести это. Для данной задачи мы будем использовать следующий XML файл:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <oracle>
        <connection value="jdbc:oracle:thin:@10.220.140.48:1521:test1" />
        <user value="secretOracleUsername" />
        <password value="111" />
    </oracle>

    <mysql>
        <connection value="jdbc:mysql:thin:@10.220.140.48:1521:test1" />
        <user value="secretMySQLUsername" />
        <password value="222" />
    </mysql>
</root>
Как вы видите, у нас тут есть три возможных сценария: root, mysql, oracle. Тогда программа будет выводить всю инфу о всех элементах внутри. Как же нам сделать такое? А достаточно просто: нам достаточно объявить логическую переменную isEntered, которая будет означать, внутри ли мы нужно нам элемента, и если внутри – считывать все данные из startElement. Код решения:

public class SAXExample {
    private static boolean isFound;

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();

        SearchingXMLHandler handler = new SearchingXMLHandler("root");
        parser.parse(new File("resource/xml_file3.xml"), handler);
        
        if (!isFound)
            System.out.println("Элемент не был найден.");
    }

    private static class SearchingXMLHandler extends DefaultHandler {
        private String element;
        private boolean isEntered;

        public SearchingXMLHandler(String element) {
            this.element = element;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            if (isEntered) {
                System.out.println(String.format("Найден элемент <%s>, его атрибуты:", qName));

                int length = attributes.getLength();
                for(int i = 0; i < length; i++)
                    System.out.println(String.format("Имя атрибута: %s, его значение: %s", attributes.getQName(i), attributes.getValue(i)));
            }

            if (qName.equals(element)) {
                isEntered = true;
                isFound = true;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (qName.equals(element))
                isEntered = false;
        }
    }
}
В данном коде мы при входе в элемент, про который нам нужна информация, выставляем флажок isEntered в true, что значит, что мы внутри элемента. И как только мы оказались внутри элемента, мы просто каждый новый элемент в startElement обрабатываем, зная, что он точно внутренний элемент нашего элемента. Таким образом, мы выводим имя элемента и его название. Если же элемент не был найден в файле, то у нас есть переменная isFound, которая устанавливается тогда, когда элемент находится, и если она false, то будет выведено сообщение, что элемент не найден. И как вы видите, в примере в конструктор SearchingXMLHandler мы передали root элемент. Вывод для него:

Найден элемент <oracle>, его атрибуты:
Найден элемент <connection>, его атрибуты:
Имя атрибута: value, его значение: jdbc:oracle:thin:@10.220.140.48:1521:test1
Найден элемент <user>, его атрибуты:
Имя атрибута: value, его значение: secretOracleUsername
Найден элемент <password>, его атрибуты:
Имя атрибута: value, его значение: 111
Найден элемент <mysql>, его атрибуты:
Найден элемент <connection>, его атрибуты:
Имя атрибута: value, его значение: jdbc:mysql:thin:@10.220.140.48:1521:test1
Найден элемент <user>, его атрибуты:
Имя атрибута: value, его значение: secretMySQLUsername
Найден элемент <password>, его атрибуты:
Имя атрибута: value, его значение: 222
Таким образом, мы получили всю информацию про внутренние элементы и их атрибуты. Задача решена. <h2>Эпилог</h2>Вы ознакомились, что SAX довольно интересный инструмент и вполне эффективный, и его можно использовать по-разному, с разными целями и так далее, достаточно только посмотреть на задачу с правильной стороны, как это показано в задаче №2 и №3, где SAX не предоставлял прямых методов для решения задачи, но, благодаря нашей смекалке, у нас получилось придумать выход из ситуации. Следующая часть статьи будет целиком посвящена DOM. Надеюсь, что вам было интересно познакомиться с SAX. Поэкспериментируйте, попрактикуйтесь и вы поймете, что все довольно просто. А на этом все, удачи вам в программировании и ждите скоро часть про DOM. Успехов вам в обучении :) Предыдущая статья: [Конкурс] Основы XML для Java программиста - Часть 2 из 3 Следующая статья: [Конкурс] Основы XML для Java программиста - Часть 3.2 из 3 - DOM
Комментарии (20)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Vitaly Demchenko Уровень 44
20 января 2024
Спасибо за статью. В продолжение можно ознакомиться со статьей на Хабре: Java. Простой SAX парсер / Хабр
Ivan Borisov Уровень 1
7 августа 2022
У меня возникла проблема с атрибутами в методе startElement(). Может кто-нибудь знает ее решение? Я поставил перед собой задачу: если xml-элемент имеет атрибуты, то нужно просто сохранить их в коллекцию List<Attributes>. Xml:

<Persons>
    <Person name="Kirill"/>
    <Person name="Matvey"/>
    <Person name="Teodor"/>
</Persons>
Класс-обработчик:

public class XMLHandler extends DefaultHandler {
    private List<Attributes> attrs = new ArrayList<>();

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {

        if (attributes.getLength() > 0) {
            attrs.add(attributes);
//            System.out.println(attributes.getValue(0));
        }
    }

    public List<Attributes> getAttrs() {
        return attrs;
    }
}
Main:

public class Main {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {

        XMLHandler handler = new XMLHandler();
        String path = "f.xml";

        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();

        parser.parse(path, handler);

        List<Attributes> attrs = handler.getAttrs();
        for (Attributes a : attrs) {
            System.out.println(a.getValue(0));
        }
    }
}
Вывод в консоль:

Teodor
Teodor
Teodor
Получается, что в коллекцию добавляются атрибуты самого последнего элемента. При этом, если в методе startElement() сделать просто вывод атрибутов в консоль, то все отобразится корректно. Почему так происходит?
22 июля 2022
Большое спасибо за статьи и приведённые примеры 👍
Joker Уровень 11
13 мая 2022
Делал первый пример, в консоль 6 раз выводит последнего работника. Думал, что уже где-то накосячил и простоя взял и скопировал код. В итоге тоже самое... Я один такой или в коде какой-то баг???
Essah King Уровень 37
4 февраля 2022
Попробовал на другом примере В конце каждой строчки выводит я так понял хеш код Название: Бельгийские Вафли.java.io.PrintStream@610455d6 Цена: $5.95.java.io.PrintStream@610455d6 Описание: две известных Бельгийских Вафли с обилием настоящего кленового сиропаjava.io.PrintStream@610455d6 Калорийность: 650java.io.PrintStream@610455d6 подскажите как можно от этого избавиться?))
LuneFox Уровень 41 Expert
8 января 2022
с м е к а л о ч к а
Vladislav Klimenko Уровень 8
1 марта 2021
Хорошо пишешь, статья наглядная, на ее основе решил пару задач на работе. Спасибо!
Алексей Уровень 37
19 февраля 2021
Спасибо
Lt_Den Уровень 34
20 октября 2020
Материал этой статьи это Эверест. Судя по количеству лайков и комментов - доходят сюда самые стойкие
Хорс Уровень 41
27 июля 2020
интересно конечно. Но сам я такое не наваяю