提问者:小点点

为什么这个反向引用在lookback中不起作用?


(.)\1

在这里测试一下。

然而,我想匹配一对字符后的字符,所以我想我可以简单地把它放在后面:

(?<=(.)\1).

不幸的是,这与任何东西都不匹配。


共1个答案

匿名用户

简短的版本:lookbehind从右到左匹配。这意味着当正则表达式引擎遇到\1时,它还没有捕获到该组中的任何内容,因此正则表达式总是失败。解决方案非常简单:

(?<=\1(.)).

在这里测试一下。

不幸的是,一旦你开始使用更复杂的模式,整个故事就变得更加微妙了。所以这里是...

首先,一些重要的承认。在这个答案中,科比就是那个教我从右到左匹配“向后看”的人(他自己通过大量实验发现了这一点)。不幸的是,我当时问的问题是一个非常复杂的例子,对于这样一个简单的问题来说,这并不是一个很好的参考。因此,我们认为制作一篇新的、更规范的文章以供将来参考和作为一个合适的复制目标是有意义的。但是,请考虑给Kobi一个赞成票来找出一个非常重要的方面。NET的正则表达式引擎,实际上没有文档记录(据我所知,MSDN在一个不明显的页面上用一句话提到它)。

请注意,rexegg.com解释的内部工作。NET的外观不同(在反转字符串、正则表达式和任何潜在捕获方面)。虽然这不会对匹配的结果产生影响,但我发现这种方法更难推理,从代码中可以很明显地看出,这不是实现实际做的事情。

所以第一个问题是,为什么它实际上比上面粗体的句子更微妙。让我们尝试使用不区分大小写的局部修饰符匹配前面有aa的字符。考虑到从右到左的匹配行为,人们可能会认为这是可行的:

(?<=a(?i)).

但是,正如您在这里看到的,这似乎根本没有使用修改器。实际上,如果我们将修饰符放在前面:

(?<=(?i)a).

...它起作用了。

另一个例子是,考虑到从右到左的匹配,这可能会令人惊讶:

(?<=\2(.)(.)).

\2是指左侧还是右侧捕获组?它指的是正确的一个,如本例所示。

(?<=(b|a.))c

它捕获b。(您可以在“表”选项卡上看到捕获。)再一次,“lookbehinds从右向左应用”并不是全部内容。

因此,这篇文章试图成为一篇全面的参考资料,介绍所有关于正则表达式方向性的内容。NET,因为我不知道有这样的资源。在中读取复杂正则表达式的技巧。NET是在三到四次传球中完成的。除了最后一个过程之外,所有过程都是从左到右的,不管lookbehinds或RegexOptions如何。从右到左。我相信是这样的,因为。NET在解析和编译正则表达式时处理这些。

这基本上就是上面的例子所显示的。如果在正则表达式中的任何地方,您都有以下代码片段:

...a(b(?i)c)d...

无论模式在哪里,也无论您是否使用RTL选项,c都不区分大小写,而abd不区分大小写(前提是它们不受前面一些其他选项的影响)或全局修饰符)。这可能是最简单的规则。

