`
alanland
  • 浏览: 641324 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

构建用于正则表达式的抽象 Java API

    博客分类:
  • Java
阅读更多

 

 

简介

来自:ibm http://www.ibm.com/developerworks/cn/java/wa-regexp/

尽管您可能认为编写需要分析文本的 Java 应用程序是一项简单任务,但象许多事情一样,它会很快变得复杂起来。那的确是我在编写代码以解析 HTML 页面时的经验。开始的时候,我偶尔会使用 Perl5 正则表达式(regexp)。但是,由于某些原因(稍后说明),我后来常常使用它们。

背景知识

在我的经验中,大多数 Java 开发人员都需要解析某种文本。通常,这意味着他们最初要花一些时间使用象 indexOf 或 substring 那样的与 Java 字符串相关的函数或方法,并且希望输入格式永远不变。但是,如果输入格式改变,那么用于读取新格式的代码维护起来就会变得更复杂、更困难。最后,代码可能需要支持自动换行(word wrapping)、区分大小写等。

由于逻辑变得更加复杂,所以维护也变得很困难。因为任何更改都可能产生副作用并使文本解析器的其它部分停止工作,所以开发人员需要时间修正这些小错误。

有一定 Perl 经验的开发人员可能也有过使用正则表达式的经验。如果够幸运(或优秀)的话,这位开发人员能够说服团队其余的人(或至少是团队领导)使用这项技术。新的方法将取消编写用来调用String 方法的多行代码,它意味着将解析器逻辑的核心委托出去,并替换为 regexp 库。

接受了有 Perl5 经验的开发人员的建议后,团队必须选择哪个 regex 实现最适合他们的项目。然后他们需要学习如何使用它。

在简要地研究了从因特网上找到的众多可选方案后,假设团队决定从人们更熟悉的库中选择一个使用,如属于 Jakarta项目的 Oro。接下来,对解析器进行较大程度地重构或几乎重新编写,并且解析器最终使用了 Oro 的类,如 Perl5Compiler 、 Perl5Matcher 等。

这一决定的后果很明显:

  • 代码与 Jakarta Oro 的类紧密地耦合在一起。
  • 团队承担了风险,因为不知道非功能性需求(如性能或线程模型)是否将得到满足。
  • 团队已花费时间和财力来学习并重新编写代码,以使它使用 regexp 库。如果他们的决定是错误的并且选择了新的库,则这一工作在成本上将不会有很大区别,因为将需要再次重新编写代码。
  • 即使库工作正常,如果他们决定应该迁移到全新的库(例如,包括在 JDK 1.4 中的库),怎么办?

 




回页首


去耦的好处

有没有办法使团队知道哪个实现最适合他们的需要呢(不仅现在能将来也能)?让我们试着寻找答案。

避免依赖任何特定的实现

前面的情形在软件工程中十分常见。在有些情况中,这样的情形会导致较大的投资和较长的延期。当不了解所有后果就作出决定而且决策制定人不太走运或缺乏必需的经验时,就常常会发生这种情况。

可将该情形概括如下:

  • 您需要某种提供者
  • 您没有选择最佳提供者的客观标准
  • 您希望能用最低的成本来评估所有的待选项
  • 所作的决定不应将您束缚在所选的提供者上

这一问题的解决方法是使代码更加独立于提供者。这引入了新的层 ― 同时去除客户机和提供者的耦合的层。

在服务器端开发中,很容易找到使用该方法的模式或体系结构。下面引用一些示例:

  • 对于 J2EE,您主要关注如何构建应用程序而不是应用程序服务器的细节。
  • 数据访问对象(Data Access Object,DAO)模式隐藏了如何访问数据库(或 LDAP 服务器、XML 文件等)的细节和复杂性,因为它提供了访问抽象持久存储层的方法,而您则不需要在客户机代码中处理数据库问题(数据实际存储在哪里)。这不是 四人组(Gang of Four,GoF)模式,而是 Sun 的 J2EE 最佳实践的一部分。

在假想的开发团队示例中,他们正在寻找这样的层:

  • 抽象所有正则表达式实现背后的概念。团队就可以着重学习和理解这些概念。他们所学的可以应用到任何实现或版本。
  • 支持新的库且没有副作用。基于插件体系结构,动态选择执行 regexp 模式的实际库,并且适配器不会被耦合。新库仅会引入对新适配器的需要。
  • 提供比较不同可选方案的方法。一个简单的基准实用程序就可以显示有趣的性能测量结果。如果对每个实现都执行这样的实用程序,团队就会获得有价值的信息并能选择最好的可选方案。

听起来不错,但……

任何去耦方法都至少有一个缺点:如果客户机代码仅需要一个实现所提供的特定功能,怎么办?您不能使用任何其它实现,因此您最终将代码与该实现耦合。也许将来会在这方面有所改善,但您现在却束手无策。

这样的示例并不象您想的那样少。在 regexp 领域中,一些编译器选项仅被某些实现支持。如果您的客户机代码需要这种特定的功能,那么这个一般层是不够的 ― 至少从迄今对它描述来看是不够的。

附加层是否应支持每个实现的所有 非公共功能,并且如果选择了不支持该实现的附加层则抛出异常?那可以是一种解决方案,但它并不支持仅定义 公共抽象概念这一最初目标。

有一个 GoF 模式非常适合这种情形: 职责链(Chain of Responsibility)。它在设计中引入了另一种间接方法。用这种方法,客户机代码向能处理其所发消息的实体列表发送消息或命令。列表项被组织成链,因此消息可按顺序被处理并且在到达链尾之前被用掉。

在这种情况中,可以通过特殊类型的消息对仅被某些实现支持的特定功能建模。由链中的每一项根据其是否了解这些功能来决定是否将该消息传给下一项。

 




回页首


定义一个公共 API

这里讲述的 API 名为 

 

RegexpPlugin 。已将它设计成遵循刚刚讨论的方法,并且它在 regexp 库和使用该库的代码之间支持去耦。

 

 

RegexpPlugin

在以下示例中,我将总结一下使用具体实现(Jakarta Oro)和使用 RegexpPlugin API 之间的差别。

我从一个非常简单的 regexp 开始:假定您必须要解析的文本只是人名。您接收的格式是象 John A. Smith这样的内容,而您只想获取名字( John)。但您不知道单词由什么分隔,是空格、换行符、制表符还是这些字符的组合。能处理这样的输入格式的 regexp 只是 .*\s*(.*?)\s+.* 。我将一步一步地说明如何使用该 regexp 来抽取信息。

第一部分是点号和星号字符 .* ,它们在这里表示 任意数量的空格和 (.*?) 组之前的任何字符 。第二部分比较引人注意(因为它被圆括号括起来)。问号表示 取第一个符合条件的项

接下来的符号表示任意数量的空格、换行或制表符( \s ),但至少要有一个( + )。最后的点号和星号 .* 仅代表文本的余下部分(对它没有兴趣)。

因此,该 regexp 相当于: 取空格前的第一段文本。让我们来编写 Java 代码。

上机实践

要在 Java 代码中使用正则表达式,通常需要完成以下七个步骤:

第 1 步:创建编译器实例。如果使用 Jakarta Oro,则必须实例化 Perl5Compiler :

org.apache.oro.text.regex.Perl5Compiler compiler =
    new org.apache.oro.text.regex.Perl5Compiler();

 

使用 RegexpPlugin 时的等同代码是相似的:

org.acmsl.regexpplugin.Compiler compiler =
    org.acmsl.regexpplugin.RegexpManager.createCompiler();

 

但存在差异。正如前面提到的,该 API 对实际使用哪个具体实现加以隐藏。您可以选择一个具体实现或保留缺省的 Jakarta Oro。如果所选的库在运行时不可用,则 RegexpPlugin API 会尝试用它的类名创建一个编译器。如果该操作失败,它会将异常发回 API 的客户机。

假定您一直在使用 JDK 1.4 的内置 regexp 类。那样的话,包含始终不会使用的额外 jar 文件毫无意义。那就是为什么仅仅调用 createCompiler() 方法还不够的原因。您需要管理这样的异常:每当所选的库不存在时就会抛出该异常。因而必须更新示例:

try
{
    org.acmsl.regexpplugin.Compiler compiler =
        org.acmsl.regexpplugin.RegexpManager.createCompiler();
}
catch (org.acmsl.regexpplugin.RegexpEngineNorFoundException exception)
{
    [..]
}

 

第 2 步:编译 regexp 模式。将正则表达式本身编译到 Pattern 对象中。

org.apache.oro.text.regex.Pattern pattern =
    compiler.compile(".*\\s*(.*?)\\s+.*", Perl5Compiler.MULTILINE_MASK);

 

注:您必须转义反斜杠(\)字符。

该模式对象代表以文本格式定义的正则表达式。请尽可能多地重用模式实例。然后,如果 regexp 是固定的(缺少任何可变部分,如 “(.*?)Tom.*”),则模式应是类中的静态成员。

compile 方法适合用标志(如 EXTENDED_MASK )来配置(请参阅 参考资料以获得更详细的 regexp 教程)。但是,RegexpPlugin 并不允许随意的标志。受支持的标志只有 case sensitivity 和 multiline,因为所有受支持的库都可以处理它们。

编译器实例有特定的特性来定义这些标志:

compiler.setMultiline(true);
org.acmsl.regexpplugin.Pattern pattern =
    compiler.compile(".*\\s*(.*?)\\s+.*");

 

第 3 步:创建 Matcher 对象。在 Jakarta Oro中,这一步非常简单:

org.apache.oro.text.regex.Perl5Matcher matcher =
    new org.apache.oro.text.regex.Perl5Matcher();

 

它之所以如此简单是因为它不需要构造任何信息。在后来的 regexp 中,它将变得具体。基本上,RegexpPlugin 中的步骤差不多相似。您不必亲自创建 matcher ,而是可以将其代理给 RegexpManager类:

org.acmsl.regexpplugin.Matcher matcher =
    org.acmsl.regexpplugin.RegexpManager.createMatcher();

 

区别和前面一样,您需要处理 RegexpEngineNotFoundException 。实际上, RegexpManager 需要为您所选的库或缺省库创建 matcher 适配器。如果这样的类在运行时不可用,它会抛出该异常。

第 4 步:评估正则表达式。 matcher 对象需要解释正则表达式并抽取所需的信息。这在一行代码中完成:

if (matcher.contains("John A. Smith", pattern))
{

 

如果输入文本与正则表达式匹配,则该方法返回 true。隐含的副作用是,执行该行代码之后, matcher 对象包含在输入文本中找到的第一个匹配项。接下来的一步演示如何实际获取感兴趣的信息。

通过使用 RegexpPlugin API,在此时根本没有任何不同。

第 5 步:检索找到的第一个匹配项。 这一简单的步骤仅用一行完成:

   org.apache.oro.text.regex.MatchResult matchResult = matcher.getMatch();

 

您可以声明一个局部变量来存储这样的对象,该对象含有与 regexp 匹配的一段文本。在这两种情况下,该步骤是相同的,除了变量声明(因为一个是另一个的适配器):

   org.acmsl.regexpplugin.MatchResult matchResult =
        matcher.getMatch();

 

第 6 步:获取感兴趣的 group 。 您可以使用两种方法:

  • 具体库
  • RegexpPlugin API

因为您的 regexp 是 .*\s*(.*?)\s+.* ,所以您只有一个组: (.*?)

MatchResult 对象包含已排序列表中的所有组。您只需要知道要获取的组的位置。因为该示例只有一个组,所以毫无疑问:

   String name = matchResult.group(1);
    [..]
}

 

变量 name 现在包含文本 John,那正是您需要的。

第 7 步:如果需要,则重复该过程。 如果您需要的信息可多次出现,而您想分析所有出现的信息而不只是第一个,那么您只需循环执行第 5步到第 7步,直到不满足 第 4 步中描述的条件为止:

while (matcher.contains("John A. Smith", pattern))
{





回页首


映射

除了编写公共抽象 API,主要的工作实际上是实现 Java 环境中某些已存在的 regexp 引擎的适配器。

以下各表提供了对如何从一个库迁移至另一个库的详细描述。有些情况中,概念明显不同。也有些情况中,却不是那么明显。

Regexp 概念 GNU Regexp 1.2
编译器 gnu.regexp.RE
模式 gnu.regexp.RE
匹配程序 gnu.regexp.REMatchEnumeration 
gnu.regexp.RE
匹配结果 gnu.regexp.REMatch
畸形模式异常 gnu.regexp.REException

 

Regexp 概念 Jakarta Oro 2.0.6
编译器 org.apache.oro.text.regex.Perl5Compiler
模式 org.apache.oro.text.regex.Pattern
匹配程序 org.apache.oro.text.regex.Perl5Matcher
匹配结果 org.apache.oro.text.regex.MatchResult
畸形模式异常 org.[..].regex.MalformedPatternException

 

Regexp 概念 Jakarta Regexp 1.3
编译器 org.apache.regexp.RE 
org.apache.regexp.RECompiler 
org.apache.regexp.REProgram
模式 org.apache.regexp.REProgram 
org.apache.regexp.RE
匹配程序 org.apache.regexp.RE 
org.apache.regexp.REProgram
匹配结果 org.apache.regexp.RE
畸形模式异常 org.apache.regexp.RESyntaxException

 

Regexp 概念 JDK 1.4 regex 包
编译器 java.util.regex.Pattern
模式 java.util.regex.Pattern
匹配程序 java.util.regex.Matcher
匹配结果 java.util.regex.Matcher
畸形模式异常 java.util.regex.PatternSyntaxException

 




回页首


基准

该 API 较显著的用法之一是用来比较实现、测量性能、对 Perl5 语法的兼容性或其它标准之间的差异。

为这些测试开发的基准实用程序使用 HTML 解析器来处理 Web 内容,更新有关链接、表单和表等元素的信息。但是,重要的是解析逻辑用正则表达式来表示,因此会通过 RegexpPlugin API 实现。

基准测试包括对非常简单的 HTML 页面解析 10000 次。结果在下表中显示。

Regexp 库 Benchmark 结果(秒)
Jakarta Oro 2.0.6 130,71
Jakarta Regexp 1.2 23,261
GNU Regexp 1.1.4 1,966.939
JDK1.4 33,222

您可以用多种方法在实际应用程序中改进性能。最重要的是,当您使用 regexp 库时,不需要每次都编译模式,而是编译它们并重用各自的实例。但是,如果 regexp 本身不固定,则不能忽略编译过程。

因为基准需要在实现之间切换以比较性能,所以必须始终废弃已编译模式以避免库之间的交互。但是,正如您所见,大多数已评估的库有相似的响应时间,尽管更详细的基准能让我们更好的理解每个库在不同环境下的行为。

 




回页首


结束语

正则表达式解析器有强大的功能。一旦团队适应了它,解析逻辑就会改进,这有助于降低维护。但是,开发人员需要了解 regexp 语法以理解这些代码是如何工作的。本文已经用一个非常简单的示例说明了如何使用这些库中的一个。除此之外,本文还描述了使用附加层去除客户机代码与 regexp 引擎本身之间的耦合的好处。

 

来源:http://www.ibm.com/developerworks/cn/java/wa-regexp/

 

分享到:
评论

相关推荐

    JAVA_API1.6文档(中文)

    java.util.regex 用于匹配字符序列与正则表达式指定模式的类。 java.util.spi java.util 包中类的服务提供者类。 java.util.zip 提供用于读写标准 ZIP 和 GZIP 文件格式的类。 javax.accessibility 定义了用户...

    《java学习》-java学习笔记.zip

    正则表达式在Java中用于文本匹配和搜索,是处理字符串的强大工具。Java提供了`java.util.regex`包,包含了Pattern和Matcher类来支持正则表达式的编译和匹配操作。理解如何构建和使用正则表达式对于数据验证、文本...

    J2EE开发全程实录

    元字符是指具有特殊意义的字符,它们被用于构建更复杂的正则表达式模式。 - **`.`(点)**:匹配任何单个字符(除了换行符)。 - **`*`(星号)**:匹配前面的字符零次或多次。 - **`+`(加号)**:匹配前面的字符...

    Java 1.6 API 中文 New

    java.util.regex 用于匹配字符序列与正则表达式指定模式的类。 java.util.spi java.util 包中类的服务提供者类。 java.util.zip 提供用于读写标准 ZIP 和 GZIP 文件格式的类。 javax.accessibility 定义了用户界面...

    《Java2程序设计实用教程(第2版)》习题及实验(叶核亚)

    这部分习题主要涉及Java与数据库的连接,如使用JDBC API进行数据查询、插入、更新和删除操作。学生将学习如何配置数据库驱动,执行SQL语句,处理结果集,并理解事务的概念。 通过这些习题和实验,学生不仅能系统地...

    java api最新7.0

    java.util.regex 用于匹配字符序列与正则表达式指定模式的类。 java.util.spi java.util 包中类的服务提供者类。 java.util.zip 提供用于读写标准 ZIP 和 GZIP 文件格式的类。 javax.accessibility 定义了用户界面...

    第8章JavaIO第8章JavaIO第8章JavaIO第8章JavaIO第8章JavaIO第8章JavaIO

    正则表达式在Java中通过Pattern和Matcher类实现,可以与I/O结合使用,用于在读取的文本中进行模式匹配和查找。 8.4 新IO(NIO) Java的新I/O API(非阻塞I/O),引入于Java 1.4,提供了Channel、Selector和Buffer等...

    JavaAPI1.6中文chm文档 part1

    java.util.regex 用于匹配字符序列与正则表达式指定模式的类。 java.util.spi java.util 包中类的服务提供者类。 java.util.zip 提供用于读写标准 ZIP 和 GZIP 文件格式的类。 javax.accessibility 定义了用户...

    jrexx: automat. based regex api for Java-开源

    `jrexx` 是一个开源的 Java 库,专门设计用于处理正则表达式,它基于自动机理论构建,提供了高效且功能丰富的 API。这个库的独特之处在于它的匹配算法快速,同时扩展了正则表达式的模式语法,使得在Java编程中处理...

    java 公式解析源码

    在Java中,我们可能会使用正则表达式进行词法分析,构建抽象语法树(AST)进行语法分析,最后遍历AST来执行计算。 在提供的压缩包中,`test`目录可能包含单元测试,用于验证公式解析器的正确性。这些测试用例通常...

    Java基础文档

    正则表达式用于文本匹配和搜索,Java提供`java.util.regex`包来支持正则表达式操作。 第六章涉及常用的实用类,如集合框架(List, Set, Map等),日期时间API(如`java.time`包),I/O流(处理输入输出操作),以及...

    java jdk-api-1.6 中文 chmd

    java.util.regex 用于匹配字符序列与正则表达式指定模式的类。 java.util.spi java.util 包中类的服务提供者类。 java.util.zip 提供用于读写标准 ZIP 和 GZIP 文件格式的类。 javax.accessibility 定义了用户...

    JavaAPI中文chm文档 part2

    java.util.regex 用于匹配字符序列与正则表达式指定模式的类。 java.util.spi java.util 包中类的服务提供者类。 java.util.zip 提供用于读写标准 ZIP 和 GZIP 文件格式的类。 javax.accessibility 定义了用户...

    基于Java的源代码检索系统 JCite.zip

    在JCite中,这些API用于遍历代码库中的每个源代码文件。 3. **解析器和语法分析**:为了理解Java源代码,需要对代码进行语法解析。Java的抽象语法树(AST)解析库,如ANTLR或JavaCC,可以帮助将源代码转换为易于...

    Java爬虫项目实战源码.zip

    这可能需要用到Jsoup库,它提供了友好的API用于解析和操作HTML文档。 4. **正则表达式**:对于简单的数据提取,正则表达式是非常有用的工具。开发者需要掌握如何编写和使用正则表达式匹配和提取文本。 5. **多线程...

    Java C# 语法 比较

    - 两种语言都支持正则表达式,并提供构建和使用正则表达式的API。 12. 反射与类加载机制 - Java和C#都支持反射机制,可以用来在运行时检查或修改类的行为。 - C#的反射API比Java更为广泛。 13. 事件处理 - ...

    传智播客_java基础知识汇总

    正则表达式则是字符串处理的强大工具,用于模式匹配和搜索替换。 这些文档构建了一个全面的Java学习路径,从基础语法到面向对象编程,再到高级特性,对初学者或需要巩固基础的开发者来说都是宝贵的资源。通过系统...

    Java程序设计语言

    Swing是Java提供的用于构建桌面应用GUI的库。这一章会介绍组件(JButton、JLabel等)、布局管理器、事件处理以及对话框的使用。 **第11章 Java中的网络编程** Java提供了丰富的API进行网络编程,包括Socket和...

Global site tag (gtag.js) - Google Analytics