読者です 読者をやめる 読者になる 読者になる

JSONパーサー再考

tl;dr

json-expect-parserというgemを作った。

github.com

gem install json-expect-parser

で使えます。

きもち

JSONをパースする実装はいろいろありそうで、すでに各言語に標準添付されていたり、便利なものがたくさんある。

そこであえて車輪の再発明を試みてみた記録をここに残す。

動機は「なんかおもしろそう」だっただけ。

通常、JSON parserと言えば、JSON文字列を全部読み取って各言語オブジェクトに変換するものを思い浮かべる。

他の例としては逐次読み取るStreamなタイプ。

yajlというライブラリがこれにあたるようだ。

今回試した実装も逐次読み取るStreamなタイプにあたる。

書こうと思った動機はcrystal-langのJSON実装を読んだ時だった。 crystal-langではPullParserなるものがあって、メッソド名にread_arrayread_objectread_floatなどが並ぶ。

JSON::PullParser - github.com/crystal-lang/crystal

正直これだけでは実装がよくわからないが、なんとなく、「明示的に読み込む形式を指定することで、パフォーマンスアップと異常な形だった時に検知できるものだろうか」と思った。

実際のところは「なにかわからないが読み込む」->「何になるかわからないがとにかくオブジェクト化する」->「指定のものと合ってるか確認する。間違っていればエラー」というステップになっていた。(たぶん)

僕が想像していた「明示的に読み込む形式を指定することで、パフォーマンスアップと異常な形だった時に検知できるもの」は、 「指定した形式として読み込む」->「指定した形式通りパースできれば、指定したオブジェクトとしてオブジェクト化する。パースできなければエラー」というものだった。

まあ違いはそんなにないんだけど、crystal-langの移植ではなく、自分がヒラメイた方をとにかく実装してみた。

これは、youtubeで初めて名前を見るバンドを視聴するとき、視聴する前に想像していたもののほうがカッコよくて、実際に見てみると想像よりダサくてがっかりする現象に似ている。最初の妄想のほうがカッコイイのだ。

うまく行けば

  • パースは読み込み方を限定しているから早い
  • 逐次読み込むから省メモリ
  • 形式が想定外だった時にエラーになる。ついでにパースコードがそのままドキュメント的になる。

というJSON parserが誕生する。(もちろんすでにあるのかもしれないけど、そんなことはエモの前には関係ない。)

実装

で、Rubyしかまともに書けないのでとりあえずRubyで実装した。

[
  {
    "id": 1,
    "name": "ksss",
    "admin": true
  },
  {
    "id": 2,
    "name": "foo",
    "admin": false
  },
  {
    "id": 3,
    "name": "bar",
    "admin": false
  }
]

jsonファイルがあるとすると、

require 'json/expect/parser'

File.open("t.json") do |io|
  expect = JSON::Expect::Parser.new(io)
  expect.array do
    expect.object do
      expect.key #=> "id"
      expect.integer #=> 1, 2, 3
      expect.key #=> "name"
      expect.string #=> "ksss", "foo", "bar"
      expect.key #=> "admin"
      expect.boolean #=> true, false, false
    end
  end
end

と、こんな感じでパースできる。

特徴的なのはパースの仕方をプログラマーに委ねている点。 こうすると、特定の形式のJSONしかパースできないコードになる代わりに、 期待してないJSONだった場合にエラーにするといったことが可能になる。

普通にならべて書くとkeyの順番までも固定されることになる。 順番を不定にしたければ、case文と組み合わせるとよい。

json-expect-parserは、書かれたコードの見た目からなんとなく期待するJSONの形式が予測できるというのが大きい。

expectという名前も、このコードの形が期待している感を感じ取って名付けた。グルーヴだ。

期待していない形式がエラーになるという点ではTypeStructに似ているが、json-expect-parserの場合は型定義を書かなくて済む点が異なる。*1

肝心のパフォーマンスは、Ruby標準添付のjson/ext/parserに比べて約30倍ほど遅い。ガーン。

特に遅いのがI/O部分で、そりゃ逐次処理なんだからI/O増えて遅くなるよなあ。。。

I/O部分はある程度バッファをもたせるようにして多少マシにしてみたものの、なかなか厳しそうだ。ふーむ。

せめてjson/pure/parser並にはなりたい。これは倍ほど早くなればOK。

使用メモリーの方は、目論見通りぐっと減らせていることがわかった。(get_process_mem gemを使用して計測)

今後はパフォーマンスをあげれば使いみちが見つかるのかもしれないが、見つからないかもしれない。

思いついた妄想を書きなぐれて僕は満足だ。

ついでに、mrubyにも対応

ついでにmrubyでも動作するようにしておいた。

これは、CRubyでもmrubyでも動作する世界初のgemの誕生ではないだろうか!(妄想)

ちなみに両対応のコツを書いておくと、

  • mrblibというsymlinkをlibにはる
  • testはmrubyでテスト用に固定されたディレクトリ名なので、CRuby用は別の名前にする。
  • requireを使わない。どうしても使いたい場合は$:にpathを追加する。

あとは依存を書いたmrbgem.rakeファイルを置けばOK。

君の最強のデッキ(build_config.rb)に下の一行を加えてみよう。

conf.gem :github => "ksss/json-expect-parser"

ここまで書いてみて思ったけど

これって普通のJSONパーサーをStreamにしてパース用のAPIをpublicにしただけな気もする……。

実際に、何が来てもパースするメソッドvalueも、これまでのパースAPIを組み合わせるだけで簡単に作れた。

まあ細かいことは置いておいて、JSONパーサーは量もそんなにないし書いてみると楽しいので夏休みの工作に貴方も是非。

*1:type_structについてはるびまに詳しく書いたので是非 http://magazine.rubyist.net/?0054-typestruct