10

我刚刚更详细地了解了这两个概念。我一直很擅长使用 RegEx,而且似乎我从未见过需要这 2 个零宽度断言。

我很确定我错了,但我不明白为什么需要这些构造。考虑这个例子:

Match a 'q' which is not followed by a 'u'.

2个字符串将是输入:

Iraq
quit

使用负前瞻,正则表达式如下所示:

q(?!u)

没有它,它看起来像这样:

q[^u]

对于给定的输入,这两个正则表达式都给出相同的结果(即匹配Iraq但不匹配quit)(用 perl 测试)。同样的想法也适用于lookbehinds。

我是否缺少使这些断言比经典语法更有价值的关键特性?

4

4 回答 4

19

为什么您的测试可能有效(以及为什么不应该)

您能够Iraq在测试中匹配的原因可能是您的字符串末尾包含 a \n(例如,如果您从 shell 中读取它)。如果你有一个以 结尾的字符串q,那么q[^u]就不能像其他人所说的那样匹配它,因为[^u]匹配一个非u字符 - 但关键是必须有一个字符。

我们实际上需要环视什么?

显然,在上述情况下,前瞻并不重要。您可以使用q(?:[^u]|$). 所以我们只有在q后面跟着一个非u字符或字符串结尾时才匹配。不过,前瞻还有更复杂的用途,如果你在没有前瞻的情况下进行操作,这会变得很痛苦。

该答案试图概述一些重要的标准情况,这些情况最好通过环视来解决。

让我们从查看带引号的字符串开始。匹配它们的常用方法是使用("[^"]*"不是".*?")之类的东西。在开头之后",我们简单地重复尽可能多的非引号字符,然后匹配结束引号。再一次,一个否定的字符类是非常好的。但是在某些情况下,否定字符类不会削减它:

多字符分隔符

现在,如果我们没有双引号来分隔我们感兴趣的子字符串,而是一个多字符分隔符。例如,我们正在寻找,其中允许---sometext---single 和 double 。现在你不能只使用,因为那会禁止 single 。标准技术是在每个位置使用负前瞻,并且只消耗下一个字符,如果它不是. 像这样:-sometext[^-]*----

---(?:(?!---).)*---

如果您以前没有见过它,这可能看起来有点复杂,但它肯定比替代品更好(并且通常更有效)。

不同的分隔符

您会遇到类似的情况,您的分隔符只有一个字符,但可能是两个(或更多)不同字符之一。例如,在我们最初的示例中,我们希望允许单引号和双引号字符串。当然,您可以使用'[^']*'|"[^"]*",但最好在没有替代方案的情况下处理这两种情况。使用反向引用可以轻松处理周围的引号:(['"])[^'"]*\1. 这可以确保匹配以它开始的相同字符结束。但是现在我们的限制太严格了——我们希望允许"使用单引号和'双引号字符串。类似的东西[^\1]不起作用,因为反向引用通常包含多个字符。所以我们使用与上面相同的技术:

(['"])(?:(?!\1).)*\1

那是在开头引号之后,在使用每个字符之前,我们确保它与开头字符不同。我们尽可能长时间地这样做,然后再次匹配开头字符。

重叠匹配

这是一个(完全不同的)问题,如果不进行环顾,通常根本无法解决。如果您在全局范围内搜索匹配项(或想要在全局范围内进行正则表达式替换),您可能已经注意到匹配项永远不会重叠。即,如果你在里面搜索...abcdefghi你会得到abc,和 not def,等等。如果你想确保你的匹配在其他东西之前(或包围),这可能是个问题。ghibcdcde

假设您有一个 CSV 文件,例如

aaa,111,bbb,222,333,ccc

并且您只想提取完全是数字的字段。为简单起见,我假设任何地方都没有前导或尾随空格。如果没有环视,我们可能会进行捕获并尝试:

(?:^|,)(\d+)(?:,|$)

所以我们确保我们有一个字段的开头(字符串的开头或,),然后只有数字,然后是一个字段的结尾(,或字符串的结尾)。在这之间,我们将数字捕获到 group1中。不幸的是,这不会333在上面的例子中给我们,因为,它之前的 已经是匹配的一部分,222,- 并且匹配不能重叠。环视解决问题:

(?<=^|,)\d+(?=,|$)

或者,如果您更喜欢双重否定而不是交替,这相当于

(?<![^,])\d+(?![^,])

除了能够获取所有匹配项之外,我们还摆脱了通常可以提高性能的捕获。(感谢 Adrian Pronk 的这个例子。)

多个独立条件

何时使用环视(特别是前瞻)的另一个非常经典的例子是当我们想要同时检查输入的多个条件时。假设我们要编写一个正则表达式,以确保我们的输入包含一个数字、一个小写字母、一个大写字母、一个不是这些字符的字符,并且没有空格(例如,为了密码安全)。如果没有环视,您必须考虑数字、小写/大写字母和符号的所有排列。像:

\S*\d\S*[a-z]\S*[A-Z]\S*[^0-9a-zA_Z]\S*|\S*\d\S*[A-Z]\S*[a-z]\S*[^0-9a-zA_Z]\S*|...

这些只是 24 种必要排列中的两种。如果您还想确保同一正则表达式中的最小字符串长度,则必须将它们分布在所有可能的组合中\S*- 在单个正则表达式中根本不可能做到这一点。

期待救援!我们可以简单地在字符串的开头使用几个前瞻来检查所有这些条件:

^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^0-9a-zA-Z])(?!.*\s)

因为前瞻实际上不消耗任何东西,所以在检查每个条件后,引擎将重置到字符串的开头并可以开始查看下一个。如果我们想添加一个最小字符串长度(比如8),我们可以简单地追加(?=.{8})。更简单,更易读,更易于维护。

重要提示:不是在任何实际环境中检查这些条件的最佳通用方法。如果您以编程方式进行检查,通常最好为每个条件使用一个正则表达式,并分别检查它们 - 这让您返回更有用的错误消息。但是,如果您有一些固定的框架允许您仅通过提供单个正则表达式来进行验证,则上述内容有时是必要的。此外,如果您有独立的字符串匹配标准,则值得了解一般技术。

我希望这些示例能让您更好地了解人们为什么喜欢使用环视。还有更多的应用程序(另一个经典是在数字中插入逗号(?!u)),但重要的是您要意识到和之间存在差异,[^u]并且在某些情况下,否定字符类根本不够强大。

于 2013-06-14T10:49:21.527 回答
5

q[^u]不会匹配“Iraq”,因为它会寻找另一个符号。

q(?!u)但是,将匹配“伊拉克”:

regex = /q[^u]/
/q[^u]/
regex.test("Iraq")
false
regex.test("Iraqf")
true
regex = /q(?!u)/
/q(?!u)/
regex.test("Iraq")
true
于 2013-06-14T10:05:24.683 回答
4

好吧,另一件事与其他人提到的否定前瞻一样,您可以匹配连续的字符(例如,您可以ui在 with时取反,但[^...]不能取反,或者如果您尝试,您也将取反,和。uiui[^ui]{2}uuiiiu

于 2013-06-14T10:05:19.200 回答
3

重点是不要“消耗”下一个字符,以便它可以被随后出现的另一个表达式捕获。

如果它们是正则表达式中的最后一个表达式,那么您显示的内容是等效的。

但是 egq(?!u)([a-z])会让非u角色成为下一组的一部分。

于 2013-06-14T10:04:45.070 回答