17、Java正则表达式Matcher类的深入解析与应用

Java正则表达式Matcher类的深入解析与应用

1. Matcher类概述

Matcher类的实例是有状态的对象,用于将特定的正则表达式与特定的输入字符序列进行匹配。由于Matcher对象在方法调用之间会保存内部状态,因此它们不是线程安全的。每个Matcher实例都源自一个Pattern实例,通过Matcher的 pattern() 方法可以返回创建它的Pattern对象的引用。

Matcher对象可以重复使用,但由于其有状态的特性,在开始新的匹配操作系列之前,必须将其置于已知状态。这可以通过调用 reset() 方法来实现,该方法会让对象准备好在与Matcher关联的CharSequence的开头进行模式匹配。无参数的 reset() 方法会重用上次提供给Matcher的CharSequence。如果要对新的字符序列进行匹配,可以将新的CharSequence传递给 reset() 方法,后续的匹配将针对该目标进行。

以下是一个简单的文件grep示例代码:

package com.ronsoft.books.nio.regex;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;

public class SimpleGrep {
    public static void main (String [] argv) throws Exception {
        if (argv.length < 2) {
            System.out.println ("Usage: regex file [ ... ]");
            return;
        }

        Pattern pattern = Pattern.compile (argv [0]);
        Matcher matcher = pattern.matcher ("");

        for (int i = 1; i < argv.length; i++) {
            String file = argv [i];
            BufferedReader br = null;
            String line;

            try {
                br = new BufferedReader (new FileReader (file));
            } catch (IOException e) {
                System.err.println ("Cannot read '" + file + "': " + e.getMessage());
                continue;
            }

            while ((line = br.readLine()) != null) {
                matcher.reset (line);

                if (matcher.find()) {
                    System.out.println (file + ": " + line);
                }
            }

            br.close();
        }
    }
}

该示例的操作步骤如下:
1. 检查命令行参数是否至少有两个,如果不足则输出使用说明并返回。
2. 编译正则表达式模式。
3. 创建Matcher对象。
4. 遍历每个文件,读取文件的每一行,使用 reset() 方法重置Matcher对象,然后使用 find() 方法查找匹配项,如果找到则输出文件名和匹配的行。

2. reset()方法的高级应用

reset() 方法还可以用于让Matcher处理多个不同的字符序列。以下是一个提取匹配表达式(验证电子邮件地址)的示例代码:

package com.ronsoft.books.nio.regex;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class EmailAddressFinder {
    public static void main (String [] argv) {
        if (argv.length < 1) {
            System.out.println ("usage: emailaddress ...");
        }

        Pattern pattern = Pattern.compile (
            "([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]"
            + "{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))"
            + "([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)",  
            Pattern.MULTILINE);

        Matcher matcher = pattern.matcher ("");

        for (int i = 0; i < argv.length; i++) {
            boolean matched = false;

            System.out.println ("");
            System.out.println ("Looking at " + argv [i] + " ...");

            matcher.reset (argv [i]);

            while (matcher.find())  {
                System.out.println ("\t" + matcher.group());
                matched = true;
            }

            if ( ! matched) {
                System.out.println ("\tNo email addresses found");
            }
        }
    }
}

该示例的操作步骤如下:
1. 检查命令行参数是否至少有一个,如果不足则输出使用说明。
2. 编译电子邮件地址检测模式。
3. 创建Matcher对象。
4. 遍历每个参数,使用 reset() 方法重置Matcher对象,然后使用 find() 方法查找匹配的电子邮件地址,如果找到则输出匹配项,若未找到则输出提示信息。

3. 布尔指示方法

有一组方法可以返回布尔值,用于指示正则表达式如何应用于目标字符序列。
- matches()方法 :如果整个字符序列都与正则表达式模式匹配,则返回 true ;如果模式仅匹配子序列,则返回 false 。这对于选择文件中完全符合特定模式的行很有用,其行为与Pattern类的便捷方法 matches() 相同。
- lookingAt()方法 :与 matches() 方法类似,但不要求整个序列都与模式匹配。如果正则表达式模式匹配字符序列的开头,则 lookingAt() 方法返回 true 。该方法总是从序列的开头开始扫描。如果返回 true ,则可以调用 start() end() group() 方法来确定匹配子序列的范围。
- find()方法 :执行与 lookingAt() 类似的匹配操作,但会记住上一次匹配的位置,并从该位置之后继续扫描。这允许连续调用 find() 方法遍历输入并查找嵌入的匹配项。在调用 reset() 后的第一次调用中,扫描从输入序列的第一个字符开始;在后续调用中,从上次匹配的子序列之后的第一个字符开始。每次调用时,如果找到模式则返回 true ,否则返回 false 。通常,会使用 find() 方法遍历文本以查找其中的所有匹配模式。带有位置参数的 find() 方法会隐式重置并从提供的索引位置开始扫描输入,之后如果需要,可以调用无参数的 find() 方法扫描输入序列的其余部分。

