正则表达式详解


许多语言,包括Perl、PHP、Python、JavaScript和JScript,都支持用正则表达式处理文本,一些文本编辑器用正则表达式实现高级“搜索-替换”功能。那么Java又怎样呢?本文写作时,一个包含了用正则表达式进行文本处理的Java规范需求(Specification Request)已经得到认可,你可以期待在JDK的下一版本中看到它。

然而,如果现在就需要使用正则表达式,又该怎么办呢?你可以从Apache.org下载源代码开放的Jakarta-ORO库。本文接下来的内容先简要地介绍正则表达式的入门知识,然后以Jakarta-ORO API为例介绍如何使用正则表达式。
一、正则表达式基础知识
我们先从简单的开始。假设你要搜索一个包含字符“cat”的字符串,搜索用的正则表达式就是“cat”。如果搜索对大小写不敏感,单词“catalog”、“Catherine”、“sophisticated”都可以匹配。也就是说:
1.1 句点符号
假设你在玩英文拼字游戏,想要找出三个字母的单词,而且这些单词必须以“t”字母开头,以“n”字母结束。另外,假设有一本英文字典,你可以用正则表达式搜索它的全部内容。要构造出这个正则表达式,你可以使用一个通配符——句点符号“.”。这样,完整的表达式就是“t.n”,它匹配“tan”、“ten”、“tin”和“ton”,还匹配“t#n”、“tpn”甚至“t n”,还有其他许多无意义的组合。这是因为句点符号匹配所有字符,包括空格、Tab字符甚至换行符:
1.2 方括号符号
为了解决句点符号匹配范围过于广泛这一问题,你可以在方括号(“[]”)里面指定看来有意义的字符。此时,只有方括号里面指定的字符才参与匹配。也就是说,正则表达式“t[aeio]n”只匹配“tan”、“Ten”、“tin”和“ton”。但“Toon”不匹配,因为在方括号之内你只能匹配单个字符:
1.3 “或”符号
如果除了上面匹配的所有单词之外,你还想要匹配“toon”,那么,你可以使用“|”操作符。“|”操作符的基本意义就是“或”运算。要匹配“toon”,使用“t(a|e|i|o|oo)n”正则表达式。这里不能使用方扩号,因为方括号只允许匹配单个字符;这里必须使用圆括号“()”。圆括号还可以用来分组,具体请参见后面介绍。
1.4 表示匹配次数的符号
表一显示了表示匹配次数的符号,这些符号用来确定紧靠该符号左边的符号出现的次数:

假设我们要在文本文件中搜索美国的社会安全号码。这个号码的格式是999-99-9999。用来匹配它的正则表达式如图一所示。在正则表达式中,连字符(“-”)有着特殊的意义,它表示一个范围,比如从0到9。因此,匹配社会安全号码中的连字符号时,它的前面要加上一个转义字符“\”。

图一:匹配所有123-12-1234形式的社会安全号码

假设进行搜索的时候,你希望连字符号可以出现,也可以不出现——即,999-99-9999和999999999都属于正确的格式。这时,你可以在连字符号后面加上“?”数量限定符号,如图二所示:

图二:匹配所有123-12-1234和123121234形式的社会安全号码

下面我们再来看另外一个例子。美国汽车牌照的一种格式是四个数字加上二个字母。它的正则表达式前面是数字部分“[0-9]{4}”,再加上字母部分“[A-Z]{2}”。图三显示了完整的正则表达式。

图三:匹配典型的美国汽车牌照号码,如8836KV

1.5 “否”符号
“^”符号称为“否”符号。如果用在方括号内,“^”表示不想要匹配的字符。例如,图四的正则表达式匹配所有单词,但以“X”字母开头的单词除外。

图四:匹配所有单词,但“X”开头的除外

1.6 圆括号和空白符号
假设要从格式为“June 26, 1951”的生日日期中提取出月份部分,用来匹配该日期的正则表达式可以如图五所示:

图五:匹配所有Moth DD,YYYY格式的日期

新出现的“\s”符号是空白符号,匹配所有的空白字符,包括Tab字符。如果字符串正确匹配,接下来如何提取出月份部分呢?只需在月份周围加上一个圆括号创建一个组,然后用ORO API(本文后面详细讨论)提取出它的值。修改后的正则表达式如图六所示:

图六:匹配所有Month DD,YYYY格式的日期,定义月份值为第一个组

1.7 其它符号
为简便起见,你可以使用一些为常见正则表达式创建的快捷符号。如表二所示:
表二:常用符号

例如,在前面社会安全号码的例子中,所有出现“[0-9]”的地方我们都可以使用“\d”。修改后的正则表达式如图七所示:

图七:匹配所有123-12-1234格式的社会安全号码

二、Jakarta-ORO库
有许多源代码开放的正则表达式库可供Java程序员使用,而且它们中的许多支持Perl 5兼容的正则表达式语法。我在这里选用的是Jakarta-ORO正则表达式库,它是最全面的正则表达式API之一,而且它与Perl 5正则表达式完全兼容。另外,它也是优化得最好的API之一。
Jakarta-ORO库以前叫做OROMatcher,Daniel Savarese大方地把它赠送给了Jakarta Project。你可以按照本文最后参考资源的说明下载它。
我首先将简要介绍使用Jakarta-ORO库时你必须创建和访问的对象,然后介绍如何使用Jakarta-ORO API。
▲ PatternCompiler对象
首先,创建一个Perl5Compiler类的实例,并把它赋值给PatternCompiler接口对象。Perl5Compiler是PatternCompiler接口的一个实现,允许你把正则表达式编译成用来匹配的Pattern对象。
▲ Pattern对象
要把正则表达式编译成Pattern对象,调用compiler对象的compile()方法,并在调用参数中指定正则表达式。例如,你可以按照下面这种方式编译正则表达式“t[aeio]n”:
默认情况下,编译器创建一个大小写敏感的模式(pattern)。因此,上面代码编译得到的模式只匹配“tin”、“tan”、 “ten”和“ton”,但不匹配“Tin”和“taN”。要创建一个大小写不敏感的模式,你应该在调用编译器的时候指定一个额外的参数:
创建好Pattern对象之后,你就可以通过PatternMatcher类用该Pattern对象进行模式匹配。
▲ PatternMatcher对象
PatternMatcher对象根据Pattern对象和字符串进行匹配检查。你要实例化一个Perl5Matcher类并把结果赋值给PatternMatcher接口。Perl5Matcher类是PatternMatcher接口的一个实现,它根据Perl 5正则表达式语法进行模式匹配:
使用PatternMatcher对象,你可以用多个方法进行匹配操作,这些方法的第一个参数都是需要根据正则表达式进行匹配的字符串:
· boolean matches(String input, Pattern pattern):当输入字符串和正则表达式要精确匹配时使用。换句话说,正则表达式必须完整地描述输入字符串。
· boolean matchesPrefix(String input, Pattern pattern):当正则表达式匹配输入字符串起始部分时使用。
· boolean contains(String input, Pattern pattern):当正则表达式要匹配输入字符串的一部分时使用(即,它必须是一个子串)。
另外,在上面三个方法调用中,你还可以用PatternMatcherInput对象作为参数替代String对象;这时,你可以从字符串中最后一次匹配的位置开始继续进行匹配。当字符串可能有多个子串匹配给定的正则表达式时,用PatternMatcherInput对象作为参数就很有用了。用PatternMatcherInput对象作为参数替代String时,上述三个方法的语法如下:
· boolean matches(PatternMatcherInput input, Pattern pattern)
· boolean matchesPrefix(PatternMatcherInput input, Pattern pattern)
· boolean contains(PatternMatcherInput input, Pattern pattern)
三、应用实例
下面我们来看看Jakarta-ORO库的一些应用实例。
3.1 日志文件处理
任务:分析一个Web服务器日志文件,确定每一个用户花在网站上的时间。在典型的BEA WebLogic日志文件中,日志记录的格式如下:
分析这个日志记录,可以发现,要从这个日志文件提取的内容有两项:IP地址和页面访问时间。你可以用分组符号(圆括号)从日志记录提取出IP地址和时间标记。
首先我们来看看IP地址。IP地址有4个字节构成,每一个字节的值在0到255之间,各个字节通过一个句点分隔。因此,IP地址中的每一个字节有至少一个、最多三个数字。图八显示了为IP地址编写的正则表达式:

