GemのいろいろXML/HTMLparserをまとめたい(Oga特集)

  • 49
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

動機

前に徹夜テンションのままとあるgemの紹介を書いたんだけど、いろいろとひどかったのでまじめにまとめたかったし新しいXMLパーサが出てきたので調べるついでにまとめてしまおうと思った。

ぱーさいろいろ

冷静に思い出すと結構ある

rexml

rubyの標準ライブラリ普通に使う分には申し分ないやつ
DOMParserからSAXParser、PullParserと一通りそろっている
とりあえずこれでなんとかなるケースは多い

Nokogiri

HTML/XMLパーサでなんだかんだ一番人気?
CSSで要素を取り出せるのでHTMLのパースに使うといい感じ
libxml使います

HappyMapper

XMLをオブジェクトにマッピングできるパーサ
クラス作っとけばうまいことマッピングできるのでおすすめ
(以前HappyMapperを紹介したけど糞記事すぎて申し訳ないしもはや恥ずかしくて見ることすらできない)
Nokogiriを利用してる

Mechanize

パーサというかブラウザ
HTMLパーサとしても強力だが調子に乗って使うとメモリを使い果たして落ちることもある。あった。
webスクレイピングにおすすめ
Nokogiriを利用してる

active_support/core_ext/hash

XMLとHashを相互に変換できる
とりあえずHashでいいよって場合は良さそう
階層深くなると悲しいけど浅い場合は有効っぽい

Ox

オブジェクトシリアライザ
オブジェクトをXMLで保存できるので逆に言えばXMLパーサとしても使える
HTMLのパースは厳しいと思う
HappyMapperみたいだけどアプローチが違うのですこし注意
なんだかんだでシリアライザとして使うのが一番良さそうである
libxml使います (追記 2014-10-05:libxml使ってないです)

Unlike some other Ruby XML parsers, Ox is self contained. Ox uses nothing other than standard C libraries so version issues with libXml are not an issue.

C拡張しか使ってないのでlibxmlうんぬんは問題ないみたいなことがちゃんと書いてあります。実際ケジメ案件。

Hpricot

忘れてた
これもlibxml使ってないです
HTMLパーサで昔使ってましたがなんで使わなくなったかわからない(XMLを扱う機会が増えたから?)
Nokogiriと同様にCSSで要素を指定できたと思います
癖が強いライブラリだった記憶があります

libxml-ruby

わすれてた
名前からもわかるようにlibxmlを使う
DOMもSAXもあるしrexmlの速いバージョンという印象

Oga

なんか新しく出てきた
libxmlを利用しないHTML/XMLパーサ
もはやこれだけですばらしいgemなのは確定的に明らか
ほんと環境ごとに入れるのくそめんどくさいし初心者殺しだったのが解消されます
Nokogiriと比べるとドキュメントが見やすいし設計もシンプルなのでそれ込みで初心者におすすめできそう

作者も

My personal issues:
Nokogiri is very unstable on Rubinius, see sparklemotion/nokogiri#1047
Nokogiri ships libxml and unless you set an environment variable will compile it upon Gem installation. On EC2 this takes around 10 minutes or so.
Nokogiri is written in C and a total pain to debug
Nokogiri caches a bunch of things (in particular CSS selectors) on class level and uses locks for this to make it "thread safe"
Nokogiri doesn't offer any sane APIs for parsing large HTML/XML documents. The pull parser only supports XML and the SAX API is a total train wreck
Nokogiri's documentation is limited and not very beginner friendly

と、Nokogiriディスりまくりですがぐうの音もでないほどの正論
今のところ基本的な機能のみでまだ rexml < Oga < Nokogiri ぐらいの立ち位置?
今後はCSSにも対応するみたいですし期待大です

ちょっと使ってみる

Exampleから

require "oga"

Oga.parse_xml('<people><person>Alice</person></people>')
Oga.parse_html('<link rel="stylesheet" href="foo.css">')

まあ、ここらへんはNokogiriと同じ感じ

ExampleみるとIOクラスのオブジェクトを渡しても良いとのことなので早速使ってみよう
open-uriを使ってrssを渡してみると

require "open-uri"

handle = open("http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0")
Oga.parse_xml(handle)
#<=/PATH/TO/PROJECT/vendor/bundle/ruby/2.0.0/gems/oga-0.1.3/lib/oga/xml/lexer.rb:146:in `advance_native': no implicit conversion of Tempfile into String (TypeError)

というかんじでおこられる。あいえええなんで?Tempfileはだめっぽい?
文字列に型変換できねーじゃねーかと言われてもIOの家系だしそっちの方向で変換してちょ
たぶんバグだとおもう(希望的観測)

気を取り直しておとなしくExample通りに

### Oga
require "oga"

