Обработка XML, XPath и XSL трансформации в Ruby

Изучая новую технологию всегда хочется применить её для решения текущих задач. А одна из основных задач современного программиста - это составление программ, которые собирают данные из нескольких систем, обрабатывают их и выдают результат. Это напоминает сборку модели из готовых деталей конструктора, где роль крупных блоков играют, например, базы данных, а в качестве соединительных деталей используется простой и гибкий язык программирования.

Основными источниками данных в современном программировании являются текстовые файлы, базы данных и файлы в XML формате. А обрабатывать и соединять их друг с другом попробуем с помощью скриптового языка программирования Ruby.

Обработка текстовых файлов не представляет особой сложности, поскольку возможности Ruby в области поддержки регулярных выражений делают разбор любого текстового файла не очень сложной задачей. А вот обработку XML документов можно рассмотреть подробнее.

Обработка XML


Для обработки XML существует как стандартное решение в виде rexml библиотеки, которая входит в Ruby 1.8, так и альтернативные варианты, которые в большинстве случаев представляют собой обертки (wrappers) вокруг С библиотек libxml2 и производных от неё.

Поиск расширений Ruby для обработки XML документов (http://raa.ruby-lang.org/search.rhtml?search=xml и на http://libxml.org) приводит к множеству библиотек. Но отбросив все alpha, unstable, experimental и тому подобное получаем совсем небольшой список:

* libgdome-ruby (beta): http://raa.ruby-lang.org/project/libgdome-ruby/
* libxml (production quality): http://raa.ruby-lang.org/project/libxml/
* libxslt (working): http://raa.ruby-lang.org/project/libxslt/

Я попробую сравнить найденные библиотеки между собой в решении простой задачи.
Постановка задачи

Программа 1. Генерация XML документа (generateXml.rb)

puts '<nodes>'
1000.times {|i| puts '<node sum="1" avg="' + i.to_s + '">Node sample text</node>'}
puts '</nodes>'


Задача


В сгенерированном программой 1 XML документе пройтись по всем узлам, посчитать сумму атрибутов sum и найти среднее значение атрибута avg.

Решение задачи методами rexml


Удобный API компенсируется недостатком производительности. Дело в том, что библиотека rexml написана на самом Ruby, производительность которого относительно C/C++ не велика. Также можно отметить, что XSL трансформацию библиотека rexml не поддерживает.

Программа 2. Решение задачи (parseXml_2.rb)

require "rexml/document"
include REXML

xmlStr = ''

ARGF.each {|line| xmlStr << line}

doc = Document.new xmlStr
sum = avgSum = count = 0
doc.elements.each('/nodes/node') { |e|
        count += 1
        sum += e.attributes['sum'].to_i
        avgSum += e.attributes['avg'].to_i
}
puts "count(node): #{count}, sum(sum): #{sum}, avg(avg): #{avgSum/count}"

Определение времени выполнения и результат

$ time ruby generateXml.rb | ruby parseXml_2.rb
count(node): 1000, sum(sum): 1000, avg(avg): 499

real    0m6.035s
user    0m5.450s
sys     0m0.590s

Итого - 6 секунд длился разбор документа на моем компьютере.

model name      : Celeron (Mendocino)
cpu MHz         : 534.552
cache size      : 128 KB
bogomips        : 1064.96


Решение задачи c помощью libgdome-ruby


libgdome-ruby-0.3.tar.bz2 представляет собой оболочку вокруг Gdome2 библиотеки. Gdome2 - это реализация W3C DOM Level2 на C. Поэтому для установки libgdome-ruby надо сначала установить Gdome2 библиотеку.

Тут и пригодится Portage от Gentoo Linux:

$ emerge dev-libs/gdome2

Для ручной установки замечу, что Gdome2 зависит от библиотек libxml2 (http://xmlsoft.org/) и glib (http://www.gtk.org/). Сам Gdome2 можно скачать с сайта http://phd.cs.unibo.it/gdome2/.

Далее переходим к установке Ruby расширения libgdome-ruby:

$ tar -xjf libgdome-ruby-0.3.tar.bz2
$ cd libgdome-ruby-0.3
$ ruby extconf.rb
$ make
$ make install

Программа 3. Решение задачи с использованием Gdome2 (parseXml_3.rb)

require "gdome"

xmlStr = ''
ARGF.each {|line| xmlStr << line}

domImpl = Dom::implementation
doc = domImpl.createDocFromMemory(xmlStr, 0)

sum = avgSum = count = 0
children = doc.documentElement.childNodes
(0...children.length).each{ |i|
    el = children.item(i)
    if (el.kind_of?(Dom::Element))
        count += 1
        sum += el.getAttribute('sum').to_i
        avgSum += el.getAttribute('avg').to_i
    end
}
puts "count(node): #{count}, sum(sum): #{sum}, avg(avg): #{avgSum/count}"

Можно задать резонный вопрос: почему я не использовал XPath, для выборки узлов /Nodes/Node. Отвечаю - в библиотеке libgdome-ruby XPath не реализовали, хотя в самой Gnome2 XPath присутствует в полной мере. Отметим, что XSL трансформация не реализована в Gdom2 и, как следствие, в libgdome-ruby тоже.

Время выполнения задачи с использованием gdome значительно лучше, чем с rexml:

$ time ruby generateXml.rb | ruby parseXml_3.rb
count(node): 1000, sum(sum): 1000, avg(avg): 499

real    0m0.334s
user    0m0.300s
sys     0m0.030s


Победитель - Оболочка для libxml


Заявленая автором оболочки поддержка XPath и простые и напоминающие rexml интерфейсы весьма привлекательны. Библиотека зависит от libm (математические функции), libz (zlib), libiconv и, естественно, от libxml2. Как правило, все эти библиотеки в современных дистрибутивах есть, поэтому переходим без лишних слов к установке и реализации нашей задачи:

Устанавливаем скачаный файл libxml-0.3.4.tar.gz:

$ tar -xzf libxml-0.3.4.tar.gz
$ cd libxml-0.3.4
$ ruby extconf.rb
$ make && make install

Программа 4. Решение задачи с использованием libxml (parseXml_4.rb)

require 'xml/libxml'

xmlStr = ''
ARGF.each {|line| xmlStr << line}

xp = XML::Parser.new()
xp.string = xmlStr
doc = xp.parse

sum = avgSum = count = 0
doc.find('/nodes/node').each { |e|
    count += 1
    sum += e['sum'].to_i
    avgSum += e['avg'].to_i
}
puts "count(node): #{count}, sum(sum): #{sum}, avg(avg): #{avgSum/count}"

Традиционный замер времени выполнения:

$ time ruby generateXml.rb | ruby parseXml_4.rb
count(node): 1000, sum(sum): 1000, avg(avg): 499

real    0m0.210s
user    0m0.180s
sys     0m0.030s

Можно сказать только одно: Кубок победителю :-) Ruby libxml extention показал лучший результат по эффективности и удобству интерфейсов.

Вкусность на последок: XSL трансформация с использованием libxslt


Для установки libxslt требуется, что бы libxml уже было установлено и header файлы находились в директории ../libxml относительно директории с libxslt.

$ ln -s libxml-0.3.4 libxml
$ tar -xzf libxslt-0.3.4.tar.gz
$ cd libxslt-0.3.4
$ ruby extconf.rb
$ make && make install

Для примера предположим, что нам надо вывести список файлов в директории. Довольно простая задача, но при этом требуется разделить данные от представления. Это может понадобится, например, если дизайн представления будет менятся.

Примерная реализация: получаем список файлов в директории, строим из списка файлов XML и трансформируем его в HTML при помощи XSL.

XSL файл для трансформации (filesToHtml.xsl):

<?xml version="1.0" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<html><body><ul>
<xsl:for-each select="/files/file">
    <li><xsl:value-of select="."/></li>
</xsl:for-each>
</ul></body></html>
</xsl:template>
</xsl:stylesheet>

Ruby скрипт, осуществляющий трансформацию (buildFileList.rb):

require 'xml/libxml'
require 'xml/libxslt'

xslt = XML::XSLT.file('filesToHtml.xsl')

xp = XML::Parser.new

# замечатальный пример компакности Ruby - выполнение shell комманды,
# проход по строкам её результата и составление XML в одной строке :-)
xp.string = `ls`.inject('<files>') { |xml, file|
    xml << '<file>' << file.chomp << '</file>' } + '</files>'
xslt.doc = xp.parse

s = xslt.parse
s.apply
s.print

Проверяем производительность:

$ time ruby buildFileList.rb
... результат трансформации пропущен ...
real    0m0.064s
user    0m0.030s
sys     0m0.030s


Заключение


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

Вход для пользователей