内容


第 19 单元:正则表达式

描述和搜索 Java 代码中的字符串模式

Comments

开始之前

本单元是 “Java 编程入门” 学习路径的一部分。尽管各单元中讨论的概念具有独立性,但实践组件是在您学习各单元的过程中逐步建立起来的,推荐您在继续学习之前复习 前提条件、设置和单元细节

单元目标

  • 了解有哪 3 种核心正则表达式类和它们的用途
  • 熟悉正则表达式模式语法
  • 能够执行简单和更复杂的搜索和替换
  • 知道如何通过捕获分组引用匹配值

Regular Expressions API

正则表达式 基本来讲是一种模式,用于描述一组具有该共同模式的字符串。本单元指导您开始在 Java 程序中使用正则表达式。

这里有一组具有一些共性的字符串:

  • A string
  • A longer string
  • A much longer string

请注意,这些字符串中的每个字符串都以 A 开头,以 string 结尾。Java Regular Expressions API 可帮助您提取出这些元素,查看它们之间的模式,然后使用您收集的信息执行有趣的事情。

Regular Expressions API 有 3 个您几乎总是在使用的核心类:

  • Pattern 描述了一种字符串模式。
  • Matcher 测试一个字符串,查看它是否与该模式匹配。
  • PatternSyntaxException 告诉您,您尝试定义的模式的某个方面无法被接受。

您很快就会开始使用一个使用这些类的简单的正则表达式模式。但是首先,请看一看正则表达式模式语法。

正则表达式模式语法

正则表达式模式 描述该表达式尝试在输入字符串中查找的字符串的结构。缺乏经验的人可能觉得该模式语法很奇怪,但一旦理解它,就会发现它很容易解释。表 1 列出了模式字符串中使用的一些最常见的正则表达式结构:

