侧边栏壁纸
博主头像
小小酥心

旧书不厌百回读,熟读精思子自知💪

  • 累计撰写 22 篇文章
  • 累计创建 8 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Java中正则表达式捕获组详解

小小酥心
2022-01-14 / 0 评论 / 0 点赞 / 1,027 阅读 / 6,477 字
温馨提示:
本文最后更新于 2022-01-14,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1. 捕获组分类

  • 普通捕获组(Expression)
  • 命名捕获组(?Expression)
  • 非捕获组(?:Exp) (?=Exp) (?<=Exp) (?!Exp) (?<!Exp)

2. 普通捕获组

从正则表达式左侧开始,每出现一个左括号“(”记做一个分组,分组编号从1开始。0代表整个表达式。

对于时间字符串:2017-04-25,表达式如下

(\\d{4})-((\\d{2})-(\\d{2}))

有4个左括号,所以有4个分组

编号捕获组匹配
0(\d{4})-((\d{2})-(\d{2}))2017-04-25
1(\d{4})2017
2((\d{2})-(\d{2}))04-25
3(\d{2})04
4(\d{2})25
String DATE_STRING = "2017-04-25";
String P_COMM = "(\\d{4})-((\\d{2})-(\\d{2}))";

Pattern pattern = Pattern.compile(P_COMM);
Matcher matcher = pattern.matcher(DATE_STRING);
matcher.find();//必须要有这句
System.out.printf("\nmatcher.group(0) value:%s", matcher.group(0));
System.out.printf("\nmatcher.group(1) value:%s", matcher.group(1));
System.out.printf("\nmatcher.group(2) value:%s", matcher.group(2));
System.out.printf("\nmatcher.group(3) value:%s", matcher.group(3));
System.out.printf("\nmatcher.group(4) value:%s", matcher.group(4));

3. 命名捕获组

每个以左括号开始的捕获组,都紧跟着“?”,而后才是正则表达式。

对于时间字符串:2017-04-25,表达式如下

(?<year>\\d{4})-(?<md>(?<month>\\d{2})-(?<date>\\d{2}))

有4个命名的捕获组,分别是

编号名称捕获组匹配
00(?\d{4})-(?(?\d{2})-(?\d{2}))2017-04-25
1year(?\d{4})-2017
2md(?(?\d{2})-(?\d{2}))04-25
3month(?\d{2})04
4date(?\d{2})25

命名的捕获组同样也可以使用编号获取相应值

String P_NAMED = "(?<year>\\d{4})-(?<md>(?<month>\\d{2})-(?<date>\\d{2}))";
String DATE_STRING = "2017-04-25";

Pattern pattern = Pattern.compile(P_NAMED);
Matcher matcher = pattern.matcher(DATE_STRING);
matcher.find();
System.out.printf("\n===========使用名称获取=============");
System.out.printf("\nmatcher.group(0) value:%s", matcher.group(0));
System.out.printf("\n matcher.group('year') value:%s", matcher.group("year"));
System.out.printf("\nmatcher.group('md') value:%s", matcher.group("md"));
System.out.printf("\nmatcher.group('month') value:%s", matcher.group("month"));
System.out.printf("\nmatcher.group('date') value:%s", matcher.group("date"));
matcher.reset();
System.out.printf("\n===========使用编号获取=============");
matcher.find();
System.out.printf("\nmatcher.group(0) value:%s", matcher.group(0));
System.out.printf("\nmatcher.group(1) value:%s", matcher.group(1));
System.out.printf("\nmatcher.group(2) value:%s", matcher.group(2));
System.out.printf("\nmatcher.group(3) value:%s", matcher.group(3));
System.out.printf("\nmatcher.group(4) value:%s", matcher.group(4));

Matcher.find()和matches()区别:
find()方法是部分匹配,是查找输入串中与模式匹配的子串,如果该匹配的串有组还可以使用group()函数。
matches()是全部匹配,是将整个输入串与模式匹配,如果要验证一个输入的数据是否为数字类型或其他类型,一般要用matches()。

Pattern p = Pattern.compile("hello");
Matcher m = p.matcher("123hello");
System.out.println(m.matches()); // false
System.out.println(m.find()); // true

Matcher m2 = p.matcher("hello");
System.out.println(m2.matches()); // true

4. 非捕获组

4.1 基础

在左括号后紧跟“?:”,而后再加上正则表达式,构成非捕获组(?:Expression)。

对于时间字符串:2017-04-25,表达式如下

(?:\\d{4})-((\\d{2})-(\\d{2}))

这个正则表达式虽然有四个左括号,理论上有4个捕获组。但是第一组(?:\d{4}),其实是被忽略的。当使用matcher.group(4)时,系统会报错。

编号捕获组匹配
0(\d{4})-((\d{2})-(\d{2}))2017-04-25
1((\d{2})-(\d{2}))04-25
2(\d{2})04
3(\d{2})25
public static final String P_UNCAP = "(?:\\d{4})-((\\d{2})-(\\d{2}))";
public static final String DATE_STRING = "2017-04-25";

Pattern pattern = Pattern.compile(P_UNCAP);
Matcher matcher = pattern.matcher(DATE_STRING);
matcher.find();
System.out.printf("\nmatcher.group(0) value:%s", matcher.group(0));
System.out.printf("\nmatcher.group(1) value:%s", matcher.group(1));
System.out.printf("\nmatcher.group(2) value:%s", matcher.group(2));
System.out.printf("\nmatcher.group(3) value:%s", matcher.group(3));

// Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 4
System.out.printf("\nmatcher.group(4) value:%s", matcher.group(4));

4.2 详解

非捕获组其他写法:

(?=Exp) (?<=Exp) (?!Exp) (?<!Exp)

4.2.1 (?:Exp)

下面由一个例子引出非捕获组。

有两个金额:8899¥ 和 6688$ 。显然,前一个是8899元的人民币,后一个是6688元的美元。我现在需要一个正则,要求提取出它们的货币金额和货币种类。

正则可以这写:

(\\d+)([¥$])$

测试程序如下:

Pattern p = Pattern.compile("^(\\d+)([¥$])$");
String str = "8899¥";
Matcher m = p.matcher(str);
if (m.matches()) {
    System.out.println("货币金额: " + m.group(1));
    System.out.println("货币种类: " + m.group(2));
}

输出结果为:

货币金额: 8899
货币种类: ¥

OK,满足了要求。这里的正则分成了两个组,一个是(\\d+),一个是([¥$]),前一个组匹配货币金额,后一个组匹配货币种类。


现在,我需要这个正则可以匹配浮点数。如8899.56¥。我们都知道,现在少于一元钱基本上买不到东西了,所以我希望忽略小数部分,正则还是提炼出 8899 和 ¥。那么正则如下:

(\\d+)(\\.?)(\\d+)([¥$])

这里用括号分了四组,所以要输出货币金额的整数部分和货币种类,要分别输了group(1),group(4)了。如果输出部分和正则是分开的,我希望只修改正则而不去修改输出部分的代码,也就是还是用group(1),group(2)作为输出。由此可以引出非捕获组(?:)。把前面的正则修改为:

(\\d+)(?:\\.?)(?:\\d+)([¥$])

这样,还是用group(1),group(2)做为输出,同样输出了 8899 和 ¥。

这个正则的中间两个组用到的就是非捕获组(?:),它可以理解为只分组而不捕获。

4.2.2 (?=Exp)和(?<=Exp)

有的资料把它们叫做肯定式向前查找肯定式向后查找;有的资料也叫做肯定顺序环视肯定逆序环视

4.2.2.1 (?=Exp)

看下面的例子:

Pattern p = Pattern.compile("[0-9a-z]{2}(?=aa)");
String str = "12332aa438aaf";

Matcher m = p.matcher(str);
while(m.find()){
  System.out.println(m.group());
  System.out.println(m.groupCount());
}

程序输出:

32
38

这个正则的意思是:匹配这么一个字符串,它要满足:是两位字符(数字,或字母),且后面紧跟着两个a

分析一下:

32aa 这个子串 满足这个条件,所以可以匹配到,又因为 (?=) 的部分是不捕获的,所以输出的只是 32,不包括aa。同理 38aa 也匹配这个正则,而输出仅是 38。

再深入看一下:

当str第一次匹配成功输出 32 后,程序要继续向后查找是否还有匹配的其它子串。那么这时应该从 32aa 的后一位开始向后查找,还是从 32 的后一位呢?也就是从索引 5 开始还是从 7 开始呢?有人可能想到是从 32aa 的下一位开始往后找,因为 32aa 匹配了正则,所以下一位当然是它的后面也就是从 4 开始。但实际上是从 32 的后一位也就是第一个 a 开始往后找。原因还是 (?=) 是非捕获的。

查阅API文档是这么注释的:(?=X) X, via zero-width positive lookahead。可见zero-width(零宽度)说的就是这个意思。

现在,把字符串写的更有意思些:str = “aaaaaaaa”,看一下它的输出: aa aa aa

分析一下:

这个字符串一共有8个a。
第一次匹配比较容易找到,那就是前四个:aaaa ,当然第三和第四个 a 是不捕获的,所以输出是第一和第二个a;
接着继续查找,这时是从第三个a开始,三到六,这4个a区配到了,所以输出第三和第四个a;
接着继续查找,这时是从第五个a开始,五到八,这4个a区配到了,所以输出第五和第六个a;
接着往后查找,这时是从第七个a开始,显然,第七和第八个a,不满足正则的匹配条件,查找结束。

我们再延伸一下,刚说的情况的是(?=)放在捕获的字符串后面,它如果放在前面又是什么结果呢?

例子换成:

Pattern p = Pattern.compile("(?=hopeful)hope");
String str = "hopeful";
Matcher m = p.matcher(str);
while(m.find()){
  System.out.println(m.group());
}

它的输出是hope。

正则的意思是:是否能匹配hopeful,如果能,则捕获hopeful中的hope。当然继续向后查找匹配的子串,是从f开始。

比较一下可以看出,(?=hopeful)hope 和 hope(?=ful),两个正则的效果其实是一样的。

4.2.2.2 (?<=Exp)

把正则改一下:

Pattern p = Pattern.compile("(?<=aa)[0-9a-z]{2}");

字符串还是str = “12332aa438aaf”,它的输出:43。

这个正则的意思是:匹配这么一个字符串,它要满足:是两位字符(数字或字母),且前面紧跟的是两个字母 a

同样,深入一下,把str换成str = “aaaaaaaa”,看一下输出是什么,同样也是:aa aa aa

分析一下:
第一次匹配不用说,是前四个a,输出的是第三和第四个a;
继续向后查找,从第五个a开始,程序发现,第五个和第六个a满足,因为是两位字符,且满足前面紧跟着两个a(第三和第四个a)。所以匹配成功,输出第五个和第六个a;继续向后查找,从第七个a开始,程序发现,第七个和第八个a满足,因为是两位字符,且满足前面紧跟着两个a(第五和第六个a)。所以匹配成功,输出第七和第八个a。查找结束。

4.2.3 (?!Exp)和(?<!Exp)

从外观上看,和前面一组很相似,区别就是把 ‘=’ 换成了 ‘!’,那么意义刚好也是相反的。

[0-9a-z]{2}(?!aa) 意思是:匹配两个字符,且后面紧跟着的不是aa
(?<=aa)[0-9a-z]{2} 意思是:匹配两个字符,且前面紧跟着的不是aa

用法和前面讲的差不多,这里不再详述。

总结

普通捕获组使用方便;
命名捕获组使用清晰;
非捕获组则通常作为附加条件使用。

0

评论区