Scala и XML

Обработка XML – это просто!

Scala – это достаточно новый, но уже успевший стать популярным язык программирования для создания приложений, выполняющихся виртуальными Java™-машинами (JVM). Приложения Scala компилируются в байт-код, что обеспечивает хорошую интеграцию с компонентами, написанными на Java. В то же время, синтаксис Scala в ряде случаев оказывается предпочтительнее Java, в частности, его удобнее использовать при обработке XML. Scala поддерживает несколько вариантов обхода и обработки разобранного дерева XML-документа. Кроме того, поддержка XML-конструкций в Scala реализована непосредственно на уровне языка, поэтому вам не придется представлять их в строковом виде или создавать деревья DOM. В этой статье мы расскажем о данных возможностях Scala, благодаря которым работа с XML доставит вам истинное удовольствие.

Майкл Галпин, инженер по программному обеспечению, Vitria Technology

Майкл Галпин (Michael Galpin) имеет учёную степень по математике в Калифорнийском Технологическом институте. Он является Java-разработчиком с конца 90-х гг. и работает инженером по программному обеспечению в Vitria Technology, в Саннивейл, Калифорния.



16.06.2011

В этой статье используется Scala версии 2.6.1. Scala – это достаточно молодой и бурно развивающийся язык, поэтому не исключено, что данная версия уже не будет последней к моменту прочтения статьи. Для понимания материала вам не потребуется предварительное знакомство со Scala, так как в статье будут рассмотрены необходимый синтаксис и идиомы языка. Для запуска приложений Scala вам понадобится JVM версии 1.5 или выше (мы будем использовать JDK 1.6.0_04). Несмотря на то, что в примерах вам не встретится ни строчки кода на Java, знакомство с ней облегчит усвоение материала.

Разбор XML

Frequently used acronyms

  • API: программный интерфейс приложений
  • DOM: объектная модель документа
  • HTTP: протокол передачи гипертекста
  • JSON: объектная нотация JavaScript
  • SAX: простой API для XML
  • StAX: потоковый API для XML
  • XML: расширяемый язык разметки

Вначале мы рассмотрим принципы разбора XML на Scala. Как и большинство языков программирования, Scala предоставляет несколько вариантов решения этой задачи, основными из которых являются следующие: представление документов в виде InfoSet/DOM, события SAX (push и pull) и связывание с данными, аналогичное архитектуре JAXB (Java Architecture for XML Binding). Далее мы перейдем к работе с деревьями DOM, поскольку на этом примере удобно продемонстрировать множество сильных сторон синтаксиса Scala. Однако перед этим необходимо определиться с тем, какого вида XML должен обрабатываться и какова конечная цель обработки. Другими словами, необходим пример конкретного приложения.


Демонстрационное приложение FriendFeed

FriendFeed – это один из популярных Web-сервисов, появившихся в 2008 году. С его помощью пользователи могут агрегировать информацию о своих действиях на других ресурсах, например, блогах, сервисах обмена сообщениями, YouTube, Flickr и Twitter, и публиковать ее в виде единой новостной ленты. Данные ленты можно создавать по индивидуальному принципу, т.е. охватывать действия конкретного пользователя. Не менее интересной, но пока не столь популярной возможностью является создание так называемых публичных лент FriendFeed, которые содержат суммарную информацию обо всех публичных действиях пользователей. FriendFeed предоставляет специальный API для доступа как к индивидуальным, так и к публичным лентам. Воспользовавшись этой возможностью, мы создадим приложение для получения и анализа данных публичной ленты.


Использование библиотек Java

Вначале необходимо получить доступ к публичной ленте FriendFeed. В нашем приложении будет использоваться лента, находящаяся по адресу http://friendfeed.com/api/feed/public. По умолчанию она содержит последние 30 записей в формате JSON, однако формат можно изменить на XML, добавив параметр format=xml к URI ленты. Для изменения числа выдаваемых записей (например, 100) служит другой параметр – num=100. Теперь все, что требуется – это обратиться по указанному URI. Это легко делается на Java и не представляет никаких сложностей на Scala. Пример приведен в листинге 1.

Листинг 1. Доступ к ленте FriendFeed
object FriendFeed {
  import java.net.{URLConnection, URL}
  import scala.xml._ 
  def friendFeed():Elem = {
    val url = new URL("http://friendfeed.com/api/feed/public?format=xml&num=100")
    val conn = url.openConnection
    XML.load(conn.getInputStream)
  }
}

