否定分组

否定分组是一种位置锚定技巧,而不是一种正则语法。它的用法为((?!xxx).),用于取代匹配任意字符的.,其外层括号的主要目的是将(?!xxx).组合在一起。

例如(?:(?!xxx).)*在作用上表示右边不能是xxx,然后再匹配任意单个字符。其效果等价于匹配任意单个字符,直到右边是xxx的字符,且不要求xxx必须存在,即类似于.*(?!xxx).

例如"abcat" =~ /(?:(?!cat).)*/,它的匹配过程如下:

  • 首先锚定起始位置(a的左边),其右边不是cat,于是吞掉一个字符a
  • 进行下一轮匹配,锚定a后面的位置(a右边b左边),其右边不是cat,于是吞掉字符b
  • 再锚定b后面的位置,其右边是cat,锚定失败,于是本轮匹配失败,但注意,b在上轮匹配中已经被吞掉
  • 所以最终匹配的结果是ab,因此/(?:(?!cat).)/相当于匹配任意单个字符,直到右边是cat字符

否定分组常常需要结合其他位置锚定一起使用才能有比较好的效果。

比如分析下面几个匹配的匹配结果:

# 匹配cat前的所有字符
'fox,cat,dog,parrot' =~ /\A((?!cat).)*/;
#=> "fox,"

# 匹配parrot前的所有字符
'fox,cat,dog,parrot' =~ /\A((?!parrot).)*/;
#=> "fox,cat,dog,"

# 匹配pig前的所有字符
'fox,cat,dog,parrot' =~ /\A((?!pig).)*/;
#=> "fox,cat,dog,parrot"

# 匹配连续重复字符前的所有字符
'fox,cat,dog,parrot' =~ /\A(?:(?!(.)\1).)*/;
#=> "fox,cat,dog,pa"

# cat((?!do).)*匹配从cat开始直到do前面的一个字符,即"cat,"
# 然后还要紧跟着匹配par
'fox,cat,dog,parrot' =~ /cat((?!do).)*par/;
#=> undef

除了上述技巧,否定分组还常用于如下匹配需求:

  • (1).左边任意位置(相邻或不相邻)不能有某字符(串)
  • (2).右边任意位置不能有某字符(串)

即类似于环视锚定的需求。需求(2)完全可以使用正向环视锚定来匹配,而需求(1)不一定能通过逆向环视锚定来匹配,因为逆向环视锚定要求字符数量是固定的。\K能实现变长字符的逆向环视锚定,但\K会丢弃匹配结果。

因此,否定分组一般只用于实现需求(1),且不丢弃匹配结果。

例如,想要匹配dog左边没有cat的字符串:

# 匹配失败
# \A((?!cat).)*匹配cat前任意长度的字符,然后紧跟着要匹配dog
'fox,cat,dog,parrot' =~ /\A((?!cat).)*dog/;

# 匹配成功
# 只有当pig不在dog左边时,才能匹配成功
'fox,cat,dog,parrot' =~ /\A((?!pig).)*dog/;