Merlin的魔力:用新的正则表达式库解析字符序列

1/5/2008来源:Java教程人气:6305


虽然 java 语言以前的版本支持模式匹配,但 StreamTokenizer 和 StringTokenizer 类却很少涉及用模式能够实现的功能。Java 1.4(以及现在的 1.4.1)发行版在 java.util.regex 包中加入了对使用正则表达式的模式匹配的支持。在 Merlin 的魔力系列的这篇文章中,John Zukowski 向您展示了如何用新的正则表达式库解析字符序列,从而使搜索模式的功能更加强大。
解析模式的文本字符串
正则表达式是根据文本匹配模式的方法 ? 类似于编译器生成类文件的工作原理。编译器在源代码中查找各种模式以便将源代码表达式转换为字节码。通过识别这些源代码模式,编译器能够只将有效的源代码表示转换为已编译的类文件。

什么是模式?
在正则表达式的上下文中,模式是字符序列的文本表示法。例如,假如您想知道一个字符序列中是否存在 car 这个词,您会使用模式 car,因为这是精确地表示该字符串的方法。对于更复杂的模式,您可以使用非凡字符作为占位符。假如您不是要搜索 car,而是想搜索以字母 c 开头并以字母 r 结尾的任何文本字符串,您会使用 c*r 模式,其中 * 代表第一个 r 前的任意多个字符。c*r 模式将匹配任何以 c 开头并以 r 结尾的字符串,如 cougar、cavalier 或 chrysler。

如何指定模式表达式
模式匹配的主要部分是关于要使用什么样的表达式。Pattern 先保存要使用的表达式,然后将其传递给 Matcher 类以便在字符序列的上下文中检查其匹配情况。例如,假如您想验证一个电子邮件地址,您可能要检查用户输入是否与这样一个模式匹配 ? 它包含一个字母数字序列,后跟一个 @ 符号,@ 后又跟两组用句点隔开的字符。这可以用表达式 p{Alnum}[email protected]+.p{Alpha}{2,3} 来表示。(是的,这过于简化了电子邮件地址的结构,可能会排除某些有效的电子邮件地址,但它作为示例已经足够了。)

在讨论模式语言的具体细节之前,我们来仔细看一下 p{Alnum}[email protected]+.p{Alpha}{2,3}。p{Alnum} 序列表示单个字母数字字符(A 到 Z、a 到 z 或 0 到 9)。p{Alnum} 后面的加号(+)被称为量词(quantifier)。它被应用在表达式的前面部分,表示 p{Alnum} 必须出现一次或更多次。使用星号(*)表示要出现零次或一次以上(含一次)。@ 就是意味着它必须出现在至少一个字母数字字符之后,这样整个模式匹配才能成功。w+ 与 p{Alnum}+ 类似,但添加了下划线(_)。某些序列有多个表达式。反斜杠( .)代表句点。假如前面没有反斜杠,单独一个句点代表任意字符。最后的 p{Alpha}{2, 3} 表示两个或三个字母字符。

只要学会了规范语言,您就能把握模式的所有秘密。我们来看一些更常用的表达式的种类:

文字(Literal):表达式内任何不具有非凡意义的字符都被看作是一个文字,并与自身匹配。


量词(Quantifier):某些字符或表达式,它们被用来计算一个文字或分组可以在字符序列中出现的次数,以便该序列与表达式匹配。分组是由圆括号内的一组字符指定的。
? 表示出现一次或根本不出现
* 表示出现零次或一次以上(含一次)
+ 表示出现一次或多次


字符类(Character class):一个字符类就是方括号内的一个字符集,其中,匹配可以是括号内的任意一个字符。您可以把字符类与量词结合在一起,例如,[acegikmoqsuwy]* 将是只包含字母表中奇数字母的任意字符序列。某些字符类是预先定义的:
d ? 数字(0 到 9)
D -- 非数字
s -- 空白字符,如制表符或换行符
S -- 非空白字符
w -- 单字字符(a 到 z、A 到 Z、0 到 9 以及下划线)
W -- 非单字字符(其它任意字符)


Posix 字符类(Posix character class):某些字符类仅在用于 US-ASCII 比较时才有效。例如:
p{Lower} ? 小写字符
p{Upper} ? 大写字符
p{ASCII} ? 所有 ASCII 字符
p{Alpha} ? 字母字符(p{Lower} 与 p{Upper} 相结合)
p{Digit} ? 从 0 到 9 的数字
p{Alnum} ? 字母数字字符


