1. 介绍
Lookaround是Perl 5引进的特性,这个特性极大增强了正则表达式的能力,熟练掌握该特性,可以帮助我们运用正则表达式解决更复杂的问题。Lookaround有4种类型,下面的定义取自Java API :
- (?=X) X,via zero-width positive lookahead
- (?!X) X,via zero-width negative lookahead
- (?<=X) X,via zero-width positive lookbehind
- (?<!X) X,via zero-width negative lookbehind
上面定义中的"Zero-width"是理解Lookaround特性的关键。常用的"^"、"$"、"\b"等Boundary Characters都是"Zero-width Assertions",即不消费字符,但判定当前位置是否满足特定的要求,Lookaround实际也是"Zero-width Assertions"。Boundary Characters是系统预定义的"Zero-width Assertions",而Lookaround可以看做用户自定义的"Zero-width Assertions"。
接下来给几个Lookaround应用的例子。
2. 应用举例
下面的例子除了Lookaround,主要用的都是一些正则表达式的基本特性,只有两个可能不太常见的特性:
- Reluctant quantifiers,X*? X,zero or more times
- (?:X) X,as a non-capturing group
先了解这两个特性对理解后面的例子是有帮助的。
2.1 匹配否定
匹配全数字的字符串,正则表达式很容易写,"\d+";但是要匹配不全是数字的字符串,怎么写呢?"\D+"是不行的,因为这样无法匹配包含数字的串;分析一下,只要串里包含非数字就可以,所以可以写成".*\D.*",还不算困难。
再看个例子,匹配包含连续数字的字符串,可以用".*\d\d.*"来实现;那么怎么匹配不包含连续数字的字符串呢?仔细找找规律,"\d?(\D+\d?)*"似乎可以满足要求,但理解起来就不是那么容易了。
从这两个例子看,模式的否定匹配,跟原模式完全没有关系,也没有规律可寻,不同的情况得具体分析。可以想象,对于更复杂的情况,否定匹配很可能会更难写,甚至写不出来的,或者即使写出来的,也非常难理解。
利用Lookaround特性可以很容易实现否定匹配,上面例子的Java代码如下:
Pattern.compile("(?!\\d+$).+"); // 字符串不全是数字 Pattern.compile("(?!.*?\\d\\d).+"); // 不包含连续数字
在模式的起始处,利用"Negative Lookahead"特性定义一个"Assertion",写起来很有规律,也非常容易理解。第二个例子里用了"*?"(Reluctant quantifiers),因为它比默认的"*"(Greedy quantifiers)更符合我们的意图,也更高效。
注意:在做match的时候,Java会在模式的前后自动添加"^"和"$",所以就没必要自己加了;但在有的语言或工具里,需要自己添加"^"和"$"。
2.2 与运算
下面举一个验证密码例子。出于简化的目的,只涉及"\w"中的字符,即[a-zA-Z_0-9];为了便于演示,也不考虑密码格式的定义是否合理。对密码的格式的要求如下:
- 长度在8到16之间
- 至少包含一个小写字母
- 至少包含一个大写字母
- 至少包含一个数字或_
- 开头和结尾不允许是数字
- 不允许出现连续的_
这些要求看似很复杂,实际上却是异乎寻常地简单,下面是Java代码:
Pattern.compile( "(?=.*?[a-z]) # 至少包含小写字母\n" + "(?=.*?[A-Z]) # 至少包含大写字母\n" + "(?=.*?[\\d_]) # 至少包含一个数字或_\n" + "(?!\\d|.*\\d$) # 开头和结尾不允许是数字\n" + "(?!.*?__) # 不允许出现连续的_\n" + "\\w{8,16} # 长度在8到16之间\n",Pattern.COMMENTS);
如果熟悉Lookaround,这个正则表达式是非常容易理解的,注释已经说明地很清楚了;当然,上面的这个正则表达式不是唯一的写法,更不是最优的写法。
2.3 反向引用和分组
再看一个例子,怎么判断一个字符串是否包含重复的字符?如果了解反向引用,可以用下面的正则表达式来实现:
Pattern.compile( ".*? # 第一个重复字母前面的部分\n" + "(.) # 重复字母第一次出现\n" + ".*? # 重复字母间的部分\n" + "\\1 # 重复字母第二次出现\n" + ".* # 重复字母第二次出现后的部分\n",Pattern.COMMENTS);即使不加注释,这个正则表达式也不难理解。那么它的否定匹配,判断一个字符串不包含重复字符的正则表达式怎么写呢?仔细考虑了一下,得到下面的写法:
Pattern.compile( "(?: # 非捕获分组,该分组中只包含一个字符\n" + " (.) # 一个字符的分组\n" + " (?!.*?\\1) # 该字符不能在后面的字符串中出现\n" + ")+ # 所有的字符\n",Pattern.COMMENTS);
举这个例子,主要是为了说明Lookaround中可以使用反向引用;不仅如此,在Lookaround中实际可以使用任意合法的正则表达式。而且,在Lookaround中还可以定义分组,虽然Lookaround是"Zero-width Assertions",但是可以在Lookaround中定义长度不为零的分组。
上面的不包含重复字符的正则表达式,有一个常用的小技巧,"(?:(.)(X))+",其中"X"是一个Lookaround的表达式,这种对单个字符做约束的方式,在很多情况下都会很用。但是,这种不指定位置,对所有字符都做Lookaround的做法,效率是非常差的,如果在乎性能,一定要避免这种做法。
2.4 Lookaround嵌套
Lookaound表达式里可以是用任意正则表达式,所以我们可以在Lookaround中嵌套Lookaround表达式,这些表达式都是对同一个位置做约束。
比如有这么个字符串"John has 2,000 dollars,Paul has $1,500,George has $1,200,Ringo has $1,600",现在要在","后添加空格,但是数字里的","后不添加。下面嵌套的Lookaround可以满足要求:
Pattern.compile( "(?<=,# 前面是逗号,即在逗号的后面\n" + " (?! # Negative Lookahead\n" + " (?<=\\d,) # 逗号前面是数字\n" + " (?=\\d) # 逗号后面是数字\n" + " ) # \n" + ") # \n",Pattern.COMMENTS) .matcher(s) .replaceAll(" ");Lookaround是"Zero-width",所以找到位置,直接用空格替换就是了。上面的表达式有"与"和"非的关系",根据德摩根定律,NOT (a AND b) === (NOT a OR NOT b) ,所以也可以用下面的表达式来实现:
Pattern.compile( "(?<=,# 前面是逗号,即在逗号的后面\n" + " (?: # Positive Lookahead\n" + " (?<!\\d,) # 前面不是数字\n" + " | # 或\n" + " (?!\\d) # 后面不是数字\n" + " ) # \n" + ") # \n",Pattern.COMMENTS) .matcher(s) .replaceAll(" ");
3. 其他
使用Lookaround时一定要注意,很多正则表达式引擎只支持Lookahead,不支持Lookbehind;即使支持Lookbehind,也有限制,一般只能使用固定长度的表达式,不能用"*"或者"+"这些量词。
还有很重要的一点,Lookaround是Atomic匹配,即一旦Lookaround成功,那么就不会再对Lookaround做回溯,即使后面的匹配失败,如果在Lookaround中使用了分组,一定要小心这点。
原文链接:https://www.f2er.com/regex/362667.html