05、String 模板 -- 正则表达式

  • 正则表达式 是一种强大而灵活的 文本处理工具
  • 使用 正则表达式 能够以编程的方式,构造复杂的 文本模式/模板,并对输入的字符串进行搜索
    • 一旦找到了匹配这些 模式/模板部分,就能够随心所欲地对它们进行处理
  • 正则表达式能够解决各种 字符串 处理相关的问题:
    • 匹配、搜索、替换、分割、验证。

一、基础语法 与 元字符


1、字符匹配


  • 列表
元字符说明示例匹配内容
.匹配任意一个字符换行符)a.c:以 a 开头,以 c 结尾,中间任意一个字符的三个字符的字符串abca1c
B匹配指定字符 BBB
\d匹配单个数字(等价于 [0-9]\d{3}(\d)?
+ 3 个或 4 个数字字符串
1232456
\D匹配单个非数字(等价于 [^0-9]\D+abc@#$
\w匹配单个单词字符([a-zA-Z0-9_]\w+user123name
\W匹配单个非单词字符\W+\d{2}:以至少 1 个非数字字母字符开头,2 个数字字符结尾的字符串#31#?%@10
\s匹配空白字符(空格、制表符等)\s+ \t
\S匹配非空白字符\S+abc123
  • .:匹配除 "\r\n" 之外的任何一个字符(甚至 . 本身)。
  • 若要匹配包括 “\r\n在内任何一个字符,请使用诸如 “[\s\S]” 之类的模式。

2、转义字符


  • 列表
转义字符说明匹配内容
\转义号如:Java 正则表达式中,两个 \\ 代表 一个 \
\t匹配 水平制表符(Tab)ASCII 0x09
\n匹配 换行符(LF, Line Feed)ASCII 0x0A
\r匹配 回车符(CR, Carriage Return)ASCII 0x0D
\f匹配 换页符(Form Feed)ASCII 0x0C
\xhh匹配 十六进制 ASCII 字符\x21!\x41A
\uhhhh匹配 十六进制Unicode 字符\u00A9©\u4e2d
  • \n:换行符【\n\n 是 Linux 匹配空白行
    • 同时适用于 WindowsLinux 的正则表达式应包含:一个可选的 \r一个必须被匹配的 \n
  • \r:回车符【\r\n 是 Windows 所使用的文本行结束标签

3、选择匹配符 / 分支结构


语法说明示例匹配内容
a|b匹配 a b 任意一个字符。a|bab
z|food匹配 z food 任意一个。
(z|f)ood匹配 zood food 任意一个。

4、字符集合


  • 列表
语法说明示例匹配内容
abc只匹配 abc abcabc
[abc]匹配 abc 任意一个字符。[aeiou]ae
[.*+]匹配 .*+ 任意一个字符。[.*+].*
[.*+]]匹配 .*+] 任意一个字符。[.*+]].]
[.*+-]匹配 .*+- 任意一个字符。
- 在末尾不表示范围。
[.*+-].-
[a-f]匹配 a 到 f 其中的任意一个字符[a-f]ab
[a-zA-Z]匹配 a 到 zA 到 Z 两个范围的并集中任意一个字符[A-Za-z]+Hello
[a-zA-Z0-9_-]匹配小写字母大写字母数字下划线连线符 (-)
中的任意一个字符。
[a-zA-Z0-9_-]-_
[abc[hij]]匹配abchij任意一个字符
+ 等价于 [abchij],匹配两者的并集
[a-z&&[hij]]小写字母 a 到 z 字符 h、i、j 交集
+ 匹配结果hij任意一个字符。
[^abc]匹配不是 a、b、c 的任何一个字符[^abc]d#
[^a-f]匹配不是 a、b、c、d、e、f 的任何一个字符[^a-f]h#
[a-z&&[^hij]]小写字母 a 到 z 不是 h、i、j 的任何一个字符的****交集
+ 匹配结果:不是 h、i、j 的任何一个小写字母
[a-z&&[^hij]]ab