图八:匹配IP地址

IP地址中的句点字符必须进行转义处理(前面加上“\”),因为IP地址中的句点具有它本来的含义,而不是采用正则表达式语法中的特殊含义。句点在正则表达式中的特殊含义本文前面已经介绍。
日志记录的时间部分由一对方括号包围。你可以按照如下思路提取出方括号里面的所有内容:首先搜索起始方括号字符(“[”),提取出所有不超过结束方括号字符(“]”)的内容,向前寻找直至找到结束方括号字符。图九显示了这部分的正则表达式。

图九:匹配至少一个字符,直至找到“]”

现在,把上述两个正则表达式加上分组符号(圆括号)后合并成单个表达式,这样就可以从日志记录提取出IP地址和时间。注意,为了匹配“- -”(但不提取它),正则表达式中间加入了“\s-\s-\s”。完整的正则表达式如图十所示。

图十:匹配IP地址和时间标记

现在正则表达式已经编写完毕,接下来可以编写使用正则表达式库的Java代码了。
为使用Jakarta-ORO库,首先创建正则表达式字符串和待分析的日志记录字符串:
这里使用的正则表达式与图十的正则表达式差不多完全相同,但有一点例外:在Java中,你必须对每一个向前的斜杠(“\”)进行转义处理。图十不是Java的表示形式,所以我们要在每个“\”前面加上一个“\”以免出现编译错误。遗憾的是,转义处理过程很容易出现错误,所以应该小心谨慎。你可以首先输入未经转义处理的正则表达式,然后从左到右依次把每一个“\”替换成“\\”。如果要复检,你可以试着把它输出到屏幕上。
初始化字符串之后,实例化PatternCompiler对象,用PatternCompiler编译正则表达式创建一个Pattern对象:
现在,创建PatternMatcher对象,调用PatternMatcher接口的contain()方法检查匹配情况:
接下来,利用PatternMatcher接口返回的MatchResult对象,输出匹配的组。由于logEntry字符串包含匹配的内容,你可以看到类如下面的输出:
3.2 HTML处理实例一
下面一个任务是分析HTML页面内FONT标记的所有属性。HTML页面内典型的FONT标记如下所示:
程序将按照如下形式,输出每一个FONT标记的属性:
在这种情况下,我建议你使用两个正则表达式。第一个如图十一所示,它从字体标记提取出“"face="Arial, Serif" size="+2" color="red"”。

图十一:匹配FONT标记的所有属性

第二个正则表达式如图十二所示,它把各个属性分割成名字-值对。

图十二:匹配单个属性,并把它分割成名字-值对

分割结果为:
现在我们来看看完成这个任务的Java代码。首先创建两个正则表达式字符串,用Perl5Compiler把它们编译成Pattern对象。编译正则表达式的时候,指定Perl5Compiler.CASE_INSENSITIVE_MASK选项,使得匹配操作不区分大小写。
接下来,创建一个执行匹配操作的Perl5Matcher对象。
假设有一个String类型的变量html,它代表了HTML文件中的一行内容。如果html字符串包含FONT标记,匹配器将返回true。此时,你可以用匹配器对象返回的MatchResult对象获得第一个组,它包含了FONT的所有属性:
接下来创建一个PatternMatcherInput对象。这个对象允许你从最后一次匹配的位置开始继续进行匹配操作,因此,它很适合于提取FONT标记内属性的名字-值对。创建PatternMatcherInput对象,以参数形式传入待匹配的字符串。然后,用匹配器实例提取出每一个FONT的属性。这通过指定PatternMatcherInput对象(而不是字符串对象)为参数,反复地调用PatternMatcher对象的contains()方法完成。PatternMatcherInput对象之中的每一次迭代将把它内部的指针向前移动,下一次检测将从前一次匹配位置的后面开始。
本例的输出结果如下:
3.3 HTML处理实例二
下面我们来看看另一个处理HTML的例子。这一次,我们假定Web服务器从widgets.acme.com移到了newserver.acme.com。现在你要修改一些页面中的链接:
执行这个搜索的正则表达式如图十三所示:

图十三:匹配修改前的链接

如果能够匹配这个正则表达式,你可以用下面的内容替换图十三的链接:
注意#字符的后面加上了$1。Perl正则表达式语法用$1、$2等表示已经匹配且提取出来的组。图十三的表达式把所有作为一个组匹配和提取出来的内容附加到链接的后面。
现在,返回Java。就象前面我们所做的那样,你必须创建测试字符串,创建把正则表达式编译到Pattern对象所必需的对象,以及创建一个PatternMatcher对象:
接下来,用com.oroinc.text.regex包Util类的substitute()静态方法进行替换,输出结果字符串:
Util.substitute()方法的语法如下:
这个调用的前两个参数是以前创建的PatternMatcher和Pattern对象。第三个参数是一个Substiution对象,它决定了替换操作如何进行。本例使用的是Perl5Substitution对象,它能够进行Perl5风格的替换。第四个参数是想要进行替换操作的字符串,最后一个参数允许指定是否替换模式的所有匹配子串(Util.SUBSTITUTE_ALL),或只替换指定的次数。

【结束语】在这篇文章中,我为你介绍了正则表达式的强大功能。只要正确运用,正则表达式能够在字符串提取和文本修改中起到很大的作用。另外,我还介绍了如何在Java程序中通过Jakarta-ORO库利用正则表达式。至于最终采用老式的字符串处理方式(使用StringTokenizer,charAt,和substring),还是采用正则表达式,这就有待你自己决定了。


文章二

概念

是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。在很多文本编辑器或其他工具里,正则表达式通常被用来检索和/或替换那些符合某个模式的文本内容。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的。正则表达式通常缩写成“regex”,单数有regexp、regex,复数有regexps、regexes、regexen。

基础

(摘自《正则表达式之道》)
  正则表达式由一些普通字符和一些 元字符(metacharacters)组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义,我们下面会给予解释。
  在最简单的情况下,一个正则表达式看上去就是一个普通的查找串。例如,正则表达式"testing"中没有包含任何元字符,它可以匹配"testing"和"123testing"等字符串,但是不能匹配"Testing"。
  要想真正的用好正则表达式,正确的理解元字符是最重要的事情。下表列出了所有的元字符和对它们的一个简短的描述。
