创建正则表达式
要想全面地掌握怎样构建正则表达式,可以去看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"。
逻辑运算符 | |
---|---|
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(贪婪的): 除非另有表示,否则数量表示符都是greedy的。Greedy的表达式会一直匹配下去,直到匹配不下去为止。(如果发现表达式匹配的结果与预期的不符),很有可能是因为它是greedy的,因此会一直匹配下去。
- Reluctant(勉强的): 用问号表示,它会匹配最少的字符。也称为lazy, minimal matching, non-greedy, 或ungreedy。
- Possessive(占有的): 目前只有Java支持。它更加先进,所以你可能还不太会用。用正则表达式匹配字符串的时候会产生很多中间状态,(一般的匹配引擎会保存这种中间状态,)这样匹配失败的时候就能原路返回了。占有型的表达式不保存这种中间状态,因此也就不会回头重来了。它能防止正则表达式的失控,同时也能提高运行的效率。
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
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( )方法还有一个版本,它需要一个控制正则表达式的匹配行为的参数:
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( )方法仅返回匹配的部分。
split( )
所谓分割是指将以正则表达式为界,将字符串分割成String数组。
String[] split(CharSequence charseq)
String[] split(CharSequence charseq, limit)这是一种既快又方便地将文本根据一些常见的边界标志分割开来的方法。
//: c12:SplitDemo.javajava.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。
//: c12:Resetting.javajava.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设到当前字符串的开始处。