关于Java正则表达式的一些理解
正则表达式(regular expression)是一种可以在许多现代应用程序和编程语言中使用的特殊形式的代码模式。可以使用它们来验证输入是否符合给定的文本模式,在一大段文字中查找该模式的文本,用其它文本来替换匹配该模式的文本或者重新组织匹配文本的一部分,把一块文本划分成一系列更小的文本。
在Java语言中,从jdk1.4中加入了java.util.regex包提供对正则表达式的支持,而且Java.lang.String类中的replaceAll和split函数也是调用的正则表达式来实现的。在java.util.regex包中,常用到的类是Pattern和Matcher。典型的调用顺序为:
Pattern p = Pattern.compile("a*b"); // a*b是被编译的正则表达式
Matcher m = p.matcher("aaaaab"); // aaaaab为要匹配表达式
boolean b = m.matches(); //b为匹配结果
其等价于:
boolean b = Pattern.matches("a*b", "aaaaab");//对于重复匹配效率不高
说明一下,使用Matcher 类中的matches()方法是进行完全匹配,使用find()方法可以进行部分匹配。String类的matches()方法是调用Pattern类中的matches();String 类中的contains()方法也可以判断一个字符串中是否包含某一个子串。
1.基本语法
1.1元字符
正则表达式之所以拥有巨大的魔力,就是因为有12个标点字符才产生的,
$ ( ) * + . ? [ \ ^ { |
它们被称作元字符,如果想要在下正则表达式中匹配它们,那么就需要在它们前面用一个反斜杠\来进行转义。特别应该注意的是在这个列表中并不包含右方括号]、连字符-和右花括号},前两个字符只有在它们位于一个没有转义的[之后才成为元字符,而}只有在一个没有转义的{之后才是元字符。在任何时候都没有必要对}进行转义。
对任意其它非字母数字的字符进行转义不会转变你的正则表达式的规则----至少在.NET、Java、JavaScript、PCRE、Perl、Python、Ruby都不会这样,而对一个字母数字字符进行转义则会给它一个特殊含义,或者出现一个语法错误。
如果想要匹配$()*+.?[\^{|,(匹配单个元字符,当这些元字符混合成其它语法结构另当别论)则对应的正则表达式应该为:\$\(\)\*\+\.\?\[\\\^\{\|。但是在代码里面应该这样写:
Pattern p = Pattern.compile("\\$\\(\\)\\*\\+\\.\\?\\[\\\\\\^\\{\\|");
Matcher m = p.matcher("$()*+.?[\\^{|");
boolean b = m.matches();
注意:在Java里面,要匹配反斜杠\,需要使用“\\\\”,因为在字符串里面不允许出现单个反斜杠\,所以“\\”表示一个反斜杠\,“\\\\”才能正确匹配到反斜杠\。
Java 6、PCRE、Perl支持使用正则记号<\Q>和<\E>。<\Q>会抑制所有元字符的含义,直到出现<\E>为止。如果漏掉了<\E>,那么在<\Q>之后直到正则表达式结束之前的所有字符都会被当作字符文本来对待。所以上面的正则表达式可以用如下代码匹配:
Pattern p = Pattern.compile("\\Q$()*+.?[\\^{|\\E");
Matcher m = p.matcher("$()*+.?[\\^{|");
在字符串里面还有一个特殊的字符,双引号”,如果想要匹配一个字符器里面是否包含有双引号是,可以使用如下代码:
Pattern p = Pattern.compile("\"");
Matcher m = p.matcher("\"");
注:在字符串里面双引号需要转义
1.2匹配单个字符
除了元字符之外,匹配单个字符直接使用对应的字符来匹配,当然也有一些特殊的字符,如匹配一个包含ASCII控制字符的字符串:响铃、退出、换页、换行、回车、水平制表符和垂直制表符,对应的地十六进制ASCII分别是:07、1B、0C、0A、0D、09、0B。对应的参照表1如下:
x | 字符 x |
\\ | 反斜线字符 |
\0n | 带有八进制值 0 的字符 n (0 <= n <= 7) |
\0nn | 带有八进制值 0 的字符 nn (0 <= n <= 7) |
\0mnn | 带有八进制值 0 的字符 mnn(0 <= m <= 3、0 <= n <= 7) |
\xhh | 带有十六进制值 0x 的字符 hh |
\uhhhh | 带有十六进制值 0x 的字符 hhhh |
\t | 制表符 ('\u0009') |
\n | 新行(换行)符 ('\u000A') |
\r | 回车符 ('\u000D') |
\f | 换页符 ('\u000C') |
\a | 报警 (bell) 符 ('\u0007') |
\e | 转义符 ('\u001B') |
\cx | 对应于 x 的控制符 |
表1 单个字符匹配
1.3匹配字符类
使用的方括号[]的表示法被称作是一个字符类(character class)。一个字符类匹配在一个可能的字符列表中的单个字符。首先看一下预定义的字符类有哪些,如表2所示:
预定义字符类 | |
. | 任何字符(与行结束符可能匹配也可能不匹配) |
\d | 数字:[0-9] |
\D | 非数字: [^0-9] |
\s | 空白字符:[ \t\n\x0B\f\r] |
\S | 非空白字符:[^\s] |
\w | 单词字符:[a-zA-Z_0-9] |
\W | 非单词字符:[^\w] |
表2预定义字符类
在字符类之外,上面的12个标点字符是元字符。在一个字符类中,只有其中4个字符拥有特殊功能:\、^、-和]。(也就是说,在字符类里面,除了那4个特殊字符,其它的字符都不需要使用转义符。)如果使用的是Java或者是.NET,那么左方括号[在字符类也是一个元字符,所有的其它字符都是字面量,只是把它们自身加入到了字符类中。
点号是最古老也是最简单的正则表达式特性之一。它的含义永远是匹配任意单个字符。点号是最经常被滥用的正则表达式特性,最好只有当你确实想要允许出现任意字符是地,才使用点号,而在任何场合,都应当使用一个字符类或者是否定字符类来实现。
在.NET、Java、PCRE、Perl、Python中,<(?s)>是用于“点号匹配换行符”模式的模式修饰符。
反斜杠总是会对紧跟其后的字符进行转义,这与它在字符类之外的作用一样。被转义的字符可以是单个字符,也可以是一个范围的开始或结束。另外4个元字符只有当它们被放置在特定位置时才拥有特殊含义。在使用中总是对这些元字符进行转义会使你的正则表达式更加容易让人理解。
字符类 | |
[abc] | a、b 或 c(简单类) |
[^abc] | 任何字符,除了 a、b 或 c(否定) |
[a-zA-Z] | a 到 z 或 A 到 Z,两头的字母包括在内(范围) |
[a-d[m-p]] | a 到 d 或 m 到 p:[a-dm-p](并集) |
[a-z&&[def]] | d、e 或 f(交集) |
[a-z&&[^bc]] | a 到 z,除了 b 和 c:[ad-z](减去) |
[a-z&&[^m-p]] | a 到 z,而非 m 到 p:[a-lq-z](减去) |
表3 一般字符类匹配
字母数字字符则不能使用反斜杠来转义。
如果紧跟着左括号后面是一个脱字符(^),那么就会对整个字符类取否。也就是就它会匹配不属于该字符类列表中的任意字符。一个否定字符类会匹配换行符号,除非把换行也加入到否定字符类中。
连字符(-)被放在两个字符之间的时候就会创建一个范围。该范围所组成的字符类包含连字符之前的字符、连字符之后的字符,以及按照字母表顺序位于这两个字符之间的所有字符。
<\d>和<[\d]>都会匹配单个数字,每个小写的简写都拥有一个相关联的大写简定字符,其含义正好相反。因此<\D>会匹配不是数字的任意字符,所以同<[^\d]>是等价的。
<\w>会匹配单个的单词字符(word character),所谓的单词字符指的是能够出现在一个单词中的字符,这包括了字母、数字和下划线。<\W>则会匹配不属于上述字符集合中的任意字符。在Java、JavaScript、PCRE和Ruby中,<\w>总是和<[a-zA-Z0-9_]>的含义完全相同,而在.NET和Perl中,会包含其它字母表(泰语等)的字母和数字。
<\s>匹配任意的空白字符,其中包括了空格、制表符和换行符。在.NET、Perl和JavaScript中,<\s>也会匹配杜撰Unicode标准定义这空白号的字符。在JavaScript中对于<\s>使用的是Unicode,对<\d>和<\w>则使用ASCII标准。<\S>会匹配<\s>不能匹配的任意字符。
1.4量词
当我们要匹配的正则表达式里面有一部分重复多次时,比如说匹配手机号或固话时,我们可以使用量词来进行匹配固定次数或不定次数的重复。
如下面的例子:匹配一个10位的十进制数,可以使用如下正则表达式:
Matcher m = Pattern.compile("\\d{10}").matcher("0123456789");
while (m.find()) {
System.out.println(m.group());
}
运行结果为:
0123456789
正则表达式中的数量词有Greedy (贪心)量词、Reluctant(懒惰)量词和Possessive(占有)量词三种。
首先来看一个贪心量词,如下表所示:
Greedy 数量词 | |
X? | X,一次或一次也没有 |
X* | X,零次或多次 |
X+ | X,一次或多次 |
X{n} | X,恰好 n 次 |
X{n,} | X,至少 n 次 |
X{n,m} | X,至少 n 次,但是不超过 m 次 |
表4 贪心量词
量词<{n}>,其中n是一个正整数,用来重复之前的正则记号n次。
对于固定次数的重复,使用量词<{n}>。<{1}>这样和没有任何量词是等价的,<ab{1}c>和<abc>是等价的,<{0}>是重复之前的记号0次,<ab{0}c>和<ac>是同样的正则表达式。
我们使用量词<{n,m}>,其中n是一个正整数,并且m大于n,至少 n 次,但是不超过 m 次。对于可变次数重复的情形,其中所有选择分析重复的顺序就会产生影响。如果n和m是相等的,那就是固定次数的重复。
<\d+>也一样,在一个不是量词的正则记号之后添加一个“+”,意味着一次或多次;<\d{0,}>匹配零个或多个数字,<\d*>也一样,“*”意味着0次或多次。<h?>与<h{0,1}>的效果是一样的,在一个合法和完整的非量词正则记号之后的“?”意味着0或1次。
量词还可以嵌套。<(e\d+)?>会匹配一个e之后跟着一个或是多个数字,或者是一个长度为0的匹配。
Reluctant(懒惰)量词和Possessive(占有)量词与Greedy (贪心)量词基本语法类似,见下表:
Reluctant 数量词 | |
X?? | X,一次或一次也没有 |
X*? | X,零次或多次 |
X+? | X,一次或多次 |
X{n}? | X,恰好 n 次 |
X{n,}? | X,至少 n 次 |
X{n,m}? | X,至少 n 次,但是不超过 m 次 |
Possessive 数量词 | |
X?+ | X,一次或一次也没有 |
X*+ | X,零次或多次 |
X++ | X,一次或多次 |
X{n}+ | X,恰好 n 次 |
X{n,}+ | X,至少 n 次 |
X{n,m}+ | X,至少 n 次,但是不超过 m 次 |
表5 Reluctant(懒惰)量词和Possessive(占有)量词
在贪心量词后面加上一个问号“?”可以使任何量词变为懒惰量词;同理在贪心量词后面加上一个加号“+”可以使任何量词变为占有量词。下面来讲一下几种量词的区别:
因为总是从最大匹配开始匹配,故称贪婪。
因为总是从最小匹配开始,故称懒惰
possessive量词总是读完整个输入字符串,尝试一次(而且只有一次)匹配。和greedy量词不同,possessive从不后退。
贪心量词会找到最长的可能匹配,懒惰量词则会找到最短的可能匹配,两者都会进行回退,但是占有量词不进行回退。
使用如下代码进行验证:
Matcher m = Pattern.compile("1.*a").matcher("12a34abcd");
System.out.println("Greedy 贪心量词");
while (m.find()) {
System.out.println(m.group());
}
Matcher m1 = Pattern.compile("1.*?a").matcher("12a34abc");
System.out.println("Reluctant 懒惰量词");
while (m1.find()) {
System.out.println(m1.group());
}
Matcher m2 = Pattern.compile("1.*+a").matcher("12a34abc");
System.out.println("Possessive 占有量词");
while (m2.find()) {
System.out.println(m2.group());
}
得到的结果为:
Greedy 贪心量词
12a34a
Reluctant 懒惰量词
12a
Possessive 占有量词
1.5逻辑运算符、分组与边界匹配器
1.5.1边界匹配器
首先我们来看一个问题,匹配My cat is brown中的cat,但是不会匹配category或是bobcat,看下面的正则表达式:
Matcher m = Pattern.compile("\\bcat\\b").matcher("My cat is brown");
while (m.find()) {
System.out.println(m.group());
}
运行结果为:
cat
在上面的正则表达式里面,我们使用到了单词边界匹配器。在Java里面,边界匹配器如下表:
边界匹配器 | |
^ | 行的开头 |
$ | 行的结尾 |
\b | 单词边界 |
\B | 非单词边界 |
\A | 输入的开头 |
\G | 上一个匹配的结尾 |
\Z | 输入的结尾,仅用于最后的结束符(如果有的话) |
\z | 输入的结尾 |
表6 边界匹配器
正则表达式记号<\b>被称作是一个单词边界,它会匹配一个单词的开始或结束,就它自身而言,所产生的一个长度为0的匹配。
严格来讲,<\b>会匹配如下3种位置:
在目标文本的第一个字符之前,如果第一个字符是单词字符
在目标文本的最后一个字符之后,如果最后一个字符是单词字符
在目标文本的两个字符之间,其中一个是单词字符,而另外一个不是单词字符
<\B>会匹配在目标文本中的<\b>不匹配的第一个位置。换句话说,<\B>会匹配不属于单词开始或结束的每一个位置。
单词字符就是可以在单词中出现的字符。
JavaScript、PCRE和Ruby只把ASCII字符看做是单词字符。<\w>因此与<[a-zA-z0-9]>是完全等同的;.NET和Perl把所有语言字母表中的字母和数字都当作单词字符;Python则为你提供了一个选项,只有在创建正则表达式时传递了UNICODE或是U选项,非ASCII的字符才会被包括起来;Java则表现得不是很一致,<\w>只匹配ASCII字符,但是<\b>则是支持Unicode的,因此可以支持任何字母表。
正则表达式中的记号<^>,<$>,<\A>,<\Z>和<\z>被称为定位符(anchor),它们并不匹配任意字符。事实上,它们匹配的特定的位置,也就是说把正则表达式这些位置来进行匹配。
JavaScript不支持<\A>。定位符<^>和<\A>是等价的,前提是不能打开“^和$匹配换行处”这个选项。对于除了Ruby之外的所有其它正则表达式流派来说,该选项都是默认关闭的。除非使用JavaScript,一般都推荐使用<\A>。<\A>的含义问题保持不变的,因此可以避免由于正则选项设置而造成的混淆或错误。
.NET、Java、PCRE、Perl、Ruby同时支持<\Z>和<\z>,Python只支持<\Z>,JavaScript则根本不提供对<\Z>和<\z>的支持。<\Z>和<\z>的唯一区别是当目标文本的最后一个字符是换行符的时候,在这种情形下,<\Z>可以匹配到目标文本的最结尾处,也就是在最后的换行符之后 的位置,同时也可以匹配紧跟这个换行符之前的位置;<\z>则只会匹配目标文本的最末尾处,因此如果存在一个多余的换行符,那么它无法匹配。
定位符<$>和<\Z>是等价的,前提是不能打开“^和$匹配换行处”这个选项。对于除了Ruby之外的所有其它正则表达式流派来说,该选项都是默认关闭的。
1.5.2边界匹配器
当匹配多个选择分支时,如匹配Mary,Jane and Sue went to Mary’s house中的Mary,Jane或Sue,使用的正则表达式为:
Matcher m = Pattern.compile("Mary|Jane|Sue").matcher("Mary,Jane and Sue went to Mary’s house ");
while (m.find()) {
System.out.println(m.group());
}
执行结果为:
Mary
Jane
Sue
Mary
竖线,或是称作管道符号,会把正则表达式拆分成多个选择分支,每个只会匹配一个名字,但是每次却可以匹配不同的名字。
正则表达式里面的逻辑运算符如下表表示:
Logical 运算符 | |
XY | X 后跟 Y |
X|Y | X 或 Y |
(X) | X,作为捕获组 |
表7 Logical 运算符
上面的正则表达式还有以下问题:在匹配的过程中会匹配DJanet中的Jane。如下面的的正则表达式所示:
Matcher m = Pattern.compile("Mary|Jane|Sue").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
while (m.find()) {
System.out.println(m.group());
}
执行结果为:
Mary
Jane
Sue
Mary
Jane
这个时候,可能会想到之前使用到的单词边界匹配器,把正则表达式修改一下,添加单词边界匹配:
Matcher m = Pattern.compile("\\bMary|Jane|Sue\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
while (m.find()) {
System.out.println(m.group());
}
执行结果为:
Mary
Jane
Sue
Mary
Jane
执行结果有也不是我们想要的答案,上面的正则表达式写法上面有问题,应该写成下面这样:
Matcher m = Pattern.compile("\\bMary\\b|\\bJane\\b|\\b Sue\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
“|”在所有正则操作符中拥有最低的优先级。如果想要正则表达式中的一些内容不受替代操作影响的话,那么就需要把这些选择分支进行分组,分组是通过圆括号来实现的,括号拥有在所有正则操作符中的最高优先级。
Matcher m = Pattern.compile("\\b(Mary|Jane|Sue)\\b").matcher("Mary,Jane and Sue went to Mary’s house DJanet");
while (m.find()) {
System.out.println(m.group());
}
执行结果为:
Mary
Jane
Sue
Mary
一组圆括号不仅仅是一个分组,它还是一个捕获分组,正则表达式\b(\d\d\d\d)-(\d\d)-(\d\d)\b拥有三个捕获分组,分组是按照左括号的顺序从左到右进行编号的,(\d\d\d\d)、(\d\d)、(\d\d)分别为3个分组。
分组分为捕获性分组和非捕获性分组,简单的说捕获性分组就是捕获分组所匹配的内容暂且存储在某个地方,以便下次使用,捕获性分组以(...)表示,有些地方将取得捕获性分组所匹配结果的过程称之为"反向引用",非捕获性分组不捕获分组所匹配的内容,当然也就得不到匹配的结果,非捕获性分组以(?:...)表示,在一些只需要分组匹配但是并不需要得到各个分组匹配的结果时,使用非捕获性分组可以提高匹配速度。
在匹配过程中,当正则表达式引擎到达右括号而退出分组的时候,它会把该捕获分组所匹配到的文本的子串存储起来。当我们匹配2008-08-05时,2008被保存到第一个捕获中,08在第2个捕获中,而05则在第3个捕获中。
使用\b\d\d(\d\d)-\1-\1\b可以匹配像2008-08-08这样的日期(年减去世纪、月份和该月的天数都是相同的数字),在这个正则表达式中,我们使用反向引用来在该正则表达式中的任何地方匹配相同的文本。可以使用反斜杠之后跟一个单个数字(1~9)来引用前9个捕获分组,而第(10~99)则要使用\10~\99。
注意,不要使用\01。它或者是一个八进制的转义,或者会产生一个错误。在JavaScript中此正则表达式还会匹配12—34。因为在JavaScript中,对一个还没有参与匹配的分组的反向引用总是会匹配成功,这同捕获了长度为0的匹配的分组的反向引用是一样的。
1.6常用的正则表达式
正则表达式通常用于两种任务:1.验证,2.搜索/替换。用于验证时,通常需要在前后分别加上^和$,以匹配整个待验证字符串;搜索/替换时是否加上此限定则根据搜索的要求而定,此外,也有可能要在前后加上\b而不是^和$。此表所列的常用正则表达式,除个别外均未在前后加上任何限定,请根据需要,自行处理。
说明 | 正则表达式 |
网址(URL) | [a-zA-z]+://[^\s]* |
IP地址(IP Address) | ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?) |
电子邮件(Email) | \w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)* |
QQ号码 | [1-9]\d{4,} |
HTML标记(包含内容或自闭合) | <(.*)(.*)>.*<\/\1>|<(.*) \/> |
密码(由数字/大写字母/小写字母/标点符号组成,四种都必有,8位以上) | (?=^.{8,}$)(?=.*\d)(?=.*\W+)(?=.*[A-Z])(?=.*[a-z])(?!.*\n).*$ |
日期(年-月-日) | (\d{4}|\d{2})-((0?([1-9]))|(1[1|2]))-((0?[1-9])|([12]([1-9]))|(3[0|1])) |
日期(月/日/年) | ((0?[1-9]{1})|(1[1|2]))/(0?[1-9]|([12][1-9])|(3[0|1]))/(\d{4}|\d{2}) |
时间(小时:分钟, 24小时制) | ((1|0?)[0-9]|2[0-3]):([0-5][0-9]) |
汉字(字符) | [\u4e00-\u9fa5] |
中文及全角标点符号(字符) | [\u3000-\u301e\ufe10-\ufe19\ufe30-\ufe44\ufe50-\ufe6b\uff01-\uffee] |
中国大陆固定电话号码 | (\d{4}-|\d{3}-)?(\d{8}|\d{7}) |
中国大陆手机号码 | 1[358]\d{10} |
中国大陆邮政编码 | [1-9]\d{5} |
中国大陆身份证号(15位或18位) | \d{15}(\d\d[0-9xX])? |
非负整数(正整数或零) | \d+ |
正整数 | [0-9]*[1-9][0-9]* |
负整数 | -[0-9]*[1-9][0-9]* |
整数 | -?\d+ |
小数 | (-?\d+)(\.\d+)? |