元字符 描述
.点 匹配任何单个字符。例如正则表达式r.t匹配这些字符串:rat、rut、r t,但是不匹配root。
$ 匹配行结束符。例如正则表达式weasel$ 能够匹配字符串"He's a weasel"的末尾 
但是不能匹配字符串"They are a bunch of weasels."
^ 匹配一行的开始。例如正则表达式^When in能够匹配字符串"When in the course of human events"的开始,但是不能匹配"What and When in the"
* 匹配0或多个正好在它之前的那个字符。例如正则表达式 .* 意味着能够匹配任意数量的任何字符。比如<T>.*</T> 可以匹配<T>不管是什么</T>
\ 这是引用符,用来将这里列出的这些元字符当作普通的字符来进行匹配。例如正则表达式\$被用来匹配美元符号,而不是行尾,类似的,正则表达式\.用来匹配点字符,而不是任何字符的通配符。
[ ] 
[c1-c2] 
[^c1-c2]
匹配括号中的任何一个字符。例如正则表达式r[aou]t匹配rat、rot和rut,但是不匹配ret。可以在括号中使用连字符-来指定字符的区间,例如正则表达式[0-9]可以匹配任何数字字符;还可以制定多个区间,例如正则表达式[A-Za-z]可以匹配任何大小写字母。另一个重要的用法是“排除”,要想匹配除了指定区间之外的字符——也就是所谓的补集——在左边的括号和第一个字符之间使用^字符,例如正则表达式[^269A-Z] 将匹配除了2、6、9和所有大写字母之外的任何字符。
\< \> 匹配词(word)的开始(\<)和结束(\>)。例如正则表达式\<the\>能够匹配字符串"for the wise"中的"the",但是不能匹配字符串"otherwise"中的"the"。注意:这个元字符不是所有的软件都支持的。
\( \) 将 \( 和 \) 之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个临时区域(一个正则表达式中最多可以保存9个),它们可以用 \1 到\9 的符号来引用。
| 将两个匹配条件进行逻辑“或”(Or)运算。例如正则表达式(him|her) 匹配"it belongs to him"和"it belongs to her",但是不能匹配"it belongs to them."。注意:这个元字符不是所有的软件都支持的。
+ 匹配1或多个正好在它之前的那个字符。例如正则表达式9+匹配9、99、999等。注意:这个元字符不是所有的软件都支持的。
? 匹配0或1个正好在它之前的那个字符。注意:这个元字符不是所有的软件都支持的。
{i} 
{i,j}
匹配指定数目的字符,这些字符是在它之前的表达式定义的。例如正则表达式A[0-9]{3} 能够匹配字符"A"后面跟着正好3个数字字符的串,例如A123、A348等,但是不匹配A1234。而正则表达式[0-9]{4,6} 匹配连续的任意4个、5个或者6个数字字符。注意:这个元字符不是所有的软件都支持的。

常用的正则表达式

常用的正则表达式主要有以下几种:
  匹配中文字符的正则表达式: [\u4e00-\u9fa5]
  评注:匹配中文还真是个头疼的事,有了这个表达式就好办了哦
  获取日期正则表达式:\d{4}[年|\-|\.]\d{\1-\12}[月|\-|\.]\d{\1-\31}日?
  评注:可用来匹配大多数年月日信息。
  匹配双字节字符(包括汉字在内):[^\x00-\xff]
  评注:可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)
  匹配空白行的正则表达式:\n\s*\r
  评注:可以用来删除空白行
  匹配HTML标记的正则表达式:<(\S*?)[^>]*>.*?</>|<.*? />
  评注:网上流传的版本太糟糕,上面这个也仅仅能匹配部分,对于复杂的嵌套标记依旧无能为力
  匹配首尾空白字符的正则表达式:^\s*|\s*$
  评注:可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式
  匹配Email地址的正则表达式:\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
  评注:表单验证时很实用
  匹配网址URL的正则表达式:[a-zA-z]+://[^\s]*
  评注:网上流传的版本功能很有限,上面这个基本可以满足需求
  匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
  评注:表单验证时很实用
  匹配国内电话号码:\d{4}-\d{7}|\d{3}-\d{8}
  评注:匹配形式如 0511 - 4405222 或 021 - 87888822
  匹配腾讯QQ号:[1-9][0-9]\{4,\}
  评注:腾讯QQ号从1000 0 开始
  匹配中国邮政编码:[1-9]\d{5}(?!\d)
  评注:中国邮政编码为6位数字
  匹配身份证:\d{17}[\d|X]|\d{15}
  评注:中国的身份证为15位或18位
  匹配ip地址:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。
  评注:提取ip地址时有用
  匹配特定数字:
  ^[1-9]\d*$ //匹配正整数
  ^-[1-9]\d*$ //匹配负整数
  ^-?[1-9]\d*$ //匹配整数
  ^[1-9]\d*|0$ //匹配非负整数(正整数 + 0)
  ^-[1-9]\d*|0$ //匹配非正整数(负整数 + 0)
  ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ //匹配正浮点数
  ^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ //匹配负浮点数
  ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$ //匹配浮点数
  ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$ //匹配非负浮点数(正浮点数 + 0)
  ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$ //匹配非正浮点数(负浮点数 + 0)
  评注:处理大量数据时有用,具体应用时注意修正
  匹配特定字符串:
  ^[A-Za-z]+$ //匹配由26个英文字母组成的字符串
  ^[A-Z]+$ //匹配由26个英文字母的大写组成的字符串
  ^[a-z]+$ //匹配由26个英文字母的小写组成的字符串
  ^[A-Za-z0-9]+$ //匹配由数字和26个英文字母组成的字符串
  ^\w+$ //匹配由数字、26个英文字母或者下划线组成的字符串

正则表达式匹配规则

  一切从最基本的开始。模式,是正规表达式最基本的元素,它们是一组描述字符串特征的字符。模式可以很简单,由普通的字符串组成,也可以非常复杂,往往用特殊的字符表示一个范围内的字符、重复出现,或表示上下文。例如:
  ^once
  这个模式包含一个特殊的字符^,表示该模式只匹配那些以once开头的字符串。例如该模式与字符串"once upon a time"匹配,与"There once was a man from NewYork"不匹配。正如如^符号表示开头一样,$符号用来匹配那些以给定模式结尾的字符串。
  bucket$
  这个模式与"Who kept all of this cash in a bucket"匹配,与"buckets"不匹配。字符^和$同时使用时,表示精确匹配(字符串与模式一样)。例如:
  ^bucket$
  只匹配字符串"bucket"。如果一个模式不包括^和$,那么它与任何包含该模式的字符串匹配。例如:模式
  once
  与字符串
  There once was a man from NewYorkWho kept all of his cash in a bucket.
  是匹配的。
  在该模式中的字母(o-n-c-e)是字面的字符,也就是说,他们表示该字母本身,数字也是一样的。其他一些??表符等),要用到转义序列。所有的转义序列都用反斜杠(\)打头。制表符的转义序列是:\t。所以如果我们要检测一个字符串是否以制表符开头,可以用这个模式:
  ^\t
  类似的,用\n表示“新行”,\r表示回车。其他的特殊符号,可以用在前面加上反斜杠,如反斜杠本身用\\表示,句号.用\.表示,以此类推。


正则表达式实例


创建正则表达式

你可以从比较简单的东西入手学习正则表达式。要想全面地掌握怎样构建正则表达式,可以去看JDK文档的java.util.regexPattern类的文档。

字符
B字符B
/xhh16进制值0xhh所表示的字符
/uhhhh16进制值0xhhhh所表示的Unicode字符
/tTab
/n换行符
/r回车符
/f换页符
/eEscape

正则表达式的强大体现在它能定义字符集(character class)。下面是一些最常见的字符集及其定义的方式,此外还有一些预定义的字符集:

字符集
.表示任意一个字符
[abc]表示字符abc中的任意一个(与a|b|c相同)
[^abc]abc之外的任意一个字符(否定)
[a-zA-Z]azAZ当中的任意一个字符(范围)
[abc[hij]]a,b,c,h,i,j中的任意一个字符(与a|b|c|h|i|j相同)(并集)
[a-z&&[hij]]h,i,j中的一个(交集)
/s空格字符(空格键, tab, 换行, 换页, 回车)
/S非空格字符([^/s])
/d一个数字,也就是[0-9]
/D一个非数字的字符,也就是[^0-9]
/w一个单词字符(word character),即[a-zA-Z_0-9]
/W一个非单词的字符,[^/w]

