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

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

Стаття з групи Random UA
учасників
Привіт всім читачам моєї ще не останньої статті і хочу привітати: складне про XML залишилося позаду. У цій статті буде вже код Java. Буде трохи теорії, а далі – практика. Через те, що одного матеріалу по SAX у мене вийшло на 10 сторінок у ворді, я зрозумів, що в ліміти не поміщуся. Тому 3 стаття буде розділена на 3 окремі статті, як би це дивно не звучало. Буде все так: SAX -> DOM -> JAXB. Ця стаття буде присвячена лише SAX. PS Там десь у курсі було завдання, де треба було у 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
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.