岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

livedoor Blogに移動します

今後の記事は http://iwamot.ldblog.jp/ に書きます。はてなダイアリー以外のブログサービスをまともに使ったことがなく、これを機会に使ってみたくなったというのが本音です。出戻る可能性は大いにありますが、物は試しということで。

追記(2012-04-12)

古い記事にコメントが付くので、コメントを受け付けない設定に変えました。

はてなブックマークからDeliciousに移動します

ブックマークを http://b.hatena.ne.jp/IwamotoTakashi/ から http://www.delicious.com/iwamot に移動します。もちろん今般のトラッキング問題が大きなきっかけではありますが、それより前に「新ユーザーページの使いづらさ問題」という小さなきっかけもありました。自分のブックマークでなく「マイホットエントリー」をデフォルトにするセンスが僕には合いません。

優秀なエンジニアの退職はやまず、はてなアイデアは放置されっぱなし。今回のような大きな問題が起きても、社長が外遊中のためか対応が遅い。はてなには上場より先にやるべきことがあるのではないかと苦言を呈したくもなります。

ブックマークだけでなく日記も、そのうち別のサービスに移動しようと考えています。

Product Advertising APIの旧バージョンサポートが終了

メールが届いた。2月22日をもって、2011-08-01バージョンを除くすべての旧バージョンのサポートが終了するとのこと。

リリースチェッカーでは2010-06-01バージョンを使っていたので上げた。コードの変更は特になし。

旧バージョンでのリクエストはすべて2011-08-01バージョンに変換されるとのことなので、使っている人は要注意。

JP ISBN Linkerを更新した

JP ISBN LinkerFirefox 10で動かなくなったので更新した。

node.isSameNode メソッドを使っていたため。

DOM4 仕様書で非推奨とされたため、node.isSameNode メソッドが削除されました。node1.isSameNode(node2) に代わり、=== 演算子を使用できます。例: node1 === node2

Firefox 10 for developers | MDN

Ruby Association Certified Ruby Programmer Silverに合格した

Ruby Association Certified Ruby Programmer Silverを本日受験し、無事合格した。満点だった。

受験した理由は色々あるのだが、今の会社に依存していたくなかったというのが大きい。転職時や独立時に有利になるよう、今年は取れそうな資格を手当たりしだい取ろうと思っている。まずは、いちばん好きなプログラミング言語であるRubyの資格を取ることに決めた。それが1月4日。受験日を15日に決め、すぐに申し込んだ。僕の場合、勢いが重要なのだ。

勉強法

Ruby技術者認定試験公式ガイド』を5日に入手し、模擬問題を解きながら勉強していった。

日付解いた問題正答数備考
1月5日問題1〜5030問合格ラインは正答率75%なので、これでは不合格になってしまう。誤答部分の知識を補うべく、あわててRuby 1.8.7のマニュアルを読んだ。
1月9日問題51〜10043問勉強の成果があり、合格ラインに乗った。
1月11日問題1〜5047問これで合格の自信がついた。
1月13日問題51〜10050問本番でも満点を出したいと思った。
1月14日問題1〜5049問ケアレスミスが1問あったので、本番では気をつけようと思った。

受験日当日

14時30分からの回を申し込んでいた。会場は新宿エルタワーのISAキャリアカレッジ。早く着きすぎてしまい、14時10分に名前を呼ばれるまで、しばらく待った。

受付嬢がいくつか説明してくれるのだが、何から何まで事務的で、逆に感心させられた。あれだけ心を込めずに仕事ができる人が、皮肉ではなくうらやましい。

入室後、すぐに試験開始。試験時間は90分だが、20分ぐらいでだいたい解けた。レベルは模擬問題とほとんど変わらない。見直しに10分ほどかけて、テスト終了。アンケートに答え終わると、正答率100%と表示された。

退室時刻は15時2分。「100点」と書かれたレポートを渡してくれるときも、受付嬢は無表情だった。

文字種チェック用の読みやすくて速い正規表現

前回の日記で「クライアントに的確なエラーメッセージを返すため、文字種のチェックと文字数のチェックは分けて行うべきかもしれない」と書いた。

このうち、文字数のチェックは簡単である。文字数をカウントし、許容文字数を超えていないかどうか調べるだけでよい。

文字種のチェックには悩みどころがある。どのような正規表現を使うのが適切かという点だ。読みやすく、かつ速い正規表現が知りたい。

さて、文字種チェック用の正規表現は2種類に大別できる。

  1. 禁止文字が1文字でも含まれればマッチ
  2. 許容文字のみで構成されていればマッチ

具体的には下記のようなものだ。制御文字(定義は前回の日記を参照)を禁じ、ただし改行とタブは認める例である。

  1. /[\p{C}\p{Zl}\p{Zp}&&[^\t\r\n]]/u
  2. /\A[[^\p{C}\p{Zl}\p{Zp}]\t\r\n]*\z/u

どちらも意図は明確で、読みやすさは変わらない。そう私の目には映る。となれば、速いほうが適切な正規表現ということになる。

どちらの正規表現が速いか、ベンチマークを取ってみる。環境はRuby 1.9.2-p290である。

比較する正規表現

2番の正規表現ではバックトラックを避けることもできる。独占的量指定子を使う方法だ。興味のある方は「404 Blog Not Found:regexp - possessive quantifier (独占的|絶対最大)量指定子とは何か?」を参照されたい。

バックトラックを避ける書き方を含め、下記3パターンの速度を比較する。

名前パターン意図
regexp1/[\p{C}\p{Zl}\p{Zp}&&[^\t\r\n]]/u禁止文字が1文字でも含まれればマッチ
regexp2/\A[[^\p{C}\p{Zl}\p{Zp}]\t\r\n]*\z/u許容文字のみで構成されていればマッチ
regexp2+/\A[[^\p{C}\p{Zl}\p{Zp}]\t\r\n]*+\z/u許容文字のみで構成されていればマッチ(バックトラックなし)

マッチ対象文字列のパターン

マッチ対象文字列は50文字、500文字、5000文字の3パターンを使う。文字数によってマッチに要する速度が変わると予想されるためだ。

また、禁止文字の位置によってもマッチの速度は変わる。禁止文字が先頭に来るケース、末尾に来るケース、含まれないケースの3パターンを調べれば充分だろう。禁止文字は何でもよいのだが、とりあえずNUL(\0)を使う。

文字数が3パターン、禁止文字の位置が3パターンで、全部で9パターンになる。下記の表記を用いれば「50^」「50$」「50x」「500^」「500$」「500x」「5000^」「5000$」「5000x」の9つだ。

表記意味n=5のときの表記n=5のときの文字列
n^NULが先頭に来るn文字の文字列5^"\0aaaa"
n$NULが末尾に来るn文字の文字列5$"aaaa\0"
nxNULが含まれないn文字の文字列5x"aaaaa"

ベンチマークスクリプト

ベンチマークには下記のスクリプトを使う。マッチ対象文字列の文字数(n)を引数で渡せるようにした。ファイル名は何でもよいが、bm.rb としておく。

require 'benchmark'

regexps = {
  "regexp1 " => /[\p{C}\p{Zl}\p{Zp}&&[^\t\r\n]]/u,
  "regexp2 " => /\A[[^\p{C}\p{Zl}\p{Zp}]\t\r\n]*\z/u,
  "regexp2+" => /\A[[^\p{C}\p{Zl}\p{Zp}]\t\r\n]*+\z/u
}

n = ARGV[0].to_i
strings = {
  "#{n}^" => "\0" + "a"*(n-1),  # e.g. "5^" => "\0aaaa"
  "#{n}$" => "a"*(n-1) + "\0",  # e.g. "5$" => "aaaa\0"
  "#{n}x" => "a"*n              # e.g. "5x" => "aaaaa"
}