如果你用过其它语言的正则表达式,那么你一眼就能看出反斜杠的与众不同。在其它语言里,"//"的意思是"我只是要在正则表达式里插入一个反斜杠。没什么特别的意思。"但是在Java里,"//"的意思是"我要插入一个正则表达式的反斜杠,所以跟在它后面的那个字符的意思就变了。"举例来说,如果你想表示一个或更多的"单词字符",那么这个正则表达式就应该是"//w+"。如果你要插入一个反斜杠,那就得用""。不过像换行,跳格之类的还是只用一根反斜杠:"/n/t"。

这里只给你讲一个例子;你应该JDK文档的java.util.regex.Pattern加到收藏夹里,这样就能很容易地找到各种正则表达式的模式了。

逻辑运算符
XYX 后面跟着 Y
X|YX或Y
(X)一个"要匹配的组(capturing group)". 以后可以用/i来表示第i个被匹配的组。

边界匹配符
^一行的开始
$一行的结尾
/b一个单词的边界
/B一个非单词的边界
/G前一个匹配的结束

举一个具体一些的例子。下面这些正则表达式都是合法的,而且都能匹配"Rudolph":

Rudolph
[rR]udolph
[rR][aeiou][a-z]ol.*
R.*

数量表示符

"数量表示符(quantifier)"的作用是定义模式应该匹配多少个字符。

  • Greedy(贪婪的): 除非另有表示,否则数量表示符都是greedy的。Greedy的表达式会一直匹配下去,直到匹配不下去为止。(如果你发现表达式匹配的结果与预期的不符),很有可能是因为,你以为表达式会只匹配前面几个字符,而实际上它是greedy的,因此会一直匹配下去。
  • Reluctant(勉强的): 用问号表示,它会匹配最少的字符。也称为lazy, minimal matching, non-greedy, 或ungreedy。
  • Possessive(占有的): 目前只有Java支持(其它语言都不支持)。它更加先进,所以你可能还不太会用。用正则表达式匹配字符串的时候会产生很多中间状态,(一般的匹配引擎会保存这种中间状态,)这样匹配失败的时候就能原路返回了。占有型的表达式不保存这种中间状态,因此也就不会回头重来了。它能防止正则表达式的失控,同时也能提高运行的效率。
GreedyReluctantPossessive匹配
X?X??X?+匹配一个或零个X
X*X*?X*+匹配零或多个X
X+X+?X++匹配一个或多个X
X{n}X{n}?X{n}+匹配正好n个X
X{n,}X{n,}?X{n,}+匹配至少n个X
X{n,m}X{n,m}?X{n,m}+匹配至少n个,至多m个X

再提醒一下,要想让表达式照你的意思去运行,你应该用括号把'X'括起来。比方说:

abc+

似乎这个表达式能匹配一个或若干个'abc',但是如果你真的用它去匹配'abcabcabc'的话,实际上只会找到三个字符。因为这个表达式的意思是'ab'后边跟着一个或多个'c'。要想匹配一个或多个完整的'abc',你应该这样:

(abc)+

正则表达式能轻而易举地把你给耍了;这是一种建立在Java之上的新语言。

CharSequence

JDK 1.4定义了一个新的接口,叫CharSequence。它提供了StringStringBuffer这两个类的字符序列的抽象:

interface CharSequence {
  charAt(int i);
  length();
  subSequence(int start, int end);
  toString();
}

为了实现这个新的CharSequence接口,StringStringBuffer以及CharBuffer都作了修改。很多正则表达式的操作都要拿CharSequence作参数。

PatternMatcher

先给一个例子。下面这段程序可以测试正则表达式是否匹配字符串。第一个参数是要匹配的字符串,后面是正则表达式。正则表达式可以有多个。在Unix/Linux环境下,命令行下的正则表达式还必须用引号。

当你创建正则表达式时,可以用这个程序来判断它是不是会按照你的要求工作。

//: c12:TestRegularExpression.java
// Allows you to easly try out regular expressions.
// {Args: abcabcabcdefabc "abc+" "(abc)+" "(abc){2,}" }
import java.util.regex.*;
public class TestRegularExpression {
  public static void main(String[] args) {
    if(args.length < 2) {
      System.out.println("Usage:/n" +
        "java TestRegularExpression " +
        "characterSequence regularExpression+");
      System.exit(0);
    }
    System.out.println("Input: /"" + args[0] + "/"");
    for(int i = 1; i < args.length; i++) {
      System.out.println(
        "Regular expression: /"" + args[i] + "/"");
      Pattern p = Pattern.compile(args[i]);
      Matcher m = p.matcher(args[0]);
      while(m.find()) {
        System.out.println("Match /"" + m.group() +
          "/" at positions " +
          m.start() + "-" + (m.end() - 1));
      }
    }
  }
} ///:~

Java的正则表达式是由java.util.regexPatternMatcher类实现的。Pattern对象表示经编译的正则表达式。静态的compile( )方法负责将表示正则表达式的字符串编译成Pattern对象。正如上述例程所示的,只要给Patternmatcher( )方法送一个字符串就能获取一个Matcher对象。此外,Pattern还有一个能快速判断能否在input里面找到regex的(注意,原文有误,漏了方法名)

static boolean matches( regex,  input)

以及能返回String数组的split( )方法,它能用regex把字符串分割开来。

只要给Pattern.matcher( )方法传一个字符串就能获得Matcher对象了。接下来就能用Matcher的方法来查询匹配的结果了。

boolean matches()
boolean lookingAt()
boolean find()
boolean find(int start)

matches( )的前提是Pattern匹配整个字符串,而lookingAt( )的意思是Pattern匹配字符串的开头。

find( )

Matcher.find( )的功能是发现CharSequence里的,与pattern相匹配的多个字符序列。例如:

//: c12:FindDemo.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
import java.util.*;
public class FindDemo {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    Matcher m = Pattern.compile("//w+")
      .matcher("Evening is full of the linnet's wings");
    while(m.find())
      System.out.println(m.group());
    int i = 0;
    while(m.find(i)) {
      System.out.print(m.group() + " ");
      i++;
    }
    monitor.expect(new String[] {
      "Evening",
      "is",
      "full",
      "of",
      "the",
      "linnet",
      "s",
      "wings",
      "Evening vening ening ning ing ng g is is s full " +
      "full ull ll l of of f the the he e linnet linnet " +
      "innet nnet net et t s s wings wings ings ngs gs s "
    });
  }
} ///:~

"//w+"的意思是"一个或多个单词字符",因此它会将字符串直接分解成单词。find( )像一个迭代器,从头到尾扫描一遍字符串。第二个find( )是带int参数的,正如你所看到的,它会告诉方法从哪里开始找——即从参数位置开始查找。

Groups

Group是指里用括号括起来的,能被后面的表达式调用的正则表达式。Group 0 表示整个表达式,group 1表示第一个被括起来的group,以此类推。所以;

A(B(C))D

里面有三个group:group 0是ABCD, group 1是BC,group 2是C

你可以用下述Matcher方法来使用group:

public int groupCount( )返回matcher对象中的group的数目。不包括group0。

public String group( ) 返回上次匹配操作(比方说find( ))的group 0(整个匹配)

public String group(int i)返回上次匹配操作的某个group。如果匹配成功,但是没能找到group,则返回null。

public int start(int group)返回上次匹配所找到的,group的开始位置。

public int end(int group)返回上次匹配所找到的,group的结束位置,最后一个字符的下标加一。

下面我们举一些group的例子:

//: c12:Groups.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
public class Groups {
  private static Test monitor = new Test();
  static public final String poem =
    "Twas brillig, and the slithy toves/n" +
    "Did gyre and gimble in the wabe./n" +
    "All mimsy were the borogoves,/n" +
    "And the mome raths outgrabe./n/n" +
    "Beware the Jabberwock, my son,/n" +
    "The jaws that bite, the claws that catch./n" +
    "Beware the Jubjub bird, and shun/n" +
    "The frumious Bandersnatch.";
  public static void main(String[] args) {
    Matcher m =
      Pattern.compile("(?m)(//S+)//s+((//S+)//s+(//S+))$")
        .matcher(poem);
    while(m.find()) {
      for(int j = 0; j <= m.groupCount(); j++)
        System.out.print("[" + m.group(j) + "]");
      System.out.println();
    }
    monitor.expect(new String[]{
      "[the slithy toves]" +
      "[the][slithy toves][slithy][toves]",
      "[in the wabe.][in][the wabe.][the][wabe.]",
      "[were the borogoves,]" +
      "[were][the borogoves,][the][borogoves,]",
      "[mome raths outgrabe.]" +
      "[mome][raths outgrabe.][raths][outgrabe.]",
      "[Jabberwock, my son,]" +
      "[Jabberwock,][my son,][my][son,]",
      "[claws that catch.]" +
      "[claws][that catch.][that][catch.]",
      "[bird, and shun][bird,][and shun][and][shun]",
      "[The frumious Bandersnatch.][The]" +
      "[frumious Bandersnatch.][frumious][Bandersnatch.]"
    });
  }
} ///:~

这首诗是Through the Looking Glass的,Lewis Carroll的"Jabberwocky"的第一部分。可以看到这个正则表达式里有很多用括号括起来的group,它是由任意多个连续的非空字符('/S+')和任意多个连续的空格字符('/s+')所组成的,其最终目的是要捕获每行的最后三个单词;'$'表示一行的结尾。但是'$'通常表示整个字符串的结尾,所以这里要明确地告诉正则表达式注意换行符。这一点是由'(?m)'标志完成的(模式标志会过一会讲解)。

start( )和end( )

如果匹配成功,start( )会返回此次匹配的开始位置,end( )会返回此次匹配的结束位置,即最后一个字符的下标加一。如果之前的匹配不成功(或者没匹配),那么无论是调用start( )还是end( ),都会引发一个IllegalStateException。下面这段程序还演示了matches( )lookingAt( )

//: c12:StartEnd.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
public class StartEnd {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    String[] input = new String[] {
      "Java has regular expressions in 1.4",
      "regular expressions now expressing in Java",
      "Java represses oracular expressions"
    };
    Pattern
      p1 = Pattern.compile("re//w*"),
      p2 = Pattern.compile("Java.*");
    for(int i = 0; i < input.length; i++) {
      System.out.println("input " + i + ": " + input[i]);
      Matcher
        m1 = p1.matcher(input[i]),
        m2 = p2.matcher(input[i]);
      while(m1.find())
        System.out.println("m1.find() '" + m1.group() +
          "' start = "+ m1.start() + " end = " + m1.end());
      while(m2.find())
        System.out.println("m2.find() '" + m2.group() +
          "' start = "+ m2.start() + " end = " + m2.end());
      if(m1.lookingAt()) // No reset() necessary
        System.out.println("m1.lookingAt() start = "
          + m1.start() + " end = " + m1.end());
      if(m2.lookingAt())
        System.out.println("m2.lookingAt() start = "
          + m2.start() + " end = " + m2.end());
      if(m1.matches()) // No reset() necessary
        System.out.println("m1.matches() start = "
          + m1.start() + " end = " + m1.end());
      if(m2.matches())
        System.out.println("m2.matches() start = "
          + m2.start() + " end = " + m2.end());
    }
    monitor.expect(new String[] {
      "input 0: Java has regular expressions in 1.4",
      "m1.find() 'regular' start = 9 end = 16",
      "m1.find() 'ressions' start = 20 end = 28",
      "m2.find() 'Java has regular expressions in 1.4'" +
      " start = 0 end = 35",
      "m2.lookingAt() start = 0 end = 35",
      "m2.matches() start = 0 end = 35",
      "input 1: regular expressions now " +
      "expressing in Java",
      "m1.find() 'regular' start = 0 end = 7",
      "m1.find() 'ressions' start = 11 end = 19",
      "m1.find() 'ressing' start = 27 end = 34",
      "m2.find() 'Java' start = 38 end = 42",
      "m1.lookingAt() start = 0 end = 7",
      "input 2: Java represses oracular expressions",
      "m1.find() 'represses' start = 5 end = 14",
      "m1.find() 'ressions' start = 27 end = 35",
      "m2.find() 'Java represses oracular expressions' " +
      "start = 0 end = 35",
      "m2.lookingAt() start = 0 end = 35",
      "m2.matches() start = 0 end = 35"
    });
  }
} ///:~

注意,只要字符串里有这个模式,find( )就能把它给找出来,但是lookingAt( )matches( ),只有在字符串与正则表达式一开始就相匹配的情况下才能返回truematches( )成功的前提是正则表达式与字符串完全匹配,而lookingAt( )[67]成功的前提是,字符串的开始部分与正则表达式相匹配。

匹配的模式(Pattern flags)

compile( )方法还有一个版本,它需要一个控制正则表达式的匹配行为的参数:

Pattern Pattern.compile(String regex, int flag)
flag的取值范围如下:
编译标志效果
Pattern.CANON_EQ当且仅当两个字符的"正规分解(canonical decomposition)"都完全相同的情况下,才认定匹配。比如用了这个标志之后,表达式"a/u030A"会匹配"?"。默认情况下,不考虑"规范相等性(canonical equivalence)"。
Pattern.CASE_INSENSITIVE
(?i)
默认情况下,大小写不明感的匹配只适用于US-ASCII字符集。这个标志能让表达式忽略大小写进行匹配。要想对Unicode字符进行大小不明感的匹配,只要将UNICODE_CASE与这个标志合起来就行了。
Pattern.COMMENTS
(?x)
在这种模式下,匹配时会忽略(正则表达式里的)空格字符(译者注:不是指表达式里的"//s",而是指表达式里的空格,tab,回车之类)。注释从#开始,一直到这行结束。可以通过嵌入式的标志来启用Unix行模式。
Pattern.DOTALL
(?s)
在这种模式下,表达式'.'可以匹配任意字符,包括表示一行的结束符。默认情况下,表达式'.'不匹配行的结束符。
Pattern.MULTILINE
(?m)
在这种模式下,'^'和'$'分别匹配一行的开始和结束。此外,'^'仍然匹配字符串的开始,'$'也匹配字符串的结束。默认情况下,这两个表达式仅仅匹配字符串的开始和结束。
Pattern.UNICODE_CASE
(?u)
在这个模式下,如果你还启用了CASE_INSENSITIVE标志,那么它会对Unicode字符进行大小写不明感的匹配。默认情况下,大小写不明感的匹配只适用于US-ASCII字符集。
Pattern.UNIX_LINES
(?d)
在这个模式下,只有'/n'才被认作一行的中止,并且与'.','^',以及'$'进行匹配。

在这些标志里面,Pattern.CASE_INSENSITIVEPattern.MULTILINE,以及Pattern.COMMENTS是最有用的(其中Pattern.COMMENTS还能帮我们把思路理清楚,并且/或者做文档)。注意,你可以用在表达式里插记号的方式来启用绝大多数的模式。这些记号就在上面那张表的各个标志的下面。你希望模式从哪里开始启动,就在哪里插记号。

可以用"OR" ('|')运算符把这些标志合使用:

