読者です 読者をやめる 読者になる 読者になる

正規表現再入門

最近ピザとかも出るようになった社内勉強会(仮)で発表した資料がでてきたので置いておきます。

先日、とあるサイトで再帰的パターンというものを知りまして、改めて PHP の PCRE のページを見てみると、知らない構文とか、知ってはいたけど全く使っていないなー、という構文が結構あったので、改めて一通り見てみました。

その中から、わりと知られていなさそうなものをピックアップしてみました。


ここからの内容は reveal.js でスライド表示されている Markdown ファイルをそのまま張り付けています。

スライドとして見る場合は↑の方にリンクを置いているのでそこから見てください。

.

.

.

正規表現再入門


PHP の PCRE のページ、よく見たら知らないものが いろいろあったので、改めて一通り見てみました

http://php.net/manual/ja/reference.pcre.pattern.syntax.php


デリミタ


正規表現でデリミタといえば、

//

とか

##

のように同じ文字を最初と最後に使いますが


実は対応したカッコも使えます () とか {} とか [] とか <> とか

preg_match("(\d+)", 'abc123xyz', $m);
var_dump($m); // "123"

preg_match("{\d+}", 'abc123xyz', $m);
var_dump($m); // "123"

preg_match("[\d+]", 'abc123xyz', $m);
var_dump($m); // "123"

preg_match("<\d+>", 'abc123xyz', $m);
var_dump($m); // "123"

あんまり使いみちはなさそう


空白要素とコメント


x 修飾子を付けると空白要素やコメントを無視できます

$p = <<<'EOS'
# 英字
[a-zA-Z]+

# ドット
\.

# 数字
\d+
EOS;

preg_match("/$p/x", '123abc.789xyz', $m);
var_dump($m); // "abc.789"

次のように書いているのと同じです

preg_match("/[a-zA-Z]+\.\d+/", '123abc.789xyz', $m);
var_dump($m); // "abc.789"

超長い正規表現を書くときに便利そう


コメント


