web-dev-qa-db-ja.com

ゼロ幅のルックビハインドアサーションで繰り返し数量詞を使用できないのはなぜですか?

ゼロ幅アサーション(Perl互換正規表現[PCRE])では繰り返し数量詞を使用できないという印象を常に受け​​ていました。しかし、最近、あなたが先読みアサーションでそれらを使用できることがわかりました。

ゼロ幅のルックビハインドで検索すると、PCRE正規表現エンジンはどのように機能するので、繰り返し数量詞を使用できなくなりますか?

RのPCREからの簡単な例を次に示します。

# Our string
x <- 'MaaabcccM'

##  Does it contain a 'b', preceeded by an 'a' and followed by zero or more 'c',
##  then an 'M'?
grepl( '(?<=a)b(?=c*M)' , x , Perl=T )
# [1] TRUE

##  Does it contain a 'b': (1) preceeded by an 'M' and then zero or more 'a' and
##                         (2) followed by zero or more 'c' then an 'M'?
grepl( '(?<=Ma*)b(?=c*M)' , x , Perl = TRUE )
# Error in grepl("(?<=Ma*)b(?=c*M)", x, Perl = TRUE) :
#   invalid regular expression '(?<M=a*)b(?=c*M)'
# In addition: Warning message:
# In grepl("(?<=Ma*)b(?=c*M)", x, Perl = TRUE) : PCRE pattern compilation error
#         'lookbehind assertion is not fixed length'
#         at ')b(?=c*M)'
34
Simon O'Hanlon

このような質問に対する最終的な答えはエンジンのコードにあり、答えの下部で、後読みの固定長を確保する役割を担うPCREエンジンのコードのセクションに飛び込むことができます。最高の詳細を知っています。それまでの間、より高いレベルから質問に徐々にズームインしていきましょう。

可変幅ルックビハインドvs.無限幅ルックビハインド

まず、用語について簡単に説明します。ますます多くのエンジン(PCREを含む)が、変動が決定された範囲内にある、ある種の可変幅ルックビハインドをサポートしています。

  • エンジンは、先行するものの幅がwithin5〜10文字でなければならないことを認識しています(PCREではサポートされていません)
  • エンジンは、先行するものの幅が5またはのいずれかでなければならないことを認識しています。 10文字(PCREでサポート)

対照的に、無限幅のルックビハインドでは、_a+_などの定量化されたトークンを使用できます。

無限幅のルックビハインドをサポートするエンジン

