Skip to content

第42课:正则表达式

🎯 学习目标

  • 掌握 Pattern/Matcher 的用法与预编译
  • 掌握正则语法(字符类/量词/分组/边界/预查)
  • 掌握 matches/find/group、替换、分割
  • 理解贪婪与非贪婪、性能陷阱
  • 知道常用正则(邮箱/手机/身份证)

📖 一、概念讲解:正则与 Java API

1. 正则是什么

正则表达式是描述字符串模式的"小语言"——用一个模式匹配一类字符串。

\d+       匹配一个或多个数字
[a-z]+    匹配小写字母
^\d{11}$  11位数字(手机号)

2. Java 的两个核心类

  • Pattern:编译后的正则(Pattern.compile("..."))。
  • Matcher:对输入串执行匹配(pattern.matcher(input))。
java
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher("abc123def456");
while (m.find()) {
    System.out.println(m.group());   // 123, 456
}

3. String 的正则方法

java
"abc123".matches("\\w+");       // true,整体匹配
"a,b,c".split(",");             // [a,b,c]
"123abc".replaceAll("\\d", "*"); // ***abc

String 方法内部用 Pattern/Matcher,简单场景够用,复杂/重复用 Pattern 预编译。


📖 二、正则语法

1. 字符类

\d 数字 [0-9]    \D 非数字
\w 单词字符 [a-zA-Z0-9_]   \W 非单词字符
\s 空白(空格/tab/换行)   \S 非空白
. 任意字符(默认不含换行)
[abc] a或b或c   [^abc] 非abc   [a-z] a到z

2. 量词

a?   0或1次
a*   0或多次
a+   1或多次
a{3} 恰好3次
a{3,} 至少3次
a{3,5} 3到5次

3. 分组与引用

(abc)        捕获组,group(1) 取
(?:abc)      非捕获组(不存储)
\1           引用第1个捕获组

4. 边界

^ 字符串开头    $ 字符串结尾
\b 单词边界

5. 预查(零宽断言)

(?=pattern)   正向预查(后面是 pattern)
(?!pattern)   负向预查(后面不是)
(?<=pattern)  反向预查(前面是)
(?<!pattern)  负向反向预查

📖 三、贪婪与非贪婪

java
"<a><b>".matches("<.*>")   // 贪婪,匹配整个 <a><b>(尽可能多)
"<a><b>".matches("<.*?>")  // 非贪婪,匹配 <a>(尽可能少)
  • 贪婪(默认):* + 尽可能多匹配。
  • 非贪婪(加 ?):*? +? 尽可能少匹配。

提取标签内容、引号内字符串用非贪婪。


📖 四、匹配方法

1. matches vs find

  • matches()整体匹配(整个串符合模式)。
  • find():查找子串匹配(串中找符合模式的部分)。
java
"abc123".matches("\\d+");   // false(整体不是纯数字)
Pattern.compile("\\d+").matcher("abc123").find();   // true(含数字子串)

2. group 分组

java
Pattern p = Pattern.compile("(\\d+)-(\\d+)");
Matcher m = p.matcher("12-34");
if (m.matches()) {
    m.group(0);   // "12-34"(整体)
    m.group(1);   // "12"(第1组)
    m.group(2);   // "34"(第2组)
}

3. 替换与分割

java
"123abc456".replaceAll("\\d+", "#");   // #abc#
"a, b;;c".split("[,\\s;]+");            // [a,b,c]

⚠️ 五、常见陷阱

陷阱1:忘记双重转义

Java 字符串里 \d 要写 \\d\ 是字符串转义符)。"\\d+" 才是正则 \d+

陷阱2:贪婪过度匹配

&lt;.*&gt; 贪婪会匹配到最后的 &gt;,提取标签应用 &lt;.*?&gt;(非贪婪)。

陷阱3:未预编译重复正则

循环里 str.matches("\\d+") 每次编译正则,性能差。预编译 Pattern 复用。

陷阱4:正则灾难(ReDoS)

复杂正则(如嵌套量词 (a+)+)可能指数级回溯,恶意输入导致卡死。避免复杂回溯,用简单正则或专门的解析器。

陷阱5:matches 整体匹配误解

matches() 要求整个串匹配,不是部分。要部分用 find() 或加 .* 包围。


🆚 六、Java vs C / 其他

特性C(regex.h/PCRE)Java
引擎PCRE/POSIXjava.util.regex
APIregcomp/regexecPattern/Matcher
预编译手动Pattern.compile
性能PCRE 快够用

对 C 程序员:Java 正则对应 C 的 regex.h/PCRE,语法基本一致。Java 用 Pattern/Matcher 封装,预编译复用提升性能。正则语法是通用技能,跨语言一致。


💡 七、最佳实践

  1. 预编译 Pattern 复用(static final),别循环里 matches。
  2. 双重转义\\d 不是 \d
  3. 非贪婪提取.*? 避免过度匹配。
  4. matches 整体,find 子串,按需选。
  5. 用 group 提取分组,别再 substring 拆。
  6. 避免 ReDoS:简单正则,复杂解析用专门库。
  7. 复杂校验用现成正则(邮箱/手机),但测试边界。

📝 练习预告

完成 练习/Ex42_Regex.java 中的 6 道题:

  1. 数字匹配(find 提取所有数字)
  2. 手机号验证(matches 整体)
  3. 邮箱提取(find + group)
  4. 字符串替换(replaceAll 脱敏)
  5. 多分隔符分割(split)
  6. 综合:日志解析(分组提取时间/级别/消息)

完成后对比 答案/Sol42.java,查看逐行讲解与多解法。


📖 八、Pattern 与 Matcher 深入

