PIYO - Tech & Life -

XMLとRubyのHashを相互変換する方法

Ruby XML

XMLからHashへの変換とその逆変換を考えます。ここでは2つの方法を取り上げますが、結論としてはXmlSimpleを使うほうがよさそうです。

サンプルXML

XMLのサンプルとしてはてなブログのAPIで返ってくるXMLを使います。入っている値は実際とは異なっていますがフォーマットは同じです。

1. ActiveSupportを使う

Railsに一緒に入っているActiveSupportを使ってXML文字列をRubyのHashに変換できるようになります。

require "active_support/core_ext/hash/conversions"により

  • Hash.from_xml
  • Hash#to_xml

これらのメソッドが使えるようになります。

require "active_support/core_ext/hash/conversions"
xml = %Q{
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:app="http://www.w3.org/2007/app">
  ...
</feed>
}

hash = Hash.from_xml(xml)
xml2 = hash.to_xml(hash)

上のように使い方は簡単ですが結果に癖があります。

上のhash変数を見てみるとわかるのですが、XMLのattributeが潰れることもあれば残っている場合もあり得ます。

{"feed"=>
  {"xmlns"=>"http://www.w3.org/2005/Atom",
   "xmlns:app"=>"http://www.w3.org/2007/app",
   "link"=>
    [{"rel"=>"first",
      "href"=>
       "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry"},
     {"rel"=>"next",
      "href"=>
       "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry?page=2"},
     {"rel"=>"alternate", "href"=>"http://blog_id.hatenablog.com/"}],
   "title"=>"ブログタイトル",
   "updated"=>"2013-08-27T15:17:06+09:00",
   "author"=>{"name"=>"hatena_id"},
   "generator"=>"Hatena::Blog",
   "id"=>"hatenablog://blog/2000000000000",
   "entry"=>
    {"id"=>
      "tag:blog.hatena.ne.jp,2013:blog-hatena_id-20000000000000-3000000000000000",
     "link"=>
      [{"rel"=>"edit",
        "href"=>
         "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/edit/2500000000"},
       {"rel"=>"alternate",
        "type"=>"text/html",
        "href"=>"http://blog_id.hatenablog.com/entry/2013/09/02/112823"}],
     "author"=>{"name"=>"hatena_id"},
     "title"=>"記事タイトル",
     "updated"=>"2013-09-02T11:28:23+09:00",
     "published"=>"2013-09-02T11:28:23+09:00",
     "edited"=>"2013-09-02T11:28:23+09:00",
     "summary"=>"hoge ",
     "content"=>"\n      hoge\n    ",
     "formatted_content"=>"\n      hoge\n    ",
     "control"=>{"draft"=>"yes"}}}}

この部分(タイトル部分を除く)が

<link rel="first" href="https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry" />
<link rel="next" href="https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry?page=2" />
<title>ブログタイトル</title>
<link rel="alternate" href="http://blog_id.hatenablog.com/"/>

こうなっています。

"link"=>
    [{"rel"=>"first",
      "href"=>
       "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry"},
     {"rel"=>"next",
      "href"=>
       "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry?page=2"},
     {"rel"=>"alternate", "href"=>"http://blog_id.hatenablog.com/"}]

このケースではrelhrefが残っています。

ケース2

それに対してこの部分では、、、

<content type="text/x-hatena-syntax">
  hoge
</content>

次のようになってしまいます。typeattributeが残っていません。同じように<generator>タグでもattributeが消えてしまっていることがわかります。

"content"=>"\n      hoge\n    "

ActiveSupportを使う場合には注意が必要と言えそうです。

2. XmlSimpleを使う

xmlsimpleというgemで似たようなことができます。attributeもちゃんと残せます。

$ gem install xml-simple
require "xmlsimple"

hash = XmlSimple.xml_in(xml, options) # XML文字列をHashに変換
xml = XmlSimple.xml_out(hash, options) # HashをXMLに変換

オプションが豊富で全てを説明しきれないので詳細はこちらで→XmlSimple - XML made easy

xml = # さっきと一緒の文字列
hash = XmlSimple.xml_in(xml, ContentKey:"__content__")
xml2 = XmlSimple.xml_out(hash, ContentKey:"__content__", RootName:"feed")

ここで使っているオプションは次の通り。

  • ContentKey: 値を表すキーでデフォルト値はcontentです。元々のXMLにあるcontentタグと重複するのでそれとは異なるキーを指定します。
  • RootName: HashをXMLに変換する際のRootノードの名前はデフォルトでは<opt>となっています。元のXMLに合わせるためにfeedを指定しています。

xml_inで変換したHashはこうなりました。長くてすいません。ActiveSupportのときに潰れていたような<content type=""><generator uri="">などがちゃんと残っています。

また、要素が1つであろうが複数であろうが配列になっているので、単一の値なのか配列なのかを区別してプログラムを書く必要がありません。

ActiveSupportに依存する必要もなくなるのでこちらがオススメかと。

{"xmlns"=>"http://www.w3.org/2005/Atom",
 "xmlns:app"=>"http://www.w3.org/2007/app",
 "link"=>
  [{"rel"=>"first",
    "href"=>
     "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry"},
   {"rel"=>"next",
    "href"=>
     "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/entry?page=2"},
   {"rel"=>"alternate", "href"=>"http://blog_id.hatenablog.com/"}],
 "title"=>["ブログタイトル"],
 "updated"=>["2013-08-27T15:17:06+09:00"],
 "author"=>[{"name"=>["hatena_id"]}],
 "generator"=>
  [{"uri"=>"http://blog.hatena.ne.jp/",
    "version"=>"100000000",
    "__content__"=>"Hatena::Blog"}],
 "id"=>["hatenablog://blog/2000000000000"],
 "entry"=>
  [{"id"=>
     ["tag:blog.hatena.ne.jp,2013:blog-hatena_id-20000000000000-3000000000000000"],
    "link"=>
     [{"rel"=>"edit",
       "href"=>
        "https://blog.hatena.ne.jp/hatena_id/blog_id.hatenablog.com/atom/edit/2500000000"},
      {"rel"=>"alternate",
       "type"=>"text/html",
       "href"=>"http://blog_id.hatenablog.com/entry/2013/09/02/112823"}],
    "author"=>[{"name"=>["hatena_id"]}],
    "title"=>["記事タイトル"],
    "updated"=>["2013-09-02T11:28:23+09:00"],
    "published"=>["2013-09-02T11:28:23+09:00"],
    "edited"=>["2013-09-02T11:28:23+09:00"],
    "summary"=>[{"type"=>"text", "__content__"=>"hoge "}],
    "content"=>
     [{"type"=>"text/x-hatena-syntax", "__content__"=>"\n      hoge\n    "}],
    "formatted-content"=>
     [{"type"=>"text/html",
       "xmlns:hatena"=>"http://www.hatena.ne.jp/info/xmlns#",
       "__content__"=>"\n      hoge\n    "}],
    "control"=>[{"draft"=>["yes"]}]}]}