对于这个过程,您应该完全忽略模式中的任何命名组,即表单(?

捕获组从左到右编号。不管你的正则表达式有多复杂,不管你是使用RTL选项还是嵌套几十个lookbehinds和lookahead。当您只使用未命名的捕获组时,它们会根据其开口括号的位置从左到右编号。一个例子:

(a)(?<=(b)(?=(.)).((c).(d)))(e)
└1┘    └2┘   └3┘  │└5┘ └6┘│ └7┘
                  └───4───┘

当混合使用未标记的组和显式编号的组时,这会变得有点棘手。您仍然应该从左到右阅读所有这些内容,但规则有点棘手。您可以按如下方式确定组的编号:

  • 如果组有一个显式的数字,它的数字显然是那个(并且只有那个)数字。请注意,这可能会向已经存在的组号添加额外的捕获,也可能会创建新的组号。还要注意,当您给出显式组编号时,它们不必是连续的。(?

下面是一个示例(为了简单起见,没有嵌套;记住在嵌套时按括号顺序排列):

(a)(?<1>b)(?<2>c)(d)(e)(?<6>f)(g)(h)
└1┘└──1──┘└──2──┘└3┘└4┘└──6──┘└5┘└7┘

注意显式组6如何创建一个间隙,然后捕获组g在组46之间获取未使用的间隙,而捕获组h获取7,因为6已经被使用。请记住,在这两者之间的任何地方都可能有命名组,我们现在完全忽略了这一点。

如果您想知道在这个示例中重复组(如group1)的目的是什么,您可能想要阅读关于平衡组的内容。

当然,如果正则表达式中没有命名组,则可以完全跳过此传递。

这是一个鲜为人知的特性,命名组中也有(隐式)组号。NET,可用于Regex的反向引用和替换模式。更换。一旦处理完所有未命名的组,这些组将在单独的过程中获取其编号。给他们编号的规则如下:

  • 当名称第一次出现时,该组将获得第一个未使用的号码。同样,如果正则表达式使用显式数字,这可能是所用数字的差距,或者可能比迄今为止最大的组数大一个。这会将此新号码与当前名称永久关联

一个包含所有三种类型组的更完整示例,显式地显示传递2和3:

         (?<a>.)(.)(.)(?<b>.)(?<a>.)(?<5>.)(.)(?<c>.)
Pass 2:  │     │└1┘└2┘│     ││     │└──5──┘└3┘│     │
Pass 3:  └──4──┘      └──6──┘└──4──┘          └──7──┘

现在我们知道了哪些修饰符适用于哪些令牌,哪些组有哪些数字,我们最终得到了与正则表达式引擎的执行实际对应的部分,以及我们开始往返的地方。

.NET的正则表达式引擎可以在两个方向上处理正则表达式和字符串:通常的从左到右模式(LTR)和它独特的从右到左模式(RTL)。您可以使用RegexOptions为整个正则表达式激活RTL模式。从右到左。在这种情况下,引擎将开始尝试在字符串末尾查找匹配项,并向左遍历正则表达式和字符串。例如,简单的正则表达式

a.*b

将匹配ab,然后它将尝试匹配*在它的左边(根据需要回溯),这样在它的左边某处就有一个a。当然,在这个简单的示例中,LTR和RTL模式之间的结果是相同的,但这有助于有意识地努力跟踪引擎的回溯。它可以使一些简单的东西有所不同,比如ungreedy修饰符。考虑正则表达式

a.*?b

相反我们正在尝试匹配axxbxxb。在LTR模式下,您将获得预期的匹配项axxb,因为取消冻结量词满足xx。但是,在RTL模式下,您实际上会匹配整个字符串,因为第一个b位于字符串的末尾,但随后*需要匹配所有xxbxx才能匹配a

很明显,这也会对反向引用产生影响,正如问题和答案顶部的例子所示。在LTR模式中,我们使用()\1为了匹配重复字符,在RTL模式下,我们使用\1(.),因为我们需要确保正则表达式引擎在尝试引用捕获之前遇到捕获。

考虑到这一点,我们可以从新的角度来看待环顾四周。当正则表达式引擎遇到lookback时,它将按如下方式处理它:

  • 它记住其在目标字符串中的当前位置x,以及当前处理方向

虽然前瞻看起来无害得多(因为我们几乎从未遇到过类似问题中的问题),但它的行为实际上是相同的,只是它强制执行LTR模式。当然,在大多数仅为LTR的模式中,这一点从未被注意到。但是,如果正则表达式本身在RTL模式下匹配,或者我们正在做一些疯狂的事情,比如在lookback中放入一个lookahead,那么lookahead将改变处理方向,就像lookback一样。

那么,你应该如何阅读一个做了这样有趣事情的正则表达式呢?第一步是将其拆分为单独的组件,这些组件通常是单独的标记及其相关量词。然后,根据正则表达式是LTR还是RTL,分别从上到下或从下到上。当您在过程中遇到环视时,请检查其朝向哪个方向,并跳到正确的一端,然后从那里读取环视。完成环视后,继续环视模式。

当然还有另一个陷阱。。。当您遇到一个替代(..|…|…),始终从左到右尝试备选方案,即使在RTL匹配期间也是如此。当然,在每个备选方案中,发动机从右向左行驶。

下面是一个有点做作的例子来说明这一点:

.+(?=.(?<=a.+).).(?<=.(?<=b.|c.)..(?=d.|.+(?<=ab*?))).

这是我们如何把它分开的。如果正则表达式处于LTR模式,则左侧的数字显示读取顺序。右侧的数字显示RTL模式下的读取顺序:

LTR             RTL

 1  .+          18
    (?=
 2    .         14
      (?<=
 4      a       16
 3      .+      17
      )
 5    .         13
    )
 6  .           13
    (?<=
17    .         12
      (?<=
14      b        9
13      .        8
      |
16      c       11
15      .       10
      )
12    ..         7
      (?=
 7      d        2
 8      .        3
      |
 9      .+       4
        (?<=
11        a      6
10        b*?    5
        )
      )
    )
18  .            1

我真诚地希望你永远不会在生产代码中使用像这样疯狂的东西,但是也许有一天,一个友好的同事会在被解雇之前在你公司的代码库中留下一些疯狂的只写正则表达式,在那一天,我希望这个指南可以帮助你弄清楚到底发生了什么。

为了完整起见,本节解释了regex引擎的方向性如何影响平衡组。如果你不知道什么是平衡组,你可以放心地忽略这一点。如果您想知道平衡组是什么,我在这里已经写过了,本节假设您至少对它们了解那么多。

有三种类型的组语法与平衡组相关。

  1. 明确命名或编号的组,如(?)?

外卖是,(?)?

首先,让我们看一个例子,它显示了为什么环视使情况复杂化。我们正在匹配字符串abcde... wvxyz。考虑以下正则表达式:

(?<a>fgh).{8}(?<=(?<b-a>.{3}).{2})

按照我上面介绍的顺序阅读正则表达式,我们可以看到:

  1. 正则表达式将fgh捕获到组a
  2. 引擎然后向右移动8个字符
  3. 查找切换到RTL模式
  4. 。{2} 向左移动两个字符
  5. 最后,(?

然而,从这个例子可以清楚地看到,通过改变数值参数,我们可以改变两组匹配的子串的相对位置。我们甚至可以使这些子字符串相交,或者通过使3变小或变大,使其中一个子字符串完全包含在另一个子字符串中。在这种情况下,将所有内容推到两个匹配的子字符串之间意味着什么已经不清楚了。

事实证明,有三种情况需要区分。

这是正常情况。顶部捕获从a弹出,两组匹配的子字符串之间的所有内容都被推到b上。考虑以下两个子串的两组:

abcdefghijklmnopqrstuvwxyz
   └──<a>──┘  └──<b-a>──┘

你可以用正则表达式得到它

(?<a>d.{8}).+$(?<=(?<b-a>.{11}).)

然后,mn将被推到b

这包括两个子字符串接触但不包含任何公共字符(字符之间只有公共边界)的情况。如果其中一个组在环视区域内,而另一个组不在或在不同的环视区域内,就会发生这种情况。在这种情况下,两个减法的交集将被推到b上。当子串完全包含在另一个子串中时,这仍然是正确的。

以下是几个例子来说明这一点:

        Example:              Pushes onto <b>:    Possible regex:

abcdefghijklmnopqrstuvwxyz    ""                  (?<a>d.{8}).+$(?<=(?<b-a>.{11})...)
   └──<a>──┘└──<b-a>──┘

abcdefghijklmnopqrstuvwxyz    "jkl"               (?<a>d.{8}).+$(?<=(?<b-a>.{11}).{6})
   └──<a>┼─┘       │
         └──<b-a>──┘

abcdefghijklmnopqrstuvwxyz    "klmnopq"           (?<a>k.{8})(?<=(?<b-a>.{11})..)
      │   └──<a>┼─┘
      └──<b-a>──┘

abcdefghijklmnopqrstuvwxyz    ""                  (?<=(?<b-a>.{7})(?<a>.{4}o))
   └<b-a>┘└<a>┘

abcdefghijklmnopqrstuvwxyz    "fghijklmn"         (?<a>d.{12})(?<=(?<b-a>.{9})..)
   └─┼──<a>──┼─┘
     └─<b-a>─┘

abcdefghijklmnopqrstuvwxyz    "cdefg"             (?<a>c.{4})..(?<=(?<b-a>.{9}))
│ └<a>┘ │
└─<b-a>─┘

这种情况下,我真的不明白,会考虑一个错误:当子串匹配的(?

特别令人恼火的是,这种情况可能比情况2更常见,因为如果您尝试按使用平衡组的方式使用它们,但使用的是简单的从右到左的正则表达式,则会发生这种情况。

案例3的更新:在Kobi进行了更多的测试之后,发现堆栈b上发生了一些事情。由于m.Groups[“b”],似乎没有推送任何内容。成功将是Falsem.Groups[“b”]。捕获。计数将为0。但是,在正则表达式中,条件(?(b)true | false)现在将使用true分支。也在。NET似乎可以执行(?)?