概念
是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。在很多文本编辑器或其他工具里,正则表达式通常被用来检索和/或替换那些符合某个模式的文本内容。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在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)
匹配空白行的正则表达式:ns*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]?dd?).){3}(2[0-4]d|25[0-5]|[01]?dd?)。
评注:提取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.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,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(贪婪的): 除非另有表示,否则数量表示符都是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 |
再提醒一下,要想让表达式照你的意思去运行,你应该用括号把'X'括起来。比方说:
abc+
似乎这个表达式能匹配一个或若干个'abc',但是如果你真的用它去匹配'abcabcabc'的话,实际上只会找到三个字符。因为这个表达式的意思是'ab'后边跟着一个或多个'c'。要想匹配一个或多个完整的'abc',你应该这样:
(abc)+
正则表达式能轻而易举地把你给耍了;这是一种建立在Java之上的新语言。
JDK 1.4定义了一个新的接口,叫CharSequence。它提供了String和StringBuffer这两个类的字符序列的抽象:
interface CharSequence {
charAt(int i);
length();
subSequence(int start,int end);
toString();
}
为了实现这个新的CharSequence接口,String,StringBuffer以及CharBuffer都作了修改。很多正则表达式的操作都要拿CharSequence作参数。
先给一个例子。下面这段程序可以测试正则表达式是否匹配字符串。第一个参数是要匹配的字符串,后面是正则表达式。正则表达式可以有多个。在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.regex的Pattern和Matcher类实现的。Pattern对象表示经编译的正则表达式。静态的compile( )方法负责将表示正则表达式的字符串编译成Pattern对象。正如上述例程所示的,只要给Pattern的matcher( )方法送一个字符串就能获取一个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匹配字符串的开头。
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参数的,正如你所看到的,它会告诉方法从哪里开始找——即从参数位置开始查找。
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,]" +
"[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( ),都会引发一个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.matches() start = 0 end = 35"
});
}
} ///:~ |
注意,只要字符串里有这个模式,find( )就能把它给找出来,但是lookingAt( )和matches( ),只有在字符串与正则表达式一开始就相匹配的情况下才能返回true。matches( )成功的前提是正则表达式与字符串完全匹配,而lookingAt( )[67]成功的前提是,字符串的开始部分与正则表达式相匹配。
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_INSENSITIVE,Pattern.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( )方法仅返回匹配的部分。
所谓分割是指将以正则表达式为界,将字符串分割成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,of exclamation!!points]","[Aha!,String,has,a,split(),built,in!]"
});
}
} ///:~ |
第二个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)。
下面我们来演示一下怎样使用这些替换方法。说明一下,这段程序所处理的字符串是它自己开头部分的注释,是用正则表达式提取出来并加以处理之后再传给替换方法的。
//: 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的功能是匹配'/*!' 和 '!*/' 之间的文本(注意一下分组用的括号)。接下来,我们将所有两个以上的连续空格全都替换成一个,并且将各行开头的空格全都去掉(为了让这个正则表达式能对所有的行,而不仅仅是第一行起作用,必须启用多行模式)。这两个操作都用了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。
//: 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设到当前字符串的开始处。
到目前为止,你看到的都是用正则表达式处理静态字符串的例子。下面我们来演示一下怎样用正则表达式扫描文件并且找出匹配的字符串。受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。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对象来判断文件是否存在,因为如果你用FileOutputStream或FileWriter的话,文件就会被覆盖了。
我对I/O流类库的评价是比较矛盾的;它确实能干很多事情,而且做到了跨平台。但是如果你不懂decorator模式,就会觉得这种设计太难理解了,所以无论是对老师还是学生,都得多花精力。此外这个类库也不完整,否则我也用不着去写TextFile了。此外它没有提供格式化输出的功能,而其他语言都已经提供了这种功能。
但是,一旦你真正理解了decorator模式,并且能开始灵活运用这个类库的时候,你就能感受到这种设计的好处了。这时多写几行代码就算不了什么了。
java正则表达式(例子:§ 简单的单词替换§ 电子邮件确认§ 从文件中删除控制字符§查找文件
)
正则表达式和Java编程语言byDanaNourieandMikeMcCloskey2001年8月2002年4月修订 应用程序常常需要有文本处理功能,比如单词查找、电子邮件确认或XML文档集成。这通常会涉及到模式匹配。Perl、sed或awk等语言通过使用正则表达式来改善模式匹配,正则表达式是一串字符,它所定义的模式可用来查找匹配的文本。为了使用JavaTM编程语言进行模式匹配,需要使用带有许多charAt子字串的StringTokenizer类,读取字母或符号以便处理文本。这常常导致复杂或凌乱的代码。现在不一样了。2平台标准版(J2SETM)1.4版包含一个名为java.util.regex的新软件包,使得使用正则表达式成为可能。目前的功能包括元字符的使用,它赋予正则表达式极大的灵活性本文概括地介绍了正则表达式的使用,并详细解释如何利用java.util.regex软件包来使用正则表达式,用以下常见情形作为 例子:§ 简单的单词替换§ 电子邮件确认§ 从文件中删除控制字符§查找文件 为了编译这些例子中的代码和在应用程序中使用正则表达式,需要安装J2SE1.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 八进制值的字符0mnn0mnn(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,orc(简单类) [^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对以逗号和/或空格分隔的输入字符串进行切分。 */ importjava.util.regex.*; publicclassSplitter{ publicstaticvoidmain(String[]args)throwsException{ //Createapatterntomatchbreaks Patternp=Pattern.compile(/"[,////s]+/"); //Splitinputwiththepattern String[]result= p.split(/"one,two,threefour,five/"); for(inti=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软件包在各种常见情形下的用法: 简单的单词替换 /* *Thiscodewrites/"Onedog,twodogsintheyard./" *tothestandard-outputstream: */ importjava.util.regex.*; publicclassReplacement{ publicstaticvoidmain(String[]args) throwsException{ //Createapatterntomatchcat Patternp=Pattern.compile(/"cat/"); //Createamatcherwithaninputstring Matcherm=p.matcher(/"onecat,/"+ /"twocatsintheyard/"); StringBuffersb=newStringBuffer(); booleanresult=m.find(); //LoopthroughandcreateanewString //withthereplacements while(result){ m.appendReplacement(sb,/"dog/"); result=m.find(); } //Addthelastsegmentofinputto //thenewString m.appendTail(sb); System.out.println(sb.toString()); } } 电子邮件确认 以下代码是这样一个例子:你可以检查一些字符是不是一个电子邮件地址。它并不是一个完整的、适用于所有可能情形的电子邮件确认程序,但是可以在需要时加上它。 /* *Checksforinvalidcharacters *inemailaddresses */ publicclassEmailValidation{ publicstaticvoidmain(String[]args) throwsException{ Stringinput=/"@sun.com/"; //Checksforemailaddressesstartingwith //inappropriatesymbolslikedotsor@signs. Patternp=Pattern.compile(/"^////.|^////@/"); Matcherm=p.matcher(input); if(m.find()) System.err.println(/"Emailaddressesdon/'tstart/"+ /"withdotsor@signs./"); //Checksforemailaddressesthatstartwith //www.andprintsamessageifitdoes. p=Pattern.compile(/"^www////./"); m=p.matcher(input); if(m.find()){ System.out.println(/"Emailaddressesdon/'tstart/"+ /"with///"www.///",onlywebpagesdo./"); } p=Pattern.compile(/"[^A-Za-z0-9////.////@_////-~#]+/"); m=p.matcher(input); StringBuffersb=newStringBuffer(); booleanresult=m.find(); booleandeletedIllegalChars=false; while(result){ deletedIllegalChars=true; m.appendReplacement(sb,/"/"); result=m.find(); } //AddthelastsegmentofinputtothenewString m.appendTail(sb); input=sb.toString(); if(deletedIllegalChars){ System.out.println(/"Itcontainedincorrectcharacters/"+ /",suchasspacesorcommas./"); } } } 从文件中删除控制字符 /*Thisclassremovescontrolcharactersfromanamed *file. */ importjava.util.regex.*; importjava.io.*; publicclassControl{ publicstaticvoidmain(String[]args) throwsException{ //Createafileobjectwiththefilename //intheargument: Filefin=newFile(/"fileName1/"); Filefout=newFile(/"fileName2/"); //Openandinputandoutputstream FileInputStreamfis= newFileInputStream(fin); FileOutputStreamfos= newFileOutputStream(fout); BufferedReaderin=newBufferedReader( newInputStreamReader(fis)); BufferedWriterout=newBufferedWriter( newOutputStreamWriter(fos)); //Thepatternmatchescontrolcharacters Patternp=Pattern.compile(/"{cntrl}/"); Matcherm=p.matcher(/"/"); StringaLine=null; while((aLine=in.readLine())!=null){ m.reset(aLine); //Replacescontrolcharacterswithanempty //string. Stringresult=m.replaceAll(/"/"); out.write(result); out.newLine(); } in.close(); out.close(); } } 文件查找 /* *Printsoutthecommentsfoundina.javafile. */ importjava.util.regex.*; importjava.io.*; importjava.nio.*; importjava.nio.charset.*; importjava.nio.channels.*; publicclassCharBufferExample{ publicstaticvoidmain(String[]args)throwsException{ //Createapatterntomatchcomments Patternp= Pattern.compile(/"//.*$/",Pattern.MULTILINE); //GetaChannelforthesourcefile Filef=newFile(/"Replacement.java/"); FileInputStreamfis=newFileInputStream(f); FileChannelfc=fis.getChannel(); //GetaCharBufferfromthesourcefile ByteBufferbb= fc.map(FileChannel.MAP_RO,(int)fc.size()); Charsetcs=Charset.forName(/"8859_1/"); CharsetDecodercd=cs.newDecoder(); CharBuffercb=cd.decode(bb); //Runsomematches Matcherm=p.matcher(cb); while(m.find()) System.out.println(/"Foundcomment:/"+m.group()); } } 结论 现在Java编程语言中的模式匹配和许多其他编程语言一样灵活了。可以在应用程序中使用正则表达式,确保数据在输入数据库或发送给应用程序其他部分之前,格式是正确的,正则表达式还可以用于各种各样的管理性工作。简而言之,在Java编程中,可以在任何需要模式匹配的地方使用正则表达式。