2009年3月9日月曜日

正規表現 ゼロ幅、先読み、後読み

正規表現で最近便利な使い方を覚えました。
タイトルにもあるとおり、ゼロ幅の(否定的)先読みです。


http://www.example.com/index/~~
http://www.example.com/list/~~

上記二つのディレクトリ以外を参照する
全ての URL についてマッチする正規表現が必要でした。

先頭が、index または list でなければいいのだから、
[^a-z] のような否定表現と、
(abc|xyz) のような論理和グループを用いて、

^[^(list/?|index/?)][\w/\.]+

とすればいいと思ってました。

ところが、それじゃ動かない…。
色々調べていると、以下のように書けば、出来るということが分かりました。

^(?!(list|index)(/|$))[\w/\.]+

これで、赤字の URL にはマッチしませんが、
青字の URL にはマッチするようになりました。
  • index
  • index/
  • index/sample/file.php
  • list
  • list/
  • list/directory/
  • detail
  • detail/
  • detail/action.php
  • index-hoge
  • list-fuga/input.php
それでは、正規表現の解説をしましょう。
まず、1 文字目の ^ は先頭を表す記号です。

次に、(?!(list|index)(/|$)) の部分ですが、
これをさらに細かく分けて解説しましょう。

まず、(list|index) の部分は、
list または index という文字列に相当する、という意味になります。

そのあとに続く、 (/|$) の部分は、
/ または 行末 に相当する、という意味になります。

これら二つを合わせると、
  • index
  • index/
  • list
  • list/
上記の四つの文字列に相当する正規表現になります。


?! の部分の説明は少し後にします。


後ろについている、[\w/\.]+ はややこしくみえるかもしれませんが、
[A-Za-z0-9/\.]+ と同じ意味です。
英数字、スラッシュ、ピリオドだけで構成される 1 文字以上の文字列、ということになります。
ちなみに、ピリオドの前の \ は単なるエスケープです。


さて、要件として必要な駒がそろいました。
  • (list|index)(/|$) に該当しない文字列
  • [\w/\.]+ に該当する文字列
上記二つの条件を同時に満たすにはどうすればいいでしょうか。


ここで、ゼロ幅というのが役に立ちます。
ゼロ幅というのは、^ や $ と同じで、
なにか特定の文字のことを表すわけではなく、
位置だけを判別するもののことです。


ゼロ幅を活用すれば、正規表現置換の際に、
「~という条件で検索するが、置換対象には入れない」という使い方が可能になります。
^ や $ もそうですよね。
検索対象にはなりますが、置換対象にはなりません。

今回は、文字列 (またはパターン) をゼロ幅として検索します。

例えば、
sample.txt というファイル名があるとします。

txt の拡張子をもつファイルのファイル名だけを置換する場合、
拡張子は変えたくないので、 (\.txt) をゼロ幅で検索する必要があります。
また、ゼロ幅とは、 ^ や $ と同じで、位置だけを判別するものです。
このケースでは、拡張子の前までを置換対象として検索する必要があります。

特定の文字列の前までを置換対象にする場合は、
ゼロ幅の先読みを使用します。
書式は以下のようになります。

.+(?=\.txt)

こうすれば、任意の文字が 1 文字以上で .txt の前まで、という正規表現になります。
条件にマッチする部分の前まで、を表します。

逆に、ファイル名はそのままで、拡張子だけを置換する場合、

(?<=.+\.)[\w]+$

こうすれば、(半角英数字 1 文字以上 + ピリオド) の後ろから行末まで、という正規表現になります。
これは、後読みといいます。条件にマッチする部分の後ろから、を表します。

ゼロ幅の先読み、ゼロ幅の後読みのそれぞれに、
さらに、肯定表現、否定表現があります。
  • (?=[a-z]+)        // ゼロ幅の肯定的先読み - アルファベット小文字の後ろから
  • (?![a-z]+)        // ゼロ幅の否定的先読み - アルファベット小文字ではない文字の前から
  • (?<=[a-z]+)        // ゼロ幅の肯定的後読み - アルファベット小文字の前まで
  • (?<![a-z]+)        // ゼロ幅の否定的後読み - アルファベットの小文字ではない文字の後ろまで


さて、ここまでが、 ?! の部分の説明になります。

元々必要だった要件は、
index にも list にも相当しないディレクトリ名の前からを対象としますので、
ゼロ幅の否定的先読みを選択します。
よって、以下のようになります。

(?!(list|index)(/|$))


よって、URL の第一階層のディレクトリ名が index でも list でもなく、
なおかつ、英数字、 スラッシュ、 ピリオドで構成される文字列を
正規表現でマッチさせるためには、

^(?!(list|index)(/|$))[\w/\.]+

このような表記になるというわけです。

Zend_Controller_Router_Route_Regex などでも活用できそうです。

0 件のコメント: