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"}}}}
ケース1
この部分(タイトル部分を除く)が
<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/"}]
このケースではrel
やhref
が残っています。
ケース2
それに対してこの部分では、、、
<content type="text/x-hatena-syntax">
hoge
</content>
次のようになってしまいます。type
attributeが残っていません。同じように<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"]}]}]}