正则表达式Lookaround特性的应用

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
两个方向:Lookahead和Lookbehind,两种逻辑:Positive和Negative。目前多数正则表达式引擎至少都支持Lookahead,后面的例子用Java来演示,这4种类型Java都支持。

上面定义中的"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://my.oschina.net/mononite/blog/153202

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值