В начале листинга импортируются два базовых Java-класса. Разработчики Scala не занимались созданием собственных API для выполнения действий, подобных открытию соединений HTTP, потому что для этих целей проще использовать существующие API на Java. Обратите внимание, что синтаксис Scala включает удобные конструкции для импорта нескольких классов из одного пакета. В следующей строке листинга происходит импорт базовых классов Scala для работы с XML. Символ подчеркивания в Scala выполняет ту же функцию, что и звездочка в Java, т.е. указывает на то, что должны импортироваться все классы пакета scala.xml.

Итак, для установления соединения с сервером через HTTP используется Java. Затем необходимо разобрать XML, представленный в виде объекта в Scala. В этом есть несколько интересных моментов. Во-первых, XML – это Scala-объект. Во-вторых, это синглетон. Синтаксис Scala не предусматривает статических методов, полей и инициализаторов, однако можно определять объекты (именно объекты, а не классы) в качестве синглетонов, т.е. единственных экземпляров класса. Методы синглетонов вызываются аналогично статическим методам в Java. В данном примере таким методом является XML.load. Обратите внимание на то, что хотя этот метод принадлежит объекту Scala, он принимает на вход объект Java (java.io.InputStream) в качестве параметра. Это один из примеров тесного взаимодействия между Scala и Java. Наконец, необходимо отметить, что в примере нет оператора return, который не является обязательной конструкцией в Scala. В отсутствие return возвращаемым значением метода считается результат выполнения последней строки, причем если данная строка не имеет возвращаемого значения, то компилятор Scala выдаст сообщение об ошибке. В вызове метода, показанного в листинге 1, нет ничего сложного (листинг 2).

Листинг 2. Вызов метода friendFeed
val feedXml = friendFeed

Как видите, синтаксис Scala позволяет опускать круглые скобки при вызове метода friendFeed. Кроме того, в данном примере используется возможность вывода типов (type inference), которая заключается в том, что вам необязательно указывать тип переменной feedXml. Вместо этого он будет автоматически выведен на основе типа возвращаемого значения метода friendFeed. В листинге 1 также используются некоторые синтаксические сокращения, например, ключевое слово val при объявлении объекта XML. В результате данный объект становится неизменяемым, подобно строкам в Java. Неизменяемые объекты достаточно широко используются в Scala. Существует множество преимуществ в работе с XML как с неизменяемым объектом, однако это может представлять определенные трудности для разработчиков, привыкших к методам вроде appendChild в DOM. Теперь, когда XML-содержимое ленты разобрано, можно приступать к его обработке средствами Scala.


Навигация и сопоставление с образцом

При использовании многих языков программирования XML представляется в виде дерева DOM. У этого подхода есть множество преимуществ, однако обход дерева и извлечение нужных данных часто представляет собой трудоемкую задачу. Для Java были разработаны библиотеки, которые упрощают выборку данных, реализуя язык XPath. В Scala используется похожий подход, однако он обладает дополнительными преимуществами за счет поддержки многих аспектов функционального программирования. В частности, в Scala нет операторов (таких, как + или *), взамен которых используются функции, выполняющие операции вроде сложения или умножения. Благодаря тому, что операторы являются функциями, можно определять дополнительные операторы, применимые к любому типу. Это значительно более мощный подход, чем перегрузка операторов, использующаяся в некоторых языках, например С++. При работе с XPath в Scala можно явно использовать некоторые элементы синтаксиса запросов, однако они при этом будут транслироваться в вызовы функций.

Далее обратимся непосредственно к формату XML новостной ленты FriendFeed. Пример приведен в листинге 3.