記録として、これらのエンジンは無限のルックビハインドをサポートしています。

  • .NET(C#、VB.NETなど)
  • Matthew Barnettの Python用のregexモジュール
  • JGSoft(EditPadなど。プログラミング言語では使用できません)。

私の知る限り、彼らだけです。

PCREの可変ルックビハインド

PCREでは、ドキュメントで最も関連性の高いセクションは次のとおりです。

後読みアサーションの内容は、一致するすべての文字列が固定長でなければならないように制限されています。ただし、トップレベルの選択肢が複数ある場合は、すべてが同じ固定長である必要はありません。

したがって、次のルックビハインドが有効です。

_(?<=a |big )cat
_

ただし、これらはいずれも次のとおりではありません。

  • _(?<=a\s?|big )cat_(交互の辺の幅は固定されていません)
  • _(?<=@{1,10})cat_(可変幅)
  • _(?<=\R)cat_(_\R_は_\n_、_\r\n_などと一致する可能性があるため、固定幅はありません。)
  • _(?<=\X)cat_(_\X_には、Unicode書記素クラスターに可変バイト数を含めることができるため、固定幅はありません。)
  • _(?<=a+)cat_(明らかに修正されていません)

ゼロ幅の一致が無限の繰り返しで後ろを振り返る

今これを考慮してください:

_(?<=(?=@+))(cat#+)
_

表面的には、これは固定幅のルックビハインドです。これは、ゼロ幅の一致(先読み_(?=@++)_で定義)しか検出できないためです。それは、無限の背後にある制限を回避するためのトリックですか?

いいえ。PCREはこれを窒息させます。後読みの内容がゼロ幅であっても、PCREでは後読みで無限に繰り返すことはできません。どこでも。一致するすべての文字列が固定長でなければならないとドキュメントに記載されている場合、実際には次のようになります。

そのコンポーネントのいずれかが一致するすべての文字列は、固定長である必要があります。

回避策:無限の後ろ姿のない生活

PCREでは、無限のルックビハインドが役立つ問題の2つの主な解決策は、_\K_とキャプチャグループです。

回避策#1:_\K_

_\K_アサーションは、エンジンが返す最終一致からこれまでに一致したものを削除するようにエンジンに指示します。

PCREでは無効な_(?<=@+)cat#+_が必要だとします。代わりに、次を使用できます。

_@+\Kcat#+
_

回避策#2:グループのキャプチャ

続行する別の方法は、後読みに配置したものと一致させ、キャプチャグループで対象のコンテンツをキャプチャすることです。次に、キャプチャグループから一致を取得します。

たとえば、違法な_(?<=@+)cat#+_の代わりに、次のものを使用します。

_@+(cat#+)
_

Rでは、これは次のようになります。

_matches <- regexpr("@+(cat#+)", subject, Perl=TRUE);
result <- attr(matches, "capture.start")[,1]
attr(result, "match.length") <- attr(matches, "capture.length")[,1]
regmatches(subject, result)
_

_\K_をサポートしていない言語では、これが唯一の解決策であることがよくあります。

エンジン内部:PCREコードは何と言っていますか?

究極の答えは_pcre_compile.c_にあります。このコメントで始まるコードブロックを調べると、次のようになります。

後ろを見る場合は、このブランチが固定長の文字列と一致することを確認してください

不平を言う作業はfind_fixedlength()関数によって行われることがわかります。

さらに詳しく知りたい方のために、ここで再現します。

_static int
find_fixedlength(pcre_uchar *code, BOOL utf, BOOL atend, compile_data *cd)
{
int length = -1;

register int branchlength = 0;
register pcre_uchar *cc = code + 1 + LINK_SIZE;

/* Scan along the opcodes for this branch. If we get to the end of the
branch, check the length against that of the other branches. */

for (;;)
  {
  int d;
  pcre_uchar *ce, *cs;
  register pcre_uchar op = *cc;

  switch (op)
    {
    /* We only need to continue for OP_CBRA (normal capturing bracket) and
    OP_BRA (normal non-capturing bracket) because the other variants of these
    opcodes are all concerned with unlimited repeated groups, which of course
    are not of fixed length. */

    case OP_CBRA:
    case OP_BRA:
    case OP_ONCE:
    case OP_ONCE_NC:
    case OP_COND:
    d = find_fixedlength(cc + ((op == OP_CBRA)? IMM2_SIZE : 0), utf, atend, cd);
    if (d < 0) return d;
    branchlength += d;
    do cc += GET(cc, 1); while (*cc == OP_ALT);
    cc += 1 + LINK_SIZE;
    break;

    /* Reached end of a branch; if it's a ket it is the end of a nested call.
    If it's ALT it is an alternation in a nested call. An ACCEPT is effectively
    an ALT. If it is END it's the end of the outer call. All can be handled by
    the same code. Note that we must not include the OP_KETRxxx opcodes here,
    because they all imply an unlimited repeat. */

    case OP_ALT:
    case OP_KET:
    case OP_END:
    case OP_ACCEPT:
    case OP_ASSERT_ACCEPT:
    if (length < 0) length = branchlength;
      else if (length != branchlength) return -1;
    if (*cc != OP_ALT) return length;
    cc += 1 + LINK_SIZE;
    branchlength = 0;
    break;

    /* A true recursion implies not fixed length, but a subroutine call may
    be OK. If the subroutine is a forward reference, we can't deal with
    it until the end of the pattern, so return -3. */

    case OP_RECURSE:
    if (!atend) return -3;
    cs = ce = (pcre_uchar *)cd->start_code + GET(cc, 1);  /* Start subpattern */
    do ce += GET(ce, 1); while (*ce == OP_ALT);           /* End subpattern */
    if (cc > cs && cc < ce) return -1;                    /* Recursion */
    d = find_fixedlength(cs + IMM2_SIZE, utf, atend, cd);
    if (d < 0) return d;
    branchlength += d;
    cc += 1 + LINK_SIZE;
    break;

    /* Skip over assertive subpatterns */

    case OP_ASSERT:
    case OP_ASSERT_NOT:
    case OP_ASSERTBACK:
    case OP_ASSERTBACK_NOT:
    do cc += GET(cc, 1); while (*cc == OP_ALT);
    cc += PRIV(OP_lengths)[*cc];
    break;

    /* Skip over things that don't match chars */

    case OP_MARK:
    case OP_Prune_ARG:
    case OP_SKIP_ARG:
    case OP_THEN_ARG:
    cc += cc[1] + PRIV(OP_lengths)[*cc];
    break;

    case OP_CALLOUT:
    case OP_CIRC:
    case OP_CIRCM:
    case OP_CLOSE:
    case OP_COMMIT:
    case OP_CREF:
    case OP_DEF:
    case OP_DNCREF:
    case OP_DNRREF:
    case OP_DOLL:
    case OP_DOLLM:
    case OP_EOD:
    case OP_EODN:
    case OP_FAIL:
    case OP_NOT_Word_BOUNDARY:
    case OP_Prune:
    case OP_REVERSE:
    case OP_RREF:
    case OP_SET_SOM:
    case OP_SKIP:
    case OP_SOD:
    case OP_SOM:
    case OP_THEN:
    case OP_Word_BOUNDARY:
    cc += PRIV(OP_lengths)[*cc];
    break;

    /* Handle literal characters */

    case OP_CHAR:
    case OP_CHARI:
    case OP_NOT:
    case OP_NOTI:
    branchlength++;
    cc += 2;
#ifdef SUPPORT_UTF
    if (utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]);
#endif
    break;

    /* Handle exact repetitions. The count is already in characters, but we
    need to skip over a multibyte character in UTF8 mode.  */

    case OP_EXACT:
    case OP_EXACTI:
    case OP_NOTEXACT:
    case OP_NOTEXACTI:
    branchlength += (int)GET2(cc,1);
    cc += 2 + IMM2_SIZE;
#ifdef SUPPORT_UTF
    if (utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]);
#endif
    break;

    case OP_TYPEEXACT:
    branchlength += GET2(cc,1);
    if (cc[1 + IMM2_SIZE] == OP_PROP || cc[1 + IMM2_SIZE] == OP_NOTPROP)
      cc += 2;
    cc += 1 + IMM2_SIZE + 1;
    break;

    /* Handle single-char matchers */

    case OP_PROP:
    case OP_NOTPROP:
    cc += 2;
    /* Fall through */

    case OP_HSPACE:
    case OP_VSPACE:
    case OP_NOT_HSPACE:
    case OP_NOT_VSPACE:
    case OP_NOT_DIGIT:
    case OP_DIGIT:
    case OP_NOT_WHITESPACE:
    case OP_WHITESPACE:
    case OP_NOT_WORDCHAR:
    case OP_WORDCHAR:
    case OP_ANY:
    case OP_ALLANY:
    branchlength++;
    cc++;
    break;

    /* The single-byte matcher isn't allowed. This only happens in UTF-8 mode;
    otherwise \C is coded as OP_ALLANY. */

    case OP_ANYBYTE:
    return -2;

    /* Check a class for variable quantification */

    case OP_CLASS:
    case OP_NCLASS:
#if defined SUPPORT_UTF || defined COMPILE_PCRE16 || defined COMPILE_PCRE32
    case OP_XCLASS:
    /* The original code caused an unsigned overflow in 64 bit systems,
    so now we use a conditional statement. */
    if (op == OP_XCLASS)
      cc += GET(cc, 1);
    else
      cc += PRIV(OP_lengths)[OP_CLASS];
#else
    cc += PRIV(OP_lengths)[OP_CLASS];
#endif

    switch (*cc)
      {
      case OP_CRSTAR:
      case OP_CRMINSTAR:
      case OP_CRPLUS:
      case OP_CRMINPLUS:
      case OP_CRQUERY:
      case OP_CRMINQUERY:
      case OP_CRPOSSTAR:
      case OP_CRPOSPLUS:
      case OP_CRPOSQUERY:
      return -1;

      case OP_CRRANGE:
      case OP_CRMINRANGE:
      case OP_CRPOSRANGE:
      if (GET2(cc,1) != GET2(cc,1+IMM2_SIZE)) return -1;
      branchlength += (int)GET2(cc,1);
      cc += 1 + 2 * IMM2_SIZE;
      break;

      default:
      branchlength++;
      }
    break;

    /* Anything else is variable length */

    case OP_ANYNL:
    case OP_BRAMINZERO:
    case OP_BRAPOS:
    case OP_BRAPOSZERO:
    case OP_BRAZERO:
    case OP_CBRAPOS:
    case OP_EXTUNI:
    case OP_KETRMAX:
    case OP_KETRMIN:
    case OP_KETRPOS:
    case OP_MINPLUS:
    case OP_MINPLUSI:
    case OP_MINQUERY:
    case OP_MINQUERYI:
    case OP_MINSTAR:
    case OP_MINSTARI:
    case OP_MINUPTO:
    case OP_MINUPTOI:
    case OP_NOTMINPLUS:
    case OP_NOTMINPLUSI:
    case OP_NOTMINQUERY:
    case OP_NOTMINQUERYI:
    case OP_NOTMINSTAR:
    case OP_NOTMINSTARI:
    case OP_NOTMINUPTO:
    case OP_NOTMINUPTOI:
    case OP_NOTPLUS:
    case OP_NOTPLUSI:
    case OP_NOTPOSPLUS:
    case OP_NOTPOSPLUSI:
    case OP_NOTPOSQUERY:
    case OP_NOTPOSQUERYI:
    case OP_NOTPOSSTAR:
    case OP_NOTPOSSTARI:
    case OP_NOTPOSUPTO:
    case OP_NOTPOSUPTOI:
    case OP_NOTQUERY:
    case OP_NOTQUERYI:
    case OP_NOTSTAR:
    case OP_NOTSTARI:
    case OP_NOTUPTO:
    case OP_NOTUPTOI:
    case OP_PLUS:
    case OP_PLUSI:
    case OP_POSPLUS:
    case OP_POSPLUSI:
    case OP_POSQUERY:
    case OP_POSQUERYI:
    case OP_POSSTAR:
    case OP_POSSTARI:
    case OP_POSUPTO:
    case OP_POSUPTOI:
    case OP_QUERY:
    case OP_QUERYI:
    case OP_REF:
    case OP_REFI:
    case OP_DNREF:
    case OP_DNREFI:
    case OP_SBRA:
    case OP_SBRAPOS:
    case OP_SCBRA:
    case OP_SCBRAPOS:
    case OP_SCOND:
    case OP_SKIPZERO:
    case OP_STAR:
    case OP_STARI:
    case OP_TYPEMINPLUS:
    case OP_TYPEMINQUERY:
    case OP_TYPEMINSTAR:
    case OP_TYPEMINUPTO:
    case OP_TYPEPLUS:
    case OP_TYPEPOSPLUS:
    case OP_TYPEPOSQUERY:
    case OP_TYPEPOSSTAR:
    case OP_TYPEPOSUPTO:
    case OP_TYPEQUERY:
    case OP_TYPESTAR:
    case OP_TYPEUPTO:
    case OP_UPTO:
    case OP_UPTOI:
    return -1;

    /* Catch unrecognized opcodes so that when new ones are added they
    are not forgotten, as has happened in the past. */

    default:
    return -4;
    }
  }
/* Control never gets here */
}
_
37
zx81

正規表現エンジンは左から右に動作するように設計されています

先読みの場合、エンジンは現在の位置の右側にあるテキスト全体と一致します。ただし、後読みの場合、正規表現エンジンは、ステップバックする文字列の長さを決定してから、一致をチェックします(ここでも左から右)。

したがって、*+のような無限の数量詞を提供すると、後戻りは機能しません。これは、エンジンが認識しない後退するステップ数が原因です。

後読みがどのように機能するかの例を示します(ただし、この例はかなりばかげています)。

姓を一致させたいとしますPanta場合のみ名の長さは5〜7文字です。

文字列を見てみましょう:

Full name is Subigya Panta.

正規表現を検討してください。

(?<=\b\w{5,7}\b)\sPanta

エンジンのしくみ

エンジンはポジティブルックビハインドの存在を認識し、firstはWordPanta(前に空白文字を含む)を検索します。マッチです。

これで、エンジンはルックビハインド内の正規表現と一致するように見えます。 7文字後退します(数量詞が貪欲であるため)。単語の境界は、スペースとSの間の位置と一致します。次に、7文字すべてに一致し、次のWord境界はaとスペースの間の位置に一致します。

ルックビハインド内の正規表現は一致であり、一致した文字列にはPantaが含まれているため、正規表現全体がtrueを返します。 (ルックアラウンドアサーションは幅がゼロであり、文字を消費しないことに注意してください。)

6
DrGeneral

pcrepatternのマニュアルページ は、ルックビハインドアサーションが固定幅であるか、|で区切られたいくつかの固定幅パターンである必要があるという制限を文書化し、次の理由で説明します。

ルックビハインドアサーションの実装は、代替案ごとに、現在の位置を一時的に固定長だけ戻し、一致を試みることです。現在の位置の前に十分な文字がない場合、アサーションは失敗します。

なぜ彼らがこのようにするのかはわかりませんが、彼らは前向きに実行される優れたバックトラッキングREマッチングエンジンの作成に多くの時間を費やし、別の逆方向に実行されます。明らかなアプローチは、後読みアサーションの「逆」バージョンと一致させながら、文字列を逆方向に実行することです(これは簡単です)。 「実際の」(DFA一致)REを逆にすることは可能です-正規言語の逆は正規言語です-しかし、PCREの「拡張」REは完全なIIRCチューリングであり、1つを一般に、効率的に逆方向に実行します。そして、たとえそうであったとしても、おそらく誰も実際に気にするほど気にかけていません。結局のところ、後読みアサーションは、物事の壮大な計画の中でかなりマイナーな機能です。

2