//: c12:ReFlags.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
public class ReFlags {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    Pattern p =  Pattern.compile("^java",
      Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
    Matcher m = p.matcher(
      "java has regex/nJava has regex/n" +
      "JAVA has pretty good regular expressions/n" +
      "Regular expressions are in Java");
    while(m.find())
      System.out.println(m.group());
    monitor.expect(new String[] {
      "java",
      "Java",
      "JAVA"
    });
  }
} ///:~

这样创建出来的正则表达式就能匹配以"java","Java","JAVA"...开头的字符串了。此外,如果字符串分好几行,那它还会对每一行做匹配(匹配始于字符序列的开始,终于字符序列当中的行结束符)。注意,group( )方法仅返回匹配的部分。

split( )

所谓分割是指将以正则表达式为界,将字符串分割成String数组。

String[] split(CharSequence charseq)
String[] split(CharSequence charseq, int limit)

这是一种既快又方便地将文本根据一些常见的边界标志分割开来的方法。

//: c12:SplitDemo.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
import java.util.*;
public class SplitDemo {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    String input =
      "This!!unusual use!!of exclamation!!points";
    System.out.println(Arrays.asList(
      Pattern.compile("!!").split(input)));
    // Only do the first three:
    System.out.println(Arrays.asList(
      Pattern.compile("!!").split(input, 3)));
    System.out.println(Arrays.asList(
      "Aha! String has a split() built in!".split(" ")));
    monitor.expect(new String[] {
      "[This, unusual use, of exclamation, points]",
      "[This, unusual use, of exclamation!!points]",
      "[Aha!, String, has, a, split(), built, in!]"
    });
  }
} ///:~

第二个split( )会限定分割的次数。

正则表达式是如此重要,以至于有些功能被加进了String类,其中包括split( )(已经看到了),matches( )replaceFirst( )以及replaceAll( )。这些方法的功能同PatternMatcher的相同。

替换操作

正则表达式在替换文本方面特别在行。下面就是一些方法:

replaceFirst(String replacement)将字符串里,第一个与模式相匹配的子串替换成replacement

replaceAll(String replacement),将输入字符串里所有与模式相匹配的子串全部替换成replacement

appendReplacement(StringBuffer sbuf, String replacement)sbuf进行逐次替换,而不是像replaceFirst( )replaceAll( )那样,只替换第一个或全部子串。这是个非常重要的方法,因为它可以调用方法来生成replacement(replaceFirst( )replaceAll( )只允许用固定的字符串来充当replacement)。有了这个方法,你就可以编程区分group,从而实现更强大的替换功能。

调用完appendReplacement( )之后,为了把剩余的字符串拷贝回去,必须调用appendTail(StringBuffer sbuf, String replacement)

下面我们来演示一下怎样使用这些替换方法。说明一下,这段程序所处理的字符串是它自己开头部分的注释,是用正则表达式提取出来并加以处理之后再传给替换方法的。

//: c12:TheReplacements.java
import java.util.regex.*;
import java.io.*;
import com.bruceeckel.util.*;
import com.bruceeckel.simpletest.*;
/*! Here's a block of text to use as input to
    the regular expression matcher. Note that we'll
    first extract the block of text by looking for
    the special delimiters, then process the
    extracted block. !*/
public class TheReplacements {
  private static Test monitor = new Test();
  public static void main(String[] args) throws Exception {
    String s = TextFile.read("TheReplacements.java");
    // Match the specially-commented block of text above:
    Matcher mInput =
      Pattern.compile("///*!(.*)!//*/", Pattern.DOTALL)
        .matcher(s);
    if(mInput.find())
      s = mInput.group(1); // Captured by parentheses
    // Replace two or more spaces with a single space:
    s = s.replaceAll(" {2,}", " ");
    // Replace one or more spaces at the beginning of each
    // line with no spaces. Must enable MULTILINE mode:
    s = s.replaceAll("(?m)^ +", "");
    System.out.println(s);
    s = s.replaceFirst("[aeiou]", "(VOWEL1)");
    StringBuffer sbuf = new StringBuffer();
    Pattern p = Pattern.compile("[aeiou]");
    Matcher m = p.matcher(s);
    // Process the find information as you
    // perform the replacements:
    while(m.find())
      m.appendReplacement(sbuf, m.group().toUpperCase());
    // Put in the remainder of the text:
    m.appendTail(sbuf);
    System.out.println(sbuf);
    monitor.expect(new String[]{
      "Here's a block of text to use as input to",
      "the regular expression matcher. Note that we'll",
      "first extract the block of text by looking for",
      "the special delimiters, then process the",
      "extracted block. ",
      "H(VOWEL1)rE's A blOck Of tExt tO UsE As InpUt tO",
      "thE rEgUlAr ExprEssIOn mAtchEr. NOtE thAt wE'll",
      "fIrst ExtrAct thE blOck Of tExt by lOOkIng fOr",
      "thE spEcIAl dElImItErs, thEn prOcEss thE",
      "ExtrActEd blOck. "
    });
  }
} ///:~

我们用前面介绍的TextFile.read( )方法来打开和读取文件。mInput的功能是匹配'/*!' 和 '!*/' 之间的文本(注意一下分组用的括号)。接下来,我们将所有两个以上的连续空格全都替换成一个,并且将各行开头的空格全都去掉(为了让这个正则表达式能对所有的行,而不仅仅是第一行起作用,必须启用多行模式)。这两个操作都用了StringreplaceAll( )(这里用它更方便)。注意,由于每个替换只做一次,因此除了预编译Pattern之外,程序没有额外的开销。

replaceFirst( )只替换第一个子串。此外,replaceFirst( )replaceAll( )只能用常量(literal)来替换,所以如果你每次替换的时候还要进行一些操作的话,它们是无能为力的。碰到这种情况,你得用appendReplacement( ),它能让你在进行替换的时候想写多少代码就写多少。在上面那段程序里,创建sbuf的过程就是选group做处理,也就是用正则表达式把元音字母找出来,然后换成大写的过程。通常你得在完成全部的替换之后才调用appendTail( ),但是如果要模仿replaceFirst( )(或"replace n")的效果,你也可以只替换一次就调用appendTail( )。它会把剩下的东西全都放进sbuf

你还可以在appendReplacement( )replacement参数里用"$g"引用已捕获的group,其中'g' 表示group的号码。不过这是为一些比较简单的操作准备的,因而其效果无法与上述程序相比。

reset( )

此外,还可以用reset( )方法给现有的Matcher对象配上个新的CharSequence

//: c12:Resetting.java
import java.util.regex.*;
import java.io.*;
import com.bruceeckel.simpletest.*;
public class Resetting {
  private static Test monitor = new Test();
  public static void main(String[] args) throws Exception {
    Matcher m = Pattern.compile("[frb][aiu][gx]")
      .matcher("fix the rug with bags");
    while(m.find())
      System.out.println(m.group());
    m.reset("fix the rig with rags");
    while(m.find())
      System.out.println(m.group());
    monitor.expect(new String[]{
      "fix",
      "rug",
      "bag",
      "fix",
      "rig",
      "rag"
    });
  }
} ///:~

如果不给参数,reset( )会把Matcher设到当前字符串的开始处。

正则表达式与Java I/O

到目前为止,你看到的都是用正则表达式处理静态字符串的例子。下面我们来演示一下怎样用正则表达式扫描文件并且找出匹配的字符串。受Unix的grep启发,我写了个JGrep.java,它需要两个参数:文件名,以及匹配字符串用的正则表达式。它会把匹配这个正则表达式那部分内容及其所属行的行号打印出来。

