CakeFest 2024: The Official CakePHP Conference

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

繰り返し回数の下限もしくは上限の指定をした場合、 〔繰り返しを指定した要素の〕続きがマッチに失敗すると、 繰り返し指定した要素が再評価され、繰り返し回数を変えた上で 残りのパターンがマッチするかどうか試されます。 マッチングを続けても無駄なことが明らかな場合、マッチングの性質を変え、 より速くマッチに失敗させるために、こうした動作を停止させることが 有用な場合があります。

例えば、パターン \d+foo を "123456bar" という対象文字列に適用した場合を考えてみましょう。

\d+ が 6 桁の数字すべてにマッチしますが、その後 "foo" とのマッチが失敗します。通常のマッチング処理の動作だと、5桁の数字のみが \d+ にマッチするとして再試行され、次いで 4 桁等々と続けられ、 最後には完全にマッチが失敗します。再試行無しのサブパターン (once-only subpattern) を用いると、パターンの一部が一度マッチしたら、 その後再評価されないよう指定することができます。つまり、最初に "foo" とのマッチに失敗した時点で、ただちにマッチングを取り止めることが 可能となります。表記には、

      (?>\d+)bar
      
のように、(?> で始まる特別なカッコを用います。

この種類のカッコは、一度マッチしたパターンの部分に鍵をかけ (lock up) ます。そのパターンへの再マッチは失敗し、バックトラック (backtrack) が起こらないようにします。それより前の要素に対するバックトラックは、 通常と同様に動作します。

別の説明をすると、このタイプのサブパターンは、同一のスタンドアローンの パターンが対象文字列のカレントの位置に固定されたかのように、 文字列とマッチします。

再試行無しのサブパターンは、キャプチャ用サブパターンではありません 〔つまり、値のキャプチャは行われません〕。上の簡単な例では、 できるだけ多くのものを呑み込むよう繰り返しが最大化されました。つまり、 + や +? は残りのパターンがマッチするよう数字の桁数が調整されるのに 対して、 (?>+) は数字の並び全体に対するマッチングだけしか行われません。

この構文には、どんな複雑なものでも、任意のサブパターンを含むことができ、 ネストも可能です。

再試行無しのサブパターンと戻り読み言明とを組み合わせると、 対象文字列の終端における効率的なマッチングを行うことができます。

      abcd$
      
というパターンを見てましょう。 マッチが成功しない長い文字列に適用した場合を考えます。 マッチングは左から右に行われるため、PCRE は対象文字列のすべての "a" を探し、 後に続く文字が残りのパターンにマッチするかどうか調べられます。 パターンを
      ^.*abcd$
      
のように少し変更してみます。 この場合、最初の .* は、まず文字列全体にマッチします。 ("a" がその後に続かないので)マッチが失敗すると、最後の 1 文字を除く 文字列にマッチするようバックトラックが行なわれ、続いて最後の 2 文字を 除く文字列に、という風に動作します。 "a" の検索は、やはり文字列全体に 対して、右から左に、行われるため効率は良くありません。 しかし、パターンを
      ^(?>.*)(?<=abcd)
      
のようにしてみましょう。 要素 .* に対してバックトラックは行われず、文字列全体にのみマッチします。 続く戻り読み言明は、最後の 4 文字に対するテストを 1 回だけ行います。 テストが失敗すると、マッチはただちに失敗します。長い文字列に対しては、 この方法を用いると実行時間にかなりの差が生じます。

パターン中にサブパターンがあって、その中に繰り返し数に 上限の無い要素があり、そのサブパターン自身も何回でも繰り返しが 可能な場合、マッチの失敗に非常に長い時間がかかってしまう事があります。 それを避ける唯一の方法は再試行無しのサブパターンを使うことです。 パターン

      (\D+|<\d+>)*[!?]
      
は、非数字もしくは <> で括られた数字に ! または ? が続く 任意の長さの部分文字列にマッチします。マッチが成功するような 対象文字列に対しては、速く動作します。 しかし、これを
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
      
に適用すると、マッチが失敗するまでに長い時間がかかります。これは、 この対象文字列を分割するやり方が数多くあり、それらすべてに対し マッチを試みられる事になるためです。 (この例で、終端に単一の文字ではなく [!?] を使っているのは、 PCRE と Perl の双方とも、〔終端に〕単一の文字が使われると、 より速く失敗と判定できるように最適化が行われる 〔ので、それを避ける〕ためです。マッチに必要な最後の一文字が記憶され、 文字列にその文字が無い場合、早期に失敗と判定されます。) このパターンを
      ((?>\D+)|<\d+>)*[!?]
      
のように変更した場合、非数字の部分が分割されることがなくなるので、 より速くマッチが失敗するようになります。

add a note

User Contributed Notes 1 note

up
4
Anonymous
1 year ago
Never put a "once-only subpattern" (?>...) in a "one-line" comment (#, //).
I spent almost 1 day to fix it.
Use a 'C' style comment instead !

PHP Manual says, a "one-line" comment (# , //) ends just before "?>" (PHP end tag).

These letters "?>" in a "one-line" comment (# , //) seems evaluated as a PHP end tag.

<?php

/* (?> */
echo '"C" style comment works !<br>';

# (?>
echo '"one-line" comment';

?>
To Top