Pattern 是编译后的正则,线程安全;Matcher 保存匹配状态,不是线程安全对象。

推荐写法:

java
private static final Pattern EMAIL_PATTERN =
    Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");

public static boolean isEmail(String input) {
    return EMAIL_PATTERN.matcher(input).matches();
}

原因:

text
Pattern.compile 有成本。
重复使用 static final Pattern 能减少编译开销。
Matcher 每次根据输入创建,避免状态污染。

Matcher 常用方法:

text
matches:整体匹配。
find:查找下一个匹配片段。
group:取匹配内容。
start/end:匹配片段的起止位置。
replaceAll:替换所有匹配。
appendReplacement/appendTail:复杂替换。

📖 九、贪婪、非贪婪与占有量词

Java 正则量词有三种常见模式:

类型示例含义
贪婪.*尽可能多匹配,必要时回溯
非贪婪.*?尽可能少匹配,必要时扩展
占有.*+尽可能多匹配,不回溯

示例:

java
String html = "<b>one</b><b>two</b>";

Pattern greedy = Pattern.compile("<b>.*</b>");
Pattern lazy = Pattern.compile("<b>.*?</b>");

结果:

text
greedy 匹配:<b>one</b><b>two</b>
lazy 第一次匹配:<b>one</b>

占有量词适合减少回溯,但也可能导致本可成功的匹配失败。初学阶段重点掌握贪婪和非贪婪即可。


📖 十、命名分组与复杂提取

普通分组用数字索引:

java
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher("2026-06-30");
if (m.matches()) {
    System.out.println(m.group(1));
}

命名分组更清晰:

java
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher("2026-06-30");
if (m.matches()) {
    System.out.println(m.group("year"));
    System.out.println(m.group("month"));
    System.out.println(m.group("day"));
}

命名分组适合日志解析、协议字段提取、复杂文本结构。


🧪 十一、实战案例:日志解析

日志格式:

text
2026-06-30 10:15:30 INFO  [main] com.example.App - server started

正则:

java
private static final Pattern LOG_PATTERN = Pattern.compile(
    "^(?<time>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+" +
    "(?<level>INFO|WARN|ERROR|DEBUG)\\s+" +
    "\\[(?<thread>[^]]+)]\\s+" +
    "(?<logger>[\\w.]+)\\s+-\\s+" +
    "(?<message>.*)$"
);

解析:

java
public static Map<String, String> parseLog(String line) {
    Matcher matcher = LOG_PATTERN.matcher(line);
    if (!matcher.matches()) {
        return Map.of();
    }
    return Map.of(
        "time", matcher.group("time"),
        "level", matcher.group("level"),
        "thread", matcher.group("thread"),
        "logger", matcher.group("logger"),
        "message", matcher.group("message")
    );
}

这个例子体现了正则的适用边界:固定格式文本很适合正则;层级复杂、可嵌套的语言不适合用正则硬解析。


🛡 十二、ReDoS 防护

ReDoS 是正则拒绝服务攻击,核心来自灾难性回溯。

危险模式:

text
(a+)+
([a-zA-Z]+)*
(.*a){10}

恶意输入可能让匹配时间指数级增长。

防护策略:

text
避免嵌套量词。
限制输入长度。
优先使用明确字符类。
使用非捕获组减少无意义捕获。
对用户可配置正则做审核。
复杂格式用解析器,不用正则硬扛。
必要时使用超时控制或 RE2 类无回溯引擎。

示例改造:

text
危险:^(a+)+$
更好:^a+$

🛠 十三、正则排查清单

当正则结果不对时,按下面顺序查:

text
Java 字符串是否正确双重转义。
matches 和 find 是否用错。
是否忘记 ^ 和 $。
点号是否需要匹配换行。
量词是否过于贪婪。
分组编号是否因为新增括号而变化。
是否需要非捕获组 (?:...)。
输入中是否有不可见空白。
是否存在 Unicode 字符范围问题。
是否可能出现灾难性回溯。

调试建议:

text
先用最小输入验证。
逐段增加正则复杂度。
给关键部分加命名分组。
把长正则拆成多行字符串并写注释。
为边界样例写单元测试。

📊 十四、常见模式建议

常见需求的建议:

需求建议
判断是否全数字^\\d+$
提取文本中数字\\d+ + find
多分隔符 split[,;\\s]+
简单邮箱校验使用宽松正则,复杂交给业务确认
手机号校验根据国家和业务规则定义
HTML 解析不建议正则,使用 HTML parser
JSON 解析不建议正则,使用 Jackson/Gson

不要追求一个“完美邮箱正则”。很多校验应分成格式校验和业务校验两层。


🔍 十五、自测问题

text
Pattern 和 Matcher 谁是线程安全的?
matches 和 find 的区别是什么?
为什么 Java 正则要写 \\d 而不是 \d?
贪婪和非贪婪的区别是什么?
命名分组适合什么场景?
什么是灾难性回溯?
为什么不建议用正则解析 HTML 或 JSON?
循环中为什么应预编译 Pattern?

✅ 十六、掌握标准

学完本课后,应能做到:

text
能用 Pattern/Matcher 完成匹配、提取、替换。
能正确处理 Java 字符串双重转义。
能区分 matches、find、group。
能使用分组和命名分组提取字段。
能解释贪婪、非贪婪和回溯。
能预编译 Pattern 提升重复匹配性能。
能识别 ReDoS 风险。
能判断什么时候不该用正则。

正则是强大的文本工具,但不是通用解析器。越复杂的正则,越需要测试、边界控制和性能意识。


🎓 下一步

  • 高级篇(30-42)完成!进入企业开发篇 43-JDBC