REXML で XML を解析し Hash に変換する

REXML は Ruby に標準でついてくる XMLパーサです.XML な文字列を食わせるとパースして REXML::Document として利用できます.
しかし,REXML のドキュメントを見て扱いを知らなければならないので,Hash で取りたいこともあると思います.実際 ActiveSupport の core_ext には Hash.from_xml が定義されています.
from_xml のためだけに ActiveSupport を require するのも億劫だったので,簡単に実装してみました.

class Hash
    class << self

        def from_xml(rexml)
            xml_elem_to_hash rexml.root
        end

        private

        def xml_elem_to_hash(elem)
            value = if elem.has_elements?
                children = {}
                elem.each_element do |e|
                    children.merge!(xml_elem_to_hash(e)) do |k,v1,v2|
                        v1.class == Array ?  v1 << v2 : [v1,v2]
                    end
                end
                children
            else
                elem.text
            end
            { elem.name.to_sym => value }
        end

    end
end

REXML::Document を受け取って パースされた XML を Hash に落とします.

xml = <<"XML"
<All>
  <WordList>
    <Word>foo</Word>
    <Word>bar</Word>
  </WordList>
  <Hoge></Hoge>
</All>
XML

doc = REXML::Document.new(xml)
Hash.from_xml(doc) == {:All=>{:WordList=>{:Word=>["foo", "bar"]}, :Hoge=>nil}} #=> true

こんな感じに Hash に変換してくれます.

XML を Hash に落とす上で

  • XML は入れ子構造になる
  • Hash は重複するキーを許さないが,XML は重複する要素名を許す

の2つの問題があります.
1つめは再帰を使えば簡単に解決できますが,2つ目は若干厄介です.
Hash#merge ではブロックでキー衝突時の処理を指定できるので,衝突時に Array にして詰め込むようにしています.