enyang
enyang
Published on 2025-12-31 / 18 Visits
0
0

正则表达式<1>——正则基础

1. 正则表达式简介

1.1 什么是正则表达式

正则表达式(Regular Expression,简称 Regex)是一种用于 描述字符串模式的工具。不是简单的字符串匹配,而是用 规则和模式 来定义哪些字符串是“符合要求”的。

  • 用途举例:

    1. 校验文本格式:手机号、邮箱、身份证、邮编等

    2. 搜索文本:文章关键词查找、日志匹配

    3. 替换文本:敏感信息、格式统一

    4. 提取信息:日志分析、网页抓取、数据清洗

1.2 为什么使用正则表达式

举个例子:判断一个字符串是否为手机号

传统方法:

String s = "13800138000";
boolean valid = s.length() == 11;
for (int i = 0; i < s.length(); i++) {
    if (!Character.isDigit(s.charAt(i))) {
        valid = false;
        break;
    }
}

问题:

  • 代码冗长

  • 可读性差

  • 规则一变,需要改逻辑

正则方法:

"13800138000".matches("\\d{11}")

一句话搞定:

  • \d{11}:表示 连续11个数字

  • matches():表示整个字符串必须完全匹配

正则把逻辑变成了规则描述,不需要写循环和判断。

2. 正则表达式的工作原理

正则表达式本质上是一种 模式匹配引擎,它并不是直接去判断字符串,而是通过一套复杂的逻辑对输入字符串进行扫描、尝试匹配,并返回结果。理解其原理可以写出更高效、更正确的正则,也能理解为什么有些正则匹配慢或者匹配失败。

2.1 匹配流程

正则引擎匹配字符串的流程可以拆解为三个主要步骤:模式解析、扫描字符串、返回结果

模式解析

在 Java 中写下一个正则表达式,例如 \d{3}-\d{2}-\d{4},Java 正则引擎首先会把这个字符串 编译成一个内部状态机。这个状态机有两种常见实现方式:

  1. NFA(非确定有限自动机)

    • NFA 是一种图结构,每个节点表示一个匹配状态

    • 当遇到多种匹配可能时,会保留分支,并在必要时回溯

    • Java 的正则引擎大部分使用 NFA 实现,因此支持复杂的量词和分组

  2. DFA(确定有限自动机)

    • DFA 不回溯,每个输入字符只会走一条确定路径

    • 匹配速度非常快,但对某些复杂模式的支持有限

    • Java 默认不使用 DFA,但理解 DFA 有助于理解正则效率问题

这个阶段的作用就是把“正则规则”转化成 计算机能够理解的逻辑结构,方便后续匹配操作。

扫描字符串

正则引擎从目标字符串的 第一个字符开始,逐字符尝试匹配模式。匹配过程中可能出现两种情况:

  1. 匹配成功

    • 当前字符满足模式要求

    • 引擎继续尝试匹配下一个字符

    • 如果模式中包含量词或分组,可能会尝试多种匹配路径

  2. 匹配失败

    • 当前字符不满足模式要求

    • 引擎会尝试 回溯:回到之前的分支点,尝试其他匹配路径

    • 如果没有可回溯的路径,匹配失败

    • 引擎可能会从字符串的 下一个起始位置 重新尝试匹配

举例说明:

假设模式是 \d{2},字符串是 "a12"

  • 引擎从第 0 个字符 "a" 开始匹配

  • "a" 不匹配 \d,引擎失败

  • 移动到第 1 个字符 "1" 作为新的起始位置

  • "1" 匹配第一个 \d

  • "2" 匹配第二个 \d

  • 匹配成功,返回结果 "12"

这说明正则匹配不仅仅是从左到右一次完成,而是可能回退、重新尝试,直到找到符合模式的子串或扫描完整个字符串。

返回结果

匹配完成后,正则引擎会返回匹配结果,情况有两种:

  1. 匹配成功

    • 引擎返回匹配到的字符串

    • 如果使用分组,可以返回不同分组捕获的内容

    • 可以用于提取、替换、验证

  2. 匹配失败

    • 返回 null 或空

    • 对于 find() 方法,会继续尝试下一个起始位置

    • 对于 matches() 方法,如果整个字符串没有完全匹配,则直接返回 false

2.2 完全匹配与部分匹配

在 Java 正则中,理解 完全匹配 与 部分匹配 的区别非常重要,因为它直接决定使用的方法是 matches() 还是 find()

完全匹配(matches)

  • matches() 方法要求 整个字符串必须从头到尾都符合正则模式

  • 如果字符串中有多余字符或不符合规则的字符,匹配失败

String str = "abc123";
boolean result = str.matches("\\d+"); // 检查字符串是否完全由数字组成
System.out.println(result); // 输出 false

解析:

  • 正则 \d+ 表示 一个或多个数字

  • 字符串 "abc123" 前面有非数字字符 "abc"

  • 因此整个字符串不完全匹配,返回 false

部分匹配(find)

  • find() 方法则用来查找字符串中 任意一段符合正则的子串

  • 不要求整个字符串匹配,只要存在一段满足模式即可

Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher("abc123");
if (matcher.find()) {
    System.out.println(matcher.group()); // 输出 123
}

解析:

  • 正则仍然是 \d+

  • 字符串 "abc123" 中存在 "123" 满足模式

  • find() 找到匹配的子串 "123" 并返回

  • 即使前面 "abc" 不匹配,也不会影响结果

3. Java 中正则表达式核心类

Java 使用两个类处理正则表达式:

  1. Pattern

    • 表示编译后的正则表达式

    • Pattern.compile("正则表达式")

  2. Matcher

    • 用于执行匹配操作

    • pattern.matcher("目标字符串")

Pattern pattern = Pattern.compile("\\d{11}");
Matcher matcher = pattern.matcher("13800138000");

if (matcher.matches()) {
    System.out.println("是手机号");
}

方法

功能

matches()

整个字符串完全匹配

find()

查找字符串中符合规则的子串

group()

获取匹配内容

replaceAll()

替换匹配的内容

4. 正则基础语法与元字符

正则表达式通过 元字符(Metacharacter) 来定义匹配规则。

4.1 常用单字符匹配

元字符

含义

Java 示例

.

任意字符(换行除外)

"a".matches(".")

\d

数字 [0-9]

"5".matches("\\d")

\D

非数字

"a".matches("\\D")

\w

字母、数字或下划线

"abc_123".matches("\\w+")

\W

非字母数字下划线

"@#".matches("\\W+")

\s

空白字符(空格、制表符、换行)

" ".matches("\\s")

\S

非空白字符

"a".matches("\\S")

4.2 字符集合与范围

  • [abc] → 匹配 a 或 b 或 c

  • [a-z] → 匹配小写字母

  • [A-Z0-9] → 匹配大写字母或数字

  • [^abc] → 匹配不包含 a/b/c 的任意字符

"a".matches("[abc]") // true
"x".matches("[^abc]") // true

5. 常用量词与分组

5.1 量词

量词

含义

示例

*

0 次或多次

"ab*c".matches("ac")

+

1 次或多次

"ab+c".matches("abc")

?

0 次或 1 次

"ab?c".matches("abc")

{n}

恰好 n 次

"a{3}".matches("aaa")

{n,}

至少 n 次

"a{2,}".matches("aaa")

{n,m}

n~m 次

"a{2,3}".matches("aa")

5.2 分组与捕获

  • ( ) → 分组

  • | → 或

  • 捕获分组可用 Matcher.group() 提取

String s = "2025-12-31";
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher(s);
if (m.find()) {
    System.out.println(m.group(1)); // 年
    System.out.println(m.group(2)); // 月
    System.out.println(m.group(3)); // 日
}

6. Java 正则示例

6.1 匹配邮箱

String text = "我的邮箱是 example@test.com";
String regex = "[a-zA-Z0-9_.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,}";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
    System.out.println("邮箱:" + matcher.group());
}

6.2 查找所有数字

String text2 = "手机号:13800138000 邮编:100000";
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher(text2);
while (m.find()) {
    System.out.println("数字:" + m.group());
}

6.3 替换敏感信息

String text3 = "手机号:13800138000";
String replaced = text3.replaceAll("\\d{11}", "***********");
System.out.println(replaced);

7. 贪婪匹配与懒惰匹配

  • 贪婪匹配是正则表达式的默认行为,它会尽可能多地匹配字符,只要整个匹配仍然成功。比如正则表达式 a.*b 用于匹配字符串 "a123b456b"。在这个例子中,.* 会尽可能多地匹配字符,直到最后一个符合 b 的位置。匹配结果是 "a123b456b",因为正则引擎尽量把 .* 匹配的范围扩大到最远。

    "<div>1</div><div>2</div>".replaceAll("<.*>", "X"); // 输出 X
    
  • 懒惰匹配是通过在量词后加问号 ? 实现的,它表示尽可能少地匹配字符。使用懒惰匹配时,正则引擎会从最少字符开始尝试匹配,然后根据整体模式调整匹配长度。以上面的例子为例,如果使用正则表达式 a.*?b 来匹配 "a123b456b",.*? 会尽量少匹配字符,第一个匹配到的 b 就会停止匹配,因此匹配结果是 "a123b"。如果字符串中有多个符合条件的子串,懒惰匹配会优先匹配最短的部分。

    "<div>1</div><div>2</div>".replaceAll("<.*?>", "X"); // 输出 XX


Comment