否定分组
否定分组是一种位置锚定技巧,而不是一种正则语法。它的用法为((?!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/;