//: c12:JGrep.java
// A very simple version of the "grep" program.
// {Args: JGrep.java "//b[Ssct]//w+"}
import java.io.*;
import java.util.regex.*;
import java.util.*;
import com.bruceeckel.util.*;
public class JGrep {
  public static void main(String[] args) throws Exception {
    if(args.length < 2) {
      System.out.println("Usage: java JGrep file regex");
      System.exit(0);
    }
    Pattern p = Pattern.compile(args[1]);
    // Iterate through the lines of the input file:
    ListIterator it = new TextFile(args[0]).listIterator();
    while(it.hasNext()) {
      Matcher m = p.matcher((String)it.next());
      while(m.find())
        System.out.println(it.nextIndex() + ": " +
          m.group() + ": " + m.start());
    }
  }
} ///:~

文件是用TextFile打开的(本章的前半部分讲的)。由于TextFile会把文件的各行放在ArrayList里面,而我们又提取了一个ListIterator,因此我们可以在文件的各行当中自由移动(既能向前也可以向后)。

每行都会有一个Matcher,然后用find( )扫描。注意,我们用ListIterator.nextIndex( )跟踪行号。

测试参数是JGrep.java和以[Ssct]开头的单词。

还需要StringTokenizer吗?

看到正则表达式能提供这么强大的功能,你可能会怀疑,是不是还需要原先的StringTokenizer。JDK 1.4以前,要想分割字符串,只有用StringTokenizer。但现在,有了正则表达式之后,它就能做得更干净利索了。

//: c12:ReplacingStringTokenizer.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
import java.util.*;
public class ReplacingStringTokenizer {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    String input = "But I'm not dead yet! I feel happy!";
    StringTokenizer stoke = new StringTokenizer(input);
    while(stoke.hasMoreElements())
      System.out.println(stoke.nextToken());
    System.out.println(Arrays.asList(input.split(" ")));
    monitor.expect(new String[] {
      "But",
      "I'm",
      "not",
      "dead",
      "yet!",
      "I",
      "feel",
      "happy!",
      "[But, I'm, not, dead, yet!, I, feel, happy!]"
    });
  }
} ///:~

有了正则表达式,你就能用更复杂的模式将字符串分割开来——要是交给StringTokenizer的话,事情会麻烦得多。我可以很有把握地说,正则表达式可以取代StringTokenizer

