Rubyで高速CSV解析(FasterCSVの2倍〜4倍)

Rubyの標準CSV解析が遅い。かといってFasterCSVを入れるのもいまいち・・。
ということで高速にCSV解析するコードを作成した。
PureRubyながら、FasterCSVの2倍ほど速い。

使用パーサ user system total real
自作パーサ 13.313000 0.140000 13.453000 ( 13.515000)
FasterCSV 28.937000 0.094000 29.031000 ( 29.141000)

テスト用データは郵便番号一覧の全国版。約10万レコード。
http://www.post.japanpost.jp/zipcode/dl/oogaki.html


1行が長くてカラム数が多いデータの場合は、もっと速くなる。
平均1000Byteでカラム数が150個ほどあるCSVファイル10,000行を処理させたときで、4倍程度速い。

使用パーサ user system total real
自作パーサ 4.562000 0.063000 4.625000 ( 4.656000)
FasterCSV 17.579000 0.031000 17.610000 ( 17.656000)


使い方は標準CSVのforeachと同じ。

require 'k_csv'

CsvFile = 'KEN_ALL.CSV'
KCSV::foreach(CsvFile) do |row|
  p row
end


コードは以下。これを "k_csv.rb" として保存する。

# Ruby標準添付のCSVが遅すぎなので作成。
# - ""内に改行がある場合も対応。
# - ,,の場合はnil、,"",の場合は空文字列""になる。
#
# ToDo
# - ヘッダ解析&row[ヘッダ名]でアクセスできるようにする。
# - 例外チェックするようにする。
#
class KCSV
  def KCSV.foreach(path, rs = nil, &block)
    open_reader(path, 'r', ',', rs, &block)
  end

  class << self
    private
    @@quote_mark = ?"

    def open_reader(path, mode, fs, rs, &block)
      file = File.open(path, mode)
      if block
	begin
	  parse_file(file, fs, rs, &block)
	ensure
	  file.close
	end
      end
    end

    def parse_file(file, fs, rs, &block)
      in_quote = false
      while line = file.gets do
	cols = [] unless in_quote

	cols_org = line.split(fs)
	cols_org[-1].chomp!

	cols_org.each do |column|
	  if not in_quote
	    if column.size == 0 
	      cols.push(nil);
	    else # column.size > 0
	      if column[0] == @@quote_mark
		if (column.size > 1) && (column[-1] == @@quote_mark)
		  cols.push(column[1..-2].gsub('""','"'))
		else
		  cols.push(column[1..-1])
		  in_quote = true
		end
	      else # column[0] != @@quote_mark
		cols.push(column)
	      end
	    end
	  else #in_quote
	    #行の最初がクォート中である場合は、前回との区切りは
	    #","ではなく改行である。
	    separator = cols_org[0].equal?(column) ? "\n" : fs

	    # 末尾が"の場合は基本的にquoteの出口だが、末尾が""の場合は
	    # エスケープされるので、まだ出口ではない。
	    if column[-1] == @@quote_mark && column[-2] != @@quote_mark
	      cols[-1] = cols[-1] + separator + column[0..-2]
	      cols[-1].gsub!('""', '"')
	      in_quote = false
	    else
	      cols[-1] = cols[-1] + separator + column
	    end
	  end
	end
	yield(cols) unless in_quote
      end
    end
  end
end