先来看一个例子,我们知道我国身份证是由15位数字或者18位数字或者17位数字加一个字母X组成,根据上面的规则,我们很快可以写出如下的正则表达式 \d{15}|\d{17}X|\d{18}
,我们来测试一下:
str = "12345678912345678X";
regex = "\\d{15}|\\d{17}X|\\d{18}";
pattern = Pattern.compile(regex);
matcher = pattern.matcher(str);
result= new ArrayList<>(1 << 2);
while (matcher.find()){
result.add(matcher.group());
}
log.info(JSON.toJSONString(result));
运行上面的案例,我们发现得到的结果是 ["123456789123456"]
,但是这好像并不是我们想要的结果,因为我需要匹配的是一个18位的身份证号码。灵机一动,调换一下正则的顺序是不是可以实现呢?
str = "12345678912345678X";
regex = "\\d{17}X|\\d{18}|\\d{15}";
pattern = Pattern.compile(regex);
matcher = pattern.matcher(str);
result= new ArrayList<>(1 << 2);
while (matcher.find()){
result.add(matcher.group());
}
log.info(JSON.toJSONString(result));
运行案例,竟然真的得到了我们想要的结果!为什么会出现这种情况呢?因为在大多数正则实现中,多分支选择都是左边的优先。类似地,你可以使用 “上海市|上海” 来查找 “上海” 和 “上海市”。同时我们前面学习过,?
可以表示出现 0 次或 1 次,你发现可以使用“上海市?” 来实现来查找 “上海” 和“上海市”。学以致用,很快,我们上面的案例就可以修改为 \d{15}\d{3}?|\d{17}X
str = "123456789123456";
regex = "\\d{15}\\d{3}?|\\d{17}X";
pattern = Pattern.compile(regex);
matcher = pattern.matcher(str);
result= new ArrayList<>(1 << 2);
while (matcher.find()){
result.add(matcher.group());
}
log.info(JSON.toJSONString(result));
神奇的是,这次我们竟然匹配不到任何结果了!这是因为我们想要的是 \d{3}
出现0次或者1次,但是我们上节课也学习了,在量词后面添加 ?
表示非贪婪匹配,由于 \d{3} 表示三次,加问号非贪婪还是 3 次,这就不是我们想要的结果了。这时候,必须使用括号将来把表示“三个数字”的\d{3}
这一部分括起来,也就是表示成\d{15}(\d{3})?|\d{17}X
这样。现在就比较清楚了:括号在正则中的功能就是用于分组。简单来理解就是,由多个元字符组成某个部分,应该被看成一个整体的时候,可以用括号括起来表示一个整体,这是括号的一个重要功能。其实用括号括起来还有另外一个作用,那就是“复用”。
括号在正则中可以用于分组,被括号括起来的部分“子表达式”会被保存成一个子组。那分组和编号的规则是怎样的呢?其实很简单,用一句话来说就是,第几个括号就是第几个分组。这么说可能不好理解,我们来举一个例子看一下。这里有个时间格式 2021-11-11 09:20:10,假设我们想要使用正则提取出里面的日期和时间,正则如下:(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})
,我们可以写出如上所示的正则,将日期和时间都括号括起来。这个正则中共有两个分组,日期是第 1 个,时间是第 2 个。这样要提取日期很时间也就很方便了。
str = "2021-11-16 09:20:10";
regex = "(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}:\\d{2})";
pattern = Pattern.compile(regex);
matcher = pattern.matcher(str);
if (matcher.find()){
final String dateStr = matcher.group(1);
final String timeStr = matcher.group(2);
log.info("提取到的日期是 {}", dateStr);
log.info("提取到的时间是 {}", timeStr);
}
运行代码,我们可以得到结果:
提取到的日期是 2021-11-16
提取到的时间是 09:20:10
如果正则中出现了括号,那么我们就认为,这个子表达式在后续可能会再次被引用,所以不保存子组可以提高正则的性能。除此之外呢,这么做还有一些好处,由于子组变少了,正则性能会更好,在子组计数时也更不容易出错。那到底啥是不保存子组呢?我们可以理解成,括号只用于归组,把某个部分当成“单个元素”,不分配编号,后面不会再进行这部分的引用。例如还是匹配身份证那个例子,如果我们把正则表达式修改为 \d{15}(?:\d{3})?|\d{17}X
,我们看到我们在(\d{3})
括号内前面添加了 ?:
变为了 (?:\d{3})
,这样就表示不保存自组,后续也没法再利用括号里面的内容,至于怎么再利用括号里面的内容,接下来的内容会讲到。
有的时候分组会比较复杂,例如针对上面的那个例子,我们分组可以更细致,((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2}))
,这个案例要找分组是不是就稍显复杂了,其实有一个简单的小技巧,我们可以从左边开始数(从1开始)左括号 (
的个数就是对应的分组编号。
str = "2021-11-16 09:20:10";
regex = "((\\d{4})-(\\d{2})-(\\d{2})) ((\\d{2}):(\\d{2}):(\\d{2}))";
pattern = Pattern.compile(regex);
matcher = pattern.matcher(str);
if (matcher.find()){
final String dateStr = matcher.group(1);
final String yearStr = matcher.group(2);
final String monthStr = matcher.group(3);
final String dayStr = matcher.group(4);
final String timeStr = matcher.group(5);
final String hourStr = matcher.group(6);
final String minuteStr = matcher.group(7);
final String secondStr = matcher.group(8);
log.info("提取到的日期是 {}", dateStr);
log.info("提取到的年份是 {}", yearStr);
log.info("提取到的月份是 {}", monthStr);
log.info("提取到的天是 {}", dayStr);
log.info("提取到的时间是 {}", timeStr);
log.info("提取到的小时是 {}", hourStr);
log.info("提取到的分钟是 {}", minuteStr);
log.info("提取到的秒钟是 {}", secondStr);
}
看了代码,是不是发现秒懂 O(∩_∩)O哈哈~
。
当然,针对 (? 模式标识) 我们需要把这个对应的括号去除。例如:
针对正则((?i)aes-)(\w+)((?i)-aes)
,以及需要匹配的字符串AES-safsfrsdgsdgd3243242-aes
,我们需要拿到AES–aes之间的字符串,应该获取第二个分组而不是第三个。
public static void main(String[] args) {
String regex = "((?i)aes-)(\\w+)((?i)-aes)";
String str = "AES-safsfrsdgsdgd3243242-aes";
Pattern pattern = Pattern.compile(regex);
final Matcher matcher = pattern.matcher(str);
System.out.println(matcher.replaceAll("$2"));
}
在具体写代码的时候,一个好的方法是多做单元测试,大家不要对自己盲目自信。
上面提到我们保存的分组信息是可以重新利用的,那么怎么重新利用呢?来看这样一个需求:有如下一个文字,现在需要把里面连续重复的内容替换为不重复的。
the little cat is in the little little bed, we love cat cat.
替换连续重复的内容后,我们需要得到这样的结果 the little cat is in the little bed, we love cat.
。怎么实现呢?这时候就需要使用到分组引用了。使用的方式也很简单,只需要 \分组编号
就可以了,例如我们上面的例子可以使用下面的表达式 (\w+) \1
str = "the little cat is in the little little bed, we love cat cat.";
regex = "(\\w+) \\1";
str = str.replaceAll(regex,"$1");
log.info("替换后的结果是 {}",str);
需要注意的是,有的语言引用分组使用的是\分组编号
,替换时也是使用的\分组编号
,但是java语言引用分组时使用的是\分组编号
,替换时使用的是$分组编号
。
引用分组还有一个经典的使用场景是html内容的标签是否匹配。例如判断下面的标签是否成对:
<h1>111<p>cat dog </p></h2>
str = "<h1>111<p>cat dog </p></h2>";
regex = "<([^>]+)>.*?(</\\1>)";
pattern = Pattern.compile(regex);
log.info(pattern.matcher("<title>regular expression</title>").matches() + "");
log.info(pattern.matcher("<p>laoyao bye bye</div>").matches() + "");
log.info(pattern.matcher(str).matches() + "");
我们知道英文字母是有大小写区分的,下面有一个小需求,写一个正则要求匹配下面所有的dog
,原文如下:
Dog DOG dog
,so easy!聪敏如你一定第一时间想到了下面的表达式[Dd][Oo][Gg]
,完美解决!但是如果是这个单词呢?Antidisestablishmentarianism,我想你的正则表达式应该长这样[Aa][Nn][Tt]......
,开个玩笑,虽然工作中不大会遇到这么夸张的场景,但是很多时候我们确实有匹配时不关注大写小的需求。这个时候使用我们的模式修饰符
就可以完美解决问题啦。
不区分大小写模式
模式修饰符是通过 (? 模式标识) 的方式来表示的。 我们只需要把模式修饰符放在对应的正则前,就可以使用指定的模式了。在不区分大小写模式中,由于不分大小写的英文是 Case-Insensitive,那么对应的模式标识就是 I 的小写字母 i
,所以不区分大小写的 dog 就可以写成 (?i)dog
。
我们也可以用它来尝试匹配两个连续出现的 dog、DOg,你会发现,即便是第一个 dog 和第二个 dog 大小写不一致,也可以匹配上。
点号通配模式
在之前的内容里我们讲解了元字符相关的知识,你还记得英文的点.
有什么用吗?它可以匹配上任何符号,但不能匹配换行。当我们需要匹配真正的“任意”符号的时候,可以使用 [\s\S]
或 [\d\D]
或 [\w\W]
等。但是这么写不够简洁自然,所以正则中提供了一种模式,让英文的点.
可以匹配上包括换行的任何字符。
单行匹配模式
单行的英文表示是 Single Line,单行模式对应的修饰符是 (?s)。
多行匹配模式(Multiline)
多行模式的作用在于,使 ^
和 $
能匹配上每行的开头或结尾,我们可以使用模式修饰符号(?m)
来指定这个模式。这个模式有什么用呢?在处理日志时,如果日志以时间开头,有一些日志打印了堆栈信息,占用了多行,我们就可以使用多行匹配模式,在日志中匹配到以时间开头的每一行日志。
注释模式(Comment)
在实际工作中,正则可能会很复杂,这就导致编写、阅读和维护正则都会很困难。我们在写代码的时候,通常会在一些关键的地方加上注释,让代码更易于理解。很多语言也支持在正则中添加注释,让正则更容易阅读和维护,这就是正则的注释模式。正则中注释模式是使用(?#comment) 来表示。
比如我们可以把单词重复出现一次的正则 (\w+) \1 写成下面这样,这样的话,就算不是很懂正则的人也可以通过注释看懂正则的意思。
(\w+)(?#word) \1(?#word repeat again)
注释模式则可以在正则中添加注释,让正则变得更容易阅读和维护。
今天的内容就到这里了,我们下节见,由于本人对正则的认知有限,如文中有表达不到位或者错误的地方,欢迎大家批评指正,感谢。
系列文章如下:
重学正则表达式(一)
重学正则表达式(二)
重学正则表达式(四)
重学正则表达式(五)