4. 匹配位置方法

一旦检测到匹配项,可以通过调用 start() end() 方法来确定匹配项在字符序列中的位置。 start() 方法返回匹配序列的第一个字符的索引, end() 方法返回匹配的最后一个字符的索引加一。这些值与 CharSequence.subsequence() 方法一致,可以直接用于提取匹配的子序列。

需要注意的是,有些正则表达式可以匹配空字符串,此时 start() end() 方法将返回相同的值。 start() end() 方法仅在之前通过 matches() lookingAt() find() 方法检测到匹配时才会返回有意义的值。如果没有进行匹配,或者上次匹配尝试返回 false ,则调用 start() end() 方法将导致 java.lang.IllegalStateException 异常。

5. 捕获组

正则表达式可能包含用括号括起来的子表达式,即捕获组。在正则表达式的计算过程中,与这些捕获组表达式匹配的输入子序列会被保存起来,并可以在后续的表达式中引用。匹配操作完成后,可以通过指定相应的组号从Matcher对象中检索这些保存的片段。

捕获组可以嵌套,其编号是从左到右计算其左括号的数量。整个表达式,无论是否有子组,始终被视为捕获组零。例如,正则表达式 A((B)(C(D))) 的捕获组编号如下表所示:
| 组号 | 表达式组 |
| ---- | ---- |
| 0 | A((B)(C(D))) |
| 1 | ((B)(C(D))) |
| 2 | (B) |
| 3 | (C(D)) |
| 4 | (D) |

