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

正则指引之断言

发布时间:2020-12-14 01:26:55 所属栏目:百科 来源:网络整理
导读:正则表达式中的大多数结构匹配的文本会出现在最终的匹配结果中(一般用group(0)可以得到),但是也有些结构并不真正匹配文本,而只 负责判断在某个位置左/右侧的文本是否符合要求 ,这种结构被称为 断言 (assertion)。常见的断言有三类: 单词边界 , 行起

正则表达式中的大多数结构匹配的文本会出现在最终的匹配结果中(一般用group(0)可以得到),但是也有些结构并不真正匹配文本,而只负责判断在某个位置左/右侧的文本是否符合要求,这种结构被称为断言(assertion)。常见的断言有三类:单词边界行起始/结束位置环视

单词边界

在文本处理中经常可能进行单词替换,比如把一段文本中的row都替换成line。一般想到的是调用字符串的替换方法,直接替换row。在不同语言中这些方法各不相同,但差别不大。

替换前:The row we are looking for is row 10.

替换后:The line we are looking for is line 10.

不过,这样替换也可能会造成意想不到的后果。

替换前:...tomorrow I will wear in brown standing in row 10 next to the rowdy guy...

替换后:...tomorline I will wear in blinen standing in line 10 next to the linedy guy...

不仅所有单词row都被替换成了line,其他单词内部的row也被替换成了line,这显然不是我们想要的结果。要解决这个问题,必须有办法确定单词row,而不是字符串row。为解决这类问题,正则表达式提供了专用的单词边界(word boundary),记为 b 。它匹配的是“单词边界”位置,而不是字符。也就是说, b 能够匹配这样的位置:一边是单词字符,另一边不是单词字符,匹配示例:


browb
brow
rowb
tomorrow


OK
brown



row
OK
OK
OK
rowdy

OK

表达式说明
只能是单词
b的右侧是单词字符,
所以左侧不能是单词字符
b的左侧是单词字符,
所以右侧不能是单词字符

观察表格,可以发现两点:第一,单词边界并不区分左右,在“单词边界”上,可能只有左侧是单词字符,也可能只有右侧是单词字符,总的来说,单词字符只能出现在一侧第二,单词字符要求“另一边不是单词字符”,而不是“另一边的字符不是单词字符”,也就是说,一边必须出现单词字符,另一边可以出现非单词字符,也可能没有任何字符。所以,如果字符串只包含单词word,用 bwordb应该是可以匹配的,虽然w之前和d之后都没有任何字符。

单词边界要求一侧必须出现单词字符,到底什么是单词字符呢?

一般情况下,“单词字符”的解释是 w 能匹配的字符。在javascript,PHP,Python2,Ruby中, w只能匹配 [0-9a-zA-z_]。所以在这些语言中, bw+b能准确匹配英文单词了。示例:

//单词边界匹配
Stringtext="tomorrowIwillwearinbrownstandinginrow10nexttotherowdyguy";
Patternp=Pattern.compile("b(w+)b");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group(1));
}

但是也有些单词,bw+b是无能为力的,比如e-mail和M.I.T.。因为连字符-和点号. 都不能由 w 匹配,所以 bw+b无法匹配e-mail,也无法匹配M.I.T. 。如果确实希望处理e-mail之类的“单词”,也可以把表达式改为 b[-w]+b 。

与单词边界 b 对应的还有非单词边界 B 两者的关系类似 s和S,w和W,d和D;在同一种语言中,不管 b 是如何规定的,b能匹配的位置,B就不能匹配;B能匹配的位置,b就不能匹配。但是在实际使用中,B使用频率远远少于 b。

行起始/结束位置

单词边界匹配的是某个位置而不是文本,在正则表达式中,这类匹配位置的元素叫做锚点(anchor),它用来“定位”到某个位置。常用的锚点还有^$它们分别匹配字符串的开始位置和结束位置,所以可以用来判断“整个字符串能否由表达式匹配”。依靠^,就可以用正则表达式^Some准确验证字符串“是否以Some开头”,因为^会把整个表达式的匹配“定位”在字符串的开始位置。这样,即便表达式的其他部分可以在字符串中其他位置找到匹配,整个表达式也无法匹配成功

在某些情况下,^也可以匹配字符串内部的“行起始位置”。在讲解这种情况之前,我们先来看看怎么划分行。在编辑文本时,敲回车键就输入了行终止符(Line terminal),结束当前行,新起一行。看起来,这很好理解,然而不同平台上的行终止符其实各不相同,下表列出了各平台下的“行终止符”:

平台
行终止符
UNIX/Linux
n
Windows
rn
Mac OS
n

也就是说,每一行的“起始位置”,就是“行终止符”之后的那个位置,如果没有专门的符号,就要考虑各种“行终止符”。下面的例子看得更清楚,为了让换行符“可见”,我们用NL表示。

first line

second line

last line

它其实是下面这样,其中的NL可能是 n,也可能是 rn。

first lineNLsecond lineNLlast lineNL

如果把匹配模式设定为多行模式(Multiline Mode,这是一种影响元字符匹配的设定,后面在讲)下,^就即可以匹配整个字符串的起始位置,也可以匹配换行符之后的位置(设定多行模式最简单的办法是在正则表达式之前加上 (?m) ,这里虽然出现了括号,但因为是专用于指定匹配模式,所以不会作为捕获分组)。不过一般来说,^的主要用途是与其他子表达式配合,如下例那样提取每行的第一个单词:

//提取每行的第一个单词
Stringtext="firstlinensecondlinernrlastline";
Patternp=Pattern.compile("(?m)^(w+)");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group(1));
}

如果不想定位到字符串内部的行起始位置,只关心整个字符串的起始位置,则可以使用 A ,绝大多数工具中的正则表达式都支持这个锚点,它在任何情况下(包括多行模式下)都只匹配整个字符串的起始位置,如例:

//提取匹配整段文本的第一个单词
Stringtext="firstlinensecondlinernrlastline";
Patternp=Pattern.compile("(?m)A(w+)");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group(1));
}

“行结束位置”的情况更复杂。除去“行终止符”可能由各种字符表示的情况之外,“行结束位置”可能没有任何字符,你猜猜下面文本有几个行终止符?

字符串:Some sample text

可能一:Some sample text

可能二:Some sample textNL

而且其中的NL可能是 n,也可能是 rn。如果要匹配字符串最后一个单词,不但必须考虑NL所对应字符的多种可能,而且要兼顾NL是否出现情况更加复杂。针对这种问题,正则表达式提供了“通吃”行结束符的锚点$,它匹配的同样是位置。通常它匹配的是整个字符串的结尾位置——如果最后是行终止符,则匹配行终止符之前的位置;否则,匹配最后一个字符之后的位置。也就是说,上面两种可能,$它都可以匹配。如例:

//提取匹配每行的最后一个单词
Stringtext="firstlinensecondlinernrlastline";
Patternp=Pattern.compile("(?m)(w+)$");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group(1));
}

如果指定了多行模式,$会匹配每个行终止符之前的位置。如果最后一行没有行终止符,则匹配字符串的结尾位置,就如上面这个例子。

与$类似的还有两个特殊标记 Zz ,它们不受多行模式的影响在任何情况下都匹配整个字符串的结束位置。Z和z的主要差别在于:Z等价于默认模式(非多行模式)下的$,如果字符串的末尾有行终止符,则它匹配换行符之前的位置(也就是说不仅匹配换行符还能匹配什么也没有,与单行模式下的$一样);z则不管行终止符,只匹配“整个字符串的结束位置”(也就是说不配置换行符)。示例如下:

//Z与z的使用
Stringtext1="firstlinensecondlinernrlastline";
Stringtext2="firstlinensecondlinernrlastKKKrn";
Patternp1=Pattern.compile("(w+)z");
Patternp2=Pattern.compile("(w+)Z");
Matcherm1=p1.matcher(text1);
Matcherm2=p2.matcher(text2);
if(m1.find()){
System.out.println(m1.group(1));
}
if(m2.find()){
System.out.println(m2.group(1));
}

接着说^和$的另一个特点:进行正则表达式替换时并不会被替换。也就是说,在起始/结束位置进行替换,只会在起始/结束位置添加一些字符,位置本身仍然存在。^和$的另一个常用功能是删去多余的空白,包括行首尾的空白和空行

环视

前面介绍过单词边界匹配的是这样的位置:一边是单词字符,另一边不是单词字符。从另一个角度来看,它能进行这样的判断:在某个位置向左/向右看,必须出现或不能出现某类字符。有时候,这种功能非常有用。

针对这种要求正则表达式专门提供了环视(look-around)用来“停在原地,四处张望”。环视类似单词边界,在它旁边的文本需要满足某种条件,而且本身不匹配任何字符。比如正则表达式 <(?!/),其中的 (?!/)是一个环视结构,(?!...)是这个结构的标识,/才是真正的表达式,整个结构的意思是“当前位置之后(右侧),不容许出现 / 能匹配的文本”。看起来它和 <[^/]类似,其实大不相同:如果<(?!/)匹配成功,正则表达式真正匹配完成的只有<,而不包含<之后的那个字符,这样,就能准确表示“匹配<,同时这个<之后不能是/”。

再来看表达式 (?<!/)>,其中的(?<!/)也是一个环视结构,(?<!...)是这个结构的标识,/才是真正的表达式,整个结构的意思是“在当前位置之前(左侧),不容许出现 / 能匹配的文本“(它与上面的(?!/)类似,只是多了一个<,更加形象地指向左侧)。这样,就能准确地表示“匹配>,同时>之前不能是/”。

上面已经出现两种环视:(?!...)和(?<!...),它们的名字分别是“否定顺序环视”和"否定逆序环视"。“否定”的意思是“如果正则表达式匹配成功,则在当前位置匹配失败”,而“顺序”和“逆序”则表示正则表达式需要匹配的文本所在的位置。所以总的来说,环视一共分为4种:肯定顺序环视,否定顺序环视,肯定逆序环视,否定逆序环视。见下表:

名字
记法
判断方向
结构内表达式匹配成功的返回值
肯定顺序环视
(?=...)
向右
True
否定顺序环视
(?!...)
向右
False
肯定逆序环视
(?<=...)
向左
True
否定逆序环视
(?<!...)
向左
False

这4个名字容易混淆,不妨这样记忆:在当前位置,如果是朝右判断,则是顺序环视,如果是朝左判断,同是逆序环视;如果要求子表达式能匹配的字符串必须出现,则为肯定环视,如果要求子表达式能匹配的字符串不能出现,则为否定环视。

(编辑:李大同)

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

    推荐文章
      热点阅读