(?# から次の ) まではコメントになります(ネスト不可)

preg_match('/(?#これはコメントです)\d+/', 'abc123xyz', $m);
var_dump($m); // "123"

次のように書いているのと同じです

preg_match('/\d+/', 'abc123xyz', $m);
var_dump($m); // "123"

x 修飾子の方がいいと思う


言明


マッチ結果には含めずに直前や直後の文字をテストします

// 先読みの言明 ... 数字3つの後が "abc" である文字列にマッチ
preg_match_all('/\d{3}(?=abc)/', '123abc 789xyz', $m);
var_dump($m[0]); // "123"

// 先読みの否定言明 ... 数字3つの後が "abc" ではない文字列にマッチ
preg_match_all('/\d{3}(?!abc)/', '123abc 789xyz', $m);
var_dump($m[0]); // "789"

// 戻り読みの言明 ... 数字3つの前が "abc" である文字列にマッチ
preg_match_all('/(?<=abc)\d{3}/', 'abc123 xyz789', $m);
var_dump($m[0]); // "123"

// 戻り読みの否定言明 ... 数字3つの前が "abc" ではない文字列にマッチ
preg_match_all('/(?<!abc)\d{3}/', 'abc123 xyz789', $m);
var_dump($m[0]); // "789"

そこそこ使う


単語境界の言明


\b で単語の境界の言明

preg_match_all('/\b\d\w*/', '123abc abc456 abc/789 ', $m);
var_dump($m[0]); // "123abc" "789"
  • "123abc""1" が文字列の先頭で単語の始まりなのでマッチする
  • "abc456""4" が単語の始まりじゃないのでマッチしない
  • "abc/789""/" があるので "7" が単語の始まりになりマッチする

要するに (?<!\w)(?=\w) だと思う

preg_match_all('/(?<!\w)(?=\w)\d\w*/', '123abc abc456 abc/789 ', $m);
var_dump($m[0]); // "123abc" "789"

直前が \w ではなく、かつ、\w にマッチ


知ってたけど使ったこと無い


マッチ結果の位置のリセット


マッチ結果の開始位置を \K の位置にリセットする つまり \K より前の文字がマッチ結果に含まれなくなる

preg_match('/abc\Kxyz/', 'abcxyz', $m);
var_dump($m); // "xyz"

正規表現は "abcxyz" にマッチしているけど結果は "xyz" だけ


ただしサブパターンによるキャプチャには影響しない

preg_match('/(abc\Kxyz)/', 'abcxyz', $m);
var_dump($m); // "xyz", "abcxyz"

あんまり使いみちはなさそう


名前付きサブパターン


サブパターンに名前を付けてキャプチャ結果を連想配列にする

preg_match('/(?P<sub>\d+)/', 'abc123xyz', $m);
var_dump($m['sub']); // "123"

preg_match('/(?<sub>\d+)/', 'abc123xyz', $m);
var_dump($m['sub']); // "123"

preg_match("/(?'sub'\d+)/", 'abc123xyz', $m);
var_dump($m['sub']); // "123"

なにかの WAF で Routing に使われてたかも?


重複した後方参照番号


次の例だと (abc)(yz) は異なる数字添字

preg_match_all('/123(?:(abc)|x(yz))/', '123abc 123xyz', $m);
var_dump($m[1]); // "abc" ""
var_dump($m[2]); // "" "yz"

?| を使うと (abc)(yz) は同じ数字添字

preg_match_all('/123(?|(abc)|x(yz))/', '123abc 123xyz', $m);
var_dump($m[1]); // "abc" "yz"

知っていれば使うこともあるかも


独占的量指定子


量指定子の後に + を付けるとバックトラックしなくなる

// 貪欲的
preg_match('/\w*a/', '123abc123abc#123', $m);
var_dump($m); // "123abc123a"

// 非貪欲的
preg_match('/\w*?a/', '123abc123abc#123', $m);
var_dump($m); // "123a"

// 独占的
preg_match('/\w*+a/', '123abc123abc#123', $m);
var_dump($m); // no match

言葉で説明するのは難しい・・・


/\w*+a/\w* を最も長くマッチした後に a にマッチしなければテストは失敗します


日本語的に説明すると

  • 貪欲的
    • なるべく長くマッチしてダメだったら少し短くして再試行
  • 非貪欲的
    • なるべく短くマッチしてダメだったら少し長くして再試行
  • 独占的
    • ひたすら長くマッチしてダメだったら後は知らん

次のように独占的でもそうじゃなくても同じなら (マッチしないときの)性能の向上が見込める

// 貪欲的
preg_match('/\d*a/', '123123abc', $m);
var_dump($m); // "123123a"

// 非貪欲的
preg_match('/\d*?a/', '123123abc', $m);
var_dump($m); // "123123a"

// 独占的
preg_match('/\d*+a/', '123123abc', $m);
var_dump($m); // "123123a"

次のような使い方もできます

// .jpg で終わる連続する非空白要素にマッチ
preg_match_all('/\S++(?<=\.jpg)/', 'a.jp b.jpg c.jpghoge', $m);
var_dump($m); // "b.jpg"

次のようにしても結果は同じですけど

// .jpg で終わる連続する非空白要素にマッチ
preg_match_all('/\S+\.jpg(?!\S)/', 'a.jp b.jpg c.jpghoge', $m);
var_dump($m); // "b.jpg"

覚えておいて損はないかも


後方参照の相対指定


\g{-1} のように負数を指定すると相対で後方参照できる

preg_match('/(\d+)(\w+)\g{-1}\g{-2}/', '123abcabc123', $m);
var_dump($m); // "123abcabc123" "123" "abc"
  • \g{-1} は1つ前の (\w+) でマッチした文字列
  • \g{-2} は2つ前の (\d+) でマッチした文字列

これは次の例と同じです

preg_match('/(\d+)(\w+)\2\1/', '123abcabc123', $m);
var_dump($m); // "123abcabc123" "123" "abc"
  • \2 は2つ目のサブパターンの (\w+) でマッチした文字列
  • \1 は1つ目のサブパターンの (\d+) でマッチした文字列

あまり使わなさそう


再試行無しのサブパターン


(?> で始まるサブパターンは再試行されません

preg_match('/(?>\w*)a/', '123abc123abc#123', $m);
var_dump($m); // no match

preg_match('/(?>\d*)a/', '123123abc', $m);
var_dump($m); // "123123a"

独占的量指定子とあまり変わらないような気がします

preg_match('/\w*+a/', '123abc123abc#123', $m);
var_dump($m); // no match

preg_match('/\d*+a/', '123123abc', $m);
var_dump($m); // "123123a"

条件付きサブパターン


条件に応じてパターンを使い分けることができます

(?(条件)真パターン)
(?(条件)真パターン|偽パターン)

パターンには次のものが使えます。

  • 数字
    • その番号のサブパターンにマッチしていれば真
  • 言明
    • その言明にマッチすれば真
  • "R"
    • 再帰パターンに再帰していると真
    • 再帰していないトップレベルだと偽

良い例が思いつかないのでパス


再帰的パターン


こういうの

$str = "dummy(1+1), dummy(2+(3*4)), dummy(5-dummy(6*7)), dummy(2+((6/3)*(4-1)))";
$pattern = 'dummy (\( (?: [-+*\/0-9]++ | (?1) )* \))';
preg_match_all("/$pattern/x",$str,$match);

print_r($match[0]);
/*
Array
(
    [0] => dummy(1+1)
    [1] => dummy(2+(3*4))
    [2] => dummy(6*7)
    [3] => dummy(2+((6/3)*(4-1)))
)
*/

開き括弧と閉じ括弧の対応にマッチします


わかりにくいのでバラしてみます


  • dummy
  • ( ※1番目のサブパターンの開始
    • \( ※開き括弧
    • (?:
      • [-+*\/0-9]++ ※式っぽい文字の繰り返し
      • |
      • (?1) ※1番目のサブパターンに再帰
    • )
    • *
    • \) ※閉じ括弧
  • ) ※1番目のサブパターンの終了

  • (?R) でパターン全体に再帰
  • (?1) とか (?2) とかはサブパターンに再帰
  • (?P>name) とか (?&name) とかで名前付きサブパターンに再帰

回分にマッチする正規表現もできました

$pattern = '/(.)(?:(?R)|.)?\1/u';

$str = "キツツキがトマトを食べたらしんぶんしがたけやぶやけた";
preg_match_all($pattern, $str, $m);
var_dump($m[0]); // "キツツキ" "トマト" "しんぶんし" "たけやぶやけた"

$str = "ああ、みたいな2文字でも回文になってしまうので不完全";
preg_match_all($pattern, $str, $m);
var_dump($m[0]);

Wikipedia の回分のページにあった7文字以上の回分

$doc = new DOMDocument();
$doc->loadHTMLFile('https://ja.wikipedia.org/wiki/%E5%9B%9E%E6%96%87');
$xpath = new DOMXpath($doc);
$str = $xpath->query("id('mw-content-text')")[0]->textContent;

$pattern = '/(.)(?:(?R)|.)?\1/u';
preg_match_all($pattern, $str, $m);
$a = array_unique(array_filter($m[0], function ($s) { return mb_strlen($s) >= 7; }));

print_r($a);
// akasaka
// わかみかものとかなかとのもかみかわ
// もくよとんとことんとよくも
// しみしかししかしみし
// みなくさのなははくとしれくすりなりすくれしとくははなのさくなみ
// たのむそのいかにもにかいのそむのた
// さかのなはやとりたりとやはなのかさ
// の世しばしよしばし世の
// かなのよしはしよしはしよのなか
// まさかさかさま
// アニマルマニア
// スキトキメキトキス

"たいもくよとんとことんとよくもいた" のように上手くマッチしないものもあって不完全


正規表現・・・奥が深い


おわり