【问题标题】:Why does this backreference not work inside a lookbehind?为什么这个反向引用在lookbehind 中不起作用?
【发布时间】:2016-03-16 22:15:30
【问题描述】:

通过反向引用匹配正则表达式中的重复字符很简单:

(.)\1

Test it here.

但是,我想匹配 这对字符之后的字符,所以我想我可以简单地把它放在后面:

(?<=(.)\1).

Unfortunately, this doesn't match anything.

这是为什么呢?在其他风格中,我不会感到惊讶,因为对后视有严格的限制,但 .NET 通常支持在后视中任意复杂的模式。

【问题讨论】:

  • 我在标题下有added this question to the reference:“如何阅读混合了前瞻、后视、捕获组和反向引用的 .NET 正则表达式?”
  • 同样的问题和同样的解释也适用于 JavaScript REs。

标签: .net regex regex-lookarounds


【解决方案1】:

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

(?<=\1(.)).

Test it here.

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

.NET 中的正则表达式阅读指南

首先,一些重要的致谢。教我后视是从右到左匹配的人(并通过大量实验自己弄清楚了这一点)是Kobi in this answer。不幸的是,我当时问的问题是一个非常复杂的例子,对于这样一个简单的问题并没有很好的参考。因此,我们认为制作一个新的更规范的帖子以供将来参考并作为合适的欺骗目标是有意义的。但是请考虑给 Kobi 投赞成票,因为它找出了 .NET 正则表达式引擎的一个非常重要的方面,该方面几乎没有记录(据我所知,MSDN 在一句话中提到了它on a non-obvious page)。

请注意,rexegg.com 以不同的方式解释了 .NET 后视的内部工作原理(在反转字符串、正则表达式和任何潜在捕获方面)。虽然这不会对比赛结果产生影响,但我发现这种方法更难推理,from looking at the code 很明显,这不是实现的实际作用。

所以。第一个问题是,为什么它实际上比上面的粗体句子更微妙。让我们尝试使用本地不区分大小写的修饰符匹配前面有aA 的字符。鉴于从右到左的匹配行为,人们可能会期望这会起作用:

(?<=a(?i)).

但是,as you can see here 这似乎根本没有使用修饰符。确实,如果我们把修饰符放在前面:

(?<=(?i)a).

...it works.

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

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

\2 是指左捕获组还是右捕获组?它指的是正确的as this example shows

最后一个例子:当与 abc 匹配时,是否捕获 bab

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

It captures b.(您可以在“表格”选项卡上看到捕获。)再一次“从右到左应用后视”并不是完整的故事。

因此,这篇文章试图成为有关 .NET 中正则表达式方向性的所有事情的综合参考,因为我不知道有任何此类资源。在 .NET 中读取复杂的正则表达式的诀窍是三到四遍。除了最后一次传球外,所有传球都是从左到右的,不管后视或RegexOptions.RightToLeft。我相信是这样,因为 .NET 在解析和编译正则表达式时会处理这些。

第一遍:内联修饰符

这基本上就是上面的例子所展示的。如果在你的正则表达式中的任何地方,你有这个 sn-p:

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

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

第二遍:组号[未命名组]

对于这个pass,你应该完全忽略模式中的任何命名组,即(?&lt;a&gt;...)形式的组。请注意,这不包括具有显式 数字 的组,例如 (?&lt;2&gt;...)(这是 .NET 中的东西)。

捕获组从左到右编号。无论您的正则表达式有多复杂,无论您是使用 RTL 选项还是嵌套数十个后向和前瞻,都无关紧要。当您只使用未命名的捕获组时,它们会根据左括号的位置从左到右编号。一个例子:

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

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

  • 如果组有一个明确的编号,那么它的编号显然是那个(并且只有那个)编号。请注意,这可能会向已经存在的组号添加额外的捕获,也可能会创建一个新的组号。另请注意,当您给出明确的组号时,它们不必是连续的(?&lt;1&gt;.)(?&lt;5&gt;.) 是一个完全有效的正则表达式,组号 24 未使用。
  • 如果组未标记,则使用第一个未使用的编号。由于我刚才提到的差距,这可能小于已经使用的最大数量。

这是一个示例(为简单起见,没有嵌套;嵌套时请记住按左括号对它们进行排序):

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

注意显式组6 是如何创建一个间隙的,然后捕获g 的组占用46 组之间的未使用间隙,而捕获h 的组占用7 因为@987654367 @ 已被使用。请记住,在它们之间的任何地方都可能存在命名组,我们现在完全忽略。

如果您想知道在此示例中重复组(如 group 1)的目的是什么,您可能想了解balancing groups

第三遍:组号[命名组]

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

这是一个鲜为人知的功能,即命名组在 .NET 中也具有(隐式)组号,可用于 Regex.Replace 的反向引用和替换模式。一旦处理了所有未命名的组,它们就会在单独的通道中获得它们的编号。给它们编号的规则如下:

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

一个更完整的例子,包含所有三种类型的组,明确显示了第二轮和第三轮:

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

最终通过:遵循正则表达式引擎

现在我们知道了哪些修饰符适用于哪些标记以及哪些组具有哪些数字,我们终于到了实际对应于正则表达式引擎的执行的部分,以及我们从哪里开始返回等等。

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

a.*b