5、量词


  • 列表
量词说明示例匹配内容
*匹配 0 次或多次,等价于 {0,}a*aaaaa
+匹配 1 次或多次,等价于 {1,}\d+1123
?匹配 0 次或 1 次,等价于 {0,1}colou?rcolorcolour
{n}精确匹配 n\d{4}20231234
{n,}至少匹配 n\w{3,}abc12345
{0,n}匹配最多 n\w{0,3}a123
{n,m}匹配 nm 次(包含: n 次 和 m 次)a{2,4}aaaaaa

6、边界与位置


  • 列表
元字符说明示例匹配内容
^匹配字符的开头
(多行模式下匹配行首
^HelloHello 开头的行
$匹配字符的结尾
(多行模式下匹配行尾
end$end 结尾的行
\b匹配单词边界\bcat\b“cat”, “a cat”
\B匹配非单词边界的位置\B-\B“well-known”
  • 单词字符:字母、数字、下划线([a-zA-Z0-9_])。
  • \b 用来匹配 单词的边界位置,即:
    • 单词的 开始(前一个字符是非单词字符。如:空格、标点)。
    • 单词的 结束(后一个字符是非单词字符)。
// 匹配独立单词
\bcat\b
    ✅ 匹配:"cat", "a cat" 中的 cat
    ❌ 不匹配:"category", "cat123"(非独立单词)

// 提取单词前缀
\bJava\w*
    ✅ 匹配:"Java", "JavaScript"
    ❌ 不匹配:"IJava"(前缀有非单词字符)

// 开头是单词字符,结尾是数字
^\w+\b.*\d$
    ✅ 匹配:"Pass123"
    ❌ 不匹配:"123Pass"(开头是数字,结尾是单词字符)

  • \B 匹配 非单词边界的位置,即:
    • 单词字符 之间(如:aa 中的两个 a 之间)。
    • 非单词字符 之间(如:## 中的两个 # 之间)。
// 匹配连续两个相同字母(如 "oo")
(\w)\B\1
    ✅ 匹配:"book" 中的 oo
    ❌ 不匹配:"boss"(单个 s)

// 匹配不在单词边界的连字符
\B-\B
    ✅ 匹配:"well-known" 中的 -
    ❌ 不匹配:"end-""-start"(连字符在边界)

// 匹配被单词字符包围的 @
\B@\B
    ✅ 匹配:"user@domain.com" 中的 @
    ❌ 不匹配:"@mention""email@"(@ 在边界)

7、修饰符


  • 修饰符:用于调整正则表达式的匹配行为
    • 如:忽略大小写、跨行匹配、全局搜索、启用 Unicode 模式
修饰符名称(通用)作用
i忽略大小写匹配时不区分大小写
m多行模式使 ^$ 匹配每行的开头和结尾(而非整个字符串的首尾)
s单行模式(DOTALL)允许 . 匹配包括换行符(\n)在内的任意字符
g全局匹配查找所有匹配项(而非在第一次匹配后停止)
uUnicode 模式启用完整的 Unicode 支持(如匹配四字节字符)
x宽松模式忽略正则中的空白和注释(增强可读性)
String content = "a11c8abcABCaBc";
// 匹配 abc 字符串【默认区分大小写】
String regStr = "abc";

Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
/*
    找到:abc
 */
while (matcher.find()){
    System.out.println("1找到:" + matcher.group());
}


// 匹配 abc 字符串【默认不区分大小写】
regStr = "(?i)abc";
pattern = Pattern.compile(regStr);
matcher = pattern.matcher(content);
/*
    2找到:abc
    2找到:ABC
    2找到:aBc
 */
while (matcher.find()){
    System.out.println("2找到:" + matcher.group());
}


// 表示 匹配 abc ,中间字母 b 不区分大小写 。
regStr = "a((?i)B)c";
pattern = Pattern.compile(regStr);
matcher = pattern.matcher(content);
/*
    3找到:abc
    3找到:aBc
 */
while (matcher.find()){
    System.out.println("3找到:" + matcher.group());
}


// 表示 匹配 abc ,bc 不区分大小写 。
regStr = "a(?i)bc";
pattern = Pattern.compile(regStr);
matcher = pattern.matcher(content);
/*
    3找到:abc
    3找到:aBc
 */
while (matcher.find()){
    System.out.println("4找到:" + matcher.group());
}

二、分组 – (pattern)


1、定义


  • 分组
    • 将表达式从左到右按照 () 进行分组

2、分组编号规则


  • 从左到右:
    • 左括号 “(”出现顺序编号,从 1 开始分组编号
    • 因为, 0 表示整个表达式
  • 嵌套分组:
    • 外层先编号 ,再处理内层。
// 分组1: A(B(C)), 分组2: B(C), 分组3: C
(A(B(C)))
  • 分组示例:
// Java 提取分组内容
Matcher m = Pattern.compile("(\\d+)-(\\d+)").matcher("2025-05");
if (m.find()) {
    // 2025-05
    System.out.println(m.group(0));
    // 2025
    System.out.println(m.group(1));
    // 05
    System.out.println(m.group(2));
}

三、捕获分组 – (pattern)


  • () 内的表达式 / 子模式称为“捕获组”。
  • 捕获分组:
    • 捕获组作为一个整体模板匹配,将匹配到的子串进行捕获 (存储),供后续索引名称引用
  • 示例:
    • (abc) :将 abc 作为一个整体模板去进行匹配,并捕获(存储)匹配到abc
    • (…):用于匹配 任意三个字符,并捕获(存储)任意三个字符
  • 核心作用 :提取数据、反向引用、替换操作

1、普通捕获分组 – (pattern)


  • 正则表达式 (\d{4})-(\d{2})2025-05 拆分出两个捕获组
// 匹配 "2025-05",分组 1 为年,分组 2 为月
(\d{4})-(\d{2})
  • 可以通过编号来获取匹配的分组。
    • group(0) 完整的字符串
String text = "2025-05-16";
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
    // 2025-05-16
    System.out.println(matcher.group(0));
    // "2025"
    System.out.println(matcher.group(1));
    // "05"
    System.out.println(matcher.group(2));
    // "16"
    System.out.println(matcher.group(3));
}

2、命名捕获分组 – (?pattern)


  • 每一个以左括号 “(” 开始的捕获组,都紧跟着“?”。然后,才是正则表达式。
// 分组名:year、month
(?<year>\d{4})-(?<month>\d{2})
  • 可以通过命名来获取匹配的分组,也可以用编号来获取匹配的分组。
String text = "2025-05-16";
Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
    // {year=1, day=3, month=2}
    System.out.println(matcher.namedGroups());;
    // 2025-05-16
    System.out.println(matcher.group(0));
    // "2025"
    System.out.println(matcher.group("year"));
    // "2025"
    System.out.println(matcher.group(1));
    // "05"
    System.out.println(matcher.group("month"));
    // "16"
    System.out.println(matcher.group("day"));
}

3、分组引用 / 反向引用 / 回溯引用


  • 允许在 正则表达式引用之前捕获的分组内容,常用于匹配重复对称 的结构。
  • 引用 的使用格式:
    • 正则表达式内部使用:\\组号
    • 正则表达式外部使用:$组号

  • 1、按编号引用正则内部使用 \\组号(如:\\1)。
(\d{3})-\1  # 匹配重复的三位数字,如 "123-123"
<(\w+)>.*?</\1>  # 匹配对称标签,如 <div>内容</div>
// 匹配 "123-123"
Pattern pattern = Pattern.compile("(\\d{3})-\\1");
Matcher matcher = pattern.matcher("123-123");
// true
System.out.println(matcher.find());


// 匹配重复单词,如 "the the"
Pattern pattern = Pattern.compile("\\b(\\w+)\\s+\\1\\b");
Matcher matcher = pattern.matcher("the the");
// true
System.out.println(matcher.find());
  • 2、按编号引用正则外部使用 $组号(如:$1)。
// Java 代码:YYYY-MM-DD → DD/MM/YYYY
String replaced1 = "2025-05-16".replaceAll(
        "(\\d{4})-(\\d{2})-(\\d{2})",
        "$3/$2/$1"
);
// 16/05/2025
System.out.println(replaced1);


// 隐藏手机号
    // 用 $1 来引用第一个捕获组 (手机号的前三位),用 $2 来引用第二个捕获组 (手机号的后四位)。
String replaced2 = "13812345678".replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
// 138****5678
System.out.println(replaced2);
  • 3、按名称引用${name}(需命名分组支持)。
# 匹配对称标签(命名分组)
<(?<tag>\w+)>.*?</\k<tag>>
// Java 代码:YYYY-MM-DD → DD/MM/YYYY
String replaced = "2025-05-16".replaceAll(
        "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})",
        "${day}/${month}/${year}"
);
// 16/05/2025
System.out.println(replaced);

四、非捕获分组


  • 捕获分组 与 非捕获分组 的区别
    • 捕获分组 会存储 分组 匹配到的内容。
      • 即:当 0 < n <= 分组数量 时 ,可以用 matcher.group(n) 获取到分组存储 的数据。
    • 非捕获分组 不会存储 分组 匹配到的内容。
      • 即:当 0 < n <= 分组数量 时 ,用 matcher.group(n) 去获取到分组的数据会报错
String content = "hello希望教育,希望工程,树理希望小学";
// 选择匹配
// String regStr = "希望教育|希望工程|希望小学";
// 与 选择匹配 等价的 非捕获分组写法
String regStr = "希望(?:教育|工程|小学)";

Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
/*
    找到:希望教育
    找到:希望工程
    找到:希望小学
 */
while (matcher.find()){
    // matcher.group() 内部调用的就是 matcher.group(0) 。
    System.out.println("找到:" + matcher.group());

    // 运行报错:Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 1 .
        // 体现了,非捕获分组,不存储 分组表达式匹配到的内容。
    // System.out.println(matcher.group(1));
}


// 捕获分组
regStr = "希望(教育|工程|小学)";
pattern = Pattern.compile(regStr);
matcher = pattern.matcher(content);
/*
    找到:希望教育
    教育
    找到:希望工程
    工程
    找到:希望小学
    小学
 */
while (matcher.find()){
    // matcher.group() 内部调用的就是 matcher.group(0) 。
    System.out.println("找到:" + matcher.group());

    // 体现了,捕获分组,存储 分组表达式匹配到的内容。
    System.out.println(matcher.group(1));
}

1、普通非捕获分组 – (?:pattern)


  • 左括号后紧跟 ?: 。然后,再加上正则表达式,构成 非捕获组 (?:pattern)
    • ()?: 后边的表达式作为一个整体模板去进行匹配,但不捕获(不存储)匹配到文本
  • 示例:
    • (?:abc) :将 abc 作为一个整体模板去进行匹配,但不捕获(不存储)abc
    • (?:…):用于匹配 任意三个字符,但不捕获(不存储)匹配到任意三个字符
  • 功能作用:
    • 非捕获组 仅作为逻辑分组不保存匹配的子字符串减少内存占用。可以提升匹配速度

  • 应用场景:
    • 一些表达式中,不得不使用 (),但又不需要保存 () 中子表达式匹配的内容。
    • 可以用 非捕获组 来抵消使用 () 带来的副作用

2、原子分组 – (?>pattern) 特殊的 非捕获分组


  • 原子分组(Atomic Group)/ 固化分组
    • 是一种 特殊的 非捕获分组 ,它有一个特殊的功能禁止回溯(backtracking)。
  • 核心特性
    • 禁止回溯(?>pattern) 匹配成功 后,正则引擎不会回退尝试其它可能的 匹配路径
      • 即使,后续匹配失败不会回溯该分组内部尝试其他可能性
    • 优化性能:避免复杂正则的灾难性回溯(Catastrophic Backtracking)。
    • 改变匹配逻辑:可能影响 整体匹配结果(因提前锁定路径)。

  • 回溯 的 原理:(如:普通分组 a(bc|b)c 匹配 abc 时):
    • 引擎会先尝试匹配分支 bc匹配到之后。再尝试用剩余的字符去匹配右括号后边的 c 。
      • 此时,字符 abc 已经没有剩余的字符去匹配右括号后边的 c 。因此,会失败
    • 当去匹配右括号后边的 c 失败后回溯分组内尝试匹配分支 b匹配到之后
      • 再尝试用剩余的字符 c 去匹配右括号后边的 c 。最终,成功匹配
  • 禁止回溯 的 原理:(如:原子分组 a(?>bc|b)c 匹配 abc 时):
    • 引擎会先尝试匹配分支 bc匹配到之后。再尝试用剩余的字符去匹配右括号后边的 c 。
      • 此时,字符 abc 已经没有剩余的字符去匹配右括号后边的 c 。因此,会失败
    • 匹配分支 bc 失败后,引擎不会回溯分组内尝试匹配分支 b
      • 因此,导致整体 匹配失败
  • 示例对比:
public static void main(String[] args) {
    String text1 = "abcc";
    String text2 = "abc";

    // 普通分组正则
    Pattern patternNormal = Pattern.compile("a(bc|b)c");
    // 原子分组正则
    Pattern patternAtomic = Pattern.compile("a(?>bc|b)c");

    // 正则: a(bc|b)c | 文本: abcc | 结果: 成功
    testMatch(patternNormal, text1);
    // 正则: a(?>bc|b)c | 文本: abcc | 结果: 成功
    testMatch(patternAtomic, text1);

    // 正则: a(?>bc|b)c | 文本: abcc | 结果: 成功
    testMatch(patternNormal, text2);
    // 正则: a(?>bc|b)c | 文本: abcc | 结果: 成功
    testMatch(patternAtomic, text2);
}

private static void testMatch(Pattern pattern, String text) {
    Matcher matcher = pattern.matcher(text);
    System.out.println("正则: " + pattern.pattern() + " | 文本: " + text +
            " | 结果: " + (matcher.matches() ? "成功" : "失败"));
}

五、零宽断言(Lookaround Assertions)


1、概念


  • 零宽断言:
    • 是正则表达式中用于 匹配位置机制
  • 特点:
    • 1、不消耗字符:仅检查某个位置是否满足条件,不占用匹配结果。
    • 2、零宽度:断言本身不匹配任何实际字符,只定义 约束条件
    • 3、无捕获:不会生成分组或占用分组索引

2、分类


  • 零宽断言:分为 四种类型,按方向逻辑组合分类。
类型语法作用描述示例
正向肯定预查(?=pattern)右侧必须满足 patternWindows(?=95|98)
匹配后面跟着 95 或 98Windows
正向否定预查(?!pattern)右侧必须不满足 patternWindows(?!95|98)
匹配后面不跟着 95 或 98Windows
反向肯定预查(?<=pattern)左侧必须满足 pattern(?<=95|98)Windows
匹配前面是 95 或 98Windows
反向否定预查(?<!pattern)左侧必须不满足 pattern(?<!95|98)Windows
匹配前面不是 95 或 98Windows
  • 正向肯定预查 示例:
String content = "hello希望教育,希望工程,树理希望小学";
// 只匹配后边跟着<是> 教育 或 小学 的 希望
String regStr = "希望(?=教育|小学)";

Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
/*
    找到:希望
    找到:希望
 */
while (matcher.find()){
    System.out.println("找到:" + matcher.group());
}
  • 正向否定预查 示例:
String content = "hello希望教育,希望工程,树理希望小学";
// 只匹配后边跟着<不是> 教育 或 小学 的 希望
String regStr = "希望(?!教育|小学)";

Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
/*
    找到:希望
 */
while (matcher.find()){
    System.out.println("找到:" + matcher.group());
}

六、量词 的 贪婪模式 与 非贪婪模式


  • 量词 描述了一个模式吸收输入文本的方式:
量词(贪婪型)勉强型占有型说明
X*X*?X*+匹配 0 个或多个 X等价于 {0,}X
X+X+?X++匹配 1 个或多个 X等价于 {1,}X
X?X??X?+匹配 0 个或 1 个 X等价于 {0,1}X
X{n}X{n}?X{n}+精确匹配 nX
X{n,}X{n,}?X{n,}+至少匹配 nX
X{0,n}X{0,n}?X{0,n}+匹配最多 nX
X{n,m}X{n,m}?X{n,m}+匹配 nmX包含: n 次 和 m 次)

1、贪婪型(Greedy)-- 允许回溯


  • 贪婪型量词 的默认行为,适合需要最长匹配的场景。
  • 行为:先吞掉所有 可能的字符,若后续匹配失败,则逐步释放字符进行回溯

  • 示例:
public static void main(String[] args) {
    String text = "xfooxxxxxxfoo";
    // 贪婪型
    Pattern pattern = Pattern.compile(".*foo");
    Matcher matcher = pattern.matcher(text);
    // 输出整个字符串:xfooxxxxxxfoo
    while (matcher.find()) {
        System.out.println(matcher.group());
    }
}
  • 解析.* 吞下全部字符,后续 foo 无法匹配逐步回退最后一个 foo 位置完成匹配。

2、勉强型(Reluctant)-- 允许回溯


  • 勉强型(Reluctant)也叫: 懒惰的、最少匹配的、非贪婪的、不贪婪的
    • 问号来指定,这个量词匹配满足模式所需的最少字符数
  • 勉强型量词 通过 ? 触发,适合需要最短匹配的场景。
  • 行为:最少字符开始匹配,若后续匹配失败,则逐步扩大匹配范围。

  • 示例:
public static void main(String[] args) {
    String text = "xfooxxxxxxfoo";
    // 勉强型
    Pattern pattern = Pattern.compile(".*?foo");
    Matcher matcher = pattern.matcher(text);
    // 输出:
        // xfoo
        // xxxxxxfoo
    while (matcher.find()) {
        System.out.println(matcher.group());
    }
}
  • 解析: .*? 匹配字符串,后续 foo 匹配失败后逐步扩展,最终匹配到两个结果。

3、占有型(Possessive)-- 禁止回溯


  • 这种类型的量词只有在 Java 语言中才可用(在其它语言中不可用)。
  • 占有型量词 通过 + 触发,适合性能敏感匹配结果确定的场景。
  • 行为:吞掉所有可能的字符,且不释放已匹配的字符(不回溯)。

  • 示例:
public static void main(String[] args) {
    String text = "xfooxxxxxxfoo";
    // 占有型
    Pattern pattern = Pattern.compile(".*+foo");
    Matcher matcher = pattern.matcher(text);
    // 输出 false
    System.out.println(matcher.find());
}
  • 解析:.*+吞下整个字符串无剩余字符匹配 foo ,且不回溯导致匹配失败。

七、Java 中关于正则表达式的两个类:


1、Pattern 类


  • Pattern 对象:表示编译后正则表达式

2、Matcher 类 – 代表一个匹配工具类


  • 查询
    • find():查找下一个匹配正则表达式的字符。
    • group():取出上一次与正则表达式的字符串。
    • matches():整个字符串进行匹配,只有整个字符串都匹配了才返回 true 。
    • lookingAt():尝试将从区域开头开始的输入序列与该模式匹配。
  • 索引方法
    • start():返回以前匹配的初始索引。
    • end():返回最后匹配字符之后的偏移量。
  • 替换方法
    • replaceAll(String replacement):
      • 替换模式与给定替换字符串相匹配的输入序列的每个子序列。
    • replaceFirst(String replacement) :
      • 替换模式与给定替换字符串匹配的输入序列的第一个子序列。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值