group 是正则表达式一个非常强大和核心的功能,它允许你:
- 匹配和捕获子串:从整个匹配的字符串中,提取出你感兴趣的特定部分。
- 组织和复用模式:使用括号 将一部分模式组合成一个单元,并可以对这个单元应用量词(如 , ,
{n,m})。 - 反向引用:在同一个正则表达式中,让后面的部分匹配前面捕获组的内容。
基本概念:什么是捕获组?
捕获组是通过在正则表达式中使用括号 来创建的,括号内的部分就是一个子表达式,它会形成一个“组”。
语法:
(pattern)
示例: 假设我们要匹配一个格式为 "单词-单词" 的字符串,"apple-orange"。
-
没有捕获组:
String regex = "\\w+-\\w+"; // 匹配 "apple-orange"
这个正则表达式可以匹配整个字符串,但无法单独提取出 "apple" 或 "orange"。
-
使用捕获组:
String regex = "(\\w+)-(\\w+)"; // 匹配 "apple-orange"
这个正则表达式创建了两个捕获组:
- Group 1:
(\w+),用于匹配第一个单词。 - Group 2:
(\w+),用于匹配第二个单词。 - 整个表达式匹配的结果被称为 Group 0(或整个匹配)。
- Group 1:
在 Java 中如何使用 group
Java 中处理正则表达式主要使用 java.util.regex 包下的 Pattern 和 Matcher 类。
核心方法
Matcher 类提供了以下方法来访问捕获组:
group(): 返回整个匹配的字符串(即 Group 0)。group(int groupIndex): 返回指定索引的捕获组所匹配的子串。groupCount(): 返回此匹配器的模式中的捕获组总数。
索引规则:
- Group 0: 整个匹配,索引从
0开始。 - Group 1, 2, 3, ...: 由 创建的捕获组,索引从
1开始,按照左括号出现的顺序递增。
代码示例
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexGroupExample {
public static void main(String[] args) {
String text = "我的电话是 138-1234-5678,备用电话是 186-8765-4321。";
String regex = "(\\d{3})-(\\d{4})-(\\d{4})"; // 一个区号,一个前四位,一个后四位
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
// 查找所有匹配项
while (matcher.find()) {
System.out.println("找到匹配: " + matcher.group()); // Group 0: 整个电话号码
// 获取各个捕获组
System.out.println(" 完整号码: " + matcher.group(0)); // Group 0
System.out.println(" 区号: " + matcher.group(1)); // Group 1
System.out.println(" 前四位: " + matcher.group(2)); // Group 2
System.out.println(" 后四位: " + matcher.group(3)); // Group 3
System.out.println(" 总捕获组数: " + matcher.groupCount()); // 3
System.out.println("--------------------");
}
}
}
输出:
找到匹配: 138-1234-5678
完整号码: 138-1234-5678
区号: 138
前四位: 1234
后四位: 5678
总捕获组数: 3
--------------------
找到匹配: 186-8765-4321
完整号码: 186-8765-4321
区号: 186
前四位: 8765
后四位: 4321
总捕获组数: 3
--------------------
特殊类型的捕获组
除了标准的命名捕获组,还有一些特殊类型的组。
a) 非捕获组
你只想用括号来组织表达式,但不关心这个组匹配的内容,也不希望它被计入 groupCount()。
语法:
(?:pattern)
示例: 匹配 "apple-orange" 或 "banana-orange",并提取水果名和 "orange"。
String text = "I like apple-orange and banana-orange.";
// (\\w+) 匹配水果名
// (?:...)-orange 是一个整体,但我们只关心水果名和 "orange"
String regex = "(\\w+)(?:-(\\w+))?";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("匹配: " + matcher.group(0));
System.out.println(" 水果名: " + matcher.group(1)); // 总是第一个单词
// group(2) 可能为 null,因为第二个括号是可选的
System.out.println(" 第二部分: " + (matcher.group(2) != null ? matcher.group(2) : "N/A"));
System.out.println(" 总捕获组数: " + matcher.groupCount()); // 只有2个捕获组
}
在这个例子中, 帮助我们实现了 (A)-(B) 或 (A) 的模式,但 B 不会被捕获为一个独立的组(索引2)。groupCount() 只计算 (\w+) 这一个捕获组。
b) 命名捕获组 (?<name>...) 或 (?P<name>...)
当正则表达式非常复杂,有很多捕获组时,用数字(1, 2, 3...)来引用会变得很混乱,命名捕获组允许你给每个组起一个有意义的名字,通过名字来访问。
语法 (Java 风格):
(?<name>pattern)
语法 (PCRE 风格,也受 Java 支持):
(?P<name>pattern)
访问方法:
matcher.group("name")
示例: 提取日期,并按名称访问年、月、日。
String text = "今天的日期是 2025-10-27。";
String regex = "\\b(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})\\b";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("找到日期: " + matcher.group()); // Group 0
System.out.println(" 年: " + matcher.group("year"));
System.out.println(" 月: " + matcher.group("month"));
System.out.println(" 日: " + matcher.group("day"));
}
输出:
找到日期: 2025-10-27
年: 2025
月: 10
日: 27
这种方式代码可读性更高,也更不容易出错。
高级用法:反向引用
反向引用允许你在正则表达式的后续部分,匹配前面某个捕获组已经匹配到的内容。
语法:
- 在正则表达式中:
\数字(\1,\2) - 在
String的replaceAll方法中:$数字($1,$2)
示例 1:在正则表达式中使用
匹配两个连续相同的单词,"hello hello"。
String text = "This is a test test sentence. But this is not a good good example.";
// (\\w+) 匹配一个单词,并捕获为 Group 1
// \\s+ 匹配一个或多个空格
// \\1 匹配 Group 1 的内容(即第一个单词)
String regex = "(\\w+)\\s+\\1";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("找到重复单词: " + matcher.group(0) + " (单词是: " + matcher.group(1) + ")");
}
输出:
找到重复单词: test test (单词是: test)
这里的 \1 就是指向第一个捕获组 (\w+) 的内容。
示例 2:在 String.replaceAll 中使用
假设我们想把 "apple,orange,banana" 这样的字符串格式化为 "apple | orange | banana"。
String text = "apple,orange,banana"; // (\\w+) 匹配一个单词,并捕获为 Group 1 String regex = "(\\w+),"; // 匹配 "word," // $1 指向第一个捕获组的内容 String result = text.replaceAll(regex, "$1 | "); System.out.println(result);
输出:
apple | orange | banana|
注意最后多了一个 ,因为正则表达式会匹配最后一个 "banana,",但实际上 "banana" 后面没有逗号,replaceAll 不会对 "banana" 做任何处理,一个更精确的写法是:
String regex = "(\\w+),"; // 匹配 "word,"
// 使用边界 \\b 确保只匹配单词后的逗号
String result = text.replaceAll("\\b(\\w+),", "$1 | ");
System.out.println(result); // apple | orange | banana
重要注意事项
-
IllegalStateException:在调用group()或group(int)方法之前,必须先调用find()或matches()方法,如果没有找到任何匹配,调用这些方法会抛出IllegalStateException。 -
IndexOutOfBoundsException:如果尝试访问一个不存在的组索引(group(5)但groupCount()返回 3),会抛出IndexOutOfBoundsException。 -
嵌套组:捕获组可以嵌套,在这种情况下,组的编号是按照左括号出现的顺序来决定的。 在
((A)(B(C)))中:- Group 1:
(A)(B(C)) - Group 2:
(A) - Group 3:
(B(C)) - Group 4:
(C)
- Group 1:
| 功能 | 正则语法 | Java 代码 | 描述 |
|---|---|---|---|
| 标准捕获组 | matcher.group(1) |
捕获子串,按数字索引从1开始访问。 | |
| 非捕获组 | matcher.groupCount() 不计数 |
组织模式,但不捕获内容,不计入组数。 | |
| 命名捕获组 | (?<name>...) |
matcher.group("name") |
为组命名,通过名称访问,提高可读性。 |
| 反向引用 | \数字 (在 regex 中) |
- | 在正则表达式中匹配前面捕获组的内容。 |
| 反向引用 | $数字 (在 replaceAll 中) |
text.replaceAll(..., "$1") |
在替换字符串中引用捕获组的内容。 |
掌握 group 是精通 Java 正则表达式的关键,从简单的 到复杂的命名和反向引用,它能让你灵活地处理各种复杂的文本匹配和提取任务。
