Java и XML 2
Java и XML
На этот раз я бы хотел поговорить о возможностях языка программирования Java для работы с данными, которые описаны в формате XML. Также мы подробно поговорим о сферах применения XML и о том, что этот формат описания данных дает нам с приземленной точки зрения. По ходу будут рассмотрены основные XML-парсеры для Java. Их существует огромное количество, поэтому под основными следует понимать те, которые лучше других зарекомендовали себя как таковые.
Начнем непосредственно с понятия анализа документа XML. Анализ — это разбор документа и разделение его на составляющие элементы. Анализ XML может проводиться как минимум двумя способами, каждый из которых хорош в определенных условиях и при определенных задачах. Эти способы правильнее было бы назвать методами разбора. В первом случае мы предварительно анализируем XML-документ и сохраняем дерево элементов XML в оперативной памяти. После этого, перемещаясь по названному дереву, мы можем получить любую интересующую нас информацию. Такой метод принято называть анализом по DOM (Document Object Model, DOM — объектное представление (модель) документа). Этот способ очень удобен для представления XML-документов и их изменения благодаря своей простоте и свободе перемещения по дереву DOM. Однако, в случае очень больших XML-документов для хранения их структуры в оперативной памяти потребуется слишком много ресурсов, что в некоторых случаях недопустимо. Понятно, что никаких ограничений на объем XML-документов в спецификации не предусмотрено (что вполне логично), поэтому использование tree-based метода анализа XML-документов не всегда является разумным решением.
Если первый метод разбора XML-документа основывался на представлении этого документа в виде структурированного дерева, то второй в корне от него отличается. Simple API for XML, SAX — метод, основывающийся на событиях (event-based). Использование SAX ограничивает круг анализируемых частей XML-документа. Т.е. возможен анализ только строго определенных элементов и подэлементов, при этом остальная часть XML-документа просто не рассматривается. Однако, ввиду данных ограничений у этого метода существуют также весьма ощутимые преимущества. Так, благодаря своей ориентированности на события, SAX дает заметный выигрыш не только в меньшем объеме потребляемых ресурсов оперативной памяти, но и в производительности в целом (скорость).
Может создаться впечатление, что какой-то из этих двух методов заведомо лучше другого, и всегда выгоднее пользоваться именно им. Но это не так. SAX и DOM существуют как конкурирующие методы анализа и ни в коем случае не исключают друг друга. На этом хотелось бы сделать акцент. Поэтому мы рассмотрим примеры использования обоих методов.
Для начала поставим некую задачу, которую и будем решать, одновременно знакомясь с методами анализа XML. Для этих целей как нельзя кстати подойдет бессмертный пример списка книг, хранящихся в домашней коллекции или библиотеке. Нашей задачей будет написать простенький анализатор подобного XML-файла и произвести форматированный вывод содержащихся в коллекции или библиотеке книг. В принципе, специально для подобной цели предназначен язык XSL и FO-трансформеры, которые позволяют в соответствии с правилами, описанными на языке XSL, представлять данные XML в самых различных форматах. Однако нам нужно уметь анализировать XML-документы в своей программе, а не просто визуализировать содержащиеся в документе данные.
Для анализа XML-документа методом SAX нам понадобятся как минимум классы интерфейса SAX2 и непосредственно SAX-парсер. Причем последний выгоднее всего подключать из командной строки java, т.е. уже при выполнении программы. Благодаря такой архитектуре технология SAX позволяет подключать все новые и новые улучшенные парсеры от третьих производителей, не прибегая к изменению кода программы. Поэтому, единожды написав код, мы либо пользователь вправе выбирать, каким парсером пользоваться. Это весьма весомое преимущество SAX.
Небольшое отступление. SAX был разработан несколькими программистами из maillist'а xml-dev. Реализована эта технология была Дэвидом Мэггинсоном, который, следя за происходящим в списке рассылки, объединил все воедино и написал первый код для SAX. На сегодняшний день работы с SAX не завершены и уже существует спецификация стандарта SAX2.
Интерфейс SAX состоит из нескольких основных частей, каждая из которых отвечает за закрепленные за ней задачи. Мы не будем заниматься их разбором, потому как все это выявится уже в процессе решения нашей задачи. Все классы интерфейса SAX находятся в пакетах org.xml.sax и org.xml.sax.helpers. Поэтому в первую очередь их нужно включить в список подключаемых классов. Как уже говорилось выше, метод анализа SAX основывается на модели событий. Это фактически означает, что в своем приложении нам нужно определить пользовательский класс, производный от HandlerBase, и переопределить некоторые методы. Позже в теле функции main() основного public-класса мы передаем методом класса Parser.setDocumentHandler() наш класс-обработчик. В этом классе, производном от HandlerBase, содержатся все переопределенные нами обработчики событий. Обработчики событий — это обычные методы, которые будут вызываться в процессе анализа XML-документа. Каждый из этих методов отвечает за свое событие. Например, уведомление о начале элемента, конце элемента, начале документа, внешних сущностей и пр. Каждый из методов, отвечающих за свое событие, получает соответствующие параметры. Например, для метода startElement(String, AttributeList) это имя элемента и список атрибутов.
Настало время написать программу, которая решала бы поставленную нами задачу. Для начала определим XML-документ, содержащий описание списка книг библиотеки и определим для него DTD.
<?xml version="1.0" ?>
<!DOCTYPE library
[
<!ELEMENT library (book)+>
<!ELEMENT book (title, author, published>
<!ATTLIST book id CDATA #REQU-IRED>
] >
<library>
<book id = "1">
<title> Дочь некроманта</title>
<author> Ник Перумов</author>
<published> ЭКСМО-Пресс, 2002</published>
</book>
<book id = "2">
<title> Земля без радости</title>
<author> Ник Перумов</author>
<published> ЭКСМО-Пресс, 2000</published>
</book>
<book id = "3">
<title> Гибель Богов</title>
<author> Ник Перумов</author>
<published> ЭКСМО-Пресс, 2002</published>
</book>
…
</library>
Не будем останавливаться на описании того, что и как определено в этом XML-документе. Об этом я рассказывал в предыдущих своих статьях, посвященных XML. Итак, нам нужно разобрать этот XML-документ и вывести данные на STDOUT в отформатированном виде, например:
Title: Дочь некроманта
Author: Ник Перумов
Published: ЭКСМО-Пресс, 2002
…
Вот код программы, которая решит нашу задачу:
import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
public class LibraryParser
{
public static void main(String args[])
{
if (args.length < 1)
{
System.out.println("USAGE: java -Dorg.xml.parser=<class-of-parser> LibraryParser <xml-file> ");
System.exit(1);
}
try
{
File fp = new File(args[0]);
InputSource xml_file = new InputSource(fp.toURL().toString());
Parser sax = ParserFactory.makeParser();
LibraryParserHandler lph = new LibraryParserHandler();
sax.setDocumentHandler(lph);
sax.setDTDHandler(lph);
sax.setEntityResolver(lph);
sax.setErrorHandler(lph);
sax.parse(xml_file);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
class LibraryParserHandler extends HandlerBase
{
public static final int BOOK = 1;
public static final int TITLE = 2;
public static final int AUTHOR = 3;
public static final int PUBLISHED = 4;
int curr = -1;
Locator locator_global;
public void characters(char[] ch, int start, int length)
{
switch (curr)
{
case TITLE : System.out.println("Title: " + new String(ch, start, length)); break;
case AUTHOR : System.out.println("Author: " + new String(ch, start, length)); break;
case PUBLISHED : System.out.println("Published: " + new String(ch, start, length)); break;
}
}
public void endElement(String name)
{
if (name.equals("book")) System.out.println("\n");
}
public void setDocumentLocator(Locator locator)
{
locator_global = locator;
}
public void startElement(String name, AttributeList att)
{
if (name.equals("book"))
{
curr = this.BOOK;
System.out.println("\n #" + att.getValue("id"));
}
else if (name.equals("title")) curr = this.TITLE;
else if (name.equals("author")) curr = this.AUTHOR;
else if (name.equals("published")) curr = this.PUBLISHED;
}
public void error(SAXParseException e)
{
if (locator_global != null)
System.out.println("Error at line " + locator_global.getLineNumber() + " and column " + locator_global.getColumnNumber());
e.printStackTrace();
}
public void fatalError(SAXParseException e)
{
if (locator_global != null)
System.out.println("Fatal error at line " + locator_global.getLineNumber() + " and column " + locator_global.getColumnNumber());
e.printStackTrace();
}
public void warning(SAXParseException e)
{
if (locator_global != null)
System.out.println("Warning at line " + locator_global.getLineNumber() + " and column " + locator_global.getColumnNumber());
e.printStackTrace();
}
}
Для того, чтобы откомпилировать и выполнить эту программу, необходимо иметь SAX API и SAX-драйвер. В нашем случае идеально бы подошел драйвер от IBM: com.ibm.xml.sax.Validating SAXPar-ser. Вот примерные командные строки для компиляции и запуска соответственно:
javac -classpath sax2.jar;./ LibraryParser.java
java -cp sax2.jar;ibmparser.jar;./ -Dorg.xml.sax.parser=com.ibm.xml.sax.ValidatingSAXParser LibraryParser sample.xml
где sax2.jar — SAX API,
ibmparser.jar — XML-парсер от IBM,
sample.xml — это наш XML-документ, приведенный выше.
Наша программа представлена в виде двух классов. Первый объявлен как public, а второй расширяет интерфейс базового класса HandlerBase. Именно второй класс представляет собой набор событий, с помощью которых мы программируем действия и порядок анализа XML-документа. В этом классе мы переопределили следующие методы: characters(), endElement(), startElement(), setDocumentLocator(), error(), fatalError(), warning().
сharacters() вызывается при обработке символьных данных внутри элементов. Если такие данные были обнаружены, но методу передается массив символов (char[] ch), индекс начала этих данных и их длина.
endElement() вызывается в конце каждого элемента. В нашем примере этим событием мы в случае конца элемента <book> </book> выводим два символа перехода на новую строку, чтобы отделить несколько записей условным разделителем.
startElement() вызывает при обнаружении начала какого-либо элемента.
setDocumentLocator() позволяет отследить строчку и символ, которые анализируются в данный момент.
Методы error(), warning(), fatalError() по содержимому в нашем случае практически идентичны. Они определены в интерфейсе ErrorHandler, который наследуется классом HandlerBase. Все они вызываются при возникновении ошибок или предупреждений определенного вида.
Итак, все же давайте разберем алгоритм, который использовался для решения поставленной задачи. Очевидно, что для того, чтобы вывести определенную информацию внутри элементов, недостаточно просто определить нужные методы. Тут необходимо было организовать некоторого вида взаимодействие между этими методами. В частности, между startElement() и characters(). Для этого мы ввели поле curr, в которое метод startElement() записывает номер текущего элемента. При вызове characters() мы смотрим на содержимое этой переменной и в соответствии с тем, какое значение в ней содержится, выводим полученные данные в определенном формате.
Далее в основном классе в методе main() мы создаем объект xml_file класса InputSource, который представляет собой входной поток файла XML-документа. После этого с помощью метода класса ParserFactory makeParser() мы получаем объект sax класса Parser, для которого и устанавливаем вновь созданный объект класса LibraryParserHandler в качестве обработчика всех событий вызываемого парсера. И, наконец, запускаем парсер методом parse(), передав ему объект xml_file в качестве параметра.
Помимо стандартного интерфейса SAX существует множество отличных от него способов реализации метода анализа XML-документов по модели событий. Например, Java-сообщество предложило свой вариант решения этой проблемы — JAXP. JAXP представляет собой набор интерфейсов и парсеров как для SAX, так и для анализа по DOM. Не станем сейчас останавливаться на этой реализации, так как для этого нужно писать отдельную статью. Помимо JAXP существует также огромное количество небольших парсеров от третьих производителей. Эти парсеры в большинстве своем не поддерживают принятый стандарт SAX и навязывают свое оригинальное решение. Иногда подобные продукты интересны тем, что как правило занимают минимум места на диске и при этом намного выигрывают в скорости.
На этом первую часть разговора на тему взаимодействия Java и XML завершим. Осталось только сказать, что все упомянутые в этой статье интерфейсы и парсеры вы сможете совершенно бесплатно скачать с сайтов производителей или с сайта http://java.linux.by. В следующий раз мы будем говорить о DOM.
Алексей Литвинюк
На этот раз я бы хотел поговорить о возможностях языка программирования Java для работы с данными, которые описаны в формате XML. Также мы подробно поговорим о сферах применения XML и о том, что этот формат описания данных дает нам с приземленной точки зрения. По ходу будут рассмотрены основные XML-парсеры для Java. Их существует огромное количество, поэтому под основными следует понимать те, которые лучше других зарекомендовали себя как таковые.
Начнем непосредственно с понятия анализа документа XML. Анализ — это разбор документа и разделение его на составляющие элементы. Анализ XML может проводиться как минимум двумя способами, каждый из которых хорош в определенных условиях и при определенных задачах. Эти способы правильнее было бы назвать методами разбора. В первом случае мы предварительно анализируем XML-документ и сохраняем дерево элементов XML в оперативной памяти. После этого, перемещаясь по названному дереву, мы можем получить любую интересующую нас информацию. Такой метод принято называть анализом по DOM (Document Object Model, DOM — объектное представление (модель) документа). Этот способ очень удобен для представления XML-документов и их изменения благодаря своей простоте и свободе перемещения по дереву DOM. Однако, в случае очень больших XML-документов для хранения их структуры в оперативной памяти потребуется слишком много ресурсов, что в некоторых случаях недопустимо. Понятно, что никаких ограничений на объем XML-документов в спецификации не предусмотрено (что вполне логично), поэтому использование tree-based метода анализа XML-документов не всегда является разумным решением.
Если первый метод разбора XML-документа основывался на представлении этого документа в виде структурированного дерева, то второй в корне от него отличается. Simple API for XML, SAX — метод, основывающийся на событиях (event-based). Использование SAX ограничивает круг анализируемых частей XML-документа. Т.е. возможен анализ только строго определенных элементов и подэлементов, при этом остальная часть XML-документа просто не рассматривается. Однако, ввиду данных ограничений у этого метода существуют также весьма ощутимые преимущества. Так, благодаря своей ориентированности на события, SAX дает заметный выигрыш не только в меньшем объеме потребляемых ресурсов оперативной памяти, но и в производительности в целом (скорость).
Может создаться впечатление, что какой-то из этих двух методов заведомо лучше другого, и всегда выгоднее пользоваться именно им. Но это не так. SAX и DOM существуют как конкурирующие методы анализа и ни в коем случае не исключают друг друга. На этом хотелось бы сделать акцент. Поэтому мы рассмотрим примеры использования обоих методов.
Для начала поставим некую задачу, которую и будем решать, одновременно знакомясь с методами анализа XML. Для этих целей как нельзя кстати подойдет бессмертный пример списка книг, хранящихся в домашней коллекции или библиотеке. Нашей задачей будет написать простенький анализатор подобного XML-файла и произвести форматированный вывод содержащихся в коллекции или библиотеке книг. В принципе, специально для подобной цели предназначен язык XSL и FO-трансформеры, которые позволяют в соответствии с правилами, описанными на языке XSL, представлять данные XML в самых различных форматах. Однако нам нужно уметь анализировать XML-документы в своей программе, а не просто визуализировать содержащиеся в документе данные.
Для анализа XML-документа методом SAX нам понадобятся как минимум классы интерфейса SAX2 и непосредственно SAX-парсер. Причем последний выгоднее всего подключать из командной строки java, т.е. уже при выполнении программы. Благодаря такой архитектуре технология SAX позволяет подключать все новые и новые улучшенные парсеры от третьих производителей, не прибегая к изменению кода программы. Поэтому, единожды написав код, мы либо пользователь вправе выбирать, каким парсером пользоваться. Это весьма весомое преимущество SAX.
Небольшое отступление. SAX был разработан несколькими программистами из maillist'а xml-dev. Реализована эта технология была Дэвидом Мэггинсоном, который, следя за происходящим в списке рассылки, объединил все воедино и написал первый код для SAX. На сегодняшний день работы с SAX не завершены и уже существует спецификация стандарта SAX2.
Интерфейс SAX состоит из нескольких основных частей, каждая из которых отвечает за закрепленные за ней задачи. Мы не будем заниматься их разбором, потому как все это выявится уже в процессе решения нашей задачи. Все классы интерфейса SAX находятся в пакетах org.xml.sax и org.xml.sax.helpers. Поэтому в первую очередь их нужно включить в список подключаемых классов. Как уже говорилось выше, метод анализа SAX основывается на модели событий. Это фактически означает, что в своем приложении нам нужно определить пользовательский класс, производный от HandlerBase, и переопределить некоторые методы. Позже в теле функции main() основного public-класса мы передаем методом класса Parser.setDocumentHandler() наш класс-обработчик. В этом классе, производном от HandlerBase, содержатся все переопределенные нами обработчики событий. Обработчики событий — это обычные методы, которые будут вызываться в процессе анализа XML-документа. Каждый из этих методов отвечает за свое событие. Например, уведомление о начале элемента, конце элемента, начале документа, внешних сущностей и пр. Каждый из методов, отвечающих за свое событие, получает соответствующие параметры. Например, для метода startElement(String, AttributeList) это имя элемента и список атрибутов.
Настало время написать программу, которая решала бы поставленную нами задачу. Для начала определим XML-документ, содержащий описание списка книг библиотеки и определим для него DTD.
<?xml version="1.0" ?>
<!DOCTYPE library
[
<!ELEMENT library (book)+>
<!ELEMENT book (title, author, published>
<!ATTLIST book id CDATA #REQU-IRED>
] >
<library>
<book id = "1">
<title> Дочь некроманта</title>
<author> Ник Перумов</author>
<published> ЭКСМО-Пресс, 2002</published>
</book>
<book id = "2">
<title> Земля без радости</title>
<author> Ник Перумов</author>
<published> ЭКСМО-Пресс, 2000</published>
</book>
<book id = "3">
<title> Гибель Богов</title>
<author> Ник Перумов</author>
<published> ЭКСМО-Пресс, 2002</published>
</book>
…
</library>
Не будем останавливаться на описании того, что и как определено в этом XML-документе. Об этом я рассказывал в предыдущих своих статьях, посвященных XML. Итак, нам нужно разобрать этот XML-документ и вывести данные на STDOUT в отформатированном виде, например:
Title: Дочь некроманта
Author: Ник Перумов
Published: ЭКСМО-Пресс, 2002
…
Вот код программы, которая решит нашу задачу:
import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
public class LibraryParser
{
public static void main(String args[])
{
if (args.length < 1)
{
System.out.println("USAGE: java -Dorg.xml.parser=<class-of-parser> LibraryParser <xml-file> ");
System.exit(1);
}
try
{
File fp = new File(args[0]);
InputSource xml_file = new InputSource(fp.toURL().toString());
Parser sax = ParserFactory.makeParser();
LibraryParserHandler lph = new LibraryParserHandler();
sax.setDocumentHandler(lph);
sax.setDTDHandler(lph);
sax.setEntityResolver(lph);
sax.setErrorHandler(lph);
sax.parse(xml_file);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
class LibraryParserHandler extends HandlerBase
{
public static final int BOOK = 1;
public static final int TITLE = 2;
public static final int AUTHOR = 3;
public static final int PUBLISHED = 4;
int curr = -1;
Locator locator_global;
public void characters(char[] ch, int start, int length)
{
switch (curr)
{
case TITLE : System.out.println("Title: " + new String(ch, start, length)); break;
case AUTHOR : System.out.println("Author: " + new String(ch, start, length)); break;
case PUBLISHED : System.out.println("Published: " + new String(ch, start, length)); break;
}
}
public void endElement(String name)
{
if (name.equals("book")) System.out.println("\n");
}
public void setDocumentLocator(Locator locator)
{
locator_global = locator;
}
public void startElement(String name, AttributeList att)
{
if (name.equals("book"))
{
curr = this.BOOK;
System.out.println("\n #" + att.getValue("id"));
}
else if (name.equals("title")) curr = this.TITLE;
else if (name.equals("author")) curr = this.AUTHOR;
else if (name.equals("published")) curr = this.PUBLISHED;
}
public void error(SAXParseException e)
{
if (locator_global != null)
System.out.println("Error at line " + locator_global.getLineNumber() + " and column " + locator_global.getColumnNumber());
e.printStackTrace();
}
public void fatalError(SAXParseException e)
{
if (locator_global != null)
System.out.println("Fatal error at line " + locator_global.getLineNumber() + " and column " + locator_global.getColumnNumber());
e.printStackTrace();
}
public void warning(SAXParseException e)
{
if (locator_global != null)
System.out.println("Warning at line " + locator_global.getLineNumber() + " and column " + locator_global.getColumnNumber());
e.printStackTrace();
}
}
Для того, чтобы откомпилировать и выполнить эту программу, необходимо иметь SAX API и SAX-драйвер. В нашем случае идеально бы подошел драйвер от IBM: com.ibm.xml.sax.Validating SAXPar-ser. Вот примерные командные строки для компиляции и запуска соответственно:
javac -classpath sax2.jar;./ LibraryParser.java
java -cp sax2.jar;ibmparser.jar;./ -Dorg.xml.sax.parser=com.ibm.xml.sax.ValidatingSAXParser LibraryParser sample.xml
где sax2.jar — SAX API,
ibmparser.jar — XML-парсер от IBM,
sample.xml — это наш XML-документ, приведенный выше.
Наша программа представлена в виде двух классов. Первый объявлен как public, а второй расширяет интерфейс базового класса HandlerBase. Именно второй класс представляет собой набор событий, с помощью которых мы программируем действия и порядок анализа XML-документа. В этом классе мы переопределили следующие методы: characters(), endElement(), startElement(), setDocumentLocator(), error(), fatalError(), warning().
сharacters() вызывается при обработке символьных данных внутри элементов. Если такие данные были обнаружены, но методу передается массив символов (char[] ch), индекс начала этих данных и их длина.
endElement() вызывается в конце каждого элемента. В нашем примере этим событием мы в случае конца элемента <book> </book> выводим два символа перехода на новую строку, чтобы отделить несколько записей условным разделителем.
startElement() вызывает при обнаружении начала какого-либо элемента.
setDocumentLocator() позволяет отследить строчку и символ, которые анализируются в данный момент.
Методы error(), warning(), fatalError() по содержимому в нашем случае практически идентичны. Они определены в интерфейсе ErrorHandler, который наследуется классом HandlerBase. Все они вызываются при возникновении ошибок или предупреждений определенного вида.
Итак, все же давайте разберем алгоритм, который использовался для решения поставленной задачи. Очевидно, что для того, чтобы вывести определенную информацию внутри элементов, недостаточно просто определить нужные методы. Тут необходимо было организовать некоторого вида взаимодействие между этими методами. В частности, между startElement() и characters(). Для этого мы ввели поле curr, в которое метод startElement() записывает номер текущего элемента. При вызове characters() мы смотрим на содержимое этой переменной и в соответствии с тем, какое значение в ней содержится, выводим полученные данные в определенном формате.
Далее в основном классе в методе main() мы создаем объект xml_file класса InputSource, который представляет собой входной поток файла XML-документа. После этого с помощью метода класса ParserFactory makeParser() мы получаем объект sax класса Parser, для которого и устанавливаем вновь созданный объект класса LibraryParserHandler в качестве обработчика всех событий вызываемого парсера. И, наконец, запускаем парсер методом parse(), передав ему объект xml_file в качестве параметра.
Помимо стандартного интерфейса SAX существует множество отличных от него способов реализации метода анализа XML-документов по модели событий. Например, Java-сообщество предложило свой вариант решения этой проблемы — JAXP. JAXP представляет собой набор интерфейсов и парсеров как для SAX, так и для анализа по DOM. Не станем сейчас останавливаться на этой реализации, так как для этого нужно писать отдельную статью. Помимо JAXP существует также огромное количество небольших парсеров от третьих производителей. Эти парсеры в большинстве своем не поддерживают принятый стандарт SAX и навязывают свое оригинальное решение. Иногда подобные продукты интересны тем, что как правило занимают минимум места на диске и при этом намного выигрывают в скорости.
На этом первую часть разговора на тему взаимодействия Java и XML завершим. Осталось только сказать, что все упомянутые в этой статье интерфейсы и парсеры вы сможете совершенно бесплатно скачать с сайтов производителей или с сайта http://java.linux.by. В следующий раз мы будем говорить о DOM.
Алексей Литвинюк
Компьютерная газета. Статья была опубликована в номере 44 за 2002 год в рубрике программирование :: java