将匹配b,然后它会尝试将.* 匹配到它的左侧(必要时回溯),以便在它的左侧某处有一个a。当然,在这个简单的例子中,LTR 和 RTL 模式之间的结果是相同的,但它有助于有意识地努力跟随引擎进行回溯。它可以对像不贪婪的修饰符这样简单的东西产生影响。考虑正则表达式

a.*?b

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

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

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

  • 它会记住它在目标字符串中的当前位置x 以及它当前的处理方向。
  • 现在它强制执行 RTL 模式,无论它当前处于何种模式。
  • 然后lookbehind的内容从右到左匹配,从当前位置x开始。
  • 一旦lookbehind处理完毕,如果通过,正则表达式引擎的位置将重置为位置x,并恢复原来的处理方向。

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

那么你应该如何真正阅读一个能做这样有趣事情的正则表达式呢?第一步是将其拆分为单独的组件,这些组件通常是单独的标记及其相关量词。然后根据正则表达式是 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

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

高级部分:平衡组

为了完整起见,本节说明平衡组如何受正则表达式引擎的方向性影响。如果你不知道什么是平衡组,你可以放心地忽略它。如果您知道什么是平衡组,I've written about it here,本节假设您至少对它们了解这么多。

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

  1. 明确命名或编号的组,例如 (?&lt;a&gt;...)(?&lt;2&gt;...)(甚至是隐含编号的组),我们在上面已经讨论过了。
  2. (?&lt;-a&gt;...)(?&lt;-2&gt;...) 等捕获堆栈之一弹出的组。它们的行为与您期望的一样。当遇到它们时(以上述正确的处理顺序),它们只是从相应的捕获堆栈中弹出。值得注意的是,这些不会获得隐含的组号。
  3. “适当的”平衡组(?&lt;b-a&gt;...) 通常用于捕获字符串b 的最后一个。当与从右到左模式混合时,它们的行为会变得很奇怪,这就是本节的内容。

要点是,(?&lt;b-a&gt;...) 功能在从右到左模式下实际上无法使用。然而,经过大量实验后,(奇怪的)行为实际上似乎遵循一些规则,我在这里概述了这些规则。

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

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

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

  1. 正则表达式将fgh 捕获到组a
  2. 然后引擎向右移动 8 个字符。
  3. lookbehind 切换到 RTL 模式。
  4. .{2} 向左移动两个字符。
  5. 最后,(?&lt;b-a&gt;.{3}) 是平衡组,它将捕获关闭组 a 并将 something 推送到组 b。在这种情况下,该组匹配 lmn,我们按预期将 ijk 推送到组 b

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

原来要区分三种情况。

案例 1:(?&lt;a&gt;...) 匹配 (?&lt;b-a&gt;...) 左侧

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

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

使用正则表达式可能会得到什么

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

然后mn 将被推送到b

案例 2:(?&lt;a&gt;...)(?&lt;b-a&gt;...) 相交

这包括两个子字符串接触但不包含任何公共字符(仅字符之间的公共边界)的情况。如果其中一组在环视内,而另一组不在或在不同的环视内,则可能发生这种情况。在这种情况下,两个子字符串的交集将被推送到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>─┘

案例 3:(?&lt;a&gt;...) 匹配 (?&lt;b-a&gt;...) 的右侧

这种情况我不太明白,会考虑一个错误:当(?&lt;b-a&gt;...)匹配的子字符串正确地离开(?&lt;a&gt;...)匹配的子字符串时(它们之间至少有一个字符,这样它们就不会' t 共享一个共同的边界),什么都不推送b。我真的没有任何意思,甚至没有空字符串——捕获堆栈本身仍然是空的。但是,匹配组仍然成功,并从a 组中弹出相应的捕获。

对此特别烦人的是,这种情况可能比情况 2 更常见,因为如果您尝试按原定使用平衡组的方式使用平衡组,就会发生这种情况,但很明显——向左正则表达式。

案例 3 的更新:Kobi 进行了更多测试后,发现 某事 发生在堆栈 b 上。似乎没有推送任何内容,因为m.Groups["b"].Success 将是Falsem.Groups["b"].Captures.Count 将是0。但是,在正则表达式中,条件 (?(b)true|false) 现在将使用 true 分支。同样在 .NET 中,之后似乎可以执行 (?&lt;-b&gt;) (之后访问 m.Groups["b"] 将引发异常),而 Mono 在匹配正则表达式时会立即引发异常。确实是错误。

【讨论】:

  • 根据我的屏幕,你们俩都问过,并同时给出了这个非常长的答案(17 秒前)。你是怎么做到的?
  • @Quantic 提出问题时,有一个复选框“回答您自己的问题 - share your knowledge, Q&A-style”,它为您提供了将答案与问题一起写下的第二种感觉。
  • @WiktorStribiżew:就性能而言,反转字符串将是一件愚蠢的事情,而您可以反转当前指针前进的方向。源代码准确地揭示了这一点:github.com/dotnet/corefx/blob/master/src/…
  • 只是好奇 - 为什么您将交替称为“另一个捕获”,而不仅仅是正则表达式从左到右解析然后然后的一般想法的一部分运行 LTR/RTL 根据全局标志以及您是否处于前瞻/后退?
  • @Rawling 虽然你是对的,这是根本原因,但人们可能会认为 running 交替 RTL 也会尝试从右到左的替代方案,所以我认为它是值得特别注意。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-21
  • 1970-01-01
  • 2013-03-21
  • 2017-06-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多