表 1. 常见正则表达式结构
正则表达式结构符合匹配条件的内容
.任何字符
?前面的零 (0) 或一 (1) 个字符或数字
*前面的零 (0) 或更多个字符或数字
+前面的一 (1) 或更多个字符或数字
[]一个字符或数字范围
^后面的条件的否定条件(即 “非后面的条件”)
\d任何数字(也可表示为 [0-9]
\D任何非数字(也可表示为 [^0-9]
\s任何空格字符(也可表示为 [\n\t\f\r]
\S任何非空格字符(也可表示为 [^\n\t\f\r]
\w任何单词字符(也可表示为 [a-zA-Z_0-9]
\W任何非单词字符(也可表示为 [^\w]

前几种结构称为量化器,因为它们对它们之前的内容进行了量化。\d 等结构是预定义的字符类。任何在一种模式中没有特殊含义的字符都是字面常量,将会自行匹配。

模式匹配

掌握 表 1 中的模式语法后,就可以理解清单 1 中的简单示例,这个示例使用了 Java Regular Expressions API 中的类。

清单 1. 使用正则表达式进行模式匹配
  Pattern pattern = Pattern.compile("[Aa].*string");
  Matcher matcher = pattern.matcher("A string");
  boolean didMatch = matcher.matches();
  Logger.getAnonymousLogger().info (didMatch);
  int patternStartIndex = matcher.start();
  Logger.getAnonymousLogger().info (patternStartIndex);
  int patternEndIndex = matcher.end();
  Logger.getAnonymousLogger().info (patternEndIndex);

首先,清单 1 通过调用 compile()Pattern 上的一个静态方法)创建了一个 Pattern 类,使用一个字符串字面常量表示想要匹配的模式。该字面常量使用正则表达式模式语法。在本例中,该模式的中文翻译为:

找到一个具有以下形式的字符串:Aa 后跟零或多个字符,后跟 string

匹配方法

接下来,清单 1Pattern 上调用 matcher()。该调用创建一个 Matcher 实例。然后 Matcher 会搜索您传入的字符串,寻找与您在创建 Pattern 时使用的模式字符串的匹配结果。

每个 Java 语言字符串都是一个带索引的字符集合,索引从 0 开始,到字符串长度减 1 结束。Matcher 从 0 开始解析该字符串,寻找与它匹配的结果。完成处理后,Matcher 包含有关在输入字符串中找到(或未找到)匹配结果的信息。可以在 Matcher 上调用各种方法来访问该信息:

  • matches() 告诉您整个输入序列是否与该模式准确匹配。
  • start() 告诉您匹配的字符串在输入字符串中的起点的索引值。
  • end() 告诉您匹配的字符串在输入字符串中的起点的索引值加 1 的结果。

清单 1 找到了一个从 0 开始,到 7 结束的匹配结果。因此,对 matches() 的调用返回 true,对 start() 的调用返回 0,对 end() 的调用返回 8

lookingAt() 与 matches() 的对比分析

如果您的字符串中包含的元素比您搜索的模式中字符数要多,可以使用 lookingAt() 代替 matches()lookingAt() 方法搜索与一种指定模式匹配的子字符串。例如,考虑下面这个字符串:

Here is a string with more than just the pattern.

如果在此字符串中搜索 a.*string,而且您使用了 lookingAt(),则会获得一个匹配结果。但是,如果您使用 matches(),则会返回 false,因为字符串中包含比模式中更多的内容。

正则表达式中的复杂模式

简单的搜索可使用正则表达式类轻松完成,但您也可以使用 Regular Expressions API 执行高度复杂的操作。

Wiki 几乎完全基于正则表达式。Wiki 内容基于来自用户的字符串输入,该输入被使用正则表达式解析和格式化。任何用户都可以通过输入一个 wiki 词组在 wiki 中创建另一个主题的链接,这个词组通常是一系列串联的单词,每个单词以一个大写字母开头,类似这样:

MyWikiWord

假设一个用户输入下面这个字符串:

Here is a WikiWord followed by AnotherWikiWord, then YetAnotherWikiWord.

您可以使用正则表达式模式在这个字符串中搜索 wiki 单词,类似这样:

[A-Z][a-z]*([A-Z][a-z]*)+

这是搜索 wiki 单词的代码:

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
  Logger.getAnonymousLogger().info("Found this wiki word: " + matcher.group());
}

运行此代码,可以在控制台中看到 3 个 wiki 单词。

替换字符串

搜索匹配值很有用,但也可以在找到匹配字符串后处理它们。可以将匹配的字符串替换为其他字符串,就像在文字处理程序中搜索一些文本并将其替换为其他文本一样。Matcher 拥有两个替换字符串元素的方法:

  • replaceAll() 将所有匹配值替换为一个指定的字符串。
  • replaceFirst() 仅将第一个匹配值替换为一个指定的字符串。

使用 Matcherreplace 方法很简单:

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("replacement");
Logger.getAnonymousLogger().info("After: " + result);

跟之前一样,此代码寻找 wiki 单词。当 Matcher 找到一个匹配值时,它将该 wiki 单词文本替换为它的替换值。运行该代码时,可以在控制台上看到以下消息:

Before: Here is WikiWord followed by AnotherWikiWord, then SomeWikiWord.
After: Here is replacement followed by replacement, then replacement.

如果使用了 replaceFirst(),则会看到以下消息:

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
After: Here is a replacement followed by AnotherWikiWord, then SomeWikiWord.

匹配和操作分组

搜索一个正则表达式模式的匹配结果时,可以获得有关找到的结果的信息。您已在 Matcherstart()end() 方法上看到过该功能。但也可以通过捕获分组 来引用匹配值。

在每种模式中,通常通过会将模式的各部分放在圆括号中来创建分组。分组从左向右编号,从 1 开始编号(分组 0 表示完整的匹配结果)。清单 2 中的代码将每个 wiki 单词替换为一个 “包含” 该单词的字符串:

清单 2. 匹配分组
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("blah$0blah");
Logger.getAnonymousLogger().info("After: " + result);

运行清单 2 的代码,可以获得以下控制台输出:

  Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
  After: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,then blahSomeWikiWordblah.

清单 2 通过在替换字符串中包含 $0 来引用完整匹配结果。$ int 格式的替换字符串的任何部分引用该整数所标识的分组(所以 $1 引用分组 1,依此类推)。换句话说,$0 等效于 matcher.group(0);

可以使用其他一些方法达到同样的替换目的。无需调用 replaceAll(),您可以这样做:

StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
  matcher.appendReplacement(buffer, "blah$0blah");
}
matcher.appendTail(buffer);
Logger.getAnonymousLogger().info("After: " + buffer.toString());

您会获得同样的结果:

  Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
  After: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,then blahSomeWikiWordblah.

测试您的理解情况

  1. 哪句话最恰当地描述了 ? 量化器?
    1. 匹配 0 或多次
    2. 匹配 1 或多次
    3. 匹配出现的结果并将匹配值附加到输出分组
    4. 匹配一次或完全不匹配
    5. 上述选项都不是
  2. 哪句话最恰当地描述了 + 量化器?
    1. 匹配 0 或多次
    2. 匹配 1 或多次
    3. 匹配出现的结果并将匹配值附加到输出分组
    4. 匹配一次或完全不匹配
    5. 上述选项都不是
  3. 哪句话最恰当地描述了 * 量化器?
    1. 匹配 0 或多次
    2. 匹配 1 或多次
    3. 匹配出现的结果并将匹配值附加到输出分组
    4. 匹配一次或完全不匹配
    5. 上述选项都不是
  4. 对或错:Matcher 类用于描述 Pattern 类的输入字符串。
  5. 哪个答案最恰当地描述了以下正则表达式字符串的应用:[A-Z]?\d
    1. 匹配从 A 到 Z 的任何字符 1 或多次,后跟一个可选的数字。
    2. 匹配从 A 到 Z 的任何字符 0 或多次,后跟一个可选的数字。
    3. 匹配从 A 到 Z 的任何字符 1 或多次,后跟一个数字。
    4. 匹配从 A 到 Z 的任何字符 0 或多次,后跟一个数字。
    5. 上述选项都不是。
  6. 检查以下代码,选择最恰当地描述了匹配结果(按顺序)的响应。

    @Test
    public void testFindMatches() {
       
       String input = "Do you run? Ran? No, bro, run! Bro, I ran and run.";
       
       String regex = "r[au]n";
       
       Pattern pattern = Pattern.compile(regex);
       Matcher matcher = pattern.matcher(input);
       
       int matchCount = 0;
       StringBuilder matchHolder = new StringBuilder();
       while (matcher.find()) {
       if (matchCount > 0) 
       matchHolder.append(',');
       matchHolder.append(matcher.group());
       matchCount++;
       }
       
       System.out.println("Matches: " + matchHolder.toString());
       
    }
    1. run,Ran,run,ran,run
    2. run,run,run,run
    3. run,ran,run,Ran,run
    4. run,run,ran,run
    5. 指定的模式与输入字符串的所有部分都不匹配。
  7. 编程练习,第 1 部分:创建一个新类(将它命名为 MyRegExMatcher),编写一个名为 matchesAll 的方法,该方法采用了两个参数(一个名为 regex 的字符串和一个名为 input 的字符串),并返回一个 boolean。现在编写返回 false 的方法。
  8. 编程练习,第 2 部分:创建一个 JUnit 测试案例来调用您为问题 7 编写的方法。您的 JUnit 测试将使用您能想到的与此输入字符串匹配的最简单正则表达式来调用该方法:The quick brown fox jumped over the lazy dogs

    备注:该正则表达式可能仅包含量化器,而且必须仅包含字母 l 和 x(不含其他字母)。

  9. 编程练习,第 3 部分:实现问题 7 中的方法,使您的测试案例通过(如果测试案例未通过,那么您的正则表达式可能是错的)。如果整个输入字符串与正则表达式模式匹配,则返回 true,否则返回 false。提示:使用 Pattern 类和 Matcher 类,就像在 清单 1 中看到的一样。

核对您的答案。

进一步探索

Java Regular Expressions API

Java - 正则表达式

正则表达式测试页面

Java 教程:正则表达式

上一单元:嵌套类下一单元:泛型


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1039317
ArticleTitle=第 19 单元:正则表达式
publish-date=11012016