xml_file = open("./all.xml")
doc = Oga.parse_xml(xml_file)
titles = doc.xpath("//item/title").map{|t| t.text}

あっはいできました
一応他と比べると

require "open-uri"

xml = ""
open("http://www.nicovideo.jp/ranking/fav/hourly/all?rss=2.0") do |f|
  xml = f.read
end

### rexml
require "rexml/document"

doc = REXML::Document.new xml
titles = []
doc.elements.each('//item/title'){|e| titles << e.text}


### Nokogiri
require "nokogiri"

doc = Nokogiri::XML res
titles = doc.xpath("//item/title").map{|e| e.content}

うn、そんなに変わるようなところじゃないよね…
rexmlとNokogiriの折衷という印象

私的まとめ

とりあえずここまで

  • libxml入れなくて済むようになりそうでほんとにうれしい
  • 将来的にはXMLはrexmlかOgaを使ってHTMLはNokogiriかOgaを使っていくことになりそう
あとOgaが計測するまでもなく体感で遅い しかもIOを渡した方が遅いとかどういうこと? > - High performance, if something doesn't perform well enough it's a bug バグみたいですね

あまりに遅すぎるのが気になって別の環境でちゃんと試したところそこまで遅くなるというのはなかったです。
IOを渡すと一番速かったです。たぶん一番速い。
pry上で実行してたのが悪かったのかな…

ということで下記のコードで計測してみたのでお納めください

# -*- coding: utf-8 -*-
require "open-uri"

xml = ""
open("http://www.nicovideo.jp/ranking/fav/hourly/all?rss=2.0") do |f|
  xml = f.read
end


### Ox speed test
require 'ox'

print "Ox:"
t1 = Time.now
doc = Ox.parse(xml)
titles = doc.locate("rss/channel/item/title").map{|n| n.nodes[0]} #xpathではない何か
puts Time.now - t1


### libxml-ruby speed test
require 'libxml'

print "libxml-ruby:"
t1 = Time.now
doc = LibXML::XML::Document.string(xml)
titles = doc.find("//item/title").map{|e|e.content}
puts Time.now - t1


### rexml speed test
require 'rexml/document'

titles = []
print "rexml:"
t1 = Time.now
doc = REXML::Document.new xml
doc.elements.each('//item/title'){|e| titles << e.text} #mapが使えない
puts Time.now - t1


### Oga speed test
require "oga"

print "oga:"
t1 = Time.now
doc = Oga.parse_xml(xml)
titles = doc.xpath("//item/title").map{|t| t.text}
puts Time.now - t1


### Oga(IO handle) speed test
print "oga(IO):"
file = open("./all.xml")
t1 = Time.now
doc = Oga.parse_xml(file)
titles = doc.xpath("//item/title").map{|t| t.text}
puts Time.now - t1


### Nokogiri speed test
require 'nokogiri'

print "Nokogiri:"
t1 = Time.now
doc = Nokogiri::XML xml
titles_xpath = doc.xpath("//item/title").map{|e| e.content}
puts Time.now - t1


### Nokogiri(css) speed test
print "Nokogiri(css):"
t1 = Time.now
doc = Nokogiri::XML xml
titles_css = doc.css("item title").map{|e| e.content}
puts Time.now - t1


### HappyMapper(おまけ)
require 'happymapper'

module Nico
  # Types = [String, Float, Time, Date, DateTime, Integer, Boolean]
  class Item
    include HappyMapper

    # tag "item"
    element :title, String #, :tag => "title"
    element :link, String #, :tag => "link"
    element :pubDate, DateTime #, :tag => "pubDate"
    element :description, String #, :tag => "description"
  end

  class Channel
    include HappyMapper

    # tag "channel"
    element :title, String #, :tag => "title"
    has_many :item, Item #, :tag => "item"
  end

  class Rss
    include HappyMapper

    # tag "rss"
    has_one :channel, Channel #, :tag => "channel"
  end
end


print "HappyMapper:"
t1 = Time.now
titles = Nico::Rss.parse(xml).channel.item.map{|e| e.title}
puts Time.now - t1

計測結果(s)(CPU:Core2Quad)

Ox:0.002937478
libxml-ruby:0.006396109
rexml:0.277476386
oga:0.361477066
oga(IO):0.000775947
Nokogiri:0.003354732
Nokogiri(css):0.003626173
HappyMapper:0.036153557

私の環境では最速はOgaのIO渡すコードで最遅はOgaの文字列渡すコードという結果に。セプクします。

Ogaはまだまだ発展途上なので現状ではなんとも言えないですね(最悪のまとめ)

Oga最高だしみんなOga使えばいいと思う
Yorick Peterse様には何とお詫び申し上げたらいいかわからない()
(追記:2014-10-05)

後半Gレコ見ながら書いてたからまた崩壊してないか心配
Ogaの発展が待ち遠しくても、待て!