アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

正規表現の先読み・後読み

コレほんと今まで知らなかったのが残念でならない…。
これを駆使すれば無駄なコードが結構減る場面もありそう。

先読み・後読みとは

先読みは「lookahead」、後読みは「lookbehind」の和訳です。
(後読みは戻り読みと呼ばれることも)

かなりざっくりした説明をすると、
「直後にAという文字列を持つBという文字列」とか
「直前にAという文字列を持たないBという文字列」のようなマッチングができます。

どうやって使うのか

まず、書き方と意味をまとめて紹介すると以下の通り。

肯定的先読み B(?=A)  直後にAを持つB
否定的先読み B(?!A)  直後にAを持たないB
肯定的後読み (?<=A)B 直前にAを持つB
否定的後読み (?<!A)B 直前にAを持たないB

例えば、「B(?=A)」のパターンに対して
「CBA」はマッチしますが
「ABC」はマッチしません。

PHPで書くと以下のような感じですね。

preg_match("/B(?=A)/", "CBA"); // マッチする
preg_match("/B(?=A)/", "ABC"); // マッチしない

「B(?=A)」と「BA」は何が違うのか

このような単純なパターンがマッチするかどうかの判定レベルなら
先読み後読みを使わず「BA」でも問題ありません。
この書き方が役に立つのは、マッチした文字列を抽出するときになります。

「BA」でマッチングした場合はもちろん「BA」という文字列がマッチしますが、
「B(?=A)」でマッチングした場合、あくまで「Aの前にあるB」を抽出することになるので
マッチした文字列は「B」になります。

preg_match("/B(?=A)/", "CBA", $matches);
// → $matches[0] は "B"

preg_match("/BA/", "CBA", $matches);
// → $matches[0] は "BA"

もうちょっと踏み込んだ理解

このような書き方は、「^」や「$」と同じく、位置を示すものです。
例えば「(?=A)」はAという文字列の直前の位置、
「(?<=A)」はAという文字列の直後の位置というわけです。

以下のような比較をしてみると腑に落ちやすいのではないでしょうか。

$str = "abcdefg";

preg_match("/(.*)d(.*)/", $str, $matches);
// 「文字列dの前」「文字列dの後」を抽出
// → $matches[1] は「abc」、$matches[2] は「efg」

preg_match("/(.*)(?=d)(.*)/", $str, $matches);
// 「文字列dの直前の前」「文字列dの直前の後」を抽出
// → $matches[1] は「abc」、$matches[2] は「defg」

preg_match("/(.*)(?<=d)(.*)/", $str, $matches);
// 「文字列dの直後の前」「文字列dの直後の後」を抽出
// → $matches[1] は「abcd」、$matches[2] は「efg」

否定形はちょっとややこしいですが、考え方はもちろん同じで
例えば「(?!A)」はAという文字列の直前を除いたすべての位置、と理解すればOKです。

活用例

活用例1:特定の複数ワードが順不同で含まれているかどうかをチェックする

例えば「^(?=.*a)(?=.*b)」で「aとbが両方とも順不同で含まれている」というパターンになります。
これは先読み後読みがないと一発チェックは難しそう。

以前別記事で書いたので詳しくはそちらへ。
正規表現で、複数のワードが順不同で含まれているかどうかを判定する - アナログCPU:5108843109

活用例2:文字列の複雑な置換

ある文字列について、
「lookbehind」という単語の「look」だけ取り除きたいという場合の書き方。
コードはPHPでの例。

$str = "hello lookahead/lookbehind world";
$str = preg_replace("/(?<![a-zA-Z])look(?=behind[^a-zA-Z]?)/", "", $str);
// → $str は「hello lookahead/behind world」

ここでは、「lookbehind」の文字列の前にも後にも半角英字が付かない場合を
「lookbehindという単語」とみなしています。
なので、lookという文字列に対し「直前にa~z,A~Zのいずれも持たない」という後読み、
「直後にbehind(の直後には英字を含まない)という文字列を持つ」という先読みを利用し
該当する場合に空文字に置き換えています。

これを先読み後読みを使わず実装しようとすると、
置換すべき文字列の位置を求めたあと、改めてreplaceすることになるのではないでしょうか。

活用例3:HTMLタグのスクレイピング

例えば「特定のタグに挟まれたテキストのみをスクレイピングしたい」というケースで
効率よく記述できるようになるケースもあります。

$str = "<p>test</p>";
preg_match("/(?<=\<p\>).*(?=\<\/p\>)/", $str, $matches);
// → $matches[0] は "test"

ここではかなり単純な例なので、先読み後読みを使わずとも実現可能ではありますが、
複雑になればなるほど活用しやすくなるかと思います。

ちなみに:PHPにて、先読み後読みを使わず抽出する例
カッコで括ればその部分を抽出できるので、
戻り値にちょっと無駄が多いものの、この例では先読み後読みがなくてもOKです。

$str = "<p>test</p>";
preg_match("/\<p\>(.*)\<\/p\>/", $str, $matches);
// → $matches[1] は "test"