杰瑞科技汇

Java正则如何实现非贪婪匹配?

贪婪 vs. 非贪婪

在正则表达式中,量词(如 , , , {n,m})默认是“贪婪”的,这意味着它会尝试匹配尽可能多的字符。

Java正则如何实现非贪婪匹配?-图1
(图片来源网络,侵删)

而“非贪婪”模式则相反,它会尝试匹配尽可能少的字符。


贪婪模式(默认行为)

这是 Java 正则表达式的默认行为,当你使用一个量词时,它会从左到右扫描字符串,并尽可能多地“吃掉”字符,直到无法满足整个正则表达式为止,然后它会回退,尝试找到下一个可能的匹配。

示例:

假设我们有字符串 "<div>第一部分</div><div>第二部分</div>",我们想用正则表达式 "<div>.*</div>" 来匹配一个完整的 div 标签。

Java正则如何实现非贪婪匹配?-图2
(图片来源网络,侵删)
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GreedyExample {
    public static void main(String[] args) {
        String text = "<div>第一部分</div><div>第二部分</div>";
        // .* 会匹配尽可能多的字符
        String regex = "<div>.*</div>";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(text);
        if (matcher.find()) {
            System.out.println("找到的匹配: " + matcher.group());
        }
    }
}

输出结果:

找到的匹配: <div>第一部分</div><div>第二部分</div>

为什么?

  1. 首先从第一个 <div> 开始。
  2. 它贪婪地匹配 第一部分</div><div>第二部分 中的所有字符。
  3. 它尝试匹配 </div>,成功了。
  4. 因为这是它能找到的最长的、满足条件的字符串,所以它就返回了整个字符串,这通常不是我们想要的结果。

非贪婪模式(如何实现)

要使用非贪婪模式,你只需要在任何一个量词(, , , {n,m})的后面加上一个问号 。

  • : 非贪婪匹配前面的元素零次或多次
  • : 非贪婪匹配前面的元素一次或多次
  • : 非贪婪匹配前面的元素零次或一次
  • {n,m}? : 非贪婪匹配前面的元素至少 n 次,但不超过 m

示例:

现在我们修改上面的例子,将 改为 。

import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NonGreedyExample {
    public static void main(String[] args) {
        String text = "<div>第一部分</div><div>第二部分</div>";
        // .*? 会匹配尽可能少的字符
        String regex = "<div>.*?</div>";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(text);
        // 使用 while 循环找到所有匹配项
        while (matcher.find()) {
            System.out.println("找到的匹配: " + matcher.group());
        }
    }
}

输出结果:

找到的匹配: <div>第一部分</div>
找到的匹配: <div>第二部分</div>

为什么?

  1. 首先从第一个 <div> 开始。
  2. 它是“懒惰”的,所以它先尝试匹配最少的字符,它先看 能否匹配零个字符。
  3. 匹配零个字符,那么正则表达式就变成了 <div></div>,这在文本中找不到。
  4. 匹配一个字符(),再检查 <div>第</div> 是否能找到,还是不行。
  5. 这个过程继续, 一次只匹配一个字符,直到它匹配了 第一部分
  6. 整个正则表达式 <div>第一部分</div> 在文本中找到了第一个匹配。
  7. 匹配完成后,引擎从匹配结束的位置继续向后搜索,用同样的非贪婪逻辑找到了第二个匹配。

更详细的对比

让我们用一个更简单的例子来彻底理解。

文本: xxxxxx

正则表达式 模式 匹配结果 解释
x* 贪婪 xxxxxx 匹配所有可用的 x
x*? 非贪婪 (空字符串) 匹配零次,这是最少的可能。
x+ 贪婪 xxxxxx 匹配所有可用的 x
x+? 非贪婪 x 匹配一次,这是最少的可能。
x?? 贪婪 x 匹配一次( 优先匹配一次)。
x?? 非贪婪 (空字符串) 匹配零次,这是最少的可能。
x{2,4} 贪婪 xxxxxx 尽可能多地匹配,会尝试匹配4个,但文本有6个,所以匹配了4个。
x{2,4}? 非贪婪 xx 尽可能少地匹配,满足至少2个即可。

实际应用场景

非贪婪模式在处理 HTML/XML 标签、日志文件、代码等多行结构时非常有用。

场景:从日志中提取每条错误信息

假设日志格式如下:

[INFO] 系统启动成功。
[ERROR] 磁盘空间不足。
[DEBUG] 加载配置文件 config.xml。
[ERROR] 数据库连接失败。

我们想提取所有 [ERROR] 开头和结尾的信息。

错误的贪婪做法:

String log = "[INFO] ...\n[ERROR] 磁盘空间不足,\n[DEBUG] ...\n[ERROR] 数据库连接失败。";
String regex = "\\[ERROR\\].*\\]"; // 错误:.* 会跨行匹配
// ... 代码 ...
// 预期输出(但会出错):
// [ERROR] 磁盘空间不足。
// [ERROR] 数据库连接失败。

这个正则表达式会因为 的贪婪性而匹配整个字符串,因为它会跨过换行符去寻找最后一个 ]

正确的非贪婪做法:

String log = "[INFO] ...\n[ERROR] 磁盘空间不足,\n[DEBUG] ...\n[ERROR] 数据库连接失败。";
// 使用 [\s\S] 匹配包括换行符在内的所有字符,并使用非贪婪模式
String regex = "\\[ERROR\\][\\s\S]*?\\]";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(log);
while (matcher.find()) {
    System.out.println(matcher.group());
}

输出结果:

[ERROR] 磁盘空间不足。
[ERROR] 数据库连接失败。

关键点:

  • 默认不匹配换行符。 不会跨行。
  • 如果需要跨行匹配,可以使用 [\s\S](匹配任何空白或非空白字符)或 (?s).(?s) 是一个内联标志,表示 匹配所有字符,包括换行符)。
  • 在跨行匹配的场景下,几乎总是需要使用非贪婪模式 或 来精确地匹配你想要的结构单元。

特性 贪婪模式 非贪婪模式
行为 匹配尽可能多的字符 匹配尽可能少的字符
语法 , , , {n,m} (默认) , , , {n,m}?
适用场景 当你需要匹配一个整体,且内部结构复杂时(如匹配整个HTML文档) 当你需要匹配一个由重复模式构成的多个独立单元时(如匹配多个独立的HTML标签)
核心思想 “给我所有能匹配的!” “给我第一个能匹配的!”

记住这个简单的规则:当你发现你的正则表达式匹配的范围比你期望的要大时,很可能就是贪婪模式在作祟,尝试在量词后面加上 ,把它变成非贪婪模式,通常就能解决问题。

分享:
扫描分享到社交APP
上一篇
下一篇