WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

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) 形式の文字列リテラルに変換する方法をご紹介しました。まあ、あまり頻繁に使うことはないでしょうが、機会があれば活用してやってください。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles