livedoor Blogに移動します
今後の記事は http://iwamot.ldblog.jp/ に書きます。はてなダイアリー以外のブログサービスをまともに使ったことがなく、これを機会に使ってみたくなったというのが本音です。出戻る可能性は大いにありますが、物は試しということで。
追記(2012-04-12)
古い記事にコメントが付くので、コメントを受け付けない設定に変えました。
はてなブックマークからDeliciousに移動します
ブックマークを http://b.hatena.ne.jp/IwamotoTakashi/ から http://www.delicious.com/iwamot に移動します。もちろん今般のトラッキング問題が大きなきっかけではありますが、それより前に「新ユーザーページの使いづらさ問題」という小さなきっかけもありました。自分のブックマークでなく「マイホットエントリー」をデフォルトにするセンスが僕には合いません。
優秀なエンジニアの退職はやまず、はてなアイデアは放置されっぱなし。今回のような大きな問題が起きても、社長が外遊中のためか対応が遅い。はてなには上場より先にやるべきことがあるのではないかと苦言を呈したくもなります。
ブックマークだけでなく日記も、そのうち別のサービスに移動しようと考えています。
JP ISBN Linkerを更新した
JP ISBN Linker がFirefox 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〜50 | 30問 | 合格ラインは正答率75%なので、これでは不合格になってしまう。誤答部分の知識を補うべく、あわててRuby 1.8.7のマニュアルを読んだ。 |
1月9日 | 問題51〜100 | 43問 | 勉強の成果があり、合格ラインに乗った。 |
1月11日 | 問題1〜50 | 47問 | これで合格の自信がついた。 |
1月13日 | 問題51〜100 | 50問 | 本番でも満点を出したいと思った。 |
1月14日 | 問題1〜50 | 49問 | ケアレスミスが1問あったので、本番では気をつけようと思った。 |
受験日当日
14時30分からの回を申し込んでいた。会場は新宿エルタワーのISAキャリアカレッジ。早く着きすぎてしまい、14時10分に名前を呼ばれるまで、しばらく待った。
受付嬢がいくつか説明してくれるのだが、何から何まで事務的で、逆に感心させられた。あれだけ心を込めずに仕事ができる人が、皮肉ではなくうらやましい。
入室後、すぐに試験開始。試験時間は90分だが、20分ぐらいでだいたい解けた。レベルは模擬問題とほとんど変わらない。見直しに10分ほどかけて、テスト終了。アンケートに答え終わると、正答率100%と表示された。
退室時刻は15時2分。「100点」と書かれたレポートを渡してくれるときも、受付嬢は無表情だった。
文字種チェック用の読みやすくて速い正規表現
前回の日記で「クライアントに的確なエラーメッセージを返すため、文字種のチェックと文字数のチェックは分けて行うべきかもしれない」と書いた。
このうち、文字数のチェックは簡単である。文字数をカウントし、許容文字数を超えていないかどうか調べるだけでよい。
文字種のチェックには悩みどころがある。どのような正規表現を使うのが適切かという点だ。読みやすく、かつ速い正規表現が知りたい。
さて、文字種チェック用の正規表現は2種類に大別できる。
- 禁止文字が1文字でも含まれればマッチ
- 許容文字のみで構成されていればマッチ
具体的には下記のようなものだ。制御文字(定義は前回の日記を参照)を禁じ、ただし改行とタブは認める例である。
- /[\p{C}\p{Zl}\p{Zp}&&[^\t\r\n]]/u
- /\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" |
nx | NULが含まれない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) -------------------------------------------------------
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」だけか
徳丸さんの正規表現例で使われている「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したんですね。