Java 正则表达式基础教程 – wiki基地


Java 正则表达式基础教程:从入门到精通

正则表达式(Regular Expression,简称 Regex 或 Regexp)是一种强大的文本处理工具,用于描述、匹配、查找和操作符合特定模式的字符串。在数据验证、文本搜索与替换、日志分析、爬虫数据提取等众多领域,正则表达式都扮演着至关重要的角色。

Java 语言通过 java.util.regex 包提供了对正则表达式的全面支持。本教程将带您深入了解 Java 正则表达式的基础知识,包括核心类、基本语法、常用方法以及实际应用。无论您是初学者还是希望系统回顾,本文都将为您提供详尽的指导。

1. 什么是正则表达式?为什么在 Java 中使用它?

正则表达式本质上是一个由字符和特殊符号组成的字符串模式。它定义了一套规则,用于匹配其他字符串。想象一下,您想从一大段文本中找出所有符合邮箱格式的字符串,或者检查用户输入的密码是否同时包含大小写字母和数字。使用传统的字符串方法(如 indexOfsubstringcontains)来完成这些任务会变得异常复杂和冗长。正则表达式则提供了一种简洁、灵活且强大的方式来表达这些复杂的模式匹配需求。

在 Java 中,java.util.regex 包提供了 PatternMatcherPatternSyntaxException 三个核心类来处理正则表达式。

  • Pattern:表示一个已编译的正则表达式。正则表达式在使用前必须先被编译成 Pattern 对象。这是因为编译是一个相对耗时的操作,编译后的 Pattern 对象可以被重复用于创建多个 Matcher 对象,从而提高效率。
  • Matcher:是一个匹配器对象,它对输入字符串执行匹配操作。您可以通过 Pattern 对象创建一个 Matcher 对象,然后使用 Matcher 的各种方法(如 matchesfindreplace 等)来执行具体的匹配、查找或替换任务。
  • PatternSyntaxException:当正则表达式的语法不正确时抛出的异常。

使用 Java 的 java.util.regex 包,您可以轻松实现各种复杂的字符串处理任务,例如:

  • 验证数据格式:检查邮箱、手机号码、身份证号、URL、IP地址等是否符合特定格式。
  • 搜索特定模式:从大量文本中查找所有符合某个模式的子字符串。
  • 提取信息:从结构化或半结构化文本(如日志文件、HTML/XML 内容)中提取所需数据。
  • 替换文本:将符合某个模式的子字符串替换为其他内容。
  • 分割字符串:使用某个模式作为分隔符将字符串分割成多个部分。

总而言之,掌握 Java 正则表达式将极大地提升您处理字符串的能力。

2. 正则表达式基本语法元素

正则表达式的强大源于其丰富的语法元素。这些元素可以组合起来构建复杂的模式。下面我们将介绍一些最基础和常用的语法元素。

