1. 分组的基本概念

分组使用圆括号 () 将一部分正则表达式组合在一起,形成一个子表达式单元。

2. 分组的类型与语法

2.1 捕获分组(基本分组)

(pattern)          # 基本捕获分组

功能:

  • 将多个元素作为一个整体
  • 捕获匹配内容供后续使用
  • 创建反向引用

示例:

# 匹配重复单词
(\w+)\s+\1          # 匹配 "hello hello" 中的 "hello hello"

# 分组与量词结合
(abc){2,3}          # 匹配 "abcabc" 或 "abcabcabc"

2.2 非捕获分组

(?:pattern)         # 非捕获分组

功能:

  • 只分组不捕获
  • 不创建反向引用
  • 提高性能

示例:

# 匹配多种域名后缀
www\.(?:baidu|google|github)\.(?:com|cn|org)

# 与量词结合
(?:abc){2,}         # 匹配多个"abc"但不捕获

2.3 命名分组

(?<name>pattern)    # 命名分组(Python、.NET等)
(?P<name>pattern)   # 命名分组(Python风格)
(?'name'pattern)    # 命名分组(.NET风格)

功能:

  • 为分组指定有意义的名称
  • 提高可读性
  • 便于维护

示例:

# 匹配日期并命名分组
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})

# 在Python中使用
import re
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
match = re.search(pattern, "2023-10-01")
if match:
    print(match.group('year'))   # 输出: 2023

2.4 原子分组(防止回溯)

(?>pattern)         # 原子分组

功能:

  • 匹配成功后不会回溯
  • 防止灾难性回溯
  • 提高性能

示例:

# 防止灾难性回溯
(?>a+)b             # 匹配多个a后接b,不会回溯

# 对比普通分组
(a+)b               # 可能产生回溯

3. 分组编号规则

3.1 编号顺序

分组按左括号出现的顺序编号,从1开始。

((a)(b(c)))         # 分组编号示例
# 整个表达式: ((a)(b(c)))
# 组1: (a)(b(c))
# 组2: (a)
# 组3: (b(c))
# 组4: (c)

3.2 验证分组编号

# 测试分组编号
(a)(b)(c)d\1\2\3    # 匹配 "abcdabc"

4. 反向引用

4.1 基本反向引用

\1, \2, \3, ...     # 引用对应编号的分组

示例:

# 匹配HTML标签
<(\w+)>.*?</\1>     # 匹配 <div>内容</div>

# 匹配重复字符
(\w)\1+             # 匹配 "aa", "bbb", "cccc"

4.2 命名分组的反向引用

\k<name>            # 引用命名分组
\g<name>            # 另一种语法

示例:

# 命名分组的反向引用
(?<tag>\w+)>.*?</\k<tag>>

# 匹配引号内的内容
(['"])(.*?)\1       # 匹配单引号或双引号内的内容

5. 分组在替换中的使用

5.1 基本替换引用

$1, $2, $3, ...     # 在替换字符串中引用分组
\1, \2, \3, ...     # 在某些语言中的语法

示例:

import re

# 日期格式转换
text = "2023-10-01"
result = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', text)
print(result)  # 输出: 10/01/2023

5.2 命名分组在替换中的使用

# 使用命名分组进行替换
text = "John Doe"
result = re.sub(r'(?P<first>\w+)\s+(?P<last>\w+)', r'\g<last>, \g<first>', text)
print(result)  # 输出: Doe, John

6. 零宽断言(Lookaround)分组

6.1 正向先行断言

(?=pattern)         # 匹配后面跟着pattern的位置

示例:

# 匹配后面跟着空格的单词
\w+(?=\s)           # 在 "hello world" 中匹配 "hello"

6.2 负向先行断言

(?!pattern)         # 匹配后面不跟着pattern的位置

示例:

# 匹配不是以ing结尾的单词
\w+(?!ing\b)        # 匹配 "walk" 但不匹配 "walking"

6.3 正向后行断言

(?<=pattern)        # 匹配前面是pattern的位置

示例:

# 匹配前面是$的数字
(?<=\$)\d+          # 在 "$100" 中匹配 "100"

6.4 负向后行断言

(?<!pattern)        # 匹配前面不是pattern的位置

示例:

# 匹配前面不是$的数字
(?<!\$)\d+          # 匹配 "100" 但不匹配 "$100" 中的 "100"

7. 条件匹配分组

7.1 条件表达式

(?(id)yes|no)       # 如果分组id匹配,则匹配yes,否则匹配no

示例:

# 匹配带括号或不带括号的电话号码
(\((\d{3})\))?\s*(\d{3})-(\d{4})(?(1)|-\w+)?
# 可匹配: (123) 456-7890 或 123-456-7890

8. 分组的性能考虑

8.1 非捕获分组的优势

# 性能较差 - 不必要的捕获
(\d{4})-(\d{2})-(\d{2})

# 性能较好 - 使用非捕获分组
(?:\d{4})-(?:\d{2})-(?:\d{2})

8.2 避免过度分组

# 不必要的分组
(\w)+               # 错误:只捕获最后一个字符

# 正确的做法
(\w+)               # 捕获整个单词

9. 复杂分组示例

9.1 嵌套分组

# 匹配嵌套的HTML标签(简化版)
<(\w+)>(.*?)</\1>   # 匹配简单的标签对

# 匹配IP地址
((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

9.2 复杂条件匹配

# 匹配多种日期格式
(?:(?<year>\d{4})?<month>\d{2}?<day>\d{2}|(?<month>\d{1,2})/(?<day>\d{1,2})/(?<year>\d{4}))

10. 各语言中的分组差异

10.1 JavaScript

// 命名分组(ES2018+)
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = pattern.exec("2023-10-01");
console.log(match.groups.year); // 输出: 2023

// 替换中的使用
"2023-10-01".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");

10.2 Python

import re

# 命名分组
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
match = re.search(pattern, "2023-10-01")
if match:
    print(match.group('year'))

# 编译正则时的分组名称
compiled = re.compile(pattern)

10.3 Java

// Java中的命名分组(JDK7+)
Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher matcher = pattern.matcher("2023-10-01");
if (matcher.find()) {
    System.out.println(matcher.group("year"));
}

11. 最佳实践与注意事项

11.1 分组使用原则

  1. 明确需求:确定是否需要捕获内容
  2. 性能优先:不需要捕获时使用非捕获分组
  3. 可读性:复杂正则使用命名分组
  4. 避免过度嵌套:简化分组结构

11.2 常见错误

# 错误:字符类中的分组
[(abc)]             # 实际匹配: '(', 'a', 'b', 'c', ')'

# 正确:字符类不需要分组
[abc]               # 匹配a、b或c中的任意一个字符

# 错误:量词位置错误
(\w){3,5}           # 捕获最后一个字符,而不是整个单词

# 正确:量词应在分组内
(\w{3,5})           # 捕获整个单词

11.3 测试与调试

import re

# 查看所有分组
pattern = r'(\d{4})-(\d{2})-(\d{2})'
text = "2023-10-01"
match = re.search(pattern, text)

if match:
    print("完整匹配:", match.group(0))
    for i in range(1, len(match.groups()) + 1):
        print(f"分组{i}:", match.group(i))

分组是正则表达式中最强大的特性之一,正确使用可以大大提高正则的表达能力和可维护性。

更多推荐