strings.each do |str_type, str|
  puts str_type
  Benchmark.bm(10) do |bm|
    regexps.each do |re_type, re|
      bm.report("#{re_type}: "){1.upto(100000){re === str}}
    end
  end
  puts "-" * 55
end

ベンチマークの結果

マッチ対象文字列が50文字の場合
$ ruby bm.rb 50
50^
                user     system      total        real
regexp1 :   0.060000   0.000000   0.060000 (  0.056586)
regexp2 :   0.060000   0.000000   0.060000 (  0.063264)
regexp2+:   0.070000   0.000000   0.070000 (  0.065915)
-------------------------------------------------------
50$
                user     system      total        real
regexp1 :   0.340000   0.000000   0.340000 (  0.351705)
regexp2 :   0.250000   0.000000   0.250000 (  0.245143)
regexp2+:   0.200000   0.000000   0.200000 (  0.203424)
-------------------------------------------------------
50x
                user     system      total        real
regexp1 :   0.270000   0.000000   0.270000 (  0.268856)
regexp2 :   0.190000   0.000000   0.190000 (  0.189364)
regexp2+:   0.200000   0.000000   0.200000 (  0.198914)
-------------------------------------------------------
マッチ対象文字列が500文字の場合
$ ruby bm.rb 500
500^
                user     system      total        real
regexp1 :   0.050000   0.000000   0.050000 (  0.053325)
regexp2 :   0.070000   0.000000   0.070000 (  0.065296)
regexp2+:   0.060000   0.000000   0.060000 (  0.068444)
-------------------------------------------------------
500$
                user     system      total        real
regexp1 :   2.580000   0.000000   2.580000 (  2.588114)
regexp2 :   2.120000   0.000000   2.120000 (  2.124538)
regexp2+:   1.440000   0.000000   1.440000 (  1.435810)
-------------------------------------------------------
500x
                user     system      total        real
regexp1 :   2.090000   0.010000   2.100000 (  2.098699)
regexp2 :   1.440000   0.000000   1.440000 (  1.436356)
regexp2+:   1.890000   0.000000   1.890000 (  1.904694)
-------------------------------------------------------
マッチ対象文字列が5000文字の場合
$ ruby bm.rb 5000
5000^
                user     system      total        real
regexp1 :   0.050000   0.000000   0.050000 (  0.046017)
regexp2 :   0.050000   0.000000   0.050000 (  0.050167)
regexp2+:   0.050000   0.000000   0.050000 (  0.051950)
-------------------------------------------------------
5000$
                user     system      total        real
regexp1 :  21.100000   0.000000  21.100000 ( 21.110980)
regexp2 :  17.960000   3.350000  21.310000 ( 21.442088)
regexp2+:  14.350000   0.000000  14.350000 ( 14.356223)
-------------------------------------------------------
5000x
                user     system      total        real
regexp1 :  21.960000   0.000000  21.960000 ( 22.012767)
regexp2 :  14.750000   3.240000  17.990000 ( 17.993943)
regexp2+:  14.570000   0.000000  14.570000 ( 14.578166)
-------------------------------------------------------

結論

ベンチマークの結果から、以下のことが分かる。

  • マッチ対象文字列の先頭に禁止文字があるケースでは、どの正規表現を使っても速度に大差はない
  • それ以外の場合「regexp2+」が速い

よって、文字種チェックには「許容文字のみで構成されていればマッチ(バックトラックなし)」が適切という結論になる。正規表現エンジンが独占的量指定子に対応していなければ、次点の「regexp2」(バックトラックあり)を使えばよい。

なお、文字数チェックを文字種チェックより先にすべきことも分かる。マッチ対象文字列が長くなるほどマッチに時間がかかっているのが明らかだからだ。

Ruby製Webアプリでの書式チェック用正規表現

制御文字は不許可にすべき

前回の日記で「単なる書式チェック(文字種や長さなどのチェック)なので、アプリの要件にしたがって粛々と行えばよい」と書いた「(c)パラメータ文字列の妥当性検証」であるが、実は考えるべきことがそれなりにある。アプリの要件として、各パラメータの「許容文字種」と「許容文字数」を決める必要があるのだ。