有一个例外情况,以 (? 开头的组是纯非捕获组,其值不会被保存,也不会用于捕获组的编号。

以下是处理捕获组的部分API方法:

package java.util.regex;

public final class Matcher {
    public int start()
    public int start (int group)
    public int end()
    public int end (int group)

    public int groupCount()
    public String group()
    public String group (int group)
}
  • groupCount() 方法返回正则表达式模式中的捕获组数量,该值源自原始的Pattern对象,且不可变。组号必须为正且小于 groupCount() 返回的值,传递超出范围的组号将导致 java.lang.IndexOutOfBoundsException 异常。
  • 可以将捕获组编号传递给 start() end() 方法,以确定与给定捕获组子表达式匹配的子序列。有可能整个表达式成功匹配,但一个或多个捕获组未匹配。如果请求的捕获组当前未设置, start() end() 方法将返回 -1。如前所述,整个正则表达式被视为组零,无参数调用 start() end() 方法等同于传递参数零,调用组零的 start() end() 方法永远不会返回 -1。
  • 可以使用 start() end() 方法返回的值从输入CharSequence中提取匹配的子序列,但 group() 方法提供了更简单的方式。带有数字参数的 group() 方法返回特定捕获组的匹配子序列的字符串。如果调用无参数的 group() 方法,则返回整个正则表达式匹配的子序列(组零)。例如:
String match0 = input.subSequence (matcher.start(), matcher.end()).toString();
String match2 = input.subSequence (matcher.start (2), matcher.end (2)).toString();

等同于:

String match0 = matcher.group();
String match2 = matcher.group(2);
6. 替换方法

正则表达式最常见的应用之一是进行搜索和替换。 replaceFirst() replaceAll() 方法使这一操作变得非常简单。它们的行为基本相同,不同之处在于 replaceFirst() 方法在找到第一个匹配项后停止,而 replaceAll() 方法会一直迭代直到所有匹配项都被替换。这两个方法都接受一个字符串参数,用于替换输入字符序列中匹配模式的部分。

package java.util.regex;

public final class Matcher {
    public String replaceFirst (String replacement)
    public String replaceAll (String replacement)
}

捕获组可以在正则表达式中进行反向引用,也可以在提供给 replaceFirst() replaceAll() 方法的替换字符串中引用。可以通过在捕获组编号前加上美元符号将其嵌入到替换字符串中。当替换字符串被替换到结果字符串中时,每个 $g 的出现都会被 group(g) 返回的值替换。如果要在替换字符串中使用字面美元符号,必须在其前面加上反斜杠( \$ )。要传递反斜杠,必须将其加倍( \\ )。如果要在捕获组引用后连接字面数字,需要用反斜杠将它们与组号分隔开,例如 123$2\456

以下是一些替换示例:
| 正则表达式模式 | 输入 | 替换字符串 | replaceFirst()结果 | replaceAll()结果 |
| ---- | ---- | ---- | ---- | ---- |
| a b | aabfooaabfooabfoob | - | -fooaabfooabfoob | -foo-foo-foo- |
| \p{Blank} | fee fie foe fum | _ | fee_fie foe fum | fee_fie_foe_fum |
| ([bB])yte | Byte for byte | $1ite | Bite for byte | Bite for bite |
| \d\d\d\d([- ]) | card #1234-5678-1234 | xxxx$1 | card #xxxx-5678-1234 | card #xxxx-xxxx-1234 |
| (up|left)(
)(right|down) | left right, up down | $3$2$1 | right left, up down | right left, down up |
| ([CcPp][hl]e[ea]se) | I want cheese. Please. | $1 | I want cheese . Please. | I want cheese . Please . |

以下是一个正则表达式替换的示例代码:

package com.ronsoft.books.nio.regex;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexReplace {
    public static void main (String [] argv) {
        if (argv.length < 3) {
            System.out.println ("usage: regex replacement input ...");
            return;
        }

        String regex = argv [0];
        String replace = argv [1];

        Pattern pattern = Pattern.compile (regex);
        Matcher matcher = pattern.matcher ("");

        System.out.println ("         regex: '" + regex + "'");
        System.out.println ("   replacement: '" + replace + "'");

        for (int i = 2; i < argv.length; i++) {
            System.out.println ("------------------------");

            matcher.reset (argv [i]);

            System.out.println ("         input: '" + argv [i] + "'");
            System.out.println ("replaceFirst(): '" + matcher.replaceFirst (replace) + "'");
            System.out.println ("  replaceAll(): '" + matcher.replaceAll (replace) + "'");
        }
    }
}

该示例的操作步骤如下:
1. 检查命令行参数是否至少有三个,如果不足则输出使用说明并返回。
2. 保存正则表达式和替换字符串。
3. 编译正则表达式。
4. 创建Matcher对象。
5. 遍历每个输入字符串,使用 reset() 方法重置Matcher对象,然后分别调用 replaceFirst() replaceAll() 方法进行替换并输出结果。

7. 反斜杠处理

需要注意的是,正则表达式会解释提供的字符串中的反斜杠,并且Java编译器要求在字面字符串中每个反斜杠都用两个反斜杠表示。这意味着如果要在正则表达式中转义反斜杠,在编译后的字符串中需要两个反斜杠。要在编译后的正则表达式字符串中连续出现两个反斜杠,在Java源代码中需要连续出现四个反斜杠。

例如,要生成替换序列 a\b ,传递给 replaceAll() 方法的字符串字面参数必须是 a\\\\b 。以下是一个演示正则表达式中反斜杠行为的示例代码:

package com.ronsoft.books.nio.regex;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class BackSlashes {
    public static void main (String [] argv) {
        String rep = "a\\\\b";
        String input = "> XYZ <=> ABC <";
        Pattern pattern = Pattern.compile ("ABC|XYZ");
        Matcher matcher = pattern.matcher (input);

        System.out.println (matcher.replaceFirst (rep));
        System.out.println (matcher.replaceAll (rep));

        rep = "\\\\r\\\\n";
        input = "line 1\nline 2\nline 3\n";
        pattern = Pattern.compile ("\\n");
        matcher = pattern.matcher (input);

        System.out.println ("");
        System.out.println ("Before:");
        System.out.println (input);

        System.out.println ("After (dos-ified, escaped):");
        System.out.println (matcher.replaceAll (rep));
    }
}

该示例的操作步骤如下:
1. 定义替换字符串和输入字符串,编译正则表达式,创建Matcher对象,对输入字符串中的 ABC XYZ 进行替换并输出结果。
2. 重新定义替换字符串和输入字符串,编译新的正则表达式,创建Matcher对象,将输入字符串中的换行符替换为转义的DOS风格的回车换行符并输出替换前后的结果。

8. 追加方法

Matcher API中的两个追加方法 appendReplacement() appendTail() 在遍历输入字符序列并反复调用 find() 方法时非常有用。

package java.util.regex;

public final class Matcher {
    public StringBuffer appendTail (StringBuffer sb)
    public Matcher appendReplacement (StringBuffer sb, String replacement)
}

这两个方法不会返回已经完成替换的新字符串,而是将结果追加到你提供的 StringBuffer 对象中。这允许在每次找到匹配项时对替换进行决策,或者累积对多个输入字符串进行匹配的结果。使用 appendReplacement() appendTail() 方法可以完全控制搜索和替换过程。

Matcher对象会记住一个追加位置,该位置用于记录之前调用 appendReplacement() 方法已经复制出的输入字符序列的数量。当调用 appendReplacement() 方法时,会执行以下过程:
1. 从当前追加位置开始读取输入字符,并将其追加到提供的 StringBuffer 中,最后复制的字符是匹配模式的第一个字符之前的那个字符,即 start() 方法返回的索引减一对应的字符。
2. 将替换字符串追加到 StringBuffer 中,并按照前面所述的方式替换其中嵌入的捕获组引用。
3. 将追加位置更新为匹配模式之后的字符的索引,即 end() 方法返回的值。

appendReplacement() 方法只有在之前的匹配操作成功(通常是调用 find() 方法)时才能正常工作。如果上次匹配返回 false ,或者在调用 reset() 后立即调用该方法,将抛出 java.lang.IllegalStateException 异常。

由于在模式的最后一次匹配之后,输入中可能还有剩余字符,而 appendReplacement() 方法不会复制这些字符,并且在 find() 方法找不到更多匹配项后 end() 方法也不会返回有用的值,因此 appendTail() 方法用于在这种情况下复制输入的剩余部分。它只是将从当前追加位置到输入末尾的所有字符复制并追加到给定的 StringBuffer 中。

以下是一个典型的 appendReplacement() appendTail() 方法的使用场景代码:

Pattern pattern = Pattern.compile ("([Tt])hanks");
Matcher matcher = pattern.matcher ("Thanks, thanks very much");
StringBuffer sb = new StringBuffer();

while (matcher.find()) {
    if (matcher.group(1).equals ("T")) {
        matcher.appendReplacement (sb, "Thank you");
    } else {
        matcher.appendReplacement (sb, "thank you");
    }
}

matcher.appendTail (sb);

该代码的操作步骤如下:
1. 编译正则表达式模式。
2. 创建Matcher对象并关联输入字符串。
3. 创建 StringBuffer 对象。
4. 使用 find() 方法查找匹配项,根据捕获组的值选择不同的替换字符串,调用 appendReplacement() 方法进行替换。
5. 调用 appendTail() 方法复制输入的剩余部分。

以下是上述代码对 StringBuffer 应用的更改序列:
| 追加位置 | 执行操作 | 结果StringBuffer |
| ---- | ---- | ---- |
| 0 | appendReplacement (sb, “Thank you”) | Thank you |
| 6 | appendReplacement (sb, “thank you”) | Thank you, thank you |
| 14 | appendTail (sb) | Thank you, thank you very much |

通过这一系列的追加操作, StringBuffer 对象 sb 最终包含字符串 "Thank you, thank you very much" 。以下是一个完整的正则表达式追加/替换示例代码:

package com.ronsoft.books.nio.regex;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexAppend {
    // 这里可以添加具体的测试代码逻辑
}

在这个简单的例子中,由于匹配模式的第一个字母与替换字符串的第一个字母相同,因此可以使用捕获组的值。但在更复杂的情况下,输入和替换值之间可能没有重叠。使用 Matcher.find() Matcher.appendReplacement() 方法可以通过编程方式对每个替换进行调解,可能在每个点注入不同的替换值。

综上所述,Matcher类提供了丰富的功能来处理正则表达式的匹配、替换和追加操作,通过合理使用这些方法,可以高效地完成各种文本处理任务。在实际应用中,需要根据具体需求选择合适的方法,并注意处理反斜杠和捕获组等细节问题。

9. 实际应用场景分析

在实际开发中,Matcher类的各种方法可以应用于不同的场景。下面通过几个具体的场景来进一步说明其应用方式。

9.1 数据清洗

在处理大量文本数据时,常常需要对数据进行清洗,去除不符合要求的字符或格式。例如,要去除字符串中的所有空白字符,可以使用以下代码:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class DataCleaning {
    public static void main(String[] args) {
        String input = "  This is a   test   string.  ";
        Pattern pattern = Pattern.compile("\\s+");
        Matcher matcher = pattern.matcher(input);
        String result = matcher.replaceAll("");
        System.out.println(result);
    }
}

操作步骤如下:
1. 定义输入字符串 input
2. 编译正则表达式 \\s+ ,用于匹配一个或多个空白字符。
3. 创建Matcher对象。
4. 使用 replaceAll() 方法将所有匹配的空白字符替换为空字符串。
5. 输出清洗后的结果。

9.2 格式验证

在用户输入数据时,需要对输入的格式进行验证,确保数据符合要求。例如,验证用户输入的手机号码是否为11位数字:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class PhoneNumberValidation {
    public static void main(String[] args) {
        String phoneNumber = "13800138000";
        Pattern pattern = Pattern.compile("^\\d{11}$");
        Matcher matcher = pattern.matcher(phoneNumber);
        if (matcher.matches()) {
            System.out.println("Valid phone number");
        } else {
            System.out.println("Invalid phone number");
        }
    }
}

操作步骤如下:
1. 定义手机号码字符串 phoneNumber
2. 编译正则表达式 ^\\d{11}$ ,用于匹配以11位数字开头和结尾的字符串。
3. 创建Matcher对象。
4. 使用 matches() 方法验证手机号码是否符合格式要求。
5. 根据验证结果输出相应信息。

10. 性能优化建议

在使用Matcher类进行正则表达式匹配时,为了提高性能,可以考虑以下几点建议:

10.1 预编译正则表达式

在多次使用相同的正则表达式时,应该将其预编译为Pattern对象,避免重复编译带来的性能开销。例如:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class PrecompileRegex {
    private static final Pattern PATTERN = Pattern.compile("([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)");

    public static void main(String[] args) {
        String input = "test@example.com";
        Matcher matcher = PATTERN.matcher(input);
        if (matcher.find()) {
            System.out.println("Found email: " + matcher.group());
        }
    }
}
10.2 合理选择匹配方法

根据具体的需求,选择合适的匹配方法。如果只需要判断整个字符串是否匹配,使用 matches() 方法;如果只需要判断字符串开头是否匹配,使用 lookingAt() 方法;如果需要查找字符串中的所有匹配项,使用 find() 方法。

10.3 减少捕获组的使用

捕获组会增加正则表达式的复杂度和匹配的开销。如果不需要引用捕获组的内容,尽量避免使用捕获组。

11. 总结

Matcher类是Java中处理正则表达式匹配的重要工具,它提供了丰富的方法来完成各种文本处理任务,如匹配、替换、追加等。通过深入理解Matcher类的各种方法和特性,以及合理运用正则表达式,可以高效地处理各种复杂的文本数据。

在实际应用中,要注意以下几点:
- 处理反斜杠和捕获组的细节,避免出现错误。
- 预编译正则表达式,提高性能。
- 根据具体需求选择合适的匹配方法。

通过不断实践和积累经验,能够更好地掌握Matcher类的使用,为开发工作带来便利。

12. 拓展阅读与参考资源

为了进一步深入学习和掌握Java正则表达式的使用,可以参考以下资源:
- 官方文档 :Java的官方文档是学习Java技术的重要资源,可以详细了解Matcher类和Pattern类的各种方法和特性。
- 在线教程 :许多在线教程提供了丰富的正则表达式示例和讲解,如W3Schools、菜鸟教程等。
- 书籍 :《Java核心技术》等书籍对Java的正则表达式进行了详细的介绍和讲解。

通过阅读这些资源,可以加深对Java正则表达式的理解,提高使用技能。

13. 流程图总结

下面是一个使用mermaid绘制的流程图,总结了使用Matcher类进行文本处理的一般流程:

graph TD;
    A[定义正则表达式] --> B[编译正则表达式为Pattern对象];
    B --> C[创建Matcher对象并关联输入字符串];
    C --> D{选择匹配方法};
    D --> |matches()| E{整个字符串是否匹配};
    D --> |lookingAt()| F{字符串开头是否匹配};
    D --> |find()| G{是否找到匹配项};
    E --> |是| H[处理匹配结果];
    E --> |否| I[处理不匹配情况];
    F --> |是| H;
    F --> |否| I;
    G --> |是| H;
    G --> |否| I;
    H --> J[根据需求进行替换或追加操作];
    J --> K[输出结果];
    I --> K;

这个流程图展示了使用Matcher类进行文本处理的基本步骤,包括定义和编译正则表达式、创建Matcher对象、选择匹配方法、处理匹配结果以及进行替换或追加操作等。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值