RubyでXMLのパースってどうしてんの?
Rubyに限らないけどXML/HTMLのパースって結構する頻度高いよね
で,いつもNokogiri使ってたんだけど特定のタグ以下のデータをすべて使うとかよくある場面なのだけどいちいち指定しないといけないのちょっとめんどくさい
せっかく木構造なんだからオブジェクトに直接マッピングすればいいのにね!
って思って調べてみると何やらHappyMapperとかいう素敵なライブラリがあるらしい
なのに日本語の記事とか一切見なくていっぱい悲しかったので書くことにした
調べてみた
とりあえずちょっとしたコード書いて紹介していくのぜ
ニコニコ見ながらこの記事作ってるのでニコニコのRSSをそれぞれでパースしてみるよ
例のアレの総合ランキング(毎時)-ニコニコ動画
うn requre 'rss'
とかいわない
とりあえず /rss/channel/item/title
を全部(100個)引っ張ってみた
わかりやすいように丁寧にパースしてるよ
環境は
- ruby 1.9.3p194
- gem 1.8.23
ですん
インストール等はググるなり下記のページから飛ぶなりすればいいと思うよ
コードだよ
RSSのデータを取ってくる
よくやるいつものこのコード
require 'net/http'
require 'uri'
uri = URI.parse "http://www.nicovideo.jp/ranking/fav/hourly/are?rss=2.0"
res = Net::HTTP.get uri
XMLを文字列でとってきたのでこれをそれぞれのパーサーに投げてみる
REXML
標準ライブラリ
これだけでも十分便利
require 'rexml/document'
doc = REXML::Document.new res
titles_rx = []
doc.elements.each('/rss/channel/item/title'){|e| titles_rx << e.text}
titles_rx.first(5).each{|e| puts e}
基本はこんな感じ
Nokogiri
みんな大好き!高性能なXMLパーサー
一番の特徴はcssのセレクタが使えるところですかね
パーサーでは一番使われていると思う(当社比)
XMLよりHTMLのほうが真価を発揮できるんだけど,今回はXMLでしかも単純なものなのでほとんど力を発揮できてなくてかわいそうだけどホントは出来る子なんです
require 'nokogiri'
xml = Nokogiri::XML res
titles_xpath = xml.xpath("/rss/channel/item/title").map{|e| e.content}
titles_xpath.first(5).each{|e| puts e}
REXMLとほとんど変わらないけどmapでちょっといい感じになる
ステキステキー
HappyMapper
この記事の主役ですね.
上記のような単なるパース機能だけじゃなくオブジェクトへのマッピングもできちゃうスグレモノ
O/RマッパーならぬObject/XMLマッパーとでも言うべき?
require 'happymapper'
module Nico
class Item
include HappyMapper
element :title, String
end
class Channel
include HappyMapper
has_many :item, Item
end
class Rss
include HappyMapper
has_one :channel, Channel
end
end
titles_hm = Nico::Rss.parse(res).channel.item.map{|e| e.title}
titles_hm.first(5).each {|e| puts e}
コード長くなってるじゃねぇか!
って思うかもしれないけど精神衛生的にだいぶよろしいと思いませんか(重症)
railsユーザーも見覚えある感じだと思う
それに実際に取り出してる所を見てみると
titles_hm = Nico::Rss.parse(res).channel.item.map{|e| e.title}
う・・・うつくしい・・・はっ!?
と,とにかく!文字列による操作ではなくメソッドチェーンで掘り下げているとてもRubyらしいコードで僕も満足です(ほっこり)
うま味が少ないと思うかもしれませんが実際その通りなのでぐうの音も出ないけど,それはこんな単純な操作なのがいけないんだ!俺は悪くねぇ!
ってことでもうちょっと踏み込んでみるよ
で,めんどくさい感じのところはどうなんよ?
それぞれライブラリがのどういうものかなんとなく感じていただけたと思う・・・(思わない)
そんなこんなで冒頭に言った「特定タグ以下のデータを取ってくるのにどれだけめんどくさくなるか」を比較してみたよ
itemタグ以下のtitle,link,pubDate,descriptionのデータをとってみる
class Item
attr_accessor :title, :link, :pubDate, :description
end
これに入れるよ
めんどくさいので今度は手抜きコードだよ
REXML
items_rx = []
doc = REXML::Document.new res
doc.elements.each('//item') do |e|
i = Item.new
i.title = e.elements['title'].text
i.link = e.elements['link'].text
i.pubDate = e.elements['pubDate'].text
i.description = e.elements['description'].text
items_rx << i
end
items_rx.first(5).each{|e| puts e.title}
DRYな人にはつらいコード
Nokogiri
xml = Nokogiri::XML res
items_css = xml.css("item").map do |e|
i = Item.new
i.title = e.css("title").first.content
i.link = e.css("link").first.content
i.pubDate = e.css("pubDate").first.content
i.description = e.css("description").first.content
i
end
items_css.first(5).each{|e| puts e.title}
Nokogiriの方がつらみがある
.first挟まないといけなかったり最後にiを評価してるところには哀愁すら感じる
HappyMapper
class Item
include HappyMapper
tag "item"
element :title, String
element :link, String
element :pubDate, DateTime
element :description, String
end
items_hm = Item.parse(res)
items_hm.first(5).each {|e| puts e.title}
puts "------\n"
wow
such extreme
parseメソッド呼ぶだけでだけでオブジェクトにすーっとマッピングしてくれる・・・これはありがたい
行数もそんなに変わらないし増えてくるとこっちのほうがいいよね!
これなら納得いただけるだろう!ふんす!(満足気)
疲れてきたのでそろそろまとめるね・・・
特徴まとめる
- REXML
- 標準ライブラリなのでインストール不要
- xpathで指定
- 機能は他に比べると弱い(十分だけど)
- Nokogiri
- gem install nokogiri
- libxmlとかいる
- cssとxpathで指定
- 痒いところに手が届く
- gem install nokogiri
- HappyMapper
- gem install happymapper
- オブジェクトのデータ階層で指定
- お膳立てがいるけど便利
という感じ?詳しくはググれ
つまり
環境汚したくない時や特定のタグだけ取れればいいって時はREXML
HTMLをパースしたい時や初心者でよくわからない人は全部のせのNokogiri
取得したいタグが多い時やオブジェクトにマッピングしたい時はHappyMapper
こんなところですかね
おまけ
REXML
他にもREXMLにはSAXと呼ばれるスタイルで書くためのライブラリもある(今回のはDOMスタイルっていう奴)
なんかリスナーを登録するとなんかやってくれるらしいっすよ(他人事)
- rexml/parsers/sax2parser
- rexml/parsers/streamparser
さらに他にも
- rexml/parsers/pullparser
- rexml/parsers/ultralightparser
でもよくわからないので今回は紹介しなかったよ!
だってややこしいんだもん!もん!
他にも
ActiveSupportのcore_extを導入するとHashのクラスメソッドにfrom_xmlってのが追加されるみたいで
これはXMLをHashで返してくれるメソッドみたい
使った事無いので知らない
require 'active_support/core_ext/hash/conversions'
hash = Hash.from_xml(xml)
きっとこんな感じで利用できて,あとは
hash["rss"]["channel"]["item"].map{|e|e["title"]}
みたいな感じでとれるんだと思う(てきとー)
一発でパースするのは便利そう
だけどHashは微妙に扱いづらいので敬遠しちゃうかも
おわりに
nokogiri-happymapperっていうのがあってNokogiriとHappyMapperのいいとこ取りした奴がある(早く言えよ)
マッピングしたいデータをcssで指定してちぇけだーん!ワザマエ!
困ったらこれ入れてパパパッとやって終わり!
オチがついたところで以上!
今度はHappyMapperの解説ができるといいですね(するとはいってない)
yamlとかjsonとかも同じ感じで出来ればもっと捗ると思うんで誰かお願いします
って誰かが言ってました(言いだしっぺ回避)
参考
- REXML
- REXMLのリファレンス
- Nokogiri (Nokogiriはぐぐれば色々出てくるので大丈夫だと思う)
- HappyMapper
- nokogiri-happymapper
- railsのHash
- core_extのHashについて日本語解説