正则表达式Lookaround特性的应用
1. 介绍Lookaround是Perl 5引进的特性,这个特性极大增强了正则表达式的能力,熟练掌握该特性,可以帮助我们运用正则表达式解决更复杂的问题。Lookaround有4种类型,下面的定义取自Java API :
上面定义中的"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,主要用的都是一些正则表达式的基本特性,只有两个可能不太常见的特性:
先了解这两个特性对理解后面的例子是有帮助的。 2.1 匹配否定匹配全数字的字符串,正则表达式很容易写,"d+";但是要匹配不全是数字的字符串,怎么写呢?"D+"是不行的,因为这样无法匹配包含数字的串;分析一下,只要串里包含非数字就可以,所以可以写成".*D.*",还不算困难。 再看个例子,匹配包含连续数字的字符串,可以用".*dd.*"来实现;那么怎么匹配不包含连续数字的字符串呢?仔细找找规律,"d?(D+d?)*"似乎可以满足要求,但理解起来就不是那么容易了。 从这两个例子看,模式的否定匹配,跟原模式完全没有关系,也没有规律可寻,不同的情况得具体分析。可以想象,对于更复杂的情况,否定匹配很可能会更难写,甚至写不出来的,或者即使写出来的,也非常难理解。 利用Lookaround特性可以很容易实现否定匹配,上面例子的Java代码如下:
Pattern.compile("(?!d+$).+"); // 字符串不全是数字 Pattern.compile("(?!.*?dd).+"); // 不包含连续数字 在模式的起始处,利用"Negative Lookahead"特性定义一个"Assertion",写起来很有规律,也非常容易理解。第二个例子里用了"*?"(Reluctant quantifiers),因为它比默认的"*"(Greedy quantifiers)更符合我们的意图,也更高效。 注意:在做match的时候,Java会在模式的前后自动添加"^"和"$",所以就没必要自己加了;但在有的语言或工具里,需要自己添加"^"和"$"。
2.2 与运算下面举一个验证密码例子。出于简化的目的,只涉及"w"中的字符,即[a-zA-Z_0-9];为了便于演示,也不考虑密码格式的定义是否合理。对密码的格式的要求如下:
这些要求看似很复杂,实际上却是异乎寻常地简单,下面是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 Lookaheadn" + " (?<=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 Lookaheadn" + " (?<!d,) # 前面不是数字n" + " | # 或n" + " (?!d) # 后面不是数字n" + " ) # n" + ") # n",Pattern.COMMENTS) .matcher(s) .replaceAll(" "); 3. 其他使用Lookaround时一定要注意,很多正则表达式引擎只支持Lookahead,不支持Lookbehind;即使支持Lookbehind,也有限制,一般只能使用固定长度的表达式,不能用"*"或者"+"这些量词。 还有很重要的一点,Lookaround是Atomic匹配,即一旦Lookaround成功,那么就不会再对Lookaround做回溯,即使后面的匹配失败,如果在Lookaround中使用了分组,一定要小心这点。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |