GitHub

Tensho

Заметки непутевого программиста

REXML

14/12/2018

Сегодня мой друг спросил: “Есть ли возможность парсить XML в Ruby из коробки?”. Несмотря на огромную популярность и де-факто стандарт Nokogiri, в закромах стандартной библиотеки Ruby таки есть классы для парсинга XML – REXML. Давайте пощупаем их за вымя и в будущем лишний раз не будем ставить “пол мира” для того, чтобы обработать несколько конфигурационных XML файлов. Для начала пройдемся по дереву элементов и выведем в STDOUT строкое представление каждого из узлов используя поиск в глубину:

require 'rexml/document'

xml = <<-XML
<a>
  <b one="1">X</b>
  <b two="2">
    <c>Y</c>
  </b>
  <b three="3">Z</b>
</a>
XML

doc = REXML::Document.new(xml)

# DFS
traverse = -> (node) do
  p node
  return if node.elements.empty?
  node.elements.each { |node| traverse.call(node) }
  nil
end

traverse.call(doc.root)
#=> <a> ... </>
#=> <b one='1'> ... </>
#=> <b two='2'> ... </>
#=> <c> ... </>
#=> <b three='3'> ... </>

А теперь пройдемся по самому элементу. Возьмем, например, элемент <b> с аттрибутом two через XPath и посмотрим, что возвращают разные методы на нем:

require 'rexml/xpath'

node = REXML::XPath.first(doc.root, '/*/b[@two]')
#=> <b two='2'> ... </>

node.attributes
#=> { "two" => "2" }

node.elements
#=> #<REXML::Elements:0x00007fb3df8a19d0 @element=<b two='2'> ... </>>

node.elements.count
#=> 1

node.elements.first
#=> <c> ... </> # Любопытно, результат отображается не строковым литералом

node.elements.first.text
#=> "Y"

node.text('c') # text работает с XPath
#=> "Y"

node.node_type
#=> :element

node.get_text('c').node_type
#=> :text

Так же есть еще куча классов для манипулирования деревом элементов, написания всевозможных парсеров, валидации документа. Попробуем на закуску имплементировать простенький стримовый парсер:

require 'rexml/parsers/sax2parser'

parser = REXML::Parsers::SAX2Parser.new(xml)

parser.listen(:start_element) do |uri, localname, qname, attributes|
  puts "START: #{localname}"
end

parser.listen(:end_element) do |uri, localname, qname|
  puts "END: #{localname}"
end

parser.parse
#=> START: a
#=> START: b
#=> END: b
#=> START: b
#=> START: c
#=> END: c
#=> END: b
#=> START: b
#=> END: b
#=> END: a

REXML присутствует с давних времен (1.8.7), следовательно это хороший выбор для тех, кто поддерживает старые версии Ruby.