在sun的java jdk 1.40版本中,java自带了支持正则表达式的包,本文就抛砖引玉地介绍了如何使用java.util.regex包。
可粗略估计一下,除了偶尔用linux的外,其他linu x用户都会遇到正则表达式。正则表达式是个极端强大工具,而且在字符串模式-匹配和字符串模式-替换方面富有弹性。在unix世界里,正则表达式几乎没有什么限制,可肯定的是,它应用非常之广泛。
正则表达式的引擎已被许多普通的unix工具所实现,包括grep,awk,vi和emacs等。此外,许多使用比较广泛的脚本语言也支持正则表达式,比如python,tcl,javascript,以及最著名的perl。
我很早以前就是个perl方面的黑客,如果你和我一样话,你也会非常依赖你手边的这些强大的text-munging工具。近几年来,像其他程序开发者一样,我也越来越关注java的开发。
java作为一种开发语言,有许多值得推荐的地方,但是它一直以来没有自带对正则表达式的支持。直到最近,借助于第三方的类库,java开始支持正则表达式,但这些第三方的类库都不一致、兼容性差,而且维护代码起来很糟糕。这个缺点,对我选择java作为首要的开发工具来说,一直是个巨大的顾虑之处。
你可以想象,当我知道sun的java jdk 1.40版本包含了java.util.regex(一个完全开放、自带的正则表达式包)时,是多么的高兴!很搞笑的说,我花好些时间去挖掘这个被隐藏起来的宝石。我非常惊奇的是,java这样的一个很大改进(自带了java.util.regex包)为什么不多公开一点呢?!
最近,java双脚都跳进了正则表达式的世界。java.util.regex包在支持正则表达也有它的过人之处,另外java也提供详细的相关说明文档。使得朦朦胧胧的regex神秘景象也慢慢被拨开。有一些正则表达式的构成(可能最显著的是,在于糅合了字符类库)在perl都找不到。
在regex包中,包括了两个类,pattern(模式类)和matcher(匹配器类)。pattern类是用来表达和陈述所要搜索模式的对象,matcher类是真正影响搜索的对象。另加一个新的例外类,patternsyntaxexception,当遇到不合法的搜索模式时,会抛出例外。
即使对正则表达式很熟悉,你会发现,通过java使用正则表达式也相当简单。要说明的一点是,对那些被perl的单行匹配所宠坏的perl狂热爱好者来说,在使用java的regex包进行替换操作时,会比他们所以前常用的方法费事些。
本文的局限之处,它不是一篇正则表达式用法的完全教程。如果读者要对正则表达进一步了解的话,推荐阅读jeffrey frieldl的mastering regular expressions,该书由o'reilly出版社出版。我下面就举一些例子来教读者如何使用正则表达式,以及如何更简单地去使用它。
设计一个简单的表达式来匹配任何电话号码数字可能是比较复杂的事情,原因在于电话号码格式有很多种情况。所有必须选择一个比较有效的模式。比如:(212) 555-1212, 212-555-1212和212 555 1212,某些人会认为它们都是等价的。
首先让我们构成一个正则表达式。为简单起见,先构成一个正则表达式来识别下面格式的电话号码数字:(nnn)nnn-nnnn。
第一步,创建一个pattern对象来匹配上面的子字符串。一旦程序运行后,如果需要的话,可以让这个对象一般化。匹配上面格式的正则表达可以这样构成:(/d{3})/s/d{3}-/d{4},其中/d单字符类型用来匹配从0到9的任何数字,另外{3}重复符号,是个简便的记号,用来表示有3个连续的数字位,也等效于(/d/d/d)。/s也另外一个比较有用的单字符类型,用来匹配空格,比如space键,tab键和换行符。
是不是很简单?但是,如果把这个正则表达式的模式用在java程序中,还要做两件事。对java的解释器来说,在反斜线字符(/)前的字符有特殊的含义。在java中,与regex有关的包,并不都能理解和识别反斜线字符(/),尽管可以试试看。但为避免这一点,即为了让反斜线字符(/)在模式对象中被完全地传递,应该用双反斜线字符(/)。此外圆括号在正则表达中两层含义,如果想让它解释为字面上意思(即圆括号),也需要在它前面用双反斜线字符(/)。也就是像下面的一样:
//(//d{3}//)//s//d{3}-//d{4}
现在介绍怎样在java代码中实现刚才所讲的正则表达式。要记住的事,在用正则表达式的包时,在你所定义的类前需要包含该包,也就是这样的一行:
import java.util.regex.*;
下面的一段代码实现的功能是,从一个文本文件逐行读入,并逐行搜索电话号码数字,一旦找到所匹配的,然后输出在控制台。
bufferedreader in; pattern pattern = pattern.compile("//(//d{3}//)//s//d{3}-//d{4}"); in = new bufferedreader(new filereader("phone")); string s; while ((s = in.readline()) != null) { matcher matcher = pattern.matcher(s); if (matcher.find()) { system.out.println(matcher.group()); } } in.close();
对那些熟悉用python或javascript来实现正则表达式的人来说,这段代码很平常。在python和javascript这些语言中,或者其他的语言,这些正则表达式一旦明确地编译过后,你想用到哪里都可以。与perl的单步匹配相比,看起来多多做了些工作,但这并不很费事。
find()方法,就像你所想象的,用来搜索与正则表达式相匹配的任何目标字符串,group()方法,用来返回包含了所匹配文本的字符串。应注意的是,上面的代码,仅用在每行只能含有一个匹配的电话号码数字字符串时。可以肯定的说,java的正则表达式包能用在一行含有多个匹配目标时的搜索。本文的原意在于举一些简单的例子来激起读者进一步去学习java自带的正则表达式包,所以对此就没有进行深入的探讨。
这相当漂亮吧! 但是很遗憾的是,这仅是个电话号码匹配器。很明显,还有两点可以改进。如果在电话号码的开头,即区位号和本地号码之间可能会有空格。我们也可匹配这些情况,则通过在正则表达式中加入/s?来实现,其中?元字符表示在模式可能有0或1个空格符。
第二点是,在本地号码位的前三位和后四位数字间有可能是空格符,而不是连字号,更有胜者,或根本就没有分隔符,就是7位数字连在一起。对这几种情况,我们可以用(-|)?来解决。这个结构的正则表达式就是转换器,它能匹配上面所说的几种情况。在()能含有管道符|时,它能匹配是否含有空格符或连字符,而尾部的?元字符表示是否根本没有分隔符的情况。
最后,区位号也可能没有包含在圆括号内,对此可以简单地在圆括号后附上?元字符,但这不是一个很好的解决方法。因为它也包含了不配对的圆括号,比如"(555" 或 "555)"。相反,我们可以通过另一种转换器来强迫让电话号码是否带有有圆括号:(/(/d{3}/)|/d{3})。如果我们把上面代码中的正则表达式用这些改进后的来替换的话,上面的代码就成了一个非常有用的电话号码数字匹配器:
pattern pattern =
pattern.compile("(//(//d{3}//)|//d{3})//s?//d{3}(-|)?//d{4}");
可以确定的是,你可以自己试着进一步改进上面的代码。
现在看看第二个例子,它是从friedl的中改编过来的。其功能是用来检查文本文件中是否有重复的单词,这在印刷排版中会经常遇到,同样也是个语法检查器的问题。
匹配单词,像其他的一样,也可以通过好几种的正则表达式来完成。可能最直接的是/b/w+/b,其优点在于只需用少量的regex元字符。其中/w元字符用来匹配从字母a到u的任何字符。+元字符表示匹配匹配一次或多次字符,/b元字符是用来说明匹配单词的边界,它可以是空格或任何一种不同的标点符号(包括逗号,句号等)。
现在,我们怎样来检查一个给定的单词是否被重复了三次?为完成这个任务,需充分利用正则表达式中的所熟知的向后扫描。如前面提到的,圆括号在正则表达式中有几种不同的用法,一个就是能提供组合类型,组合类型用来保存所匹配的结果或部分匹配的结果(以便后面能用到),即使遇到有相同的模式。在同样的正则表达中,可能(也通常期望)不止有一个组合类型。在第n个组合类型中匹配结果可以通过向后扫描来获取到。向后扫描使得搜索重复的单词非常简单:/b(/w+)/s+/1/b。
圆括号形成了一个组合类型,在这个正则表示中它是第一组合类型(也是仅有的一个)。向后扫描/1,指的是任何被/w+所匹配的单词。我们的正则表达式因此能匹配这样的单词,它有一个或多个空格符,后面还跟有一个与此相同的单词。注意的是,尾部的定位类型(/b)必不可少,它可以防止发生错误。如果我们想匹配"paris in the the spring",而不是匹配"java's regex package is the theme of this article"。根据java现在的格式,则上面的正则表达式就是:pattern pattern =pattern.compile("//b(//w+)//s+//1//b");
最后进一步的修改是让我们的匹配器对大小写敏感。比如,下面的情况:"the the theme of this article is the java's regex package.",这一点在regex中能非常简单地实现,即通过使用在pattern类中预定义的静态标志case_insensitive :
pattern pattern =pattern.compile("//b(//w+)//s+//1//b",
pattern.case_insensitive);
有关正则表达式的话题是非常丰富,而且复杂的,用java来实现也非常广泛,则需要对regex包进行的彻底研究,我们在这里所讲的只是冰山一角。即使你对正则表达式比较陌生,使用regex包后会很快发现它强大功能和可伸缩性。如果你是个来自perl或其他语言王国的老练的正则表达式的黑客,使用过regex包后,你将会安心地投入到java的世界,而放弃其他的工具,并把java的regex包看成是手边必备的利器。
charsequence
jdk 1.4定义了一个新的接口,叫charsequence。它提供了string和stringbuffer这两个类的字符序列的抽象:
charsequence { charat( i); length(); subsequence( start, end); tostring(); }
为了实现这个新的charsequence接口,string,stringbuffer以及charbuffer都作了修改。很多正则表达式的操作都要拿charsequence作参数。
pattern和matcher
先给一个例子。下面这段程序可以测试正则表达式是否匹配字符串。第一个参数是要匹配的字符串,后面是正则表达式。正则表达式可以有多个。在unix/linux环境下,命令行下的正则表达式还必须用引号。
java.util.regex.*; testregularexpression { main(string[] args) { (args.length < 2) { system.out.println( + + ); system.exit(0); } system.out.println(/); ( i = 1; i < args.length; i++) { system.out.println( /); pattern p = pattern.compile(args[i]); matcher m = p.matcher(args[0]); (m.find()) { system.out.println(" + m.group() + at positions " + m.start() + + (m.end() - 1)); } } } }
java的正则表达式是由java.util.regex的pattern和matcher类实现的。pattern对象表示经编译的正则表达式。静态的compile( )方法负责将表示正则表达式的字符串编译成pattern对象。正如上述例程所示的,只要给pattern的matcher( )方法送一个字符串就能获取一个matcher对象。此外,pattern还有一个能快速判断能否在input里面找到regex的
matches(?regex, ?input)
以及能返回string数组的split( )方法,它能用regex把字符串分割开来。
只要给pattern.matcher( )方法传一个字符串就能获得matcher对象了。接下来就能用matcher的方法来查询匹配的结果了。
matches()
lookingat()
find()
find( start)
matches( )的前提是pattern匹配整个字符串,而lookingat( )的意思是pattern匹配字符串的开头。
find( )
matcher.find( )的功能是发现charsequence里的,与pattern相匹配的多个字符序列。例如:
java.util.regex.*; com.bruceeckel.simpletest.*; java.util.*; finddemo { test monitor = test(); main(string[] args) { matcher m = pattern.compile() .matcher(); (m.find()) system.out.println(m.group()); i = 0; (m.find(i)) { system.out.print(m.group() + ); i++; } monitor.expect( string[] { , , , , , , , , + + }); } }
"//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的结束位置,最后一个字符的下标加一。
java.util.regex.*; com.bruceeckel.simpletest.*; groups { test monitor = test(); string poem = + + + + + + + ; main(string[] args) { matcher m = pattern.compile() .matcher(poem); (m.find()) { ( j = 0; j <= m.groupcount(); j++) system.out.print( + m.group(j) + ); system.out.println(); } monitor.expect( string[]{ + , , + , + , + , + , , + }); } }
这首诗是through the looking glass的,lewis carroll的"jabberwocky"的第一部分。可以看到这个正则表达式里有很多用括号括起来的group,它是由任意多个连续的非空字符('/s+')和任意多个连续的空格字符('/s+')所组成的,其最终目的是要捕获每行的最后三个单词;'$'表示一行的结尾。但是'$'通常表示整个字符串的结尾,所以这里要明确地告诉正则表达式注意换行符。这一点是由'(?m)'标志完成的(模式标志会过一会讲解)。
start( )和end( )
如果匹配成功,start( )会返回此次匹配的开始位置,end( )会返回此次匹配的结束位置,即最后一个字符的下标加一。如果之前的匹配不成功(或者没匹配),那么无论是调用start( )还是end( ),都会引发一个illegalstateexception。下面这段程序还演示了matches( )和lookingat( ):
java.util.regex.*; com.bruceeckel.simpletest.*; startend { test monitor = test(); main(string[] args) { string[] input = string[] { , , }; pattern p1 = pattern.compile(), p2 = pattern.compile(); ( i = 0; i < input.length; i++) { system.out.println( + i + + input[i]); matcher m1 = p1.matcher(input[i]), m2 = p2.matcher(input[i]); (m1.find()) system.out.println( + m1.group() + + m1.start() + + m1.end()); (m2.find()) system.out.println( + m2.group() + + m2.start() + + m2.end()); (m1.lookingat()) system.out.println( + m1.start() + + m1.end()); (m2.lookingat()) system.out.println( + m2.start() + + m2.end()); (m1.matches()) system.out.println( + m1.start() + + m1.end()); (m2.matches()) system.out.println( + m2.start() + + m2.end()); } monitor.expect( string[] { , , , + , , , + , , , , , , , , , + , , }); } }
注意,只要字符串里有这个模式,find( )就能把它给找出来,但是lookingat( )和matches( ),只有在字符串与正则表达式一开始就相匹配的情况下才能返回true。matches( )成功的前提是正则表达式与字符串完全匹配,而lookingat( )成功的前提是,字符串的开始部分与正则表达式相匹配。
匹配的模式(pattern flags)
compile( )方法还有一个版本,它需要一个控制正则表达式的匹配行为的参数:
pattern pattern.compile(string regex, 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_insensitive,pattern.multiline,以及pattern.comments是最有用的(其中pattern.comments还能帮我们把思路理清楚,并且/或者做文档)。注意,你可以用在表达式里插记号的方式来启用绝大多数的模式。这些记号就在上面那张表的各个标志的下面。你希望模式从哪里开始启动,就在哪里插记号。
可以用"or" ('|')运算符把这些标志合使用:
java.util.regex.*; com.bruceeckel.simpletest.*; reflags { test monitor = test(); main(string[] args) { pattern p = pattern.compile(, pattern.case_insensitive | pattern.multiline); matcher m = p.matcher( + + ); (m.find()) system.out.println(m.group()); monitor.expect( string[] { , , }); } }
这样创建出来的正则表达式就能匹配以"java","java","java"...开头的字符串了。此外,如果字符串分好几行,那它还会对每一行做匹配(匹配始于字符序列的开始,终于字符序列当中的行结束符)。注意,group( )方法仅返回匹配的部分。
split( )
所谓分割是指将以正则表达式为界,将字符串分割成string数组。
string[] split(charsequence charseq)
string[] split(charsequence charseq, limit)
这是一种既快又方便地将文本根据一些常见的边界标志分割开来的方法。
java.util.regex.*; com.bruceeckel.simpletest.*; java.util.*; splitdemo { test monitor = test(); main(string[] args) { string input = ; system.out.println(arrays.aslist( pattern.compile().split(input))); system.out.println(arrays.aslist( pattern.compile().split(input, 3))); system.out.println(arrays.aslist( .split())); monitor.expect( string[] { , , }); } }
第二个split( )会限定分割的次数。
正则表达式是如此重要,以至于有些功能被加进了string类,其中包括split( )(已经看到了),matches( ),replacefirst( )以及replaceall( )。这些方法的功能同pattern和matcher的相同。
替换操作
正则表达式在替换文本方面特别在行。下面就是一些方法:
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)。
下面我们来演示一下怎样使用这些替换方法。说明一下,这段程序所处理的字符串是它自己开头部分的注释,是用正则表达式提取出来并加以处理之后再传给替换方法的。
java.util.regex.*; java.io.*; com.bruceeckel.util.*; com.bruceeckel.simpletest.*; thereplacements { test monitor = test(); main(string[] args) exception { string s = textfile.read(); matcher minput = pattern.compile(, pattern.dotall) .matcher(s); (minput.find()) s = minput.group(1); s = s.replaceall(, ); s = s.replaceall(, ); system.out.println(s); s = s.replacefirst(, ); stringbuffer sbuf = stringbuffer(); pattern p = pattern.compile(); matcher m = p.matcher(s); (m.find()) m.appendreplacement(sbuf, m.group().touppercase()); m.appendtail(sbuf); system.out.println(sbuf); monitor.expect( string[]{ , , , , , , , , , }); } }
用textfile.read( )方法来打开和读取文件。minput的功能是匹配'/*!' 和 '!*/' 之间的文本(注意一下分组用的括号)。接下来,我们将所有两个以上的连续空格全都替换成一个,并且将各行开头的空格全都去掉(为了让这个正则表达式能对所有的行,而不仅仅是第一行起作用,必须启用多行模式)。这两个操作都用了string的replaceall( )(这里用它更方便)。注意,由于每个替换只做一次,因此除了预编译pattern之外,程序没有额外的开销。
replacefirst( )只替换第一个子串。此外,replacefirst( )和replaceall( )只能用常量(literal)来替换,所以如果每次替换的时候还要进行一些操作的话,它们是无能为力的。碰到这种情况,得用appendreplacement( ),它能在进行替换的时候想写多少代码就写多少。在上面那段程序里,创建sbuf的过程就是选group做处理,也就是用正则表达式把元音字母找出来,然后换成大写的过程。通常你得在完成全部的替换之后才调用appendtail( ),但是如果要模仿replacefirst( )(或"replace n")的效果,你也可以只替换一次就调用appendtail( )。它会把剩下的东西全都放进sbuf。
你还可以在appendreplacement( )的replacement参数里用"$g"引用已捕获的group,其中'g' 表示group的号码。不过这是为一些比较简单的操作准备的,因而其效果无法与上述程序相比。
reset( )
此外,还可以用reset( )方法给现有的matcher对象配上个新的charsequence。
java.util.regex.*; java.io.*; com.bruceeckel.simpletest.*; resetting { test monitor = test(); main(string[] args) exception { matcher m = pattern.compile() .matcher(); (m.find()) system.out.println(m.group()); m.reset(); (m.find()) system.out.println(m.group()); monitor.expect( string[]{ , , , , , }); } }
如果不给参数,reset( )会把matcher设到当前字符串的开始处。
如果你曾经用过perl或任何其他内建正则表达式支持的语言,你一定知道用正则表达式处理文本和匹配模式是多么简单。如果你不熟悉这个术语,那么“正则表达式”(regular expression)就是一个字符构成的串,它定义了一个用来搜索匹配字符串的模式。
许多语言,包括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),还是采用正则表达式,这就有待你自己决定了。
jakarta-oro篇
由于工作的需要,本人经常要面对大量的文字电子资料的整理工作,因此曾对在java中正则表达式的应用有所关注,并对其有一定的了解,希望通过本文与同行进行有关方面的心得交流。
正则表达式:
正则表达式是一种可以用于模式匹配和替换的强有力的工具,一个正则表达式就是由普通的字符(例如字符 a 到 z)以及特殊字符(称为元字符)组成的文字模式,它描述在查找文字主体时待匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。
正则表达式在字符数据处理中起着非常重要的作用,我们可以用正则表达式完成大部分的数据分析处理工作,如:判断一个串是否是数字、是否是有效的email地址,从海量的文字资料中提取有价值的数据等等,如果不使用正则表达式,那么实现的程序可能会很长,并且容易出错。对这点本人深有体会,面对大量工具书电子档资料的整理工作,如果不懂得应用正则表达式来处理,那么将是很痛苦的一件事情,反之则将可以轻松地完成,获得事半功倍的效果。
由于本文目的是要介绍如何在java里运用正则表达式,因此对刚接触正则表达式的读者请参考有关资料,在此因篇幅有限不作介绍。
java对正则表达式的支持:
在jdk1.3或之前的jdk版本中并没有包含正则表达式库可供java程序员使用,之前我们一般都在使用第三方提供的正则表达式库,这些第三方库中有源代码开放的,也有需付费购买的,而现时在jdk1.4的测试版中也已经包含有正则表达式库---java.util.regex。
故此现在我们有很多面向java的正则表达式库可供选择,以下我将介绍两个较具代表性的 jakarta-oro和java.util.regex,首先当然是本人一直在用的 jakarta-oro:
jakarta-oro正则表达式库
1.简介:
jakarta-oro是最全面以及优化得最好的正则表达式api之一,jakarta-oro库以前叫做oromatcher,是由daniel f. savarese编写,后来他将其赠与jakarta project,读者可在apache.org的网站下载该api包。
许多源代码开放的正则表达式库都是支持perl5兼容的正则表达式语法,jakarta-oro正则表达式库也不例外,他与perl 5正则表达式完全兼容。
2.对象与其方法:
★patterncompiler对象:
我们在使用jakarta-oro api包时,最先要做的是,创建一个perl5compiler类的实例,并把它赋值给patterncompiler接口对象。perl5compiler是patterncompiler接口的一个实现,允许你把正则表达式编译成用来匹配的pattern对象。
patterncompiler compiler=new perl5compiler();
★pattern对象:
要把所对应的正则表达式编译成pattern对象,需要调用compiler对象的compile()方法,并在调用参数中指定正则表达式。举个例子,你可以按照下面这种方式编译正则表达式"s[ahkl]y":
pattern pattern=null; try { pattern=compiler.compile("s[ahkl]y "); } catch (malformedpatternexception e) { e.printstacktrace(); }
在默认的情况下,编译器会创建一个对大小写敏感的模式(pattern)。因此,上面代码编译得到的模式只匹配"say"、"shy"、 "sky"和"sly",但不匹配"say"和"sky"。要创建一个大小写不敏感的模式,你应该在调用编译器的时候指定一个额外的参数:
pattern=compiler.compile("s[ahkl]y",perl5compiler.case_insensitive_mask);
pattern对象创建好之后,就可以通过patternmatcher类用该pattern对象进行模式匹配。
★patternmatcher对象:
patternmatcher对象依据pattern对象和字符串展开匹配检查。你要实例化一个perl5matcher类并把结果赋值给patternmatcher接口。perl5matcher类是patternmatcher接口的一个实现,它根据perl 5正则表达式语法进行模式匹配:
patternmatcher matcher=new perl5matcher();
patternmatcher对象提供了多个方法进行匹配操作,这些方法的第一个参数都是需要根据正则表达式进行匹配的字符串:
1、boolean matches(string input, pattern pattern):当要求输入的字符串input和正则表达式pattern精确匹配时使用该方法。也就是说当正则表达式完整地描述输入字符串时返回真值。
2、boolean matchesprefix(string input, pattern pattern):要求正则表达式匹配输入字符串起始部分时使用该方法。也就是说当输入字符串的起始部分与正则表达式匹配时返回真值。
3、boolean contains(string input, pattern pattern):当正则表达式要匹配输入字符串的一部分时使用该方法。当正则表达式为输入字符串的子串时返回真值。
但以上三种方法只会查找输入字符串中匹配正则表达式的第一个对象,如果当字符串可能有多个子串匹配给定的正则表达式时,那么你就可以在调用上面三个方法时用patternmatcherinput对象作为参数替代string对象,这样就可以从字符串中最后一次匹配的位置开始继续进行匹配,这样就方便的多了。
用patternmatcherinput对象作为参数替代string时,上述三个方法的语法如下:
- boolean matches(patternmatcherinput input, pattern pattern)
- boolean matchesprefix(patternmatcherinput input, pattern pattern)
- boolean contains(patternmatcherinput input, pattern pattern)
★util.substitute()方法:
查找后需要要进行替换,我们就要用到util.substitute()方法,其语法如下:
public static string substitute(patternmatcher matcher,
pattern pattern,substitution sub,string input,
int numsubs)
前两个参数分别为patternmatcher和pattern对象。而第三个参数是个substiution对象,由它来决定替换操作如何进行。第四个参数是要进行替换操作的目标字符串,最后一个参数用来指定是否替换模式的所有匹配子串(util.substitute_all),或只进行指定次数的替换。
在这里我相信有必要详细解说一下第三个参数substiution对象,因为它将决定替换将怎样进行。
substiution:
substiution是一个接口类,它为你提供了在使用util.substitute()方法时控制替换方式的手段,它有两个标准的实现类:stringsubstitution与perl5substitution。当然,同时你也可以生成自己的实现类来定制你所需要的特殊替换动作。
stringsubstitution:
stringsubstitution 实现的是简单的纯文字替换手段,它有两个构造方法:
stringsubstitution()->缺省的构造方法,初始化一个包含零长度字符串的替换对象。
stringsubstitution(java.lang.string substitution)->初始化一个给定字符串的替换对象。
perl5substitution:
perl5substitution 是stringsubstitution的子类,它在实现纯文字替换手段的同时也允许进行针对math类里各匹配组的perl5变量的替换,所以他的替换手段比其直接父类stringsubstitution更为多元化。
它有三个构造器:
perl5substitution()
perl5substitution(java.lang.string substitution)
perl5substitution(java.lang.string substitution, int numinterpolations)
前两种构造方法与stringsubstitution一样,而第三种构造方法下面将会介绍到。
在perl5substitution的替换字符串中可以包含用来替代在正则表达式里由小扩号围起来的匹配组的变量,这些变量是由$1, $2,$3等形式来标识。我们可以用一个例子来解释怎样使用替换变量来进行替换:
假设我们有正则表达式模式为b/d+:(也就是b[0-9]+:),而我们想把所有匹配的字符串中的"b"都改为"a",而":"则改为"-",而其余部分则不作修改,如我们输入字符串为"example b123:",经过替换后就应该变成"example a123-"。要做到这点,我们就首先要把不做替换的部分用分组符号小括号包起来,这样正则表达式就变为"b(/d+):",而构造perl5substitution对象时其替换字符串就应该是"a$1-",也就是构造式为perl5substitution("a$1-"),表示在使用util.substitute()方法时只要在目标字符串里找到和正则表达式" b(/d+): "相匹配的子串都用替换字符串来替换,而变量$1表示如果和正则表达式里第一个组相匹配的内容则照般原文插到$1所在的为置,如在"example b123:"中和正则表达式相匹配的部分是"b123:",而其中和第一分组"(/d+)"相匹配的部分则是"123",所以最后替换结果为"example a123-"。
有一点需要清楚的是,如果你把构造器perl5substitution(java.lang.string substitution,int numinterpolations)
中的numinterpolations参数设为interpolate_all,那么当每次找到一个匹配字串时,替换变量($1,$2等)所指向的内容都根据目前匹配字串来更新,但是如果numinterpolations参数设为一个正整数n时,那么在替换时就只会在前n次匹配发生时替换变量会跟随匹配对象来调整所代表的内容,但n次之后就以一致以第n次替换变量所代表内容来做为以后替换结果。
举个例子会更好理解:
假如沿用以上例子中的正则表达式模式以及替换内容来进行替换工作,设目标字符串为"tank b123: 85 tank b256: 32 tank b78: 22",并且设numinterpolations参数为interpolate_all,而util.substitute()方法中的numsub变量设为substitute_all(请参考上文util.substitute()方法内容),那么你获得的替换结果将会是:
tank a123- 85 tank a256- 32 tank a78- 22
但是如果你把numinterpolations设为2,并且numsubs依然设为substitute_all,那么这时你获得的结果则会是:
tank a123- 85 tank a256- 32 tank a256- 22
你要注意到最后一个替换所用变量$1所代表的内容与第二个$1一样为"256",而不是预期的"78",因为在替换进行中,替换变量$1只根据匹配内容进行了两次更新,最后一次就使第二次匹配时所更新的结果,那么我们可以由此知道,如果numinterpolations设为1,那么结果将是:
tank a123- 85 tank a123- 32 tank a123- 22
3.应用示例:
刚好前段时间公司准备出一个《伊索预言》的英语学习互动教材,其中有电子档资料的整理工作,我们就以此为例来看一下jakarta-oro与jdbc2.0 api结合起来对数据库内的资料进行简单提取与整理的实现。假设由录入部的同事送过来的存放在ms sqlserver 7数据库里的电子档的表结构如下(注:或许在不同的dbms中有相应的正则表达式的应用,但这不在本文讨论范围内):
表名:aesop, 表中每条记录包含有三列:
id(int):单词索引号
word(varchar):单词
content(varchar):存放单词的相关解释与例句等内容
其中content列中内容的格式如下:
[音标] [词性] (解释){(例句一/例句解释/例句中该词的词性: 单词在句中的意思) (例句二/例句解释/例句中该词的词性: 单词在句中的意思)}
如对应单词kevin,content中的内容如下:
['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词: 凯文)( kevin is living in zhuhai now./凯文现住在珠海/名词: 凯文)}
我们的例子主要针对content列中内容进行字符串处理。
★查找单个匹配:
首先,让我们尝试把contnet列中的[音标]字段的内容列示出来,由于所有单词的记录中都有这一项并且都在字串开始位置,所以这个查找工作比较简单:
1、确定相应的正则表达式:/[[^]]+/]
这个是很简单的正则表达式,其意思是要求相匹配的字符串必须为以一对中括号包含的所有内容,如['kevin] 、[名词]等,但内容中不包括"]"符号,也就是要避免出现"[][]"会作为一个匹配对象的情况出现(有关正则表达式的基础知识请参照有关资料,这里不再详述)。
注意,在java中,你必须对每一个向前的斜杠("/")进行转义处理。所以我们要在上面的正则表达式里每个"/"前面加上一个"/"以免出现编译错误,也就是在java中初始化正则表达式的字符串的语句应该为:
string restring=" //[[^]]+//]";
并且在表达式里每个符号中间不能有空格,否则就会同样出现编译错误。
2、实例化patterncompiler对象,创建pattern对象
patterncompiler compiler=new perl5compiler();
pattern pattern=compiler.compile(restring);
3、创建patternmatcher对象,调用patternmatcher接口的contain()方法检查匹配情况:
patternmatcher matcher=new perl5matcher(); if (matcher.contains(content,pattern)) { //处理代码片段 }
这里matcher.contains(content,pattern)中的参数 content是从数据库里取来的字符串变量。该方法只会查到第一个匹配的对象字符串,但是由于音标项均在conetnet内容字符串中的起始位置,所以用这个方法就已经可以保证把每条记录里的音标项找出来了,但更为直接与合理的办法是使用boolean matchesprefix(patternmatcherinput input, pattern pattern)方法,该方法验证目标字符串是否以正则表达式所匹配的字串为起始。
具体实现的完整的程序代码如下:
package regularexpressions; //import…… import org.apache.oro.text.regex.*; //使用jakarta-oro正则表达式库前需要把它加到classpath里面,如果用ide是//jbuilder,那么也可以在jbuilder里直接自建新库。 public class yisuo { public static void main(string[] args) { try { //使用jdbc driver进行dbms连接,这里我使用的是一个第三方jdbc //driver,microsoft本身也有一个面向sqlserver7/2000的免费jdbc //driver,但其性能真的是奇差,不用也罢。 class.forname("com.jnetdirect.jsql.jsqldriver"); connection con = drivermanager.getconnection("jdbc:jsqlconnect://kevin:1433", "kevin chen", "re"); statement stmt = con.createstatement(resultset.type_scroll_sensitive, resultset.concur_updatable); //为使用jakarta-oro库而创建相应的对象 string rsstring = " //[[^]]+//]"; patterncompiler orocom = new perl5compiler(); pattern pattern = orocom.compile(rsstring); patternmatcher matcher = new perl5matcher(); resultset uprs = stmt.executequery("select * from aesop"); while (uprs.next()) { stirng word = uprs.getstring("word"); stirng content = uprs.getstring("content"); if (matcher.contains(content, pattern)) { //或if(matcher.matchesprefix(content,pattern)){ matchresult result = matcher.getmatch(); stirng pure = result.tostring(); system.out.println(word + "的音标为:" + pure); } } } catch (exception e) { system.out.println(e); } } }
输出结果为:kevin的音标为['kevin]
在这个处理中我是用tostring()方法来取得结果,但是如果正则表达式里是用了分组符号(圆括号),那么就可以用group(int gid)的方法来取得相应各组匹配的结果,如正则表达式改为" (/[[^]]+/])",那么就可以用以下方法来取得结果:pure=result.group(0);
用程序验证,输出结果同样为:kevin的音标为['kevin]
而如果正则表达式为(/[[^]]+/])(/[[^]]+/]),则会查找到两个连续的方括号所包含的内容,也就找到[音标] [词性]两项,但是两项的结果分别在两个组里面,分别由下面语句获得结果:
result.group(0)->返回[音标] [词性]两项内容,也就是与整个正则表达式相匹配的结果字符串,在这里也就为['kevin] [名词]
result.group(1) ->返回[音标]项内容,结果应是['kevin]
result.group(2) ->返回[词性]项内容,结果应是[名词]
继续用程序验证,发现输出并不正确,主要是当内容有中文时就不能成功匹配,考虑到可能是jakarta-oro正则表达式库版本不支持中文的问题,回看一下原来我一直用的还是2.0.1的老版本,马上到jakarta.org上下载最新的2.0.4版本装上再用程序验证,得出的结果就和预期一样正确。
★查找多个匹配:
经过第一步的尝试使用jakarta-oro后,我们已经知道了如何正确使用该api包来查找目标字符串里一个匹配的子串,下面我们接着来看一看当目标字符串里包含不止一个匹配的子串时我们如何把它们一个接一个找出来进行相应的处理。
首先我们先试个简单的应用,假设我们想把contnet字段内容里所有用方括号包起来的字串都找出来,很清楚地,contnet字段的内容里面就只有两项匹配的内容:[音标]和 [词性],刚才我们其实已经把它们分别找出来了,但是我们所用的方法是分组方法,把"[音标] [词性]"作为一整个正则表达式匹配的内容先找到,再根据分组把[音标]和 [词性]分别挑出来。但是现在我们需要做的是把[音标]和[词性]分别做为与同一个正则表达式匹配的内容,先找到一个接着再找下一个,也就是刚才我们的表达式为(/[[^]]+/])(/[[^]]+/]),而现在应为" /[[^]]+/] "。
我们已经知道在匹配操作的三个方法里只要用patternmatcherinput对象作为参数替代string对象就可以从字符串中最后一次匹配的位置开始继续进行匹配,实现的程序片段如下:
patternmatcherinput input=new patternmatcherinput(content); while (matcher.contains(input,pattern)) { result=matcher.getmatch(); system.out.println(result.group(0)) }
输出结果为:['kevin]
[名词]
接着我们来做复杂一点的处理,就是我们要先把下面内容:
['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词: 凯文)( kevin is living in zhuhai now. /凯文现住在珠海/名词: 凯文)}中的整个例句部分(也就是由大括号所包含的部分)找出来,再分别把例句一和例句二找出,而各例句中的各项内容(英文句、中文句、词性、解释)也要分项列出。
第一步当然是要定出相应的正则表达式,需要有两个,一是和整个例句部分(也就是由大括号包起来的部分)匹配的正则表达式:"/{.+/}",
另一个则要和每个例句部分匹配(也就是小括号中的内容),:/(([^)]+/)
而且由于要把例句的各项分离出来,所以要再把里面的各部分用分组的方法匹配出来:" ([^(]+)/(.+)/(.+):([^)]+) "。
为了简便起见,我们不再和从数据库里读出,而是构造一个包含同样内容的字符串变量,程序片段如下:
try{ string content="['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词:凯文) (kevin is living in zhuhai now./凯文现住在珠海/名词: 凯文)}"; string ps1="//{.+//}"; string ps2="//([^)]+//)"; string ps3="([^(]+)/(.+)/(.+):([^)]+)"; string sentence; patterncompiler orocom=new perl5compiler(); pattern pattern1=orocom.compile(ps1); pattern pattern2=orocom.compile(ps2); pattern pattern3=orocom.compile(ps3); patternmatcher matcher=new perl5matcher(); //先找出整个例句部分 if (matcher.contains(content,pattern1)) { matchresult result=matcher.getmatch(); string example=result.tostring(); patternmatcherinput input=new patternmatcherinput(example); //分别找出例句一和例句二 while (matcher.contains(input,pattern2)){ result=matcher.getmatch(); sentence=result.tostring(); //把每个例句里的各项用分组的办法分隔出来 if (matcher.contains(sentence,pattern3)){ result=matcher.getmatch(); system.out.println("英文句: "+result.group(1)); system.out.println("句子中文翻译: "+result.group(2)); system.out.println("词性: "+result.group(3)); system.out.println("意思: "+result.group(4)); } } } } catch(exception e) { system.out.println(e); }
输出结果为:
英文句: kevin loves comic.
句子中文翻译: 凯文爱漫画
词性: 名词
意思: 凯文
英文句: kevin is living in zhuhai now.
句子中文翻译: 凯文现住在珠海
词性: 名词
意思: 凯文
★查找替换:
以上的两个应用都是单纯在查找字符串匹配方面的,我们再来看一下查找后如何对目标字符串进行替换。
例如我现在想把第二个例句进行改动,换为:kevin has seen《leon》seveal times,because it is a good film./ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。
也就是把
['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词: 凯文)( kevin is living in zhuhai now. /凯文现住在珠海/名词: 凯文)}
改为:
['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词: 凯文)( kevin has seen《leon》seveal times,because it is a good film./ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。)}
之前,我们已经了解util.substitute()方法与substiution接口,以及substiution的两个实现类stringsubstitution和perl5substitution,我们就来看看怎么用util.substitute()方法配合perl5substitution来完成我们上面提出的替换要求,确定正则表达式:
我们要先找到其中的整个例句部分,也就是由大括号包起来的字串,并且把两个例句分别分组,所以正则表达式为:"/{(/([^)]+/))(/([^)]+/))/}",如果用替换变量来代替分组,那么上面的表达式可以看为"/{$1$2/}",这样就可以更容易看出替换变量与分组间的关系。
根据上面的正则表达式perl5substitution类可以这样构造:
perl5substitution("{$1( kevin has seen《leon》seveal times,because it is a good film./ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。)}")
再根据这个perl5substitution对象来使用util.substitute()方法便可以完成替换了,实现的代码片段如下:
try{ string content="['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词: 凯文)(kevin lives in zhuhai now./凯文现住在珠海/名词: 凯文)}"; string ps1="//{(//([^)]+//))(//([^)]+//))//}"; string sentence; string pure; patterncompiler orocom=new perl5compiler(); pattern pattern1=orocom.compile(ps1); patternmatcher matcher=new perl5matcher(); string result=util.substitute(matcher, pattern1,new perl5substitution( "{$1( kevin has seen《leon》seveal times,because it is a good film./ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。)}",1), content,util.substitute_all); system.out.println(result); } catch(exception e) { system.out.println(e); }
输出结果是正确的,为:
['kevin] [名词](人名凯文){(kevin loves comic./凯文爱漫画/名词: 凯文)( kevin has seen《leon》seveal times,because it is a good film./ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。)}
至于有关使用numinterpolations参数的构造器用法,读者只要根据上面的介绍自己动手试一下就会清楚了,在此就不再例述。
总结:
本文首先介绍了jakarta-oro正则表达式库的对象与方法,并且接着举例让读者对实际应用有进一步的了解,虽然例子都比较简单,但希望读者们在看了该文后对jakarta-oro正则表达式库有一定的认知,在实际工作中有所帮助与启发。
其实在jakarta org里除了jakarta-oro外还有一个百分百的纯java正则表达式库,就是由jonathan locke赠与jakarta org的regexp,在该包里面包含了完整的文档以及一个用于调试的applet例子,对其有兴趣的读者可以到此下载。
参考资料:
本文的主要参考文章,该文在介绍jakarta-oro的同时也为读者详尽解析了正则表达式的基本语法。
一个基于perl的正则表达式详尽教程(虽然该教程是基于perl的,但是你并不需要有perl的经验,虽然那会有所帮助),以及一个不错的正则表达式简例教程。
最不可缺少的当然是jakarta-oro的帮助文档http://jakarta.apache.org/oro/api/
关于作者
陈广佳 kevin chen,汕头大学电子信息工程系工科学士,台湾大新出版社珠海区开发部,现正围绕中日韩电子资料使用java开发电子词典等相关项目。可通过e-mail:cgjmail@163.net于他联系。
java.util.regex篇
现在jdk1.4里终于有了自己的正则表达式api包,java程序员可以免去找第三方提供的正则表达式库的周折了,我们现在就马上来了解一下这个sun提供的迟来恩物- -对我来说确实如此。
1.简介:
java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包。
它包括两个类:pattern和matcher
pattern | 一个pattern是一个正则表达式经编译后的表现模式。 |
matcher | 一个matcher对象是一个状态机器,它依据pattern对象做为匹配模式对字符串展开匹配检查。 |
首先一个pattern实例订制了一个所用语法与perl的类似的正则表达式经编译后的模式,然后一个matcher实例在这个给定的pattern实例的模式控制下进行字符串的匹配工作。
以下我们就分别来看看这两个类:
2.pattern类:
pattern的方法如下:
static pattern |
compile(string regex) 将给定的正则表达式编译并赋予给pattern类 |
static pattern |
compile(string regex, int flags) 同上,但增加flag参数的指定,可选的flag参数包括:case insensitive,multiline,dotall,unicode case, canon eq |
int |
flags() 返回当前pattern的匹配flag参数. |
matcher |
matcher(charsequence input) 生成一个给定命名的matcher对象 |
static boolean |
matches(string regex, charsequence input) 编译给定的正则表达式并且对输入的字串以该正则表达式为模开展匹配,该方法适合于该正则表达式只会使用一次的情况,也就是只进行一次匹配工作,因为这种情况下并不需要生成一个matcher实例。 |
string |
pattern() 返回该patter对象所编译的正则表达式。 |
string[] |
split(charsequence input) 将目标字符串按照pattern里所包含的正则表达式为模进行分割。 |
string[] |
split(charsequence input, int limit) 作用同上,增加参数limit目的在于要指定分割的段数,如将limi设为2,那么目标字符串将根据正则表达式分为割为两段。 |
一个正则表达式,也就是一串有特定意义的字符,必须首先要编译成为一个pattern类的实例,这个pattern对象将会使用 matcher()方法来生成一个matcher实例,接着便可以使用该 matcher实例以编译的正则表达式为基础对目标字符串进行匹配工作,多个matcher是可以共用一个pattern对象的。
现在我们先来看一个简单的例子,再通过分析它来了解怎样生成一个pattern对象并且编译一个正则表达式,最后根据这个正则表达式将目标字符串进行分割:
import java.util.regex.*; public class replacement{ public static void main(string[] args) throws exception { // 生成一个pattern,同时编译一个正则表达式 pattern p = pattern.compile("[/]+"); //用pattern的split()方法把字符串按"/"分割 string[] result = p.split( "kevin has seen《leon》seveal times,because it is a good film." +"/ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部" +"好电影。/名词:凯文。"); for (int i=0; i<result.length; i++) system.out.println(result[i]); } }
输出结果为:
kevin has seen《leon》seveal times,because it is a good film.
凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。
名词:凯文。
很明显,该程序将字符串按"/"进行了分段,我们以下再使用 split(charsequence input, int limit)方法来指定分段的段数,程序改动为:
tring[] result = p.split("kevin has seen《leon》seveal times,because it is a good film./ 凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。",2);
这里面的参数"2"表明将目标语句分为两段。
输出结果则为:
kevin has seen《leon》seveal times,because it is a good film.
凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。/名词:凯文。
由上面的例子,我们可以比较出java.util.regex包在构造pattern对象以及编译指定的正则表达式的实现手法与我们在上一篇中所介绍的jakarta-oro 包在完成同样工作时的差别,jakarta-oro 包要先构造一个patterncompiler类对象接着生成一个pattern对象,再将正则表达式用该patterncompiler类的compile()方法来将所需的正则表达式编译赋予pattern类:
patterncompiler orocom=new perl5compiler();
pattern pattern=orocom.compile("regular expressions");
patternmatcher matcher=new perl5matcher();
但是在java.util.regex包里,我们仅需生成一个pattern类,直接使用它的compile()方法就可以达到同样的效果:
pattern p = pattern.compile("[/]+");
因此似乎java.util.regex的构造法比jakarta-oro更为简洁并容易理解。
3.matcher类:
matcher方法如下:
matcher |
appendreplacement(stringbuffer sb, string replacement) 将当前匹配子串替换为指定字符串,并且将替换后的子串以及其之前到上次匹配子串之后的字符串段添加到一个stringbuffer对象里。 |
stringbuffer |
appendtail(stringbuffer sb) 将最后一次匹配工作后剩余的字符串添加到一个stringbuffer对象里。 |
int |
end() 返回当前匹配的子串的最后一个字符在原目标字符串中的索引位置 。 |
int |
end(int group) 返回与匹配模式里指定的组相匹配的子串最后一个字符的位置。 |
boolean |
find() 尝试在目标字符串里查找下一个匹配子串。 |
boolean |
find(int start) 重设matcher对象,并且尝试在目标字符串里从指定的位置开始查找下一个匹配的子串。 |
string |
group() 返回当前查找而获得的与组匹配的所有子串内容 |
string |
group(int group) 返回当前查找而获得的与指定的组匹配的子串内容 |
int |
groupcount() 返回当前查找所获得的匹配组的数量。 |
boolean |
lookingat() 检测目标字符串是否以匹配的子串起始。 |
boolean |
matches() 尝试对整个目标字符展开匹配检测,也就是只有整个目标字符串完全匹配时才返回真值。 |
pattern |
pattern() 返回该matcher对象的现有匹配模式,也就是对应的pattern 对象。 |
string |
replaceall(string replacement) 将目标字符串里与既有模式相匹配的子串全部替换为指定的字符串。 |
string |
replacefirst(string replacement) 将目标字符串里第一个与既有模式相匹配的子串替换为指定的字符串。 |
matcher |
reset() 重设该matcher对象。 |
matcher |
reset(charsequence input) 重设该matcher对象并且指定一个新的目标字符串。 |
int |
start() 返回当前查找所获子串的开始字符在原目标字符串中的位置。 |
int |
start(int group) 返回当前查找所获得的和指定组匹配的子串的第一个字符在原目标字符串中的位置。 |
(光看方法的解释是不是很不好理解?不要急,待会结合例子就比较容易明白了)
一个matcher实例是被用来对目标字符串进行基于既有模式(也就是一个给定的pattern所编译的正则表达式)进行匹配查找的,所有往matcher的输入都是通过charsequence接口提供的,这样做的目的在于可以支持对从多元化的数据源所提供的数据进行匹配工作。
我们分别来看看各方法的使用:
★matches()/lookingat ()/find():
一个matcher对象是由一个pattern对象调用其matcher()方法而生成的,一旦该matcher对象生成,它就可以进行三种不同的匹配查找操作:
- matches()方法尝试对整个目标字符展开匹配检测,也就是只有整个目标字符串完全匹配时才返回真值。
- lookingat ()方法将检测目标字符串是否以匹配的子串起始。
- find()方法尝试在目标字符串里查找下一个匹配子串。
以上三个方法都将返回一个布尔值来表明成功与否。
★replaceall ()/appendreplacement()/appendtail():
matcher类同时提供了四个将匹配子串替换成指定字符串的方法:
- replaceall()
- replacefirst()
- appendreplacement()
- appendtail()
replaceall()与replacefirst()的用法都比较简单,请看上面方法的解释。我们主要重点了解一下appendreplacement()和appendtail()方法。
appendreplacement(stringbuffer sb, string replacement) 将当前匹配子串替换为指定字符串,并且将替换后的子串以及其之前到上次匹配子串之后的字符串段添加到一个stringbuffer对象里,而appendtail(stringbuffer sb) 方法则将最后一次匹配工作后剩余的字符串添加到一个stringbuffer对象里。
例如,有字符串fatcatfatcatfat,假设既有正则表达式模式为"cat",第一次匹配后调用appendreplacement(sb,"dog"),那么这时stringbuffer sb的内容为fatdog,也就是fatcat中的cat被替换为dog并且与匹配子串前的内容加到sb里,而第二次匹配后调用appendreplacement(sb,"dog"),那么sb的内容就变为fatdogfatdog,如果最后再调用一次appendtail(sb),那么sb最终的内容将是fatdogfatdogfat。
还是有点模糊?那么我们来看个简单的程序:
//该例将把句子里的"kelvin"改为"kevin" import java.util.regex.*; public class matchertest{ public static void main(string[] args) throws exception { //生成pattern对象并且编译一个简单的正则表达式"kelvin" pattern p = pattern.compile("kevin"); //用pattern类的matcher()方法生成一个matcher对象 matcher m = p.matcher("kelvin li and kelvin chan are both working in kelvin chen's kelvinsoftshop company"); stringbuffer sb = new stringbuffer(); int i=0; //使用find()方法查找第一个匹配的对象 boolean result = m.find(); //使用循环将句子里所有的kelvin找出并替换再将内容加到sb里 while(result) { i++; m.appendreplacement(sb, "kevin"); system.out.println("第"+i+"次匹配后sb的内容是:"+sb); //继续查找下一个匹配对象 result = m.find(); } //最后调用appendtail()方法将最后一次匹配后的剩余字符串加到sb里; m.appendtail(sb); system.out.println("调用m.appendtail(sb)后sb的最终内容是:"+ sb.tostring()); } }
最终输出结果为:
第1次匹配后sb的内容是:kevin
第2次匹配后sb的内容是:kevin li and kevin
第3次匹配后sb的内容是:kevin li and kevin chan are both working in kevin
第4次匹配后sb的内容是:kevin li and kevin chan are both working in kevin chen's kevin
调用m.appendtail(sb)后sb的最终内容是:kevin li and kevin chan are both working in kevin chen's kevinsoftshop company.
看了上面这个例程是否对appendreplacement(),appendtail()两个方法的使用更清楚呢,如果还是不太肯定最好自己动手写几行代码测试一下。
★group()/group(int group)/groupcount():
该系列方法与我们在上篇介绍的jakarta-oro中的matchresult .group()方法类似(有关jakarta-oro请参考上篇的内容),都是要返回与组匹配的子串内容,下面代码将很好解释其用法:
import java.util.regex.*; public class grouptest{ public static void main(string[] args) throws exception { pattern p = pattern.compile("(ca)(t)"); matcher m = p.matcher("one cat,two cats in the yard"); stringbuffer sb = new stringbuffer(); boolean result = m.find(); system.out.println("该次查找获得匹配组的数量为:"+m.groupcount()); for(int i=1;i<=m.groupcount();i++){ system.out.println("第"+i+"组的子串内容为: "+m.group(i)); } } }
输出为:
该次查找获得匹配组的数量为:2
第1组的子串内容为:ca
第2组的子串内容为:t
matcher对象的其他方法因比较好理解且由于篇幅有限,请读者自己编程验证。
4.一个检验email地址的小程序:
最后我们来看一个检验email地址的例程,该程序是用来检验一个输入的email地址里所包含的字符是否合法,虽然这不是一个完整的email地址检验程序,它不能检验所有可能出现的情况,但在必要时您可以在其基础上增加所需功能。
import java.util.regex.*; public class email { public static void main(string[] args) throws exception { string input = args[0]; //检测输入的email地址是否以 非法符号"."或"@"作为起始字符 pattern p = pattern.compile("^//.|^//@"); matcher m = p.matcher(input); if (m.find()){ system.err.println("email地址不能以'.'或'@'作为起始字符"); } //检测是否以"www."为起始 p = pattern.compile("^www//."); m = p.matcher(input); if (m.find()) { system.out.println("email地址不能以'www.'起始"); } //检测是否包含非法字符 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; //如果里面包含非法字符如冒号双引号等,那么就把他们消去,加到sb里面 m.appendreplacement(sb, ""); result = m.find(); } m.appendtail(sb); input = sb.tostring(); if (deletedillegalchars) { system.out.println("输入的email地址里包含有冒号、逗号等非法字符,请修改"); system.out.println("您现在的输入为: "+args[0]); system.out.println("修改后合法的地址应类似: "+input); } } }
例如,我们在命令行输入:java email www.kevin@163.net
那么输出结果将会是:email地址不能以'www.'起始
如果输入的email为@kevin@163.net
则输出为:email地址不能以'.'或'@'作为起始字符
当输入为:cgjmail#$%@163.net
那么输出就是:
输入的email地址里包含有冒号、逗号等非法字符,请修改
您现在的输入为: cgjmail#$%@163.net
修改后合法的地址应类似: cgjmail@163.net
5.总结:
本文介绍了jdk1.4.0-beta3里正则表达式库--java.util.regex中的类以及其方法,如果结合与上一篇中所介绍的jakarta-oro api作比较,读者会更容易掌握该api的使用,当然该库的性能将在未来的日子里不断扩展,希望获得最新信息的读者最好到及时到sun的网站去了解。
6.结束语:
本来计划再多写一篇介绍一下需付费的正则表达式库中较具代表性的作品,但觉得既然有了免费且优秀的正则表达式库可以使用,何必还要去找需付费的呢,相信很多读者也是这么想的:,所以有兴趣了解更多其他的第三方正则表达式库的朋友可以自己到网上查找或者到我在参考资料里提供的网址去看看。
参考资料
java.util.regex的帮助文档
dana nourie 和mike mccloskey所写的regular expressions and the java" programming language
需要更多的第三方正则表达式资源以及基于它们所开发的应用程序请看
原文链接:https://blog.csdn.net/allwefantasy/article/details/3136570/