为什么不能在零宽度后面使用重复量词来查看断言

时间:2014-05-30 22:39:28

标签: regex r pcre

我一直认为你不能在零宽度断言中使用重复量词(Perl Compatible Regular Expressions [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)'

3 个答案:

答案 0 :(得分:31)

这个问题的最终答案是在引擎的代码中,在答案的最底部,您将能够深入了解PCRE引擎代码的部分,该代码负责确保外观中的固定长度 - 如果您'对了解最精细的细节很感兴趣。与此同时,让我们逐步放大更高层次的问题。

可变宽度外观与无限宽度外观

首先,快速澄清条款。越来越多的引擎(包括PCRE)支持某种形式的可变宽度后视,其中变化落在确定的范围内,例如:

  • 引擎知道前面的宽度必须 5到10个字符(PCRE不支持)
  • 引擎知道前面的宽度必须 5 十个字符(PCRE支持)

相比之下,在无限宽度的后视中,您可以使用量化的标记,例如a+

支持无限宽幅后视的引擎

为了记录,这些引擎支持无限的后视:

  • .NET(C#,VB.NET等)
  • Matthew Barnett的regex module for Python
  • JGSoft(EditPad等;不提供编程语言)。

据我所知,他们是唯一的人。

PCRE中的变量Lookbehind

在PCRE中,文档中最相关的部分是:

  

lookbehind断言的内容受到限制,以便全部   它匹配的字符串必须有固定的长度。但是,如果有的话   几个顶级替代品,他们并非都必须具有相同的替代品   固定长度。

因此,以下lookbehind是有效的:

(?<=a |big )cat

然而,这些都不是:

  • (?<=a\s?|big )cat(交替的两侧没有固定的宽度)
  • (?<=@{1,10})cat(可变宽度)
  • (?<=\R)cat\R没有固定宽度,因为它可以与\n\r\n等匹配。)
  • (?<=\X)cat\X没有固定宽度,因为Unicode字形集群可以包含可变数量的字节。)
  • (?<=a+)cat(显然没有修复)

使用零宽度匹配但无限重复看后面

现在考虑一下:

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

从表面上看,这是一个固定宽度的lookbehind,因为它只能找到零宽度匹配(由前瞻(?=@++)定义)。这是解决无限外观限制的伎俩吗?

没有。 PCRE会扼杀这个。即使后视镜的内容为零宽度,PCRE也不允许在后视镜中进行无限重复。无处不在。当文档说它匹配的所有字符串必须有一个固定长度时,它应该是:

  

任何组件匹配的所有字符串都必须具有固定的字符串   长度。

解决方法:没有无限外观的生活

在PCRE中,无限外观有助于解决问题的两个主要解决方案是\K和捕获群组。

解决方法#1:\K

\K断言告诉引擎放弃与它返回的最终匹配相匹配的内容。

假设您需要(?<=@+)cat#+,这在PCRE中是不合法的。相反,您可以使用:

@+\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中找到。如果您检查以此注释开头的代码块:

  

如果是lookbehind,请检查此分支是否与固定长度的字符串匹配

你发现咕噜咕噜的工作是由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 */
}

答案 1 :(得分:6)

正则表达式引擎设计为从左到右工作

对于前瞻,引擎匹配当前位置右侧的整个文本。但是,对于lookbehinds,正则表达式引擎确定要退回的字符串长度,然后检查匹配(再次从左到右)。

所以,如果你提供一些无限量词,如*+,lookbehind将无法工作,因为引擎不知道要向后退步的步数。

我将举例说明lookbehind是如何工作的(虽然这个例子非常愚蠢)。

假设您要匹配姓氏Panta仅当的名字长度为5-7个字符时才匹配。

我们拿字符串:

Full name is Subigya Panta.

考虑正则表达式:

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

引擎如何工作

引擎确认存在正向后视,因此首先搜索单词Panta(前面带有空格字符)。这是一场比赛。

现在,引擎看起来与lookbehind内的正则表达式相匹配。它向后退7个字符(因为量词是贪婪的)。单词边界与空格和S之间的位置匹配。然后它匹配所有7个字符,然后下一个字边界匹配a和空格之间的位置。

lookbehind内的正则表达式是匹配的,因此整个正则表达式返回true,因为匹配的字符串包含Panta。 (请注意,环绕声断言是零宽度,不消耗任何字符。)

答案 2 :(得分:2)

pcrepattern man page文件说明后方断言的限制必须是固定宽度,或者是由|分隔的几个固定宽度模式,然后解释这是因为:

  

对于每种替代方案,lookbehind断言的实现是   暂时将当前位置移回固定长度并且   然后尝试匹配。如果之前没有足够的字符   当前的位置,断言失败。

我不确定他们为什么会这样做,但我的猜测是他们花了很多时间写一个好的回溯重新匹配引擎,但是他们并不想复制所有努力写下另一个倒退的努力。显而易见的方法是向后移动字符串 - 这很容易 - 同时匹配&#34;反向&#34;你的lookbehind断言的版本。扭转真实的&#34; (DFA-matchable)RE是可能的 - 常规语言的反面是常规语言 - 但PCRE&#34;&#34;扩展&#34; RE是完整的IIRC图案,一般来说甚至不可能翻转一个以有效地向后运行。即使它是,也许没有人真的关心到足以打扰。毕竟,在宏观计划中,后视断言是一个非常小的特征。