使用反向引用在正则表达式中匹配重复字符非常简单:
(.)\1
然而,我想匹配成对字符后面的字符,因此我认为可以在后顾中简单地放置它:
(?<=(.)\1).
为什么会这样?在其他正则表达式引擎中,我不会感到惊讶,因为对回顾后发限制有着强烈的限制,但.NET通常支持在回顾后发内部使用任意复杂的模式。
使用反向引用在正则表达式中匹配重复字符非常简单:
(.)\1
然而,我想匹配成对字符后面的字符,因此我认为可以在后顾中简单地放置它:
(?<=(.)\1).
为什么会这样?在其他正则表达式引擎中,我不会感到惊讶,因为对回顾后发限制有着强烈的限制,但.NET通常支持在回顾后发内部使用任意复杂的模式。
\1
时,它尚未将任何内容捕获到该组中,因此正则表达式始终失败。解决方案非常简单:(?<=\1(.)).
不幸的是,一旦你开始使用更复杂的模式,完整的故事就会变得更加微妙。因此,这里是...
首先,要感谢一些重要的人。教我正则表达式中反向引用是从右到左匹配的人,并通过大量实验自己弄清楚这个问题的Kobi in this answer。不幸的是,我当时提出的问题非常复杂,对于这个简单的问题来说并不是一个很好的参考。因此,我们认为为了将来的参考和适当的重复目标,应该发表一个新的、更加规范的帖子。但请考虑给Kobi点赞,因为他找出了.NET的正则表达式引擎中一个非常重要但几乎没有文献记载的方面(据我所知,MSDN在一个不明显的页面on a non-obvious page中只提到了一句话)。
请注意rexegg.com解释了.NET的回顾后发生的事情,它是通过反转字符串、正则表达式和任何潜在的捕获来解释的。虽然这不会对匹配结果产生影响,但我发现这种方法更难理解,并且从代码看,很明显这不是实际实现的方式。a
或A
前导的字符。考虑到从右到左的匹配行为,人们可能会期望这样做可以起作用:(?<=a(?i)).
(?<=(?i)a).
另一个例子,可能会让人感到惊讶的是右到左匹配的情况下:
(?<=\2(.)(.)).
\2
是指左边还是右边的捕获组?它指的是右边的捕获组,正如这个例子所示。
最后一个例子:当匹配字符串abc
时,会捕获b
还是ab
?
(?<=(b|a.))c
它捕获了b
。(您可以在“表格”选项卡上看到捕获结果。)再次强调,“lookbehinds从右向左应用”并不是全部内容。
因此,本文试图成为.NET正则表达式方向性的全面参考资料,因为我不知道有任何这样的资源。阅读.NET中复杂正则表达式的诀窍是进行三到四次解析。除了最后一次解析外,其余所有解析都是从左到右进行的,无论是否存在lookbehinds或RegexOptions.RightToLeft
。我认为这是因为.NET在解析和编译正则表达式时处理这些内容。
这基本上就是上面的示例所展示的内容。如果您的正则表达式中的任何位置都有以下代码片段:
...a(b(?i)c)d...
c
都是不区分大小写的,而a
、b
和d
则不是(前提是它们没有受到其他先行或全局修饰符的影响)。这可能是最简单的规则。(?<a>...)
的组。请注意,这不包括具有显式数字的组,例如(?<2>...)
(这是.NET中的一个东西)。(a)(?<=(b)(?=(.)).((c).(d)))(e)
└1┘ └2┘ └3┘ │└5┘ └6┘│ └7┘
└───4───┘
(?<1>.)(?<5>.)
是一种具有组号2
至4
未使用的完全有效的正则表达式。这里有一个示例(为简单起见没有嵌套;当它们嵌套时,请记得按其开放括号对它们进行排序):
(a)(?<1>b)(?<2>c)(d)(e)(?<6>f)(g)(h)
└1┘└──1──┘└──2──┘└3┘└4┘└──6──┘└5┘└7┘
6
创建了一个间隔,然后捕获组g
获取了组4
和6
之间未使用的间隔,而捕获组h
获取了7
,因为6
已经被使用。请记住,命名组可能出现在这些组之间,但我们现在完全忽略它们。1
重复出现的目的是什么,你可能想读一下平衡组。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
。当然,在这个简单的例子中,从左到右模式和从右到左模式的结果是相同的,但刻意跟随引擎的回溯过程对于像非贪婪修饰符这样的简单情况也很重要。考虑正则表达式
a.*?b
axxbxxb
。在LTR模式下,您会得到预期的匹配axxb
,因为不贪婪量词满足于xx
。然而,在RTL模式下,实际上您将匹配整个字符串,因为第一个b
被找到在字符串末尾,但是然后.*?
需要匹配xxbxx
的所有内容才能匹配a
。
显然,这对于反向引用也有影响,就像问题和本答案顶部的示例所示。在LTR模式下,我们使用(.)\1
来匹配重复字符,在RTL模式下,我们使用\1(.)
,因为我们需要确保正则表达式引擎在尝试引用之前遇到捕获。
有了这个想法,我们可以以新的方式看待环视。当正则表达式引擎遇到回顾时,它按以下方式处理:
x
以及当前处理方向。x
开始,从右到左匹配回顾内容。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
(?<a>...)
或(?<2>...)
(甚至是隐含编号的组),我们已经在上面处理过了。(?<-a>...)
和(?<-2>...)
。它们的行为符合您的预期。当它们被遇到时(按照上述正确的处理顺序),它们将仅从相应的捕获堆栈中弹出。值得注意的是,这些组没有隐式编号。(?<b-a>...)
通常用于捕获自b
最后一个字符以来的字符串。当与从右到左模式混合使用时,它们的行为会变得奇怪,这就是本节讨论的内容。结论是,(?<b-a>...)
功能在从右到左模式下实际上无法使用。然而,在进行了大量实验之后,这种(奇怪的)行为实际上似乎遵循某些规则,我在这里进行了概述。
abcde...wvxyz
。考虑以下正则表达式:(?<a>fgh).{8}(?<=(?<b-a>.{3}).{2})
按照我上面给出的顺序阅读正则表达式,我们可以看到:
fgh
捕获到组a
中。.{2}
向左移动两个字符。(?<b-a>.{3})
是平衡组,它从组a
弹出捕获,将something推入组b
。在这种情况下,该组匹配lmn
,我们将ijk
按预期推入组b
。但是,从这个例子中可以清楚地看出,通过更改数字参数,我们可以更改两个组匹配的子字符串的相对位置。我们甚至可以通过使3
更小或更大来使这些子字符串相交,或者其中一个完全包含在另一个中。在这种情况下,不再清楚将两个匹配的子字符串之间的所有内容推入意味着什么。
事实证明,有三种情况需要区分。
(?<a>...)
匹配到(?<b-a>...)
左侧这是正常情况。顶部捕获从a
中弹出,两个组匹配的子字符串之间的所有内容都被推送到b
中。考虑以下两个组的子字符串:
abcdefghijklmnopqrstuvwxyz
└──<a>──┘ └──<b-a>──┘
你可以通过正则表达式获得它
(?<a>d.{8}).+$(?<=(?<b-a>.{11}).)
那么mn
将被推入b
。
(?<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>─┘
(?<b-a>...)
匹配(?<a>...)
的右侧我并不真正理解这种情况,认为它是一个错误:当由(?<b-a>...)
匹配的子字符串恰好位于由(?<a>...)
匹配的子字符串的左侧(它们之间至少有一个字符,以使它们不共享公共边界)时,没有将任何内容推送到b
。我的意思是什么都没有,甚至没有空字符串——捕获堆栈本身仍然为空。但是,匹配该组仍然成功,并且相应的捕获从a
组中弹出。
这个问题特别让人烦恼的是,这种情况可能比情况2更常见,因为这是您尝试以平凡的从右到左的正则表达式使用平衡组的方式,但这样会发生什么。
第三个案例的更新:经过Kobi进行了更多测试后,发现在堆栈b
上发生了某些事情。似乎没有推送任何东西,因为m.Groups["b"].Success
将为False
,m.Groups["b"].Captures.Count
将为0
。然而,在正则表达式中,条件(?(b)true|false)
现在将使用true
分支。此外,在.NET中似乎可以在之后执行(?<-b>)
(之后访问m.Groups["b"]
将引发异常),而Mono在匹配正则表达式时会立即抛出异常。确实是一个错误。