2010-05-30
正規表現の先読み・後読みを極める!
柔軟性の高い正規表現を書こうとすると,避けて通れないのが先読み・後読みです.
先読み・後読みに関して,いままではとりあえず的な理解をしていたのですが,それだと説明できない正規表現に遭遇したので,説明できるまで理解を深めてみました.
とりあえず的な理解
正規表現を使って間もない人が先読み・後読みを理解するための説明です.
肯定的先読み(?=pattern)
次の正規表現では直後にbarがあるfoo(barは含まない)に一致します.
foo(?=bar)
否定的先読み(?!pattern)
次の正規表現では直後にbarがないfoo(barは含まない)に一致します.
foo(?!bar)
肯定的後読み(?<=pattern)
次の正規表現では直前に barがあるfoo(barは含まない)に一致します.
(?<=bar)foo
否定的後読み(?<!pattern)
次の正規表現では直前にbarがないfoo(barは含まない)に一致します.
(?<!bar)foo
先読み・後読みはアンカー
上記程度の理解だと次の内容を説明できません.*1
この内容を理解するためには「先読み・後読みはアンカー」という考え方が必要になってきます.
アンカーとは文字列内の特定の位置を表す物であり,文字列の先頭を表す ^ や末尾を表す $ がそれにあたります.普通の正規表現では文字に対してマッチしますが,アンカーは位置に対してマッチします.
なので, /^/ という正規表現でパターンマッチの判定をすると必ずtrueを返します.
下の図では赤矢印がアンカーのマッチした位置を示しています.
php > echo preg_match('/^/', 'hogefuga'); 1
※preg_matchはマッチすれば1,しなければ0を返すPHPの関数です
先読み・後読みがアンカーだという考え方の下,改めてそれぞれの表現を見ていきましょう.
肯定的先読み(?=pattern)
文字列内にpatternが現れると,patternの直前の位置にマッチします.
php > echo preg_match('/(?=fuga)/', 'hogefuga'); 1
ここではfugaの手前の位置にマッチしているので,fugaの手前の文字列を全て抽出するには次のようにします.
php > preg_match('/.*(?=fuga)/', 'hogefuga', $matches); echo $matches[0]; hoge
(?=fuga)の前後の文字列を抽出すると,より一層理解が深まるかもしれません.
php > preg_match('/(.*)(?=fuga)(.*)/', 'hogefuga', $matches); echo $matches[1]."\n"; echo $matches[2]; hoge fuga
fugaも抽出されていることから,(?=fuga)はあくまで位置を表していることがわかるかと思います.
否定的先読み(?!pattern)
とりあえず全ての位置にマッチしておいて,文字列内にpatternが現れると,patternの直前の位置をマッチから外します.つまり,肯定的先読みでマッチする位置以外にマッチします.
php > echo preg_match('/(?!fuga)/', 'hogefuga'); 1
次の正規表現では直後にfugaが現れない1文字を抽出します.上記の説明から,eとfの間の位置にはマッチせず,その直前にあるeは抽出されません.
php > preg_match_all('/.(?!fuga)/', 'hogefuga', $matches); foreach($matches[0] as $match) echo $match."\n"; h o g f u g a
ちなみに次の正規表現は絶対にマッチしない正規表現となっています.
php > echo preg_match('/(?!fuga)fuga/', 'hogefuga'); 0
肯定的後読み(?<=pattern)
文字列内にpatternが現れると,patternの直後(patternの最後の文字の直後)の位置にマッチします.
php > echo preg_match('/(?<=hoge)/', 'hogefuga'); 1
否定的後読み(?<!pattern)
とりあえず全ての位置にマッチしておいて,文字列内にpatternが現れると,patternの直後(patternの最後の文字の直後)の位置をマッチから外します.つまり,肯定的後読みでマッチする位置以外にマッチします.
php > echo preg_match('/(?<!hoge)/', 'hogefuga'); 1
先読みを使ってhogeで始まらない文字列かを判定する
まず,任意の位置に対してhogeで始まらない位置を抽出するには否定的先読みを使うことで実現できます.
この結果に対して,文字列の先頭を表すアンカーである ^ も併用することで位置を先頭のみに限ることができます.
以上を踏まえると,2つの正規表現を合わせた/^(?!hoge)/を使うことで,hogeで始まらない文字列かを判定することができます.
hogefugaはhogeで始まっているのでfalseを返します.
php > echo preg_match('/^(?!hoge)/', 'hogefuga'); 0
ちなみにfugaで始まらない文字列かを判定するには/^(?!fuga)/になります.
hogefugaはfugaで始まっていないのでtrueを返します.
php > echo preg_match('/^(?!fuga)/', 'hogefuga'); 1
参考
2011/2/16追記
詳説正規表現を読んでみましたが,この辺の内容はかなり序盤の方に丁寧に説明されてますね…