重学正则表达式(三)

先来看一个例子,我们知道我国身份证是由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)
注释模式则可以在正则中添加注释,让正则变得更容易阅读和维护。

今天的内容就到这里了,我们下节见,由于本人对正则的认知有限,如文中有表达不到位或者错误的地方,欢迎大家批评指正,感谢。

系列文章如下:
重学正则表达式(一)
重学正则表达式(二)
重学正则表达式(四)
重学正则表达式(五)

猜你喜欢

转载自blog.csdn.net/hxj413977035/article/details/121348271