Листинг 3. Пример содержимого ленты FriendFeed в формате XML
<feed>
    <entry>
        <updated>2008-03-26T05:06:36Z</updated>
        <service>
            <profileUrl>http://twitter.com/karlerikson</profileUrl>
            <id>twitter</id>
            <name>Twitter</name>
        </service>
        <title>Listening to Panic at the Disco on Kimmel</title>
        <link>http://twitter.com/karlerikson/statuses/777188586</link>
        <published>2008-03-26T05:06:36Z</published>
        <id>f18ebf10-06be-98e2-6059-fa78fa44584b</id>
        <user>
            <profileUrl>http://friendfeed.com/karlerikson</profileUrl>
            <nickname>karlerikson</nickname>
            <id>f294a86c-e6f3-11dc-8203-003048343a40</id>
            <name>Karl Erikson</name>
        </user>
    </entry>
    <entry>
        <updated>2008-03-26T05:06:35Z</updated>
        <service>
            <profileUrl>http://twitter.com/asfaq</profileUrl>
            <id>twitter</id>
            <name>Twitter</name>
        </service>
        <title>@ceetee lol</title>
        <link>http://twitter.com/asfaq/statuses/777188582</link>
        <published>2008-03-26T05:06:35Z</published>
        <id>d4099bb0-8186-5aa1-ce1f-672246c0fe9c</id>
        <user>
            <profileUrl>http://friendfeed.com/asfaq</profileUrl>
            <nickname>asfaq</nickname>
            <id>41e24568-ee6b-11dc-a88d-003048343a40</id>
            <name>Asfaq</name>
        </user>
    </entry>
    <entry>
        <updated>2008-03-26T05:06:31Z</updated>
        <service>
            <profileUrl>http://twitter.com/chrisjlee</profileUrl>
            <id>twitter</id>
            <name>Twitter</name>
        </service>
        <title>sleep..</title>
        <link>http://twitter.com/chrisjlee/statuses/777188561</link>
        <published>2008-03-26T05:06:31Z</published>
        <id>8c4ec232-3ad5-28e1-16c0-00a428294c9c</id>
        <user>
            <profileUrl>http://friendfeed.com/chrisjlee</profileUrl>
            <nickname>chrisjlee</nickname>
            <id>5af39ad4-53b6-45d8-ae25-ef7c50fe9568</id>
            <name>Chris</name>
        </user>
    </entry>
    <entry>
        <updated>2008-03-26T05:06:49Z</updated>
        <service>
            <profileUrl>
                http://www.google.com/reader/shared/09566745492004297397
            </profileUrl>
            <id>googlereader</id>
            <name>Google Reader</name>
        </service>
        <title>Poketo First Editions Show!!</title>
        <link>
            http://www.poketo.com/blog/2008/03/24/poketo-first-editions-show/
        </link>
        <published>2008-03-26T05:06:49Z</published>
        <id>4caefceb-d71c-59c9-8199-45c5adbc60f2</id>
        <user>
            <profileUrl>http://friendfeed.com/misterjt</profileUrl>
            <nickname>misterjt</nickname>
            <id>e745cc8a-f9e4-11dc-a477-003048343a40</id>
            <name>Jason Toney</name>
        </user>
    </entry>
</feed>

Для нашего приложения следует сначала получить список пользователя на основе выбранного сервиса. Другими словами, необходимо отфильтровать содержимое ленты по имени сервиса. Пример реализации данной функции в Scаla приведен в листинге 4.

Листинг 4. Отбор записей ленты на основании имени сервиса
def filterFeed(feed:Elem, feedId:String):Seq[Node] = {
   var results = new Queue[Node]()
   feed\"entry" foreach{(entry) =>
     if (search(entry\"service"\"id" last, feedId)){
       results += (entry\"user"\"nickname").last
     }
   }
   return results
 }
 
 def search(p:Node, Name:String):Boolean = p match {
   case <id>{Text(Name)}</id> => true
   case _ => false
 }

Функция filterFeed принимает на вход два параметра: элемент XML, представляющий ленту, и идентификатор сервиса. Вначале создается очередь (Queue) с именем results, в которую будут помещаться узлы XML (объекты Node). Данная очередь представляет собой типизированную коллекцию наподобие интерфейсов List или Map в Java, только в отличие от Java тип коллекции заключается в квадратные, а не в круглые скобки. Строка feed\"entry" – это выражение, аналогичное XPath, в котором символ обратной косой черты обозначает метод класса scala.xml.Elem. Этот метод возвращает список всех дочерних элементов, имеющих указанное имя, т.е. в нашем случае – список элементов <entry> ленты. Данный список представляет собой экземпляр класса scala.xml.NodeSeq, являющийся расширением Seq[Node]. Будучи наследником Seq, он предоставляет метод foreach, принимающий на вход объект-замыкание (closure).

