加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

正则表达式Lookaround特性的应用

发布时间:2020-12-14 02:17:49 所属栏目:百科 来源:网络整理
导读:1. 介绍 Lookaround是Perl 5引进的特性,这个特性极大增强了正则表达式的能力,熟练掌握该特性,可以帮助我们运用正则表达式解决更复杂的问题。Lookaround有4种类型,下面的定义取自Java API : (?=X) X,via zero-width positive lookahead (?!X) X,via zero

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.*",还不算困难。

再看个例子,匹配包含连续数字的字符串,可以用".*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];为了便于演示,也不考虑密码格式的定义是否合理。对密码的格式的要求如下:

  • 长度在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 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中使用了分组,一定要小心这点。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读