进过前面的学习,我们已经可以使用正则解决很多场景的问题了,但是来看这样一个场景。原文如下:tom and helen are best friends, tomorrow they will go to school together.
要求替换语句中的 tom 为 hanmeimei。easy!
str = "tom and helen are best friends, tomorrow they will go to school together.";
regex = "tom";
str = str.replaceAll(regex,"lilei");
log.info(str);
运行上面的代码,我们得到了
lilei and helen are best friends, lileiorrow they will go to school together.
请问 lileiorrow
是个什么东西?oh mygod!为什么和我们预期的不一样。想必大家已经看出来了,因为tomorrow
中tom
满足了,我们的正则表达式tom
,所以tomorrow
被替换成了lileiorrow
。知道了原因所在,那么解决的思路也就出来了,即我们的正则需要有一个边界的概念,我要匹配的就是 tom
这三个字母,m
就是结尾,后面不应该紧跟其他的字母!也就是说,在有些情况下,我们对要匹配的文本的位置也有一定的要求。为了解决这个问题,正则中提供了一些结构,只用于匹配位置,而不是文本内容本身,这种结构就是断言。常见的断言有三种:单词边界、行的开始或结束以及环视。
单词边界(Word Boundary)
单词的组成一般可以用元字符 \w+ 来表示,\w 包括了大小写字母、下划线和数字(即 [A-Za-z0-9_])。那如果我们能找出单词的边界,也就是当出现了\w 表示的范围以外的字符,比如引号、空格、标点、换行等这些符号,我们就可以在正则中使用\b 来表示单词的边界。 \b 中的 b 可以理解为是边界(Boundary)这个单词的首字母。因此,上面的的例子我们只需要把正则调整为 \btom\b
,也可以用^tom$
就可以得到我们想要的结果了。
行的开始或结束
和单词的边界类似,在正则中还有文本每行的开始和结束,如果我们要求匹配的内容要出现在一行文本开头或结尾,就可以使用 ^
和 $
来进行位置界定。
在计算机中,回车\r
和换行\n
其实是两个概念,并且在不同的平台上,换行的表示也是不一样的。windows的换行符使用\r\n
表示,linux、macos使用\n
表示。那么,匹配行的开始或结束有什么用呢?最常见的例子就是日志收集,我们在收集日志的时候,通常可以指定日志行的开始规则,比如以时间开头,那些不是以时间开头的可能就是打印的堆栈信息。在这种情况下,我们就通过日期时间开头来判断哪一行是日志的第一行,在日期时间后面的日志都属于同一条日志。除非我们看见下一个日期时间的出现,才是下一条日志的开始。
再来看一个例子,web开发中我们经常需要校验用户输入的信息,例如某个输入框要求用户输入6位数字,如果你的校验正则是这样的\d{6}
,你会发现 123456
、1234567
都能匹配上。在多行模式下,^ 和 $ 符号可以匹配每一行的开头或结尾。
解决这个问题还有一种做法,我们可以在使用正则校验前,先判断一下字符串的长度,如果不满足长度要求,那就不需要再用正则去判断了。
环视( Look Around)也叫先行断言
环视就是要求匹配部分的前面或后面要满足(或不满足)某种规则,有些地方也称环视为零宽断言。
举个栗子:邮政编码的规则是第一位是 1-9,一共
有 6 位数字组成。现在要求你写出一个正则,提取文本中的邮政编码。根据规则,我们很容易就可以写出邮编的组成 [1-9]\d{5}
。
使用以下3个案例进行匹配:
123456 可以正常匹配到结果 123456
1234567 匹配到了 123456
最离谱的是 123456654321 竟然匹配到了 123456、654321两个结果
我们需要的是只匹配满足条件的6位数字,不能多也不能少,也就是说,除了文本本身组成符合这 6 位数的规则外,这 6 位数左边或右边都不能是数字。
正则是通过环视来解决这个问题的。解决这个问题的正则有四种。
正则 | 名称 | 示例 |
---|---|---|
(?<=expression) | 零宽正向后行断言 | (?<=\d)cat 左边是数字的cat ,可以匹配到0cat |
(?<!expression) | 零宽负向后行断言 | (?<!\d)cat 左边不是数字的cat ,可以匹配到ccat |
(?=expression) | 零宽正向先行断言 | six(?=\d) 右边是数字的six ,可以匹配到six6 |
(?!expression) | 零宽负向先行断言 | six(?!\d) 右边不是数字的six ,可以匹配到sixx |
<
代表看左边,没有<
是看右边,!
是非的意思
因此,针对刚刚邮编的问题,就可以写成左边不是数字,右边也不是数字的 6 位数的正则。即 (?<!\d)[1-9]\d{5}(?!\d)。这样就能够符合要求了。
其实表示单词边界的 \b 如果用环视的方式来写。单词可以用 \w+ 来表示,单词的边界其实就是那些不能组成单词的字符,即左边和右边都不能是组成单词的字符,
(?<!\w)
表示左边不能是单词组成字符,(?!\w)
右边不能是单词组成字符,即\b\w+\b
也可以写成(?<!\w)\w+(?!\w)
。另外,根据前面学到的知识,非\w
也可以用\W
来表示。那单词的正则可以写成(?<=\W)\w+(?=\W)
。当然单词的边界使用\b
表示明显更简洁,也更容易阅读和书写。
再来看这个栗子:
要求匹配cat* dog* cat dog pig
中的 cat*
、dog*
,如果你的正则是这样cat*|dog*
,那么它匹配到的结果将会是cat
、dog
、 cat
、dog
4个结果,而不是你希望的cat*
、dog*
。
这是因为*
是我们学到的元字符,它在正则表达式中有特殊的含义,这时如果要匹配*
字符本身,我们就需要对它就行转义,正则表达式中使用\
进行转义。因此,上面的正则调整为cat\*|dog\*
就可以匹配到我们想要的结果啦。
今天的内容就到这里了,我们下节见,由于本人对正则的认知有限,如文中有表达不到位或者错误的地方,欢迎大家批评指正,感谢。
系列文章如下:
重学正则表达式(一)
重学正则表达式(二)
重学正则表达式(三)
重学正则表达式(五)