正则表达式Lookaround特性的应用

前端之家收集整理的这篇文章主要介绍了正则表达式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://www.f2er.com/regex/362667.html

猜你在找的正则表达式相关文章