Narcissus の正規表現2008年05月22日 13時34分

前のエントリで書き忘れてた - 最速チュパカブラ研究会」にて、Narcissus で使われている正規表現が参考になるという話が出ています。

  • 文字列リテラル /^"(?:\\.|[^"])*"|^'(?:[^']|\\.)*'/
  • 正規表現リテラル /^\/((?:\\.|[^\/])+)\/([gimy]*)/
  • コメント /^\/(?:\*(?:.|\n)*?\*\/|\/.*)/

一流の人が書いたものを使いましょうというのに異を唱えるつもりはありませんが、そのままコピー & ペーストしていては意味がありません。ここはやはり一文字一文字心をこめて写経しましょう……ではなく、どうしてその書き方でうまくいくのかをきちんと考えた上で使いましょう。

文字列リテラルにマッチする正規表現

上記の文字列リテラルを表す正規表現から、一重引用符でくくられた文字列にマッチする部分だけを抜き出すと '(?:[^']|\\.)*' となります。途中 \\. とあるのはエスケープシーケンスを許容するためですね。ではエスケープシーケンスを含む文字列 'It\'s sunny.' が、この正規表現パターンとどうマッチしていくのか見ていきましょう。

  1. 文字 ' がパターン ' とマッチする。
  2. 文字 I がパターン [^'] とマッチする。
  3. 文字 t がパターン [^'] とマッチする。
  4. 文字 \ がパターン [^'] とマッチする。
  5. 文字 ' がパターン [^'] とのマッチに失敗する。
  6. 文字 ' がパターン \\. とのマッチに失敗する。
  7. 文字 ' がパターン ' とマッチする。
  8. パターンの終端まで来たので、マッチ成功として終了する。

マッチは成功しましたが、得られた文字列は 'It\' だけです。エスケープシーケンスを適切に処理できていません。

実はこの正規表現、文字列リテラルにマッチするという目的からすると間違っています。すなわち、Narcissus のバグというわけです。エスケープシーケンスを正しく処理するためには、グループ内の選択の順番を入れ替えて '(?:\\.|[^'])*' としなくてはいけません。

これで大丈夫かと思いきや、まだ落とし穴があります。'It\'s sunny. という終端のない文字列とマッチする場合を考えて見ましょう。この場合、最終的には文字 ' がパターン 'と、文字 It\ がパターン [^'] と、文字 ' がパターン ' とマッチしてマッチが終了します。不正な形式の文字列に対してもマッチ成功となってしまうのです。

しかし、この問題は Narcissus では表面化しません。Narcissus ではマッチした文字列から文字列値を取得するために eval 関数を使っているからです。'It\' という文字列を eval 関数で解析すれば、当然終端のない文字列としてエラーが出ます。そして 'It\' のような文字列がマッチするのはそもそも入力にエラーが含まれていたときだけです。エラーを含む入力があったときにエラーになるという、ごく当然の結果になるのです。

なお、不正な文字列に対してはマッチが失敗してほしいというときは、パターンを '(?:\\.|[^'\\])*' とする必要があります。さらに、一般的な入力にはエスケープシーケンスでない文字のほうが多く含まれると考えられるので、これは '(?:[^'\\]|\\.)*' と選択の順番を入れ替えたほうが実行速度が上がるような気がします。しかし、なぜか SpiderMonkey では前者に対して後者のほうが約 60% も遅く、JScript でも両者の実行時間はほぼ同じです。

正規表現リテラルにマッチする正規表現

JavaScript 1.5 までは /[/]/ という入力は /[/ までが正規表現リテラルとみなされ、エラーとなっていました。しかし、JavaScript 1.6 以降では JScript の動作に合わせ、/[/]/ が 1 文字のスラッシュにマッチする正規表現として扱われます。文字クラス内ではスラッシュのエスケープが不要になったのです。上記の正規表現ではこのような入力を扱えないため、以下のように書き換える必要があります。

/^\/((?:\\.|\[(?:\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/

これも文字列リテラルのときと同じく、終端のない文字クラスを含む正規表現リテラルなどにもマッチしてしまう (が、Narcissus では問題が表面化しない) ことに注意してください。

任意の文字にマッチする正規表現

上記のコメントにマッチする正規表現内で、任意の文字にマッチするパターンとして (?:.|\n) が使われていますが、これは [\s\S] などとしたほうが速いです (SpiderMonkey、JScript ともに約 20% の高速化)。

ただし、Safari 2 以下で後者のパターンを使うと、ASCII の範囲外の文字 (UTF-8 でのマルチバイト文字) でのマッチに不具合が出るかもしれません。Safari 2 以下では \uxxxx というパターンに関する不具合もあったりと、ASCII の範囲外の文字と正規表現との組み合わせがいまいち良くないようです (伝聞)。

まとめ

  • Narcissus で使われている正規表現は、Narcissus での使用を前提としています。下手にそのまま流用すると思わぬところで引っかかるかもしれません。
  • 「文字列リテラルにマッチする正規表現」の内容はすべて「詳説 正規表現」に書かれているはずです (少なくとも第 2 版には書かれていました)。正規表現を語るうえで外せない一冊だと思います。
  • 弘法にも筆の誤りはありますが、その数は非常に少ないです。下手に自分で考えて誤爆するより、一流の人が書いたものを使いましょう。ただし、それがどういう意味なのかは考えたほうがいいです。

なお、上述した文字列リテラル及び正規表現リテラルにマッチする正規表現の不具合は、すでに修正されています。

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2008/05/22/3535735/tb