要想进一步学习正则表达式,建议你看Mastering Regular Expression, 2nd Edition,作者Jeffrey E. F. Friedl (O'Reilly, 2002)。

总结

Java的I/O流类库应该能满足你的基本需求:你可以用它来读写控制台,文件,内存,甚至是Internet。你还可以利用继承来创建新的输入和输出类型。你甚至可以利用Java会自动调用对象的toString( )方法的特点(Java仅有的"自动类型转换"),通过重新定义这个方法,来对要传给流的对象做一个简单的扩展。

但是Java的I/O流类库及其文档还是留下了一些缺憾。比方说你打开一个文件往里面写东西,但是这个文件已经有了,这么做会把原先的内容给覆盖了 。这时要是能有一个异常就好了——有些编程语言能让你规定只能往新建的文件里输出。看来Java是要你用File对象来判断文件是否存在,因为如果你用FileOutputStreamFileWriter的话,文件就会被覆盖了。

我对I/O流类库的评价是比较矛盾的;它确实能干很多事情,而且做到了跨平台。但是如果你不懂decorator模式,就会觉得这种设计太难理解了,所以无论是对老师还是学生,都得多花精力。此外这个类库也不完整,否则我也用不着去写TextFile了。此外它没有提供格式化输出的功能,而其他语言都已经提供了这种功能。

但是,一旦你真正理解了decorator模式,并且能开始灵活运用这个类库的时候,你就能感受到这种设计的好处了。这时多写几行代码就算不了什么了。



 

java正则表达式4例


java正则表达式(例子:§ 简单的单词替换 § 电子邮件确认 § 从文件中删除控制字符 §查找文件
)
                                 正则表达式和Java编程语言by Dana Nourie and Mike McCloskey2001年8月2002年4月修订
     应用程序常常需要有文本处理功能,比如单词查找、电子邮件确认或XML文档 集成。这通常会涉及到模式匹配。Perl、sed或awk等语言通过使用正则表达式来 改善模式匹配,正则表达式是一串字符,它所定义的模式可用来查找匹配的文本。 为了使用JavaTM编程语言进行模式匹配,需 要使用带有许多charAt子字串的StringTokenizer 类,读取字母或符号以便处理文本。这常常导致复杂或凌乱的代码。现在不一样了。2平台标准版(J2SETM)1.4版包含一个名 为java.util.regex的新软件包,使得使用正则表达式成为可能。 目前的功能包括元字符的使用,它赋予正则表达式极大的灵活性本文概括地介绍了正则表达式的使用,并详细解释如何利用 java.util.regex软件包来使用正则表达式,用以下常见情形作为
 例子:§ 简单的单词替换 § 电子邮件确认 § 从文件中删除控制字符 §查找文件
 为了编译这些例子中的代码和在应用程序中使用正则表达式,需要安装 J2SE 1.4版。构造正则表达式正则表达式是一种字符模式,它描述的是一组字符串。你可以使用 java.util.regex软件包,查找、显示或修改输入序列中出现的 某个模式的一部分或全部。正则表达式最简单的形式是一个精确的字符串,比如“Java”或 “programming”。正则表达式匹配还允许你检查一个字符串是否符合某个具体的 句法形式,比如是不是一个电子邮件地址。为了编写正则表达式,普通字符和特殊字符都要使用://$ ^ . *
+ ? [/' /']
//.        
正则表达式中出现的任何其他字符都是普通字符,除非它前面有个 //。特殊字符有着特别的用处。例如,.可匹配除了换行符之外的任意字符。与 s.n这样的正则表达式匹配的是任何三个字符的、以s 开始以n结束的字符串,包括sun和son 。在正则表达式中有许多特殊字符,可以查找一行开头的单词,忽略大小写或 大小写敏感的单词,还有特殊字符可以给出一个范围,比如a-e表 示从a到e的任何字母。使用这个新软件包的正则表达式用法与Perl类似,所以如果你熟悉Perl中正则 表达式的使用,就可以在Java语言中使用同样的表达式语法。如果你不熟悉正则 表达式,下面是一些入门的例子:构造 匹配于
  
字符  
x 字符 x
反斜线字符
//0n 八进制值的字符0n (0 <= n <= 7)
//0nn 八进制值的字符 0nn (0 <= n <= 7)
//0mnn 八进制值的字符0mnn 0mnn (0 <= m <= 3, 0 <= n <= 7)
//xhh 十六进制值的字符0xhh
//uhhhh 十六进制值的字符0xhhhh
//t 制表符(/'//u0009/')
//n 换行符 (/'//u000A/')
//r 回车符 (/'//u000D/')
//f 换页符 (/'//u000C/')
//a 响铃符 (/'//u0007/')
//e 转义符 (/'//u001B/')
//cx T对应于x的控制字符 x
  
字符类
[abc] a, b, or c (简单类)
[^abc] 除了a、b或c之外的任意 字符(求反)
[a-zA-Z] a到z或A到Z ,包含(范围)
[a-z-[bc]] a到z,除了b和c : [ad-z](减去)
[a-z-[m-p]] a到z,除了m到 p: [a-lq-z]
[a-z-[^def]] d, e, 或 f
  
预定义的字符类
. 任意字符(也许能与行终止符匹配,也许不能)
//d 数字: [0-9]
//D 非数字: [^0-9]
//s 空格符: [ //t//n//x0B//f//r]
//S 非空格符: [^//s]
//w 单词字符: [a-zA-Z_0-9]
//W 非单词字符: [^//w]
有关进一步的详情和例子,请参阅 Pattern类的文档。

类和方法
下面的类根据正则表达式指定的模式,与字符序列进行匹配。
Pattern类
Pattern类的实例表示以字符串形式指定的正则表达式,其语 法类似于Perl所用的语法。
用字符串形式指定的正则表达式,必须先编译成Pattern类的 实例。生成的模式用于创建Matcher对象,它根据正则表达式与任 意字符序列进行匹配。多个匹配器可以共享一个模式,因为它是非专属的。
用compile方法把给定的正则表达式编译成模式,然后用 matcher方法创建一个匹配器,这个匹配器将根据此模式对给定输 入进行匹配。pattern 方法可返回编译这个模式所用的正则表达 式。
split方法是一种方便的方法,它在与此模式匹配的位置将给 定输入序列切分开。下面的例子演示了:
/*
* 用split对以逗号和/或空格分隔的输入字符串进行切分。
*/
import java.util.regex.*;

public class Splitter {
public static void main(String[] args) throws Exception {
// Create a pattern to match breaks
Pattern p = Pattern.compile(/"[,s]+/");
// Split input with the pattern
String[] result = 
 p.split(/"one,two, three   four ,  five/");
for (int i=0; i<result.length; i++)
System.out.println(result[i]);
}
}
Matcher类 
Matcher类的实例用于根据给定的字符串序列模式,对字符序 列进行匹配。使用CharSequence接口把输入提供给匹配器,以便 支持来自多种多样输入源的字符的匹配。
通过调用某个模式的matcher方法,从这个模式生成匹配器。 匹配器创建之后,就可以用它来执行三类不同的匹配操作:
§ matches方法试图根据此模式,对整个输入序列进行匹配。 
§ lookingAt方法试图根据此模式,从开始处对输入序列进 行匹配。 
§ find方法将扫描输入序列,寻找下一个与模式匹配的地方。 
这些方法都会返回一个表示成功或失败的布尔值。如果匹配成功,通过查询 匹配器的状态,可以获得更多的信息
这个类还定义了用新字符串替换匹配序列的方法,这些字符串的内容如果需 要的话,可以从匹配结果推算得出。
appendReplacement方法先添加字符串中从当前位置到下一个 匹配位置之间的所有字符,然后添加替换值。appendTail添加的 是字符串中从最后一次匹配的位置之后开始,直到结尾的部分。
例如,在字符串blahcatblahcatblah中,第一个 appendReplacement添加blahdog。第二个 appendReplacement添加blahdog,然后 appendTail添加blah,就生成了: blahdogblahdogblah。请参见示例 简单的单词替换。
CharSequence接口
CharSequence接口为许多不同类型的字符序列提供了统一的只 读访问。你提供要从不同来源搜索的数据。用String, StringBuffer 和CharBuffer实现CharSequence,,这样就可以很 容易地从它们那里获得要搜索的数据。如果这些可用数据源没一个合适的,你可 以通过实现CharSequence接口,编写你自己的输入源。
Regex情景范例
以下代码范例演示了java.util.regex软件包在各种常见情形 下的用法:
简单的单词替换
/*
* This code writes /"One dog, two dogs in the yard./"
* to the standard-output stream:
*/
import java.util.regex.*;

public class Replacement {
public static void main(String[] args) 
 throws Exception {
// Create a pattern to match cat
Pattern p = Pattern.compile(/"cat/");
// Create a matcher with an input string
Matcher m = p.matcher(/"one cat,/" +
   /" two cats in the yard/");
StringBuffer sb = new StringBuffer();
boolean result = m.find();
// Loop through and create a new String 
// with the replacements
while(result) {
m.appendReplacement(sb, /"dog/");
result = m.find();
}
// Add the last segment of input to 
// the new String
m.appendTail(sb);
System.out.println(sb.toString());
}
}
电子邮件确认
以下代码是这样一个例子:你可以检查一些字符是不是一个电子邮件地址。 它并不是一个完整的、适用于所有可能情形的电子邮件确认程序,但是可以在 需要时加上它。
/*
* Checks for invalid characters
* in email addresses
*/
public class EmailValidation {
public static void main(String[] args) 
 throws Exception {
 
String input = /"@sun.com/";
//Checks for email addresses starting with
//inappropriate symbols like dots or @ signs.
Pattern p = Pattern.compile(/"^.|^@/");
Matcher m = p.matcher(input);
if (m.find())
System.err.println(/"Email addresses don/'t start/" +
/" with dots or @ signs./");
//Checks for email addresses that start with
//www. and prints a message if it does.
p = Pattern.compile(/"^www./");
m = p.matcher(input);
if (m.find()) {
System.out.println(/"Email addresses don/'t start/" +
/" with ///"www.///", only web pages do./");
}
p = Pattern.compile(/"[^A-Za-z0-9.@_-~#]+/");
m = p.matcher(input);
StringBuffer sb = new StringBuffer();
boolean result = m.find();
boolean deletedIllegalChars = false;

while(result) {
deletedIllegalChars = true;
m.appendReplacement(sb, /"/");
result = m.find();
}

// Add the last segment of input to the new String
m.appendTail(sb);

input = sb.toString();

if (deletedIllegalChars) {
System.out.println(/"It contained incorrect characters/" +
   /" , such as spaces or commas./");
}
}
}
从文件中删除控制字符
/* This class removes control characters from a named
*  file.
*/
import java.util.regex.*;
import java.io.*;

public class Control {
public static void main(String[] args) 
 throws Exception {
 
//Create a file object with the file name
//in the argument:
File fin = new File(/"fileName1/");
File fout = new File(/"fileName2/");
//Open and input and output stream
FileInputStream fis = 
  new FileInputStream(fin);
FileOutputStream fos = 
new FileOutputStream(fout);

BufferedReader in = new BufferedReader(
   new InputStreamReader(fis));
BufferedWriter out = new BufferedWriter(
  new OutputStreamWriter(fos));

// The pattern matches control characters
Pattern p = Pattern.compile(/"{cntrl}/");
Matcher m = p.matcher(/"/");
String aLine = null;
while((aLine = in.readLine()) != null) {
m.reset(aLine);
//Replaces control characters with an empty
//string.
String result = m.replaceAll(/"/");
out.write(result);
out.newLine();
}
in.close();
out.close();
}
}
文件查找 
/*
* Prints out the comments found in a .java file.
*/
import java.util.regex.*;
import java.io.*;
import java.nio.*;
import java.nio.charset.*;
import java.nio.channels.*;

public class CharBufferExample {
public static void main(String[] args) throws Exception {
// Create a pattern to match comments
Pattern p = 
Pattern.compile(/"//.*$/", Pattern.MULTILINE);

// Get a Channel for the source file
File f = new File(/"Replacement.java/");
FileInputStream fis = new FileInputStream(f);
FileChannel fc = fis.getChannel();

// Get a CharBuffer from the source file
ByteBuffer bb = 
fc.map(FileChannel.MAP_RO, 0, (int)fc.size());
Charset cs = Charset.forName(/"8859_1/");
CharsetDecoder cd = cs.newDecoder();
CharBuffer cb = cd.decode(bb);

// Run some matches
Matcher m = p.matcher(cb);
while (m.find())
System.out.println(/"Found comment: /"+m.group());
}
}
结论
现在Java编程语言中的模式匹配和许多其他编程语言一样灵活了。可以在应 用程序中使用正则表达式,确保数据在输入数据库或发送给应用程序其他部分之 前,格式是正确的,正则表达式还可以用于各种各样的管理性工作。简而言之, 在Java编程中,可以在任何需要模式匹配的地方使用正则表达式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值