范围(Range):使用短线(dash)来指定包括一定范围字符的字符类。例如,[A-J] 表示从 A 到 J 的大写字母。


否定(Negation):脱字符(^)否定字符类的内容。例如,[^A-J] 表示除 A 到 J 之外的任何字符。
请参阅 Pattern API 文档(可以从参考资料找到)了解关于序列的其它具体信息。

如何有效地使用模式
既然您已经了解了如何指定模式,我们就来使用它们吧。您需要让 Pattern 类编译它们,如下所示。注重,反斜杠字符()在 String 常量中需要转义。


Pattern pattern = Pattern.compile(
"\p{Alnum}[email protected]\w+\.\p{Alpha}{2,3}");



有了一个编译好的模式后,您可以使用 Pattern 类根据模式把一个输入行分割为一系列单字,或者使用 Matcher 类执行一些更复杂的任务。下面说明了如何分割输入字符序列,其中使用的模式指定了分隔符,而不是字:


String Words[] = pattern.split(input);



假如您想在一个字符序列中多次匹配一个模式,上面的代码片段是一个很好的起点。但假如您想获取特定的输入,您将需要 Pattern 的 matcher() 方法。在给定某个输入时,这个方法将返回适当的 Matcher 类。接着,您使用 Matcher 实例遍历整个结果在输入序列中查找不同的模式匹配,或者使用 Matcher 实例作为查找-替换工具(后一种方法更好):


Matcher matcher = pattern.matcher(input);



要根据整个序列匹配模式,请使用 matches()。要确定是否只有序列的一部分匹配,请使用 find():


if (matcher.find()) {
// Found some string within input sequence
// That matched the compiled pattern
String match = matcher.group();
// PRocess matching pattern
}



完整的示例
这两个类(Pattern 与 Matcher)就是整个模式匹配库。提出正确的正则表达式,然后使用 Matcher 类的结果,这就是这个模式匹配库要做的全部工作。在针对 Java 语言的关于正则表达式的专门书籍出现之前,请找一本关于 Perl 的好书来进一步了解特定的模式。清单 1 提供了一个完整的示例,该示例将在特定文件中查找从命令行作为输入而传入的最长单词。

清单 1.“最长的单词”示例
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;
import java.util.regex.*;

public class Longest {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("Provide a filename");
return;
}

try {
// Map File from filename to byte buffer
FileInputStream input =
new FileInputStream(args[0]);
FileChannel channel = input.getChannel();
int fileLength = (int)channel.size();
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, fileLength);

// Convert to character buffer
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);

// Create line pattern
Pattern linePattern =
Pattern.compile(".*$", Pattern.MULTILINE);

// Create word pattern
Pattern wordBreakPattern =
Pattern.compile("[\p{Punct}\s}]");

// Match line pattern to buffer
Matcher lineMatcher =
linePattern.matcher(charBuffer);

// Holder for longest word
String longest = "";

// For each line
while (lineMatcher.find()) {

// Get line
String line = lineMatcher.group();

// Get array of words on line
String words[] = wordBreakPattern.split(line);

// Look for longest word
for (int i=0, n=words.length; i<n; i++) {
if (words[i].length() > longest.length()) {
longest = words[i];
}
}
}
// Report
System.out.println("Longest word: " + longest);

// Close
input.close();
} catch (IOException e) {
System.err.println("Error processing");
}
}
}



参考资料

请单击文章顶部或底部的讨论参加关于本文的讨论论坛。


请阅读 java.util.regex 包的 API 文档。


请试用面向 Java 版本 1.4 之前的 alphaWorks Regex for Java。


developerWorks linux 专区发表了一个 Cultured Perl 月刊专栏,这个专栏可能向您提供使用 Java 语言的正则表达式的比较深入的知识。


阅读 John Zukowski 写的 Merlin 技巧全集。


请在 developerWorks Java 技术专区上查找更多的 Java 参考资料。

关于作者
John Zukowski 通过 JZ Ventures, Inc. 从事战略性 Java 咨询,同时还担任一些 jGuru 的由社区推动的 Java 常见问题解答的常驻指导。他最近的著作有 Apress 出版的 Learn Java with JBuilder 6 与 Sybex 出版的 Mastering Java 2: J2SE 1.4 。请通过 [email protected] 与他联系。