土曜日, 5月 28, 2011

正規表現 幅ゼロ先読みでよく間違えるのでメモ

正規表現で、いつも混乱するゼロ幅先読みについてメモ。
整理せず思考過程をそのまま記述する。

まず確認として、ゼロ幅とか気にしない正規表現。
具体例が合ったほうが分かりよいので、以下の仕様で考える。

1) aとzだけが使われる
2) 文字列の長さは3以上、10以下

これにマッチする正規表現は^[az]{3,10}$である。ここまでは問題ない。
次に以下の仕様を追加する。

3)zは連続して3つ以上ならんではならない

連続した3つ以上のzとマッチする正規表現は、z{3,}なので、これを幅ゼロ否定先読みしてやればよい。
そして、こう間違える(?!z{3,})^[az]{3,10}$。(※)
この書き方では、zzzaは確かにマッチしないが、aaazzzaaaにはマッチしてしまう。

なぜなら、正規表現のカーソルはマッチした文字列の後ろに移動してしまうから。
aaazzzaaaの場合、文字列の先頭(最初のaの直前)で3連続以上のzを探した後、aaazzzaaaにマッチして、カーソルは文字列の最後に移動してしまう。

正しい正規表現はこうなる(?![az]*z{3,})^[az]{3,10}$
否定先読みの中身だけ取り出すと[az]*z{3,}。つまりaかzが先頭に0個以上あって、その後でzが3つ以上並んでいるかどうかを調べている。

なんで、※のような間違いに至るかについて、自己分析してみる。
人間が文字列を検索するときは基本的に頭から順番に一文字ずつ、対象の文字列と合っているかどうかを調べていく。
対して、正規表現は与えられた個々の正規表現単位で合っているかどうかを調べていく。
aaazzzaaaを^[az]{3,10}$と表す場合、aaazzzaaaで一つの「文字」であり、aaaやzzzという「文字」は存在していない。
この違いを忘れて、人間の検索戦略をそのまま正規表現での検索に当てはめてしまうところに、間違いの原因がある。

ところで、人間であっても実際には正規表現と似たような調べ方を行っている。
例えば「時」という漢字は「日」「寺」に分けられるが、人間が「時」という文字を検索するとき(書体の影響もあるとはいえ)、「日寺」という文字の並びとはマッチさせない。
とはいえ、あえて「時」と「日寺」をマッチさせるような遊びも楽しいのだけど。