Ruby 1.9 の String#inspect で JSON 形式への変換を高速化
Ruby 1.9.1 が公開されたので、 simple-json.rb を Ruby 1.9 に対応させました。その過程で、文字列中の特殊文字をエスケープするのに String#inspect が活用できることを発見したので、本日はそれをご紹介します。
JSON への変換以外でも、例えば Rails の ERb テンプレートで JavaScript に文字列を渡したいときに応用できると思います。覚えておくと便利な Tips ではないでしょうか。
コメントでご指摘があったとおり、 Ruby 1.9 ではネイティブで JSON 変換ができるそうです。なんだ、そもそも simple-json.rb を Ruby 1.9 に対応させる必要自体がなかったのか・・・(^^;
そんなわけで、この記事自体にはほとんど意味はなくなりました。強いて言えば、 JSON への変換を使うことで JavaScript に簡単に文字列が引き渡せる、というあたりだけ参考にしていただければと思います。
Ruby 1.9 では String#inspect が日本語をエスケープしない
Ruby 1.8 でも String#inspect は(当然)存在し、特殊文字をエスケープした文字列表現を出力してくれます。しかし、文字列にエンコーディング情報が含まれていないので、すべての非 ASCII 文字が無条件でエスケープされてしまいます。
$ ruby -e 'puts "日本語\x01\n".inspect' "\346\227\245\346\234\254\350\252\236\001\n"
いくら特殊文字がエスケープされても、これでは JavaScript に渡せませんね。
しかし、 Ruby 1.9 では文字列がエンコーディング情報を持っているので、そのエンコーディング規約に沿ったシーケンスであれば、そのままスルーしてくれます。さらに、特殊文字のエスケープも 8 進数から 16 進数表現に変わっています。
$ ruby19 -e 'puts "日本語\x01\n".inspect' "日本語\x01\n"
これなら、もう少し加工すれば JSON コンパチな文字列リテラルが作れそうです。
JSON 形式の文字列リテラルとは
それでは、 JSON コンパチな文字列リテラルとはどのようなものでしょうか。RFC 4627 には以下のように書かれています。
string = quotation-mark *char quotation-mark char = unescaped / escape ( %x22 / ; " quotation mark U+0022 %x5C / ; \ reverse solidus U+005C %x2F / ; / solidus U+002F %x62 / ; b backspace U+0008 %x66 / ; f form feed U+000C %x6E / ; n line feed U+000A %x72 / ; r carriage return U+000D %x74 / ; t tab U+0009 %x75 4HEXDIG ) ; uXXXX U+XXXX escape = %x5C ; \ quotation-mark = %x22 ; " unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
かいつまんで言うと・・・
- 文字列は二重引用符で囲む。
- U+0000-U+001F, U+0022, U+005C はエスケープしなければならない。
- エスケープシーケンスによる文字コードを指定する方法は "\u????" 形式のみ。
- その他のエスケープシーケンスとして \", \\, \/, \b, \f, \n, \r, \t が使える。
という感じでしょうか。 String#inspect の出力は文字コード表記が "\x??" なのに加え、 JSON では規定されていない \a, \v, \e というエスケープシーケンスも含まれているので、それらをすべて "\u????" に変換してやれば OK です。それらを踏まえた変換処理は以下のようになるでしょう。
def to_json_str(str) str = str.inspect.gsub(/\\x/u, '\u00').gsub(/\\a/u, '\u0007') str = str.gsub(/\\v/u, '\u000B').gsub(/\\e/u, '\u001B') end
Ruby 1.8 では(後のソースのように)若干面倒な処理だったのですが、とてもシンプルになりました。
なにに使うの?
基本的には私の simple-json.rb で使うために考えたのですが(^^ゞ、他の応用として、 ERb テンプレート等で JavaScript に文字列を安全・確実に埋め込むことができます。例えば、 JavaScript に記事タイトルの配列を渡すために以下のようにすると・・・
<script type="text/javascript"> var titles = [ <% @posts.each.map{|p| '"#{p.title}"' }.join(',') -%> ]; </script>
もしタイトル中に改行があればシンタックスエラーになりますし、さらに悪意を込めた文字列があればスクリプト・インジェクションも許してしまいます。
しかし、ここで to_json_str 関数を通せば・・・
<script type="text/javascript"> var titles = [ <% @posts.each.map{|p| to_json_str(p.title) }.join(',') -%> ]; </script>
危険な文字はことごとくエスケープされますので、 Ruby 上と同じ文字列を確実に JavaScript へ引き継ぐことができます。
ベンチマークを取ってみる
さて、ここで気になるのが実行速度です。 String#inspect はネイティブコードなので、 Ruby スクリプトで置換するより高速になると期待できますが、実際にはどうでしょうか。早速、以下のスクリプトを使って計測してみました。環境は Intel Core 2 Duo 2GHz の Mac mini です。
# -*- coding: utf-8 -*- def profile(label) time = Time.now yield puts "#{label} : #{Time.now - time}\n" end ESCAPE_CONVERSION = { '\x' => '\u00', '\a' => '\u0007', '\v' => '\u000B', '\e' => '\u001B' } fixture = <<EOS \x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f \x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f \x22\x5c EOS profile('inspect を使わない') do 100000.times do str = fixture.to_s.encode('UTF-8').gsub(/[^\x20-\x21\x23-\x5b\x5d-\xff]/n) do |chr| c = chr[0].ord if c != 0 && (index = "\"\\/\b\f\n\r\t".index(chr[0])) "\\" + '"\\/bfnrt'[index, 1] else sprintf("\\u%04X", c) end end end end profile('ブロックで置換') do 100000.times do str = fixture.to_s.encode('UTF-8').inspect str.gsub!(/\\[xave]/u){|s| ESCAPE_CONVERSION[s] } end end profile('複数の gsub で置換') do 100000.times do str = fixture.to_s.encode('UTF-8').inspect str.gsub(/\\x/u, '\u00').gsub(/\\a/u, '\u0007').gsub(/\\v/u, '\u000B').gsub(/\\e/u, '\u001B') end end profile('複数の gsub! で置換') do 100000.times do str = fixture.to_s.encode('UTF-8').inspect str.gsub!(/\\x/u, '\u00') str.gsub!(/\\a/u, '\u0007') str.gsub!(/\\v/u, '\u000B') str.gsub!(/\\e/u, '\u001B') end end
「inspect を使わない」は以前の simple-json.rb で使っていた変換処理を Ruby 1.9 用に修正したもの、その他の 3 つは String#inspect を使った処理のバリエーションです。計測結果は以下のとおり。
項目 | 実行時間(秒) |
---|---|
inspect を使わない | 17.700942 |
ブロックで置換 | 6.640527 |
複数の gsub で置換 | 8.079202 |
複数の gsub! で置換 | 8.358865 |
Ruby 1.8 | 16.459568 |
やはり、 String#inspect を使った方が倍以上高速になりました! String#inspect を使った処理では、ブロックを渡して一回の gsub で置換した方が速いみたいですね。ブロックの yield はオーバーヘッドが高いかとも思ったのですが、それよりも複数回正規表現置換を実行する負荷の方が僅かに上回るようです。そんなわけで、 simple-json.rb では「ブロックで置換」の処理を採用しています(^^)v
最後の「Ruby 1.8」というのは「inspect を使わない」とほぼ同等の処理を Ruby 1.8 で実行した結果なのですが、なんと Ruby 1.9 より若干速いという結果になっています。まあ、この程度の処理では Ruby 1.9 の VM の利点が発揮されないということなのでしょうね。
以上、本日は Ruby 1.9 の String#inspect を使って JSON (JavaScript) 形式の文字列リテラルに変換する方法をご紹介しました。まあ、あまり頻繁に使うことはないでしょうが、機会があれば活用してやってください。
詳しくはこちらの記事をどうぞ!
この記事にコメントする