你可以从比较简单的东西入手学习正则表达式。要想全面地掌握怎样构建正则表达式,可以去看JDK文档的java.util.regex的Pattern类的文档。
字符 | |
---|---|
B | 字符B |
/xhh | 16进制值0xhh所表示的字符 |
/uhhhh | 16进制值0xhhhh所表示的Unicode字符 |
/t | Tab |
/n | 换行符 |
/r | 回车符 |
/f | 换页符 |
/e | Escape |
正则表达式的强大体现在它能定义字符集(character class)。下面是一些最常见的字符集及其定义的方式,此外还有一些预定义的字符集:
字符集 | |
---|---|
. | 表示任意一个字符 |
[abc] | 表示字符a,b,c中的任意一个(与a|b|c相同) |
[^abc] | 除a,b,c之外的任意一个字符(否定) |
[a-zA-Z] | 从a到z或A到Z当中的任意一个字符(范围) |
[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加到收藏夹里,这样就能很容易地找到各种正则表达式的模式了。
逻辑运算符 | |
---|---|
XY | X 后面跟着 Y |
X|Y | X或Y |
(X) | 一个"要匹配的组(capturing group)". 以后可以用/i来表示第i个被匹配的组。 |
边界匹配符 | |
---|---|
^ | 一行的开始 |
$ | 一行的结尾 |
/b | 一个单词的边界 |
/B | 一个非单词的边界 |
/G | 前一个匹配的结束 |
举一个具体一些的例子。下面这些正则表达式都是合法的,而且都能匹配"Rudolph":
Rudolph [rR]udolph [rR][aeiou][a-z]ol.* R.*
"数量表示符(quantifier)"的作用是定义模式应该匹配多少个字符。
Greedy | Reluctant | Possessive | 匹配 |
---|---|---|---|
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之上的新语言。
JDK 1.4定义了一个新的接口,叫CharSequence。它提供了String和StringBuffer这两个类的字符序列的抽象:
CharSequence { charAt( i); length(); subSequence( start, end); toString(); }
为了实现这个新的CharSequence接口,String,StringBuffer以及CharBuffer都作了修改。很多正则表达式的操作都要拿CharSequence作参数。
先给一个例子。下面这段程序可以测试正则表达式是否匹配字符串。第一个参数是要匹配的字符串,后面是正则表达式。正则表达式可以有多个。在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匹配字符串的开头。
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参数的,正如你所看到的,它会告诉方法从哪里开始找——即从参数位置开始查找。
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的例子:
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( ),都会引发一个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( )[67]成功的前提是,字符串的开始部分与正则表达式相匹配。
compile( )方法还有一个版本,它需要一个控制正则表达式的匹配行为的参数:
flag的取值范围如下:Pattern Pattern.compile(String regex, 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( )方法仅返回匹配的部分。
所谓分割是指将以正则表达式为界,将字符串分割成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( )方法给现有的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设到当前字符串的开始处。
到目前为止,你看到的都是用正则表达式处理静态字符串的例子。下面我们来演示一下怎样用正则表达式扫描文件并且找出匹配的字符串。受Unix的grep启发,我写了个JGrep.java,它需要两个参数:文件名,以及匹配字符串用的正则表达式。它会把匹配这个正则表达式那部分内容及其所属行的行号打印出来。
java.io.*; java.util.regex.*; java.util.*; com.bruceeckel.util.*; JGrep { main(String[] args) Exception { (args.length < 2) { System.out.println(); System.exit(0); } Pattern p = Pattern.compile(args[1]); ListIterator it = TextFile(args[0]).listIterator(); (it.hasNext()) { Matcher m = p.matcher((String)it.next()); (m.find()) System.out.println(it.nextIndex() + + m.group() + + m.start()); } } } |
文件是用TextFile打开的(本章的前半部分讲的)。由于TextFile会把文件的各行放在ArrayList里面,而我们又提取了一个ListIterator,因此我们可以在文件的各行当中自由移动(既能向前也可以向后)。
每行都会有一个Matcher,然后用find( )扫描。注意,我们用ListIterator.nextIndex( )跟踪行号。
测试参数是JGrep.java和以[Ssct]开头的单词。
看到正则表达式能提供这么强大的功能,你可能会怀疑,是不是还需要原先的StringTokenizer。JDK 1.4以前,要想分割字符串,只有用StringTokenizer。但现在,有了正则表达式之后,它就能做得更干净利索了。
java.util.regex.*; com.bruceeckel.simpletest.*; java.util.*; ReplacingStringTokenizer { Test monitor = Test(); main(String[] args) { String input = ; StringTokenizer stoke = StringTokenizer(input); (stoke.hasMoreElements()) System.out.println(stoke.nextToken()); System.out.println(Arrays.asList(input.split())); monitor.expect( String[] { , , , , , , , , }); } } |
有了正则表达式,你就能用更复杂的模式将字符串分割开来——要是交给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对象来判断文件是否存在,因为如果你用FileOutputStream或FileWriter的话,文件就会被覆盖了。
我对I/O流类库的评价是比较矛盾的;它确实能干很多事情,而且做到了跨平台。但是如果你不懂decorator模式,就会觉得这种设计太难理解了,所以无论是对老师还是学生,都得多花精力。此外这个类库也不完整,否则我也用不着去写TextFile了。此外它没有提供格式化输出的功能,而其他语言都已经提供了这种功能。
但是,一旦你真正理解了decorator模式,并且能开始灵活运用这个类库的时候,你就能感受到这种设计的好处了。这时多写几行代码就算不了什么了。
如果你觉得不解渴(本章只是做个介绍,没想要面面俱到),可以去看Elliotte Rusty Harold 写的Java I/O (O'Reilly, 1999)。这本书讲得更深。