2.1. 匹配单个字符

  • 普通字符: 大多数字符(如字母、数字、标点符号等)匹配其自身。例如,cat 匹配字符串 “cat”。
  • 点 (.): 匹配除换行符(\n)之外的任意单个字符。如果启用 DOTALL 标志(或称为 s 模式),点号可以匹配包括换行符在内的任意字符。

    • 示例:a.b 可以匹配 “aab”, “acb”, “axb” 等,但不能匹配 “a\nb”。
    • 转义字符 (\): 用于转义具有特殊含义的字符,使其匹配字面值。例如,要匹配字面上的点号 .,需要使用 \.。其他需要转义的特殊字符包括 ^, $, *, +, ?, (, ), [, {, |, \

    • 示例:a\.b 只匹配字符串 “a.b”。c\\d 匹配 “c\d”。

2.2. 字符类 ([…])

字符类允许您匹配指定集合中的任意一个字符。

  • [abc]: 匹配方括号中列出的任意一个字符(a、b 或 c)。
  • [a-z]: 匹配从 a 到 z 的任意一个小写字母(这是一个字符范围)。
  • [A-Z]: 匹配从 A 到 Z 的任意一个大写字母。
  • [0-9]: 匹配从 0 到 9 的任意一个数字。
  • [a-zA-Z0-9]: 匹配任意一个字母或数字。
  • [aeiou]: 匹配任意一个元音字母。

否定字符类 ([^…])

在字符类内部,如果第一个字符是 ^,则表示匹配不在该集合中的任意一个字符。

  • [^abc]: 匹配除 a、b、c 之外的任意一个字符。
  • [^0-9]: 匹配除数字之外的任意一个字符(与 \D 相同)。

预定义字符类

Java 正则表达式提供了一些方便的预定义字符类:

  • \d: 匹配任意一个数字字符,等价于 [0-9]
  • \D: 匹配任意一个非数字字符,等价于 [^0-9]
  • \w: 匹配任意一个“单词”字符,包括字母、数字和下划线 _,等价于 [a-zA-Z0-9_]
  • \W: 匹配任意一个非“单词”字符,等价于 [^a-zA-Z0-9_]
  • \s: 匹配任意一个空白字符,包括空格、制表符 (\t)、换页符 (\f)、回车符 (\r) 和换行符 (\n)。
  • \S: 匹配任意一个非空白字符。

    • 示例:\d{3}-\d{4} 可以匹配 “123-4567″。\w+ 可以匹配一个单词。

2.3. 量词

量词指定了某个字符或字符组应该出现多少次。

  • ?: 匹配前面的元素零次或一次。使其变为可选的。
    • 示例:colou?r 可以匹配 “color” 或 “colour”。
  • *: 匹配前面的元素零次或多次。
    • 示例:a* 可以匹配 “”, “a”, “aa”, “aaa” 等。go*gle 可以匹配 “ggle”, “gogle”, “googlegle” 等。
  • +: 匹配前面的元素一次或多次。
    • 示例:a+ 可以匹配 “a”, “aa”, “aaa” 等,但不能匹配 “”。go+gle 可以匹配 “gogle”, “googlegle” 等,但不能匹配 “ggle”。

精确量词 ({})

花括号 {} 允许您指定匹配次数的精确范围。

  • {n}: 匹配前面的元素恰好 n 次。
    • 示例:\d{3} 匹配恰好三个数字。
  • {n,}: 匹配前面的元素至少 n 次。
    • 示例:\w{2,} 匹配至少两个单词字符。
  • {n,m}: 匹配前面的元素至少 n 次,但不超过 m 次。
    • 示例:.{5,10} 匹配任意字符(除换行符)至少 5 个,最多 10 个。

量词的贪婪、勉强与独占模式

默认情况下,量词是贪婪(Greedy)的。这意味着它们会尽可能多地匹配字符,直到模式的其余部分无法匹配为止。

您可以通过在量词后加上 ? 使其变为勉强(Reluctant)或非贪婪模式。勉强量词会尽可能少地匹配字符。

还有一种独占(Possessive)量词,通过在量词后加上 + 获得。独占量词会尽可能多地匹配字符,但与贪婪量词不同的是,它们永不回溯。这在某些情况下可以提高性能,但也可能导致无法匹配原本能匹配的字符串。

一般来说,贪婪量词是最常用的,勉强量词用于需要最小匹配的情况(如解析HTML/XML标签),而独占量词则在确定不需要回溯且追求性能时使用。

2.4. 位置匹配 (锚点)

位置匹配符(或称锚点)不匹配具体的字符,而是匹配一个位置。

  • ^: 匹配输入字符串的开头。如果启用 MULTILINE 标志(或称 m 模式),则匹配每行的开头。
    • 示例:^abc 只匹配以 “abc” 开头的字符串,如 “abcdef”。
  • $: 匹配输入字符串的结尾。如果启用 MULTILINE 标志,则匹配每行的结尾。
    • 示例:xyz$ 只匹配以 “xyz” 结尾的字符串,如 “uvwxyz”。
  • \b: 匹配一个词的边界。词边界是指一个单词字符 (\w) 和一个非单词字符 (\W) 之间的位置,或者是字符串的开头/结尾与一个单词字符之间的位置。
    • 示例:\bcat\b 可以匹配字符串 “the cat sat” 中的 “cat”,但不能匹配 “catalog” 或 “concatenate” 中的 “cat”。
  • \B: 匹配一个非词边界的位置。
    • 示例:\Bcat\B 可以匹配 “conccatenate” 中的 “cat”,但不能匹配 “the cat sat”。

2.5. 分组与捕获

圆括号 () 用于将多个字符组合成一个单元,可以对其应用量词,或者将其作为一个捕获组来提取匹配的子字符串。

  • (): 创建一个捕获组。匹配的内容会被”捕获”供后续使用。捕获组从 1 开始编号(组 0 始终代表整个匹配的文本)。
    • 示例:(\d{3})-(\d{4}) 可以匹配 “123-4567″。第一个捕获组 ((\d{3})) 捕获 “123”,第二个捕获组 ((\d{4})) 捕获 “4567”。
  • (?:...): 创建一个非捕获组。它用于分组以便应用量词或进行逻辑分组,但匹配的内容不会被捕获。这有助于提高性能,特别是当您只需要分组而不需要提取匹配内容时。
    • 示例:(?:abc)+ 匹配一个或多个连续的 “abc” 序列,如 “abc”, “abcabc” 等,但不捕获 “abc” 子串。

2.6. 选择 (或)

管道符 | 用于表示“或”的关系,匹配其左边或右边的模式。

  • cat|dog: 匹配 “cat” 或 “dog”。
  • (cat|dog) food: 匹配 “cat food” 或 “dog food”。注意分组的使用,以确保 | 只应用于 “cat” 和 “dog”。

2.7. 反向引用

反向引用 (\n,其中 n 是一个数字) 引用前面第 n 个捕获组匹配到的内容。

  • 示例:(\w)\1 匹配任意两个连续相同的单词字符,如 “aa”, “bb”, “cc” 等。(\w+)\s+\1 匹配一个单词,后跟一个或多个空白字符,再后跟与第一个单词完全相同的单词,如 “java java”。

3. Java 中 PatternMatcher 的使用

了解了基本语法后,我们来看看如何在 Java 代码中使用它们。

3.1. 编译正则表达式

首先,您需要使用 Pattern.compile() 方法编译正则表达式字符串,生成一个 Pattern 对象。

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

// 要匹配的正则表达式
String regex = “a.*b”;

// 编译正则表达式
Pattern pattern = Pattern.compile(regex);
“`

编译过程会检查正则表达式的语法是否正确。如果语法错误,会抛出 PatternSyntaxException

3.2. 创建匹配器

接下来,使用 Pattern 对象的 matcher() 方法,传入要进行匹配的输入字符串,生成一个 Matcher 对象。

“`java
// 要进行匹配的输入字符串
String input = “acccb”;

// 创建匹配器对象
Matcher matcher = pattern.matcher(input);
“`

Matcher 对象包含了匹配操作所需的所有信息:编译后的模式、输入字符串以及匹配状态。

3.3. 执行匹配操作

Matcher 类提供了多种方法来执行不同类型的匹配操作:

  • boolean matches(): 尝试将整个输入序列与模式进行匹配。只有当整个输入字符串都符合正则表达式时才返回 true
    “`java
    String input1 = “acccb”;
    String input2 = “acccbx”;
    String input3 = “xacccb”;

    Pattern p = Pattern.compile(“a.*b”);
    Matcher m1 = p.matcher(input1);
    Matcher m2 = p.matcher(input2);
    Matcher m3 = p.matcher(input3);

    System.out.println(input1 + ” matches: ” + m1.matches()); // true
    System.out.println(input2 + ” matches: ” + m2.matches()); // false (因为后面多了一个x)
    System.out.println(input3 + ” matches: ” + m3.matches()); // false (因为前面多了一个x)
    * `boolean find()`: 尝试查找输入序列中与模式匹配的**下一个**子序列。这个方法可以重复调用,每次调用都会从上次匹配结束的位置继续查找。java
    String input = “This is a test. The first match is test, the second is Test.”;
    Pattern p = Pattern.compile(“[Tt]est”); // 匹配 test 或 Test
    Matcher m = p.matcher(input);

    System.out.println(“Finding matches:”);
    while (m.find()) {
    // 打印找到的匹配项和它的起始/结束位置
    System.out.println(“Found \”” + m.group() + “\” at positions ” +
    m.start() + “-” + m.end());
    }
    // Output:
    // Finding matches:
    // Found “test” at positions 10-14
    // Found “Test” at positions 40-44
    * `boolean lookingAt()`: 尝试将输入序列从**开头**与模式进行匹配。与 `matches()` 不同,`lookingAt()` 即使模式只匹配了输入字符串的一部分开头也返回 `true`。java
    String input1 = “HelloWorld”;
    String input2 = “Hello Java”;
    String input3 = “Java World”;

    Pattern p = Pattern.compile(“Hello”);
    Matcher m1 = p.matcher(input1);
    Matcher m2 = p.matcher(input2);
    Matcher m3 = p.matcher(input3);

    System.out.println(input1 + ” lookingAt: ” + m1.lookingAt()); // true
    System.out.println(input2 + ” lookingAt: ” + m2.lookingAt()); // true
    System.out.println(input3 + ” lookingAt: ” + m3.lookingAt()); // false
    “`

3.4. 获取匹配结果

find()matches()lookingAt() 返回 true 时,表示找到了匹配项。您可以使用 Matcher 的方法来获取匹配到的子字符串或子组的信息:

  • String group()String group(0): 返回最近一次匹配到的整个子字符串。
  • String group(int group): 返回最近一次匹配中指定捕获组捕获的子字符串。组号从 1 开始。
  • int groupCount(): 返回此模式中的捕获组数量(不包括组 0)。
  • int start(): 返回最近一次匹配到的整个子字符串的起始索引(包含)。
  • int start(int group): 返回最近一次匹配中指定捕获组的起始索引。
  • int end(): 返回最近一次匹配到的整个子字符串的结束索引(不包含)。
  • int end(int group): 返回最近一次匹配中指定捕获组的结束索引。

“`java
String input = “Order number is 12345, customer ID is 67890.”;
// 匹配 “number is ” 后面跟着5个数字,并捕获这5个数字
Pattern p = Pattern.compile(“number is (\d{5})”);
Matcher m = p.matcher(input);

if (m.find()) {
System.out.println(“Entire match: ” + m.group(0)); // Order number is 12345
System.out.println(“Order number: ” + m.group(1)); // 12345
System.out.println(“Start index of match: ” + m.start()); // 13
System.out.println(“End index of match: ” + m.end()); // 29
System.out.println(“Number of groups: ” + m.groupCount()); // 1
System.out.println(“Start index of group 1: ” + m.start(1)); // 24
System.out.println(“End index of group 1: ” + m.end(1)); // 29
}
“`

3.5. 替换文本

Matcher 类提供了方便的替换方法:

  • String replaceAll(String replacement): 将输入字符串中所有匹配到的子字符串替换为指定的替换字符串。替换字符串可以使用 $n 来引用捕获组的内容($1 引用第一个捕获组,$2 引用第二个,等等)。
  • String replaceFirst(String replacement): 将输入字符串中第一个匹配到的子字符串替换为指定的替换字符串。

“`java
String input = “Hello World, Hello Java!”;
Pattern p = Pattern.compile(“Hello”);

String output1 = p.matcher(input).replaceAll(“Hi”);
System.out.println(output1); // Hi World, Hi Java!

String output2 = p.matcher(input).replaceFirst(“Greetings”);
System.out.println(output2); // Greetings World, Hello Java!

// 使用捕获组进行替换
String input3 = “Name: John Doe, Phone: 123-456-7890”;
// 找到电话号码,并将其格式化为 (XXX) XXX-XXXX
Pattern p3 = Pattern.compile(“(\d{3})-(\d{3})-(\d{4})”);
String output3 = p3.matcher(input3).replaceAll(“($1) $2-$3”);
System.out.println(output3); // Name: John Doe, Phone: (123) 456-7890
“`

对于更复杂的替换逻辑(例如,替换内容依赖于匹配到的具体文本,或者需要对匹配到的文本进行一些计算后再替换),可以使用 appendReplacement()appendTail() 方法:

“`java
String input = “Price: $10, Discount: $2”;
Pattern p = Pattern.compile(“\$(\d+)”); // 匹配 $ 后面的数字并捕获
Matcher m = p.matcher(input);
StringBuffer sb = new StringBuffer(); // 用于构建新的字符串

while (m.find()) {
int price = Integer.parseInt(m.group(1)); // 获取捕获的数字并转换为整数
int discountedPrice = price – 1; // 简单的计算
// 将匹配之前的内容和计算后的新内容添加到 sb
// m.appendReplacement(sb, “¥” + discountedPrice); // 替换为 ¥ + 折扣价
// 注意:替换字符串中的 $ 或 \ 需要进行转义,使用 Matcher.quoteReplacement() 是一种安全的方法
m.appendReplacement(sb, Matcher.quoteReplacement(“¥” + discountedPrice)); // 安全替换
}
// 将输入字符串中最后一次匹配之后的部分添加到 sb
m.appendTail(sb);

System.out.println(sb.toString()); // Price: ¥9, Discount: ¥1
``appendReplacement(StringBuffer sb, String replacement)方法的作用是:
1. 将当前匹配之前(即从上次
appendTailappendReplacement结束位置到当前匹配开始位置)的输入字符串追加到StringBuffer中。
2. 将
replacement字符串(其中可以使用$n引用捕获组)追加到StringBuffer` 中。
3. 更新匹配器内部的索引,指向当前匹配结束之后的位置。

appendTail(StringBuffer sb) 方法的作用是:将输入字符串中最后一次匹配结束之后的所有剩余部分追加到 StringBuffer 中。

这组方法通常在 while(m.find()) 循环中使用,提供了对替换过程更精细的控制。

3.6. 分割字符串

Java 的 String 类有一个方便的 split() 方法,它可以使用正则表达式作为分隔符。

“`java
String input = “apple,banana;orange:grape”;
// 使用逗号、分号或冒号作为分隔符
String[] fruits = input.split(“[,;:]”);

for (String fruit : fruits) {
System.out.println(fruit);
}
// Output:
// apple
// banana
// orange
// grape
“`

4. 正则表达式标志 (Flags)

在编译正则表达式时,可以指定一个或多个标志来改变匹配的行为。这些标志是 Pattern 类中的静态常量。您可以使用按位或 (|) 操作符来组合多个标志。

Pattern.compile(regex, flags)

常用标志:

  • Pattern.CASE_INSENSITIVE (或 Pattern.I): 启用不区分大小写的匹配。
  • Pattern.MULTILINE (或 Pattern.M): 在多行模式下,^$ 不仅匹配整个输入字符串的开始和结束,还匹配每一行的开始和结束(紧跟或紧前于换行符 \n 或回车符 \r 的位置)。
  • Pattern.DOTALL (或 Pattern.S): 在 dotall 模式下,点号 . 匹配包括行终止符(换行符 \n)在内的任意字符。默认情况下,. 不匹配行终止符。
  • Pattern.UNICODE_CHARACTER_CLASS (或 Pattern.U): 启用 Unicode 版本的预定义字符类(\d, \w, \s 等)和 POSIX 字符类(\p{Lower}, \p{Upper} 等)。这在处理非 ASCII 字符时非常有用。
  • Pattern.COMMENTS (或 Pattern.X): 在模式中忽略空白和 # 后面的注释,直到行尾。这有助于编写更易读的复杂正则表达式。

“`java
String input = “Hello\nworld”;
// 默认模式,. 不匹配 \n
Pattern p1 = Pattern.compile(“.“);
Matcher m1 = p1.matcher(input);
if (m1.find()) System.out.println(“Default .
: ” + m1.group()); // Hello

// Dotall 模式,. 匹配 \n
Pattern p2 = Pattern.compile(“.“, Pattern.DOTALL);
Matcher m2 = p2.matcher(input);
if (m2.find()) System.out.println(“DOTALL .
: ” + m2.group()); // Hello\nworld

String input3 = “Line 1\nLine 2”;
// 多行模式,^ 匹配每行的开头
Pattern p3 = Pattern.compile(“^Line”, Pattern.MULTILINE);
Matcher m3 = p3.matcher(input3);
while (m3.find()) System.out.println(“MULTILINE ^Line: ” + m3.group());
// Output:
// MULTILINE ^Line: Line
// MULTILINE ^Line: Line
“`

标志也可以嵌入到正则表达式字符串的开头,使用 (?imsx) 这样的语法。例如,(?i)hello 表示不区分大小写匹配 “hello”。(?s).*(?-s).* 表示前半部分启用 dotall 模式,后半部分禁用。

5. Java 字符串中的反斜杠问题

这是 Java 中使用正则表达式最常见的困惑点之一。正则表达式本身使用反斜杠 \ 作为转义字符(例如,\. 匹配字面上的点,\d 匹配数字)。然而,Java 字符串字面量也使用反斜杠 \ 作为转义字符(例如,"\n" 表示换行,"\\" 表示字面上的反斜杠)。

因此,当您在 Java 字符串中表示一个正则表达式时,任何需要在正则表达式中转义的特殊字符,如果它是通过 \ 进行转义的,那么这个 \ 本身在 Java 字符串中也需要被转义。也就是说,Java 字符串中的 \\ 在正则表达式引擎看来才是一个字面意义上的 \

  • 要匹配字面上的点 .,正则表达式是 \.。在 Java 字符串中表示为 "\\."
  • 要匹配字面上的反斜杠 \,正则表达式是 \\。在 Java 字符串中表示为 "\\\\"
  • 要匹配一个数字 \d,正则表达式是 \d。在 Java 字符串中表示为 "\\d"

示例:

“`java
// 匹配格式为 xxx.xxx.xxx.xxx 的 IP 地址 (简化版,不验证数字范围)
String ipRegex = “\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}”;
// 在 Java 字符串中,. 变成了 \.,\d 变成了 \d

String input = “My IP is 192.168.1.100.”;
Pattern ipPattern = Pattern.compile(ipRegex);
Matcher ipMatcher = ipPattern.matcher(input);

if (ipMatcher.find()) {
System.out.println(“Found IP: ” + ipMatcher.group()); // Found IP: 192.168.1.100
}
“`

记住这个“双倍反斜杠”规则是使用 Java 正则表达式的关键。

6. 实际应用示例

示例 1:验证邮箱格式 (简化版)

一个简单的邮箱格式验证:[email protected]

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

public class EmailValidator {
// 一个简化版的邮箱正则表达式
// \w+ 匹配用户名部分 (字母、数字、下划线,至少一个)
// @ 匹配 @ 符号
// \w+ 匹配域名部分 (字母、数字、下划线,至少一个)
// . 匹配点号
// \w+ 匹配顶级域名 (字母、数字、下划线,至少一个)
private static final String EMAIL_REGEX = “^\w+@\w+\.\w+$”; // 注意双反斜杠

private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);

public static boolean isValidEmail(String email) {
    if (email == null) {
        return false;
    }
    // 使用 matches() 确保整个字符串都符合模式
    return EMAIL_PATTERN.matcher(email).matches();
}

public static void main(String[] args) {
    String email1 = "[email protected]";
    String email2 = "[email protected]"; // 更复杂的例子,上面的regex不匹配
    String email3 = "[email protected]"; // 无效
    String email4 = "test@example"; // 无效

    System.out.println(email1 + " is valid: " + isValidEmail(email1)); // true
    System.out.println(email2 + " is valid (by simple regex): " + isValidEmail(email2)); // false
    System.out.println(email3 + " is valid: " + isValidEmail(email3)); // false
    System.out.println(email4 + " is valid: " + isValidEmail(email4)); // false

    // 更符合实际情况的邮箱正则会复杂得多,例如:
    String complexEmailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
    System.out.println("\nUsing complex regex:");
    System.out.println(email1 + " is valid: " + Pattern.matches(complexEmailRegex, email1)); // true
    System.out.println(email2 + " is valid: " + Pattern.matches(complexEmailRegex, email2)); // true
    System.out.println(email3 + " is valid: " + Pattern.matches(complexEmailRegex, email3)); // false
    System.out.println(email4 + " is valid: " + Pattern.matches(complexEmailRegex, email4)); // false
}

}
``
**注意:** 实际生产环境中的邮箱验证需要使用更完善的正则表达式,甚至可能需要结合其他验证方式,因为完整的邮箱规范非常复杂。上面的例子是为了演示基本用法。
Pattern.matches(regex, input)Pattern.compile(regex).matcher(input).matches()` 的一个简写形式,适用于只需要一次性匹配整个字符串的场景。

示例 2:从文本中提取所有数字

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

public class ExtractNumbers {
public static void main(String[] args) {
String text = “Invoice amount is $123.45, discount is $10. Tax rate is 0.08.”;
// 匹配一个或多个数字,可能包含一个小数点后跟一个或多个数字
String regex = “\d+(\.\d+)?”;

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

    System.out.println("Extracted numbers:");
    while (matcher.find()) {
        System.out.println(matcher.group()); // 提取整个匹配到的数字字符串
    }
    // Output:
    // Extracted numbers:
    // 123.45
    // 10
    // 0.08
}

}
“`

示例 3:使用 split() 分割 CSV 行 (简化)

假设有一个简单的 CSV 行,字段之间用逗号分隔,但字段内部可能包含空格。

“`java
public class SplitCsv {
public static void main(String[] args) {
String csvLine = “Smith, John, 42, New York”;
// 使用逗号后跟零个或多个空白字符作为分隔符
String[] fields = csvLine.split(“,\s*”);

    System.out.println("CSV fields:");
    for (String field : fields) {
        System.out.println("-" + field.trim()); // 使用 trim() 移除字段两端的空白
    }
    // Output:
    // CSV fields:
    // -Smith
    // -John
    // -42
    // -New York
}

}
“`

7. 常见陷阱与最佳实践

  • 性能: 编译正则表达式是一个相对耗时的操作。如果同一个正则表达式需要多次使用,应该将其编译成 Pattern 对象并复用,而不是每次都调用静态方法 Pattern.matches()String.split()(这些方法内部会重新编译模式)。将 Pattern 对象存储为类的常量通常是一个好的做法。
  • 可读性: 复杂的正则表达式可能非常难以阅读和理解。考虑使用 Pattern.COMMENTS 标志 (Pattern.X),在模式中加入注释和空白,或者将大型复杂的模式分解成更小的、命名清晰的部分(尽管这在 Java Regex 中实现起来不像一些其他语言那么直接)。对于特别复杂的解析任务,可能正则表达式不是最佳工具,考虑使用专用的解析库。
  • 贪婪与勉强: 理解量词的贪婪性是避免意外匹配的关键。当您发现匹配结果比预期要长时,很可能是贪婪量词导致的,尝试使用勉强量词 (?)。
  • 反斜杠逃逸: 再次强调,在 Java 字符串中表示正则表达式时,务必正确处理反斜杠。所有正则表达式中的 \ 都必须写成 Java 字符串中的 \\
  • 过度使用: 正则表达式功能强大,但并非万能。对于简单的字符串检查(如是否包含某个子串、是否以某个前缀或后缀开始/结束),Java 的 String 类自带的方法(containsstartsWithendsWithindexOf)通常更直观且效率更高。
  • 测试: 编写和调试正则表达式是出了名的困难。使用在线的正则表达式测试工具(如 regex101.com, regexr.com)可以极大地帮助您构建和理解模式。在 Java 代码中,使用小的、明确的测试用例来验证您的正则表达式行为是否符合预期。
  • 安全性: 如果正则表达式的模式是来自用户输入或不可信源,需要警惕“正则表达式拒绝服务”(ReDoS)攻击。某些结构(如嵌套量词,如 (a+)+)在匹配特定输入时会导致指数级的回溯时间,从而使程序崩溃或无响应。尽量避免使用易受 ReDoS 攻击的模式,或者对输入长度进行限制。

8. 总结

正则表达式是处理文本模式匹配和操作的利器。Java 的 java.util.regex 包提供了强大且灵活的 API 来利用这一能力。通过理解 PatternMatcher 这两个核心类,掌握基本的正则表达式语法元素(字符匹配、字符类、量词、锚点、分组、选择),并注意 Java 字符串中反斜杠的特殊处理,您就可以开始在您的 Java 项目中有效地使用正则表达式了。

记住,熟练掌握正则表达式需要时间和实践。从简单的模式开始,逐步构建和测试更复杂的模式。结合在线工具和单元测试,您将能够写出准确、高效的正则表达式,解决各种字符串处理难题。

希望这篇详细教程能帮助您建立坚实的 Java 正则表达式基础!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部