Запись ((entry) => ... обозначает замыкание, принимающее на вход единственный параметр с именем entry. Внутри замыкания вновь используется похожее на XPath выражение entry\"service"\"id", при помощи которого идентификатор сервиса извлекается из объекта Node, представляющего запись. Данное замыкание будет использоваться в функции search для сравнения с идентификатором сервиса в записи, переданной в метод (сама функция будет рассмотрена ниже). Если идентификаторы совпадают, то псевдоним пользователя, создавшего ленту, помещается в очередь результатов. Обратите внимание, что объект очереди предоставляет функцию +=, очень похожую на оператор. Однако это не более чем функция над очередью. Псевдоним пользователя извлекается из XML в виде объекта Node при помощи аналогичной конструкции, напоминающей XPath.

Далее мы рассмотрим функцию search, в которой используется одна из наиболее мощных возможностей Scala, а именно сопоставление с образцом (pattern matching). В данном случае происходит сравнение вершины, полученной в качестве параметра, с вершиной id, чьим содержимым является строка Name, также переданная в функцию. В случае положительного результата сравнения функция возвращает true, в противном случае вызывается блок case _. Подчеркивание (_) используется в Scala в качестве универсального символа, поэтому выражение case _ выполняет роль, аналогичную блоку default оператора switch в Java или C++. Пока это лишь простейший пример возможностей механизма сопоставления с образцом в Scala. Далее мы перейдем к формированию структуры XML.


Использование механизма сопоставления с образцом для формирования XML

Наше приложение должно создавать новую XML-структуру, содержащую псевдонимы всех пользователей, которые были выбраны из публичной ленты FriendFeed. Для этого существует несколько способов, в частности, можно еще раз продемонстрировать возможности сопоставления с образцом. Обратите внимание на функцию, приведенную в листинге 5.

Листинг 5. Пример функции, использующей сопоставление с образцом для формирования XML
def add(p:Node, newEntry:Node ):Node = p match {
   case <UserList>{ ch @ _* }</UserList> => 
     <UserList>{ ch }{ newEntry }</UserList>
}

Данному образцу удовлетворяет любой элемент UserList вне зависимости от типа дочерних вершин. Метод возвращает новый элемент UserList, содержащий дополнительный дочерний элемент. Другими словами, поведение метода аналогично функции appendChild в DOM. Однако есть принципиальное различие, заключающееся в том, что исходный элемент – экземпляр Node – не изменяется, поскольку является константным объектом. Вместо этого возвращается новый экземпляр Node, что может привести к значительно большему расходу памяти, чем при использовании DOM. Далее рассмотрим альтернативные варианты формирования XML в Scala.


Формирование XML

Поддержка XML на уровне синтаксиса Scala выглядит наиболее привлекательно при создании документов XML. В качестве первого примера мы создадим новый элемент XML, содержащий набор выбранных вершин UserList. Код приведен в листинге 6.

Листинг 6. Создание элемента Service, содержащего результаты анализа ленты
def results(name:String, cnt:Int, elements:NodeSeq):Any = {
   if (cnt > 0){
     return <Service id={name}>{elements}</Service>
   } 
 }

Благодаря встроенной поддержке XML в Scala, можно вставлять динамически созданные данные непосредственно внутрь структуры XML, используя стандартные типизированные коллекции. В данном примере атрибуту id присваивается значение входящего параметра name. Функция также принимает на вход последовательность вершин и добавляет их в качестве дочерних для вновь создаваемого элемента Service. Обратите внимание, что данный элемент создается только в том случае, если параметр cnt больше нуля, в противном случае функция не возвращает никакого значения. Чтобы оставить за собой такую возможность, в качестве типа возвращаемого значения используется специальный тип Any, аналогичный по своей роли классу java.lang.Object в Java. В Scala не поддерживается ключевое слово void, однако вместо него предусмотрен тип Unit. Unit является расширением Any, поэтому можно создавать функции, которые в одних случаях не будут возвращать ничего, а в других – значение произвольного типа.

Как видите, поддержка XML в синтаксисе Java открывает широкие возможности для динамического формирования XML. В следующем примере мы создадим документ stats, который будет показывать, сколько раз каждый сервис встречается в данной ленте. Пример создания документа приведен в листинге 7.

Листинг 7. Функция создания элемента Stats, содержащего данные об использовании сервисов в ленте
def stats(map:HashMap[String,Int]):Node = {
   var nodes = new Queue[Node]()
   map.foreach{(nvPair) =>
     nodes += <Service id={nvPair._1} cnt={nvPair._2.toString}/>
   }
   return <Stats>{nodes}</Stats>
}

Данная функция принимает на вход ассоциативный массив (HashMap), в котором ключами являются названия сервисов, а значениями – число их упоминаний в ленте FriendFeed. Функция перебирает все элементы HashMap, используя знакомую конструкцию foreach на основе замыканий. Внутри замыкания создается новая вершина (Node), ее атрибутами являются ключ и значения элемента HashMap, который затем добавляется в очередь (Queue). Далее функция создает корневой элемент Stats, указывая, что его дочерними вершинами должны являться элементы, содержащиеся в очереди. Итак, теперь, когда все функции созданы, остается лишь добавить недостающие компоненты для их вызова, после чего приложение можно будет тестировать.


Запуск и тестирование приложения

Для запуска приложения осталось добавить код, который будет управлять его выполнением. Для этого служит функция main, аналогичная одноименному методу в Java (листинг 8).

Листинг 8. Функция main приложения FriendFeed
def main(args:Array[String]) = {
    val feedXml = friendFeed
    var map = new HashMap[String,Int]
    args.foreach{(serviceName) =>
      val filteredEntries = filterFeed(feedXml, serviceName)
      var users:Node = <UserList/>
      filteredEntries.foreach{(user) =>
        users = add(users, user)
      }
      map += serviceName -> filteredEntries.length
      println(results(serviceName,filteredEntries.length,users))
    }
    println(stats(map))
}

Эта функция создает ленту FriendFeed. Он принимает на вход параметры, определяющие названия сервисов, для которых должна быть собрана статистика пользователей. Как видите, синтаксис Scala очень напоминает Java, в частности, функция, представляющая собой точку входа, тоже принимает на вход массив строковых параметров под названием args. Далее функция создает ассоциативный массив для документа, содержащего основную статистику, а также документы UserList для каждого сервиса. Затем она выводит содержимое всех документов в стандартный поток вывода. Для компиляции приложения выполните команду scalac FriendFeed.scala, а затем запустите его командой scala FriendFeed (листинг 9).

Листинг 9. Команды для компиляции и запуска приложения
$ scalac FriendFeed.scala
$ scala FriendFeed googlereader flickr delicious twitter blog
<Service id="twitter"><UserList><nickname>ntamaoki</nickname>
<nickname>terrazi</nickname><nickname>ntamaoki</nickname>
<nickname>terrazi</nickname><nickname>ntamaoki</nickname>
<nickname>parodi</nickname><nickname>trevor</nickname>
<nickname>cindy</nickname><nickname>christinelu</nickname>
<nickname>clint</nickname><nickname>savvyauntie</nickname>
<nickname>44gi</nickname></UserList></Service>
<Serviceid="blog"><UserList><nickname>nechipor</nickname>
<nickname>mdolla</nickname><nickname>kyhpudding</nickname>
<nickname>hanayuu</nickname><nickname>hanayuu</nickname>
</UserList></Service><Stats><Service cnt="12" id="twitter">
</Service><Service cnt="0" id="delicious"></Service><Service 
cnt="0" id="flickr"></Service><Service cnt="0" id="googlereader">
</Service><Service cnt="5" id="blog"></Service></Stats>

Разумеется, вы можете запускать приложение, передавая ему любой набор имен сервисов в качестве параметров. В Scala существуют стандартные классы для форматирования вывода фрагментов XML, которые самостоятельно устанавливают правильные отступы при печати элементов. Кроме того, поддерживается возможность потокового вывода XML, например для записи в файл. Таким образом, Scala предоставляет весь стандартный спектр функций для работы с XML, а также ряд дополнительных возможностей.


Заключение

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


Загрузка

ОписаниеИмяРазмер
Примеры кода к статье friendfeed.example.zip1 KБ

Ресурсы

Научиться

Получить продукты и технологии

  • Публичная лента FriendFeed: обратившись по этому URL, вы получите последние 30 записей ленты в формате JSON. (EN)
  • Загрузите Scala. (EN)
  • Загрузите пробные версии программного обеспечения IBM: используйте в вашем следующем проекте ознакомительные версии ПО, которые можно скачать прямо с сайта IBM developerWorks. (EN)

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Технология Java
ArticleID=681057
ArticleTitle=Scala и XML
publish-date=06162011