REXML:パーサの速度を計ってみる

REXMLにもパーサがいろいろあるので、各パーサの速度を計ってみた。
※各パーサの使い方はこちらのページを参考にした。

結果

PullParserが結構速い。
逆に、SAX2Parserが意外に遅い。使い方間違ったかしら…


~$ ruby -r profile bench_parser.rb 2>&1 | egrep "( \% | time |parse_by_)"
% cumulative self self total
time seconds seconds calls ms/call ms/call name
0.11 58.17 0.06 1 64.00 9969.00 Object#parse_by_pullparser
0.00 60.03 0.00 1 0.00 7265.00 Object#parse_by_streamparser
0.00 60.03 0.00 1 0.00 22500.00 Object#parse_by_domparser
0.00 60.03 0.00 1 0.00 19766.00 Object#parse_by_sax2parser

結論

  ∧_∧  カタカタ   / ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
 (    )  ∧ ∧ < PullParserはオススメですよ…と。
 (    )  (,,゚Д゚)  \____________
 ̄ ̄ ̄ ̄ ̄ (つ_つ____
 ̄ ̄ ̄日∇ ̄\|ThinkPad|\
        ̄   =========  \

まあ、適当な計測なんであんまり信用はできないけど…

スクリプト

はてブの人気エントリのRSSを取ってきて、タイトルとURLのalistを作成してるだけ。
キャッシングされるかも知れないので、RSSは一度ファイルに落として、いちいち開きなおす。

require 'open-uri'
require 'rexml/document'
require 'rexml/parsers/baseparser'
require 'rexml/parsers/pullparser'
require 'rexml/parsers/sax2parser'
require 'rexml/parsers/streamparser'
require 'rexml/sax2listener'
require 'rexml/streamlistener'

class SAX2ParserListener
  include REXML::SAX2Listener

  def initialize
    @titles = []
    @links = []
  end

  def start_element(uri, localname, qname, attributes)
    @in_item_tag = true if localname == 'item'
  end

  def end_element(uri, localname, qname)
    return unless @in_item_tag
    case localname
    when 'title'
      @titles << @text
    when 'link'
      @links << @text
    when 'item'
      @in_item_tag = false
    end
  end

  def characters(text)
    @text = text
  end

  def items
    @titles.zip(@links)
  end
end

def parse_by_sax2parser(f)
  parser = REXML::Parsers::SAX2Parser.new(f)
  listener = SAX2ParserListener.new
  parser.listen(listener)
  parser.parse
  listener.items
end

class StreamParserListener
  include REXML::StreamListener

  def initialize
    @titles = []
    @links = []
  end

  def tag_start(name, attrs);
    @in_item_tag = true if name == 'item'
  end

  def tag_end(name)
    return unless @in_item_tag
    case name
    when 'title'
      @titles << @text
    when 'link'
      @links << @text
    when 'item'
      @in_item_tag = false
    end
  end

  def text(text)
    @text = text
  end

  def items
    @titles.zip(@links)
  end
end

def parse_by_streamparser(f)
  listener = StreamParserListener.new
  REXML::Parsers::StreamParser.new(f, listener).parse
  listener.items
end

def parse_by_pullparser(f)
  parser = REXML::Parsers::PullParser.new(f)
  parse_item = lambda {|event|
    title = link = nil
    while (event = parser.pull) and not (event.end_element? and event[0] == 'item')
      if event.start_element? and event[0] == 'title'
        title = parser.pull[0]
      elsif event.start_element? and event[0] == 'link'
        link = parser.pull[0]
      end
    end
    [title, link]
  }

  rs = []
  while parser.has_next?
    event = parser.pull
    next unless event.start_element?
    next unless event[0] == 'item'
    rs << parse_item.call(event)
  end
  rs
end

def parse_by_domparser(f)
  root = REXML::Document.new(f).root
  root.get_elements('item').map {|item|
    [item.get_text('title').value, item.get_text('link').value]
  }
end

def log(name, items)
  sample = items.first

  puts <<EOS
---
#{name}:
  size: #{items.size}
  sample:
    title: #{sample[0]}
    link: #{sample[1]}
EOS
end

open('http://b.hatena.ne.jp/hotentry?mode=rss') {|f|
  open('sample.xml', 'w') {|out| out << f.read }
}

open('sample.xml') {|f|
  items = parse_by_domparser(f)
  log('domparser', items)
}

open('sample.xml') {|f|
  items = parse_by_pullparser(f)
  log('pullparser', items)
}

open('sample.xml') {|f|
  items = parse_by_streamparser(f)
  log('streamparser', items)
}

open('sample.xml') {|f|
  items = parse_by_sax2parser(f)
  log('sax2parser', items)
}