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

Why does this backreference not work inside a lookbehind?

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

(.)

Test it here.

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

(?<=(.)).

Unfortunately, this doesn't match anything.

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

简短版本:Lookbehinds 从右到左匹配。 这意味着当正则表达式引擎遇到 </code> 它还没有捕获任何东西到那个组中,所以正则表达式总是失败。解决方法很简单:</p> <pre><code>(?<=(.)).

Test it here.

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

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

首先,一些重要的致谢。 Kobi in this answer. Unfortunately, the question I asked back then was a very convoluted example which doesn't make for a great reference for such a simple problem. So we figured it would make sense to make a new and more canonical post for future reference and as a suitable dupe target. But please consider giving Kobi an upvote for figuring out a very important aspect of .NET's regex engine that is virtually undocumented (as far as I know, MSDN mentions it in a single sentence on a non-obvious page).

教我后视是从右到左匹配的人(他通过大量实验自己弄明白了这一点)

请注意 rexegg.com explains the inner workings of .NET's lookbehinds differently (in terms of reversing the string, the regex, and any potential captures). Although that wouldn't make a difference to the result of the match, I find that approach much harder to reason about, and from looking at the code 很明显,这并不是实现的实际作用。

所以。第一个问题是,为什么它实际上比上面加粗的句子更微妙。让我们尝试使用本地 case-insensitive 修饰符匹配前面带有 aA 的字符。考虑到 right-to-left 匹配行为,人们可能期望这会起作用:

(?<=a(?i)).

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

(?<=(?i)a).

...it works.

另一个例子,考虑到 right-to-left 匹配可能会令人惊讶,如下所示:

(?<=(.)(.)).

</code>指的是左捕获组还是右捕获组?指的是右边那个,<a href="http://regexstorm.net/tester?p=(%3f%3c%3d%5c2(.)(.)).&i=aba!%0d%0aaab!" rel="noreferrer">as this example shows</a>.</p> <p>最后一个例子:当与 <code>abc 匹配时,这会捕获 b 还是 ab

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

It captures b.(您可以在 "Table" 选项卡上查看捕获。)再一次 "lookbehinds are applied from right to left" 不是完整的故事。

因此,此 post 试图成为有关 .NET 中正则表达式方向性的所有内容的综合参考,因为我不知道有任何此类资源。在 .NET 中读取复杂的正则表达式的诀窍是 三或四遍 。除了最后一次之外,所有的都是 left-to-right,不管 lookbehinds 还是 RegexOptions.RightToLeft。我相信是这种情况,因为 .NET 在解析和编译正则表达式时会处理这些。

第一遍:内联修饰符

上面的例子基本上就是这样。如果你的正则表达式中的任何地方,你有这个片段:

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

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

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

对于此遍,您应该完全忽略模式中的任何 命名 组,即 (?<a>...) 形式的组。请注意,这不包括具有显式 数字 的组,例如 (?<2>...)(这是 .NET 中的东西)。

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

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

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

  • 如果该组有一个明确的号码,它的号码显然是那个(而且只有那个)号码。请注意,这可能会添加一个额外的捕获到一个已经存在的组号,或者它可能会创建一个新的组号。另请注意,当您提供明确的组编号时,它们不必是连续的(?<1>.)(?<5>.) 是一个完全有效的正则表达式,组号 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 因为 6 已经被使用了。请记住,它们之间的任何位置都可能存在命名组,我们现在完全忽略它们。

如果您想知道本示例中像组 1 这样的重复组的目的是什么,您可能需要阅读 balancing groups.

第三遍:组号[命名组]

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

命名组在 .NET 中也有(隐式)组号,这是一个鲜为人知的特性,可以在 backre 中使用Regex.Replace 的关系和替换模式。一旦处理了所有未命名的组,这些将在单独的传递中获得它们的编号。给他们编号的规则如下:

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

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

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

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

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

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

a.*b

会匹配一个 b,然后它会尝试匹配它左边的 .*(必要时回溯),这样在它左边的某处就有一个 a .当然,在这个简单的示例中,LTR 和 RTL 模式之间的结果是相同的,但它有助于有意识地努力跟随引擎进行回溯。对于像 ungreedy 修饰符这样简单的事情,它可以有所作为。考虑正则表达式

a.*?b

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

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

考虑到这一点,我们可以用新的眼光看待环顾四周。当正则表达式引擎遇到后视时,它会按如下方式处理它:

  • 它会记住它在目标字符串中的当前位置 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

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

高级部分:平衡组

为了完整起见,本节解释平衡组如何受正则表达式引擎的方向性影响。如果您不知道平衡组是什么,您可以安全地忽略它。如果您了解什么是平衡组,I've written about it here,并且本节假定您至少了解这些。

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

  1. 明确命名或编号的组,如 (?<a>...)(?<2>...)(或什至隐含编号的组),我们已经在上面处理过。
  2. 从捕获堆栈之一弹出的组,如 (?<-a>...)(?<-2>...)。这些行为与您期望的一样。当遇到它们时(按照上述正确的处理顺序),它们只是从相应的捕获堆栈中弹出。可能值得注意的是,这些 不会 获得隐式组号。
  3. "proper"个平衡组(?<b-a>...)通常用于捕获字符串b的最后一个。当与 right-to-left 模式混合时,它们的行为会变得很奇怪,这就是本节的内容。

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

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

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

按照我上面给出的顺序读取正则表达式,我们可以看到:

  1. 正则表达式将 fgh 捕获到组 a 中。
  2. 然后引擎向右移动 8 个字符。
  3. lookbehind 切换到 RTL 模式。
  4. .{2} 向左移动两个字符。
  5. 最后,(?<b-a>.{3}) 是平衡组,它将捕获组 a 弹出并将 某些东西 推到组 b 上。在这种情况下,该组匹配 lmn 并且我们按预期将 ijk 推入组 b

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

原来是分三种情况

案例 1:(?<a>...) 匹配 (?<b-a>...)

左侧

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

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

您可能会使用正则表达式

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

然后mn会被推到b

情况 2:(?<a>...)(?<b-a>...) 相交

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

的右侧

这种情况我不太理解,会考虑一个bug:当(?<b-a>...)匹配的子串正确地离开(?<a>...)匹配的子串(它们之间至少有一个字符,这样它们就不会共享一个公共边界),什么都不会被推送 b。我的意思是什么都没有,甚至不是一个空字符串——捕获堆栈本身仍然是空的。但是匹配组还是成功了,对应的抓包被pop掉了a组。

特别令人恼火的是,这种情况可能比情况 2 更常见,因为如果您尝试按照预期的方式使用平衡组,就会发生这种情况,但在一个普通的right-to-left 正则表达式。

案例 3 的更新:Kobi 完成更多测试后,结果表明 某些事情 发生在堆栈上 b。似乎没有推送任何内容,因为 m.Groups["b"].Success 将是 Falsem.Groups["b"].Captures.Count 将是 0。但是,在正则表达式中,条件 (?(b)true|false) 现在将使用true支线。同样在 .NET 中,似乎可以在之后执行 (?<-b>) (之后访问 m.Groups["b"] 将抛出异常),而 Mono 在匹配正则表达式时立即抛出异常。确实有问题。