【问题标题】:Why can't you use repetition quantifiers in zero-width look behind assertions?为什么不能在断言后面使用零宽度的重复量词?
【发布时间】:2014-07-20 18:57:34
【问题描述】:

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

【问题讨论】:

  • 是的,只有前瞻断言可以是可变长度的。唯一的例外是特殊的\K 代码,它是一种可以变化的后向断言的特殊形式。因此,在您的第二个示例中,以下内容适用于 perl:/a*\Kb(?=c*)/显然使用可以为零宽度的断言有点没有意义,所以也许使用+ 会是一个更好的例子
  • 因为当正则表达式引擎需要回溯时,可变长度的后视断言是 @$$ 的痛苦。
  • @mob 你能解释一下为什么它们比可变长度的前瞻断言更难处理吗?从幼稚的角度来看,这两种操作都需要查看相同数量的字符,对吧。 (我知道那肯定是错的,但是怎么会呢?)
  • this page 上以“坏消息”开头的段落可能暗示了原因。听起来正则表达式引擎实际上只能向前工作,因此通过退回n 字符并从第一个字符开始检查它们,实际上匹配了后向断言。使用可变长度的lookbehind 断言,您无法提前知道n,这意味着您必须一遍又一遍地测试字符串中每个可能的起始字符一次。一些正则表达式向导可以确认这是否 +/- 正确吗?
  • “'b' 前面有零个或多个 'a'”的概念相当荒谬,因为它总是会得到满足。 "b" 前面有至少一个 "a" .. 或没有,所以前面有零个 "a",意味着条件是空的。同样对于它后面的零个或多个“c”。

标签: regex r pcre


【解决方案1】:

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

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

因此,如果您提供一些无限量词,例如 *+,则后向不会起作用,因为引擎不知道要向后退多少步。

我将举一个例子来说明lookbehind是如何工作的(虽然这个例子很傻)。

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

让我们来看看字符串:

Full name is Subigya Panta.

考虑正则表达式:

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

引擎的工作原理

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

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

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

【讨论】:

  • 那里有语法错误:{5-7} 应该是 {5,7}。但是您的解释仅适用于 Java 和 ICU 风格,如果在编译正则表达式时可以确定最大可能长度,则它们支持可变长度后视。您的示例也适用于 .NET、JGSoft 和 Perl 6(根本没有任何限制),但在大多数情况下,它只是固定长度的后视。
  • @AlanMoore 谢谢,我做了编辑。同意,它也适用于 PCRE,并且问题与 PCRE 有关。
  • PCRE 库(当您指定 perl=TRUE 时,R 使用该库)模仿 Perl 5 的行为。因此它支持由多个固定长度备选方案组成的后视(如其他答案中所述) ,但它不允许在lookbehinds中使用量词。
【解决方案2】:

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

lookbehind 断言的实现是,对于每个备选方案, 将当前位置暂时向后移动固定长度和 然后尝试匹配。如果前面的字符不足 当前位置,断言失败。

我不确定他们为什么要这样做,但我猜他们花了很多时间编写一个可以向前运行的良好回溯 RE 匹配引擎,并且他们不想重复所有的努力来写另一个向后运行。显而易见的方法是向后遍历字符串——这很容易——同时匹配你的lookbehind断言的“反向”版本。反转“真实”(DFA 匹配)RE 是可能的——正则语言的反转是正则语言——但 PCRE 的“扩展”RE 是 IIRC 图灵完备的,甚至可能无法翻转一个通常有效地向后运行。即使是这样,也可能没有人真正关心到打扰。毕竟,lookbehind assertions 在宏伟的计划中只是一个很小的功能。

【讨论】:

    【解决方案3】:

    此类问题的最终答案在引擎代码中,在答案的底部,您将能够深入了解 PCRE 引擎代码中负责确保后向固定长度的部分 - 如果您“有兴趣了解最精细的细节。同时,让我们从更高的层面逐步放大问题。

    可变宽度后视与无限宽后视

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

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

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

    支持无限宽度后视的引擎

    郑重声明,这些引擎支持无限后视:

    • .NET(C#、VB.NET 等)
    • 马修·巴尼特的regex module for Python
    • JGSoft(EditPad 等;不支持编程语言)。

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

    PCRE 中的变量后视

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

    lookbehind 断言的内容受到限制,因此所有 它匹配的字符串必须具有固定长度。但是,如果有 几个顶级替代方案,它们不必都具有相同的 固定长度。

    因此,以下回溯是有效的:

    (?<=a |big )cat
    

    但是,这些都不是:

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

    零宽度匹配但无限重复的后视

    现在考虑一下:

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

    从表面上看,这是一个固定宽度的lookbehind,因为它只能找到一个零宽度的匹配项(由lookahead (?=@++) 定义)。这是绕过无限后视限制的技巧吗?

    没有。 PCRE 会对此感到窒息。即使lookbehind 的内容是零宽度,PCRE 也不允许在lookbehind 中无限重复。任何地方。当文档说它匹配的所有字符串必须具有固定长度时,它实际上应该是:

    它的任何组件匹配的所有字符串必须有一个固定的 长度。

    解决方法:没有无限后视的生活

    在 PCRE 中,解决无限后视问题的两个主要解决方案是 \K 和捕获组。

    解决方法 #1:\K

    \K 断言告诉引擎从它返回的最终匹配中删除匹配的内容。

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

    @+\Kcat#+
    

    解决方法 #2:捕获组

    另一种方法是匹配您在后视中放置的任何内容,并在捕获组中捕获感兴趣的内容。然后从捕获组中检索匹配项。

    例如,代替非法的(?&lt;=@+)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 */
    }
    

    【讨论】:

    • 很多有趣的信息,但仍然没有回答这个问题:PCRE 正则表达式引擎在使用零宽度查找时如何工作 这会阻止使用重复量词?
    • @HamZa 很高兴收到您的来信。我添加了一个部分Lookbehind with Zero-Width Match but Infinite Repetition,它考虑了一个包含无限重复的零宽度匹配的lookbehind:(?&lt;=(?=@+))这是你的意思吗?不确定,因为从 Simon 的例子中我无法完全判断这是否是一般的想法(起初似乎不是这样,这就是为什么我没有考虑那个有趣的案例)。
    • @HamZa 另外,对于任何有兴趣查看引擎内部的人,我追踪了pcre_compile.c 负责检查后向字符串的固定长度的部分。它调用find_fixedlength() 函数,我粘贴了它的代码,以防有人想要获得最精细细节的最终答案。
    • 谢谢你,@anubhava! :)
    猜你喜欢
    • 1970-01-01
    • 2012-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-16
    • 2014-12-27
    相关资源
    最近更新 更多