PHP正規表現上級編
Look ahead/behind (?=)(?!)(?<=)(?<!)
非常に役に立つ機能、特に[文字列が含まれていない]という判定をする際に
(?=)(?!)(?<=)(?<!)の中身は抽出されない!
後方一致(?=)
$pattern = '/foo(?=bar)/';
preg_match($pattern,'Hello foo'); // BAD
preg_match($pattern,'Hello foobar'); // OK
後方不一致(?!)
$pattern = '/foo(?!bar)/';
preg_match($pattern,'Hello foo'); // OK
preg_match($pattern,'Hello foobar'); // BAD
preg_match($pattern,'Hello foo123'); // OK
前方不一致(?!)
$pattern = '/(?<!foo)bar/';
preg_match($pattern,'Hello bar'); // OK
preg_match($pattern,'Hello foobar'); // BAD
Non-capture (?:)
マッチするけど抽出結果には反映されない
$s = "Bob said: Hi
Bboby said: Hi
Lucy said: How are you
Marry said: good, you?";
//non-capture, マッチするけど、結果$mに反映しない. しかもiなどのmode Modifiersを指定可能
$s = preg_match_all('/(?>bob|marry)\b.*?:(.*)/i', $s, $m);
//capture, マッチかつ結果$mに反映
$s = preg_match_all('/(bob|marry) said:(.*)/i', $s, $m);
Named Capture: (? … ),(?P … ),(?P=foo)
パターンに名前をつけることによって、結果を参照しやすくなったり、replaceしやすくなったり、パターンの再利用もしやすくなったりする。
$s = "Bob said: Hi
Bboby said: Hi
Lucy said: How are you
Marry said: good, you?";
$s = preg_match_all('/(?>bob|marry)\b.*?:(?<msg>.*)/i', $s, $m);
//$m['msg'] is [" Hi"," good, you?"]
Inline Modifiers: (?isx-m)
正規表現式の中にもModifierを指定できる。
以下の2つの書き方の効果は一緒
//inline形式
$s = preg_match_all('/(?i)[a-z]/', $s, $m);
//modifier形式
$s = preg_match_all('/[a-z]/i', $s, $m);
以下の書き方もOK
$s = preg_match_all('/(?is-m)[a-z]/', $s, $m);
//iとsをONにして、mをoffにする
Non-captureの場合は、[:]の前に書いてもOK
$s = preg_match_all('/(?i:[a-z])/', $s, $m);
Atomic Groups (?>)
パフォーマンスを考慮する場合はいっぱい活用できそうな機能。
()内の内容は原子的、細分化不可、一度マッチされるとbacktrackはされないため、通常と書き方と比べるとパフォーマンスがずいぶん良くなるだろうが、マッチングの判定は厳しくなるので要注意。
//null,()内の1個目のbcで失敗すると次のbはマッチされない
preg_match_all('/a(?>bc|b)c/', 'abc', $m);
//matchされるパターン
//一発目で a(bc)cまでマッチングされるため、backtrackが発生しない、regexのエンジンはコレで終了
preg_match_all('/a(?>bc|b)c/', 'abcc', $m);
/*
一般的なgroupの場合は以下のようにbacktrackが発生
a(bc)c > abc : mismatch
a(b)c > abc : match
*/
preg_match_all('/a(bc|b)c/', 'abc', $m);
/*
一般的なgroupの場合は以下の4回に渡ってmatchすることになる
(b+)c > bbbbc : match
(b+)c > bbbc : match
(b+)c > bbc : match
(b+)c > bc : match
一方、(?>b+)cは最初の一回のみ
*/
preg_match_all('/(?>b+)c/', 'bbbbc', $m);
パターンの重複利用
Subrutine (?1)
groupの再利用
//(?1) : 1個目の()で囲まれるgroup、この例だと(?1)=\w+
preg_match_all('/(\w+) (?1)/', 'how are you', $m);
//(?-1) : 1個前の()で囲まれるgroup、この例だと(?-1)=1個前の\w+
preg_match_all('/(\w+) (?-1)/', 'how are you', $m);
//(?+1) : 1個後ろの()で囲まれるgroup、この例だと後ろのgroupはないので何もマッチされない
preg_match_all('/(\w+) (?+1)/', 'how are you', $m);
結果
[["how are"],["how"]]//1番
[["how are"],["how"]]//2番
null/3番
名前を既に持ってる場合は、以下のような書き方もOK
/(?<some_name>\w+) (?&some_name)/
Pre-define(?(DEFINE)( … )( … ))
PHPでは使えません
再帰 Recursive (?R)
nested型のテキストから正しくデータを抽出したい場合は再帰が必要
以下のcssコードから{}内のスタイル定義を正しく抽出したい場合は
body { color: #888; }
@media print { body { color: #333; } }
code { color: blue; }";
再帰を使わない場合は
preg_match_all('/{.*?}/m',$css,$m1);
結果1 : $m1の中身
[
[
"{ color: #888; }",
"{ body { color: #333; }", //失敗 : {}は閉じていない
"{ color: blue; }"
]
]
再帰の観点で、プロセスを整理すると
/
{ # 1個目の '{'を検出
(?: # 抽出対象外のグループ.
[^{}]+ # '{' と '}'以外の文字列とマッチする場合は抽出.
| # そうでない場合は...
(?R) # 再帰コマンド、{}を発見した際に中身に対して繰り返して今の正規表現式でマッチングをかける。
)
* # 下の階層まで繰り返し
} # 閉じる記号の'}'までマッチングをかける。
/
PHPで書くと
preg_match_all('/{(?:[^{}]+|(?R))*}/m',$css,$m2);
結果2 : $m2の中身
[
[
"{ color: #888; }",
"{ body { color: #333; } }", //成功
"{ color: blue; }"
]
]
分岐(IF-ELSE-THEN)
文法
(?(condition)yes-pattern)
(?(condition)yes-pattern|no-pattern)
Example 1 : 日付
$pattern = '/(?(?=^\d{8}$)(\d{4})(\d{2})(\d{2})|(\d{4})\-(\d{2})\-(\d{2}))/';
preg_match_all($pattern, '20160911', $m);
preg_match_all($pattern, '2016-09-11', $m);
$ymd = !empty($m[1][0])? $m[1][0].$m[2][0].$m[3][0] : $m[4][0].$m[5][0].$m[6][0];
Example 2 : HTMLタグ検出
$pattern = '/^(<)?[a-z]+(?(1)>)$/';
preg_match($pattern, '<test>'); //OK
preg_match($pattern, '<foo'); //BAD
preg_match($pattern, 'bar>'); //BAD
preg_match($pattern, 'hello'); //OK
パラメータのエスケープ
以下のようなパラメータをHTTPのリクエストから取得する際に、
エスケープが必要
//例 : ?pattern=*_*
//warningが発生
preg_match('/'.$_REQUEST['pattern'].'/', $text);
//OK
preg_match('/'.preg_quote($_REQUEST['pattern']).'/', $text);
//OK \Q\Eの組み合わせでpreg_quoteの効果を実現
preg_match('/\Q'.$_REQUEST['pattern'].'\E/', $text);