为什么你的测试可能起作用(以及为什么它不应该)
你之所以能够在测试中匹配到Iraq
,可能是因为你的字符串末尾包含一个\n
(例如,如果你是从shell中读取)。如果你有一个以q
结尾的字符串,那么q[^u]
不能像其他人说的那样将其匹配,因为[^u]
匹配一个非u
字符 - 但关键是必须要有一个字符。
我们实际上需要什么样的先行断言?
显然,在上面的情况下,前瞻并不是至关重要的。你可以通过使用q(?:[^u]|$)
来解决这个问题。这样,我们只匹配q
后面跟着一个非u
字符或字符串的末尾。
虽然这种方式很好,但使用前瞻会更加高效。
下面是一些需要使用前瞻的重要的标准情况概述。
首先,让我们看一下引用字符串。通常的匹配方法是使用类似于"[^"]*"
(而不是".*?"
)的模式。在开头的"
后,我们只需重复尽可能多的非引号字符,然后匹配结束引号。同样,否定字符类是完全可以的。但是,在某些情况下,否定字符类无法胜任:
多字符分隔符
现在,如果我们没有双引号来限定我们感兴趣的子字符串,而是使用多字符分隔符。例如,我们正在寻找---sometext---
,其中单个和多个-
在sometext
中都是允许的。这时,你不能只使用[^-]*
,因为这会禁止使用单个-
。标准技术是在每个位置使用负向先行断言,并且只有在它不是---
的开头时才使用下一个字符。像这样:
---(?:(?!---).)*---
如果你以前没有看过这个,它可能会看起来有点复杂,但比其他方法更好(通常也更高效)。
不同的分隔符
你可能遇到类似的情况,其中分隔符只是一个字符,但可以是两个(或更多)不同的字符之一。例如,假设在我们的初始示例中,我们想要允许单引号和双引号字符串。当然,你可以使用'[^']*'|"[^"]*"
,但最好不要使用这种替代方案。用一个回溯引用可以轻松处理周围的引号:(['"])[^'"]*\1
。这确保了匹配以相同的字符结束,就像开始时那样。但现在我们太严格了——我们想允许单引号字串中的"
和双引号字串中的'
。类似于[^\1]
的东西行不通,因为回溯引用通常包含不止一个字符。所以我们使用与上面相同的技术:
(['"])(?:(?!\1).)*\1
在开头引号后,消耗每个字符之前,我们确保它不与开头字符相同。我们持续做这件事,直到匹配到开头字符。
重叠匹配
这是一个完全不同的问题,通常无法在没有环视的情况下解决。如果你想要全局地搜索匹配(或者想要在正则表达式中进行全局替换),你可能已经注意到匹配永远不会重叠。也就是说,如果你在 abcdefghi
中搜索 ...
,你会得到 abc
、def
、ghi
,而不是 bcd
、cde
等。这可能会成为一个问题,如果你想要确保你的匹配前面(或周围)有其他东西。
假设你有一个 CSV 文件,类似于:
aaa,111,bbb,222,333,ccc
而您想要提取的字段全部为数字。 为简单起见,我假设没有任何前导或尾随空格。 没有先行断言的情况下,我们可以使用捕获并尝试:
(?:^|,)(\d+)(?:,|$)
因此,我们确保有一个字段的开始(字符串的开头或,
),然后是只有数字,最后是字段的结束(,
或字符串的结尾)。在此之间,我们将这些数字捕获到组1
中。不幸的是,这在上面的例子中无法给出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]
之间存在差异,并且存在负面字符类根本不够强大的情况。
foo
后面没有bar
,可以使用foo[^b][^a][^r]
(并将其消耗掉),但使用foo(?!bar)
会更加简单易读。在if/else正则表达式语句中也可能很方便。 - HamZa<
、>
或\b
单词边界匹配吗?它们是先行断言和后行断言的简写:<
==(?<!\w)(?=\w)
;>
==(?<=\w)(?!\w)
和\b
==(?:(?<!\w)(?=\w)|(?<=\w)(?!\w))
。虽然我猜测它们被优化用于特定的功能。 - Adrian Pronk