我们需要什么Lookahead / Lookbehind Zero Width Assertions?

时间:2013-06-14 10:01:28

标签: regex regex-lookarounds

我刚刚更详细地了解了这两个概念。我一直对RegEx很好,似乎我从未见过需要这两个零宽度断言。

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

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

2个字符串将是输入:

Iraq
quit

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

q(?!u)

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

q[^u]

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

我是否缺少一个关键功能,使这些断言比传统语法更有价值?

4 个答案:

答案 0 :(得分:17)

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

您在测试中匹配Iraq的原因可能是您的字符串末尾包含\n(例如,如果您从shell中读取它)。如果您的字符串以q结尾,则q[^u]无法与其他人说的匹配,因为[^u]与非u字符匹配 - 但重点是必须是一个角色。

我们究竟需要了解什么?

显然,在上述情况下,前瞻并不重要。您可以使用q(?:[^u]|$)解决此问题。因此,只有当q后跟非u字符或字符串结尾时,我们才匹配。然而,对于前瞻者来说有更复杂的用途,如果你没有前瞻的话就会变得很痛苦。

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

让我们从查看引用的字符串开始。匹配它们的常用方法是"[^"]*" not with ".*?")。在打开"之后,我们只需重复尽可能多的非引号字符,然后匹配结束引号。同样,否定的角色类完全没问题。但有些情况下,否定的角色类并没有削减它:

多字符分隔符

现在如果我们没有双引号来分隔我们感兴趣的子字符串,但是多字符分隔符。例如,我们正在寻找---sometext----中允许使用单sometext[^-]*。现在您无法使用-,因为这样会禁止单---。标准技术是在每个位置使用负前瞻,如果它不是---(?:(?!---).)*--- 的开头,则只消耗下一个字符。像这样:

'[^']*'|"[^"]*"

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

不同的分隔符

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

...

这是在开始引用之后,在消费每个字符之前,我们确保它与开头字符不同。我们尽可能地做到这一点,然后再次匹配开场角色。

重叠比赛

这是一个(完全不同的)问题,如果没有外观通常根本无法解决。如果您在全局范围内搜索匹配项(或者想要全局替换正则表达式),您可能已经注意到匹配项永远不会重叠。即如果您在abcdefghi中搜索abc,则会获得defghibcd而不是cdeaaa,111,bbb,222,333,ccc 等等。如果您想确保您的匹配在其他内容之前(或包围),则可能会出现问题。

假设你有一个像

这样的CSV文件
(?:^|,)(\d+)(?:,|$)

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

,

因此我们确保我们有一个字段的开头(字符串的开头或,),然后只有数字,然后是字段的结尾(1或字符串的结尾)。在此之间我们将数字捕获到组333中。遗憾的是,在上面的示例中,这不会给我们,,因为在它之前的,222,已经是匹配(?<=^|,)\d+(?=,|$) 的一部分 - 并且匹配不能重叠。 Lookarounds解决了这个问题:

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

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

\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*|...

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

多个独立条件

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

\S*

这些只是24种必要排列中的两种。如果您还想确保在同一个正则表达式中使用最小字符串长度,则必须在^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^0-9a-zA-Z])(?!.*\s) 的所有可能组合中分发这些字符串长度 - 在单个正则表达式中根本无法进行。

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

8

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

重要说明:这是在任何实际设置中检查这些条件的最佳通用方法。如果您以编程方式进行检查,通常最好为每个条件设置一个正则表达式,并单独检查它们 - 这样您就可以返回更有用的错误消息。但是,如果你有一些固定的框架允许你只通过提供一个正则表达式来进行验证,上面有时是必要的。此外,如果你有一个匹配字符串的独立标准,那么值得了解一般技术。

我希望这些例子可以让您更好地了解人们为什么要使用外观。还有更多的应用程序(另一个经典是inserting commas into numbers),但重要的是你意识到[^u]和{{1}}之间存在差异,并且有些情况下否定的角色类别根本不够强大。

答案 1 :(得分:5)

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

然而,

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

regex = /q[^u]/
/q[^u]/
regex.test("Iraq")
false
regex.test("Iraqf")
true
regex = /q(?!u)/
/q(?!u)/
regex.test("Iraq")
true

答案 2 :(得分:4)

嗯,另外一件事以及其他人提到的负面前瞻,你可以匹配连续的角色(例如你可以在ui时否定[^...],你不能否定ui但是ui如果您尝试[^ui]{2},您还会否定uuiiiu

答案 3 :(得分:3)

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

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

但是,例如q(?!u)([a-z])会让非u字符成为下一组的一部分。