このうち許容文字数については話が簡単である。最短および最長の許容文字数を決め、入力値がそれを外れていたらエラーにすればよいだけだ。

問題は許容文字種である。たとえば電話番号の入力を想定したパラメータ文字列であれば「数字またはハイフン」が許容文字種となるだろう。では住所欄や感想欄はどうか。あらゆる文字を受け入れてよいのだろうか。

もちろんアプリによってはあらゆる文字を受け入れなければならない場合もあるだろう。しかし「第11回■制御文字や不正な文字エンコーディングによるぜい弱性を知ろう | 日経 xTECH(クロステック)」で徳丸浩さんが検討されている通り、いわゆる制御文字は不許可としたほうが無難なはずだ。

Perlでの制御文字チェック例

制御文字のチェック方法は、同じく徳丸さんによる「Perlによる入力値検証」を読むと分かりやすい。記事では、許容文字数のチェックも兼ねて下記の正規表現を使っている。

チェック内容 正規表現
制御文字以外100文字以下 /\A\P{Cc}{0,100}\z/
制御文字以外100文字以下
ただし,改行とタブは認める
/\A[\t\r\n\P{Cc}]{0,100}\z/

さて、いよいよ本題である。今回の日記で書きたかったのは以下の2点についてだ。

  • 不許可とすべき制御文字は「Cc」だけか
  • Rubyではどのような正規表現を使えばよいか

不許可とすべき制御文字は「Cc」だけか

徳丸さんの正規表現例で使われている「Cc」はUnicodeのGeneral Categoryの一種である。General Categoryとは各文字に割り当てられるプロパティのひとつで、文字の種別を表したものだ。説明を簡単にするためPHPマニュアルの「Unicode 文字プロパティ」をご覧いただきたい。たとえば大文字アルファベットのGeneral Categoryは「Lu」、10進数字は「Nd」となる。

以下は自作アプリの要件に含めようと考えているだけで声高に主張するわけではないのだが、「Cc」以外の「C」、すなわち「Cf」「Cn」「Co」「Cs」も不許可としてよいのではないか。特に「Cf」には、問題を起こしやすいU+FEFFやU+202Eが含まれている(参考)。

また「Zl」「Zp」も不許可として構わないだろう。現時点で入力値に使われることは考えにくいからだ。

Rubyではどのような正規表現を使えばよいか

Ruby 1.9正規表現エンジンである鬼車のドキュメントFork版鬼車のドキュメントには「Cc」のようなプロパティ指定が使えると書かれている。したがって上記徳丸さんの正規表現Ruby 1.9では問題なく使える。

鬼車ではさらに「Cc」「Cf」「Cn」「Co」「Cs」の総称「C」も使えるし、文字クラスのネストも使える。

よって、Ruby製の自作アプリでは以下のような正規表現を使うことになるだろう。

チェック内容 正規表現
制御文字以外100文字以下 /\A[^\p{C}\p{Zl}\p{Zp}]{0,100}\z/
制御文字以外100文字以下
ただし,改行とタブは認める
/\A[\t\r\n[^\p{C}\p{Zl}\p{Zp}]]{0,100}\z/

実際には、クライアントに的確なエラーメッセージを返すため、文字種のチェックと文字数のチェックは分けて行うべきかもしれない。まとめてチェックすると、どこで引っかかったのかが分からないからだ。

追記(2011-08-10)

Rubyが使っている鬼車がFork版だとわかったので、記事の一部を訂正しました。http://redmine.ruby-lang.org/issues/1889#note-28 にて、まつもとさんが「Our Oniguruma is forked one」とおっしゃっています。

ちなみに鬼車5.x系をRuby 1.9が取り込むにあたっては、Matzにっき(2007-05-25)のような問題がありました。以降の経緯は調べていませんが、結局Forkしたんですね。