Hatena::ブログ(Diary)

あらびき日記 Twitter

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

abcで始まらない任意の文字列を抽出したい場合,/^(?!abc).*/のような正規表現を記述します.

「abcで始まらない」を正規表現で表記すると?


この内容を理解するためには「先読み・後読みはアンカー」という考え方が必要になってきます.

アンカーとは文字列内の特定の位置を表す物であり,文字列の先頭を表す ^ や末尾を表す $ がそれにあたります.普通の正規表現では文字に対してマッチしますが,アンカーは位置に対してマッチします.

なので, /^/ という正規表現でパターンマッチの判定をすると必ずtrueを返します.

下の図では赤矢印がアンカーのマッチした位置を示しています.

f:id:a_bicky:20100530130716p:image

php > echo preg_match('/^/', 'hogefuga');
1

※preg_matchはマッチすれば1,しなければ0を返すPHP関数です


先読み・後読みがアンカーだという考え方の下,改めてそれぞれの表現を見ていきましょう.

肯定的先読み(?=pattern)

文字列内にpatternが現れると,patternの直前の位置にマッチします.

f:id:a_bicky:20100530130715p:image

php > echo preg_match('/(?=fuga)/', 'hogefuga');
1

ここではfugaの手前の位置にマッチしているので,fugaの手前の文字列を全て抽出するには次のようにします.

f:id:a_bicky:20100530130713p:image

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の直前の位置をマッチから外します.つまり,肯定的先読みでマッチする位置以外にマッチします.

f:id:a_bicky:20100530130714p:image

php > echo preg_match('/(?!fuga)/', 'hogefuga');
1

次の正規表現では直後にfugaが現れない1文字を抽出します.上記の説明から,eとfの間の位置にはマッチせず,その直前にあるeは抽出されません.

f:id:a_bicky:20100530130712p:image

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の最後の文字の直後)の位置にマッチします.

f:id:a_bicky:20100530130711p:image

php > echo preg_match('/(?<=hoge)/', 'hogefuga');
1

否定的後読み(?<!pattern)

とりあえず全ての位置にマッチしておいて,文字列内にpatternが現れると,patternの直後(patternの最後の文字の直後)の位置をマッチから外します.つまり,肯定的後読みでマッチする位置以外にマッチします.

f:id:a_bicky:20100530130710p:image

php > echo preg_match('/(?<!hoge)/', 'hogefuga');
1

先読みを使ってhogeで始まらない文字列かを判定する

まず,任意の位置に対してhogeで始まらない位置を抽出するには否定的先読みを使うことで実現できます.

f:id:a_bicky:20100530130709p:image


この結果に対して,文字列の先頭を表すアンカーである ^ も併用することで位置を先頭のみに限ることができます.

f:id:a_bicky:20100530130716p:image


以上を踏まえると,2つの正規表現を合わせた/^(?!hoge)/を使うことで,hogeで始まらない文字列かを判定することができます.

hogefugaはhogeで始まっているのでfalseを返します.

f:id:a_bicky:20100530130708p:image

php > echo preg_match('/^(?!hoge)/', 'hogefuga');
0

ちなみにfugaで始まらない文字列かを判定するには/^(?!fuga)/になります.

hogefugaはfugaで始まっていないのでtrueを返します.

f:id:a_bicky:20100530130714p:imagef:id:a_bicky:20100530130716p:image f:id:a_bicky:20100530130707p:image

php > echo preg_match('/^(?!fuga)/', 'hogefuga');
1

参考

K2Editorでの正規表現の解説


2011/2/16追記

詳説正規表現を読んでみましたが,この辺の内容はかなり序盤の方に丁寧に説明されていました。正規表現エンジンの仕組みなどにも触れられていて、普段正規表現を書く方は絶対に読んでおくべき一冊だと思います!!


*1:「直後にabcがない先頭」と言うこともできますが、執筆当時の私の理解度では意味不明でした

名無し名無し 2014/03/06 10:10 非常にわかりやすかった^−^v

a_bickya_bicky 2014/03/06 22:43 ありがとうございます〜

たかなしたかなし 2015/09/11 14:39 後読みが理解できずに検索してきました。
おかげで分かりました。

a_bickya_bicky 2015/10/18 12:46 お役に立てたようで何よりです!

ケニーさんケニーさん 2016/08/17 20:45 正規表現をずっと前から使いたかったのですが、よくあるメタ文字の説明やサンプルなどを一生懸命見ても全然解らなかったのが、この記事で初めて理解ができました。感謝感激です!
ありがとうございます!とても良い記事ですね。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/a_bicky/20100530/1275195072