正则表达式描述的是一种规则,符合这种限定规则的字符串我们认为它某种满足条件的,是我们所需的。在正则表达式中,主要有两种字符,一种描述的是普通的字符,另一种描述的是元字符。其中元字符是整个正则表达式的核心,并由它完成规则的制定工作。本篇文章主要从 Java 这门程序设计语言的角度理解正则表达式的应用,主要涉及以下内容:
一、正则表达式的理论基础
1、普通字符的表示
我们说正则表达式主要由普通字符和元字符组成,那么我们首先先看看普通字符该如何表示。大部分普通字符由字符本身即可表示,例如:'s','i','n','g','l','e'等。除此之外,也有一些特殊的表示方式。
以 / 0 开头,后面紧跟 1-3 位数字,表示的是一个八进制数。这个数的十进制值对应于 ASCII 编码中的相应字符。
以 / x 或者 / X 开头,后面紧跟两位字符,表示的是一个十六进制的数。该数的十进制的值对应于 ASCII 编码中相应的字符。
以 / u 开头,后面紧跟四位字符,表示一个 Unicode 编号。该编号对应于 Unicode 字符集中的一个具体字符。
另外还有一些元字符,虽然它们具有特殊的含义,但是往往在某种特殊情况下,需要将这些元字符当做普通字符使用,我们使用 '/'+ 元字符,表示转移该元字符,此后该元字符将表示一个普通字符。例如:'//','/^',它们分别表示的是 '/'和 '^',不再具有特殊含义了。下面我们开始逐渐介绍正则表达式语法中的元字符的特殊含义。
2、字符组匹配单个字符
我们用一对中括号([.....])表示字符组,整个字符组中会有多个字符位列其中,该字符组表示的含义是:匹配任意一个字符,该字符是位列字符组中的。例如:[single] 匹配的是字符's','i','n','g','l','e'中的任意一个字符。以上我们简单介绍了字符组的基本概念以及它所能匹配的内容,其实有时候为了表述连续的字符,我们会结合元字符 '-' 一起来操作字符组。例如:[0123456789],匹配的是 0 到 9 之间的任意一个数字,对于这种情况我们可以选择这样来简化操作:[0-9]。其实两者表述的含义是一样的,为了简化起见,如果遇到连续的字符表述,可以选择使用元字符来简化。同样的还有 [a-z],它匹配任意一个小写字母。对于元字符 '-' 还需要说明一点的是:该字符只有出现在两个字符之间才具有特殊含义,单独出现在字符组的所有字符之前或者之后只能表述普通字符 '-' 。下面介绍有关字符组的一些其他相关的元字符。
元字符 '^' 表示排除的意思,和元字符 '-' 类似,只有放在所有字符的最前面才具有特殊含义,否则只能表示普通字符。例如:[^1234],该字符组匹配一个字符,但是不是 1 或 2 或 3 或 4。当然,[c^yy],匹配的是四个普通字符,'c','^','y','y'。此外,需要注意一点的是,除了以上介绍的几种元字符必须置放于指定位置上才能起作用以外,其余所有元字符在字符组中统统被视作普通字符,不再具有特殊含义。
除此之外,字符组还支持嵌套使用。例如:[0-9[a-z]],该字符组匹配一个数字或者一个字母。我们也可以使用 && 加强限定规则。例如:[0-9&&[^0123]],该字符组匹配的是 0 到 9 之间任意一个数字,但是该数字不能是 0 到 3 中任意一个,也就是只能匹配 4 到 9 之间任意一个数字。最后和字符组有关的内容还是涉及一个预定义字符组,所谓预定义字符组就是对字符组的适当封装,对于一些简单的组合使用简介的调用方式。例如:
当然,它们也有相对应的大写形式,但是表示的意思却是截然相反的。
3、用于指定字符多次出现的量词
所谓的量词主要是三个元字符,它们主要用于指定量词前面的字符在匹配时可以多次出现,具体区别接下来会介绍。首先我们需要知道,这三个元字符是:+ ,*, ?。下面描述它们各自作用及相互之间的区别:
- se * cyy
对于该正则表达式而言,secyy,seecyy 都是可匹配的,并且 scyy 也是可以匹配的。这就是和元字符 + 的简单区别。
在这里我们要申明一个误区,这里的三个元字符量词作用的是紧邻该元字符前面的一个字符,并不是作用与元字符前面所有的字符,这里是需要注意的,包括笔者当初也都是误以为此的。
以上我们介绍了简单量词的概念,但是它们只能用于表示模糊的次数。可以出现多次,但是多次是多少却没有定论。对于要求字符出现精确次数的情况,我们可以使用通用量词来解决。{m,n} 是通用量词的最基本形式,它指定前面的字符出现的次数在 m 到 n 之间。看几个例子:
4、分组划分组别
在介绍分组之前,无论是使用量词还是字符组都是针对的一个字符。而分组针对的就是一串字符,我们也可以对分组使用量词,控制该分组出现的次数。我们使用()括号表示分组,例如:
- sing(le) de(cyy)
其中 le 和 cyy 分别是一个分组,对于一个完整的正则表达式,从头开始,每个分组都是有编号的,按照出现的次序,以 1 为基数递增。至于为什么要有编号,下文说。对于分组我们依然是可以使用量词控制其出现次数的,例如:
- sing(le) + cccc:在该正则表达式中,分组le可以出现一次或者多次sing(le) * cccc:在该正则表达式中,分组le可以出现零次或者多次
结合元字符 '|',可以实现和字符组一样的功效,例如:
- (happy | cyy | single)
该正则表达式可以匹配三个字符子串,happy,cyy,single。但是这里需要注意的是,元字符 | 如果用于字符组中就不再具有特殊含义,将会被作为普通字符来匹配。(这一点其实在介绍字符组的时候已经强调过)
下面解决一个上文遗留问题,分组的编号到底有什么作用。为分组编号其实是为了重新捕获和使用分组,每个分组按照出现的次序从 1 开始递增,我们使用 + 分组编号进行引用。例如:
- <(\w+)>
- (.*)
- </\1>
- :该正则表达式等效于:
- <(\w+)>
- (.*)
- </\w+>
(\w+) 表示任意个字符(字母或数字),(.*) 表示任意的符号,\1 则引用了分组 (\w+)。所以在这里,html 中所有非单标签元素都是能匹配的。当然,如果我们不想使用默认的编号来引用分组,我们其实也是可以在定义分组的时候为分组命名。为分组命名的语法格式为:(?<name>X),引用分组的语法格式为:\k<name>。例如:
- <(?<num1>
- a)>(.*)
- </\k<num1>
- >:等效于:
- <a>
- (.*)
- </a>
上述正则表达式定义了一个名为 num1 的分组,并后续进行了引用。下面介绍正则表达式的最后一块理论基础,边界匹配。
5、边界匹配
以上我们所介绍的所有内容主要还是针对单个字符或者多个字符组成的分组,我们可以限制他们的出现次数以及出现位置等。但是其实在正则表达式中,我们也是可以限制边界必须满足某种条件的。主要涉及的元字符有:^, $, \A, \Z, \z 和 \ b。
首先看元字符 ^ ,在字符组中,该元字符表示否定的意思,此处匹配正则表达式首部位置边界。例如:^abc 匹配一个以 abc 开头的字符串。
元字符 $ 匹配的字符串的尾部边界,它规定被匹配的字符串必须以什么结束。例如:
- abc$:dabc,
- abc,
- abc / n都是可匹配的
实际上,如果被匹配字符串是以指定字符结尾或者指定字符之后跟换行符,都是可匹配的。此处需要注意尾部边界匹配时的表述格式。(不同于首部匹配)
\b 匹配的是单词边界,所谓的单词边界指的就是:当一边是字符,一边是非字符的时候,此处即为单词边界。也就是单词结束的那个位置。还有一些边界,例如:\A,\b,\Z 等,各自匹配的边界如下图所示;
当然,对于边界匹配最通用的一种方式就是环视。它不局限于整个表达式的开头和结尾,它可以出现在表达式中的任何位置,既可以向前匹配,也可以向后匹配。主要分为以下四种情况:
虽然看起来有四种不同的环视类型,但是实际上分为两种,一种是向左看,一种是向右看。以上有关正则表达式的基本内容大致介绍完结,下面主要看看如何在 Java 中验证我们上述的这些理论。
二、Java API 对正则表达式的支持
在 Java 中,对正则表达式的支持,主要还是 java.util.regex 这个包,我们常用的是其中的 Pattern 和 Matcher 这两个类。其中 Pattern 绑定了一个正则表达式,也就是代表了一个规则,Matcher 绑定了一个 Pattern 和一个被处理的字符串,我们可以利用 Matcher 中的一些方法来完成匹配工作。此外,Java 中所有的正则表达式都是以字符串的形式出现的,所以自然离不开 String 这个类,该类中的很多方法的参数都是基于正则表达式的,下文将详细介绍。我们首先看 Pattern 这个类。
Pattern 主要用于编译一个正则表达式,也就是创建一个 Pattern 对象,该对象与实际的一个正则表达式想绑定,它仅仅代表一个规则,与实际要匹配的字符串无关。例如:
- String str = "//w";
- Pattern p = Pattern.compile(str);
Pattern 的 compile 方法将 str 这个正则表达式编译成一种内部结构,然后以 Pattern 实例的形式返回,至于这种内部结构是什么样子的,此处暂时不涉及。我们只需要知道,此时返回的 pattern 实例是绑定了一个正则表达式的。当然,Pattern 还有一个 compile 重载,可显式指定匹配模式。
- public static Pattern compile(String regex, int flags) {
- return new Pattern(regex, flags);
- }
此处主要有四种匹配模式可选,单行模式,多行模式,无视大小写模式,无视元字符模式(该模式下,所有元字符将会失效)。各自对应的常量:Pattern.DOTALL,Pattern.MULTILINE,Pattern.CASE_INSENSITIVE,Pattern.LITERAL。这些常量的值如下:
- public static final int DOTALL = 0x20;(32)public static final int MULTILINE = 0x08;(8)public static final int CASE_INSENSITIVE = 0x02;(2)public static final int LITERAL = 0x10;(16)
当然,我们没必要记住他们各自所对应的常量的值,在使用的时候直接调用它们的常量名即可。下面通过介绍 String 的几个基本方法,了解正则表达式在 Java 中的基本使用情况。
首先我们看 split 方法,该方法用于分割字符串,返回一个 String 数组。
- public String[] split(String regex, int limit)
第一个参数接受的是一个正则表达式,第二参数用于限定分割次数。其实从其源代码中我们大致可以知晓该方法作用原理:首先利用 indexOf 方法找到分割符首次出现的位置,将该位置以前所有字符保存,拿到剩余子串的所有内容,一样的操作。最后得到的数组就是按照分隔符分割的结果。limit 只不过强制限定了分割次数,达到次数上限,即使后面仍有分隔符可匹配,也选择放弃。(打包后面所有内容为一个分组),看个例子:
- public static void main(String[] args) {
- String str = "cyy,single.abc/https";
- String[] results = str.split("[,./]");
- for (String s: results) {
- System.out.print(s + " ");
- }
- }
- 输出结果:cyy single abc https
此处有人可能会有疑问,说好的 Pattern 和 Matcher 才是正则表达式的主要操作类,怎么没见到他们。其实在 split 内部调用的就是 Pattern 的相关方法。
- return Pattern.compile(regex).split(this, limit);
这是 String 类中 split 方法的最后一行代码,String 中的 split 方法除了最后一行代码,其余代码处理的都是 regex 为普通单个字符的情况,而对于多个字符乃至包含元字符的时候都是由 Pattern 中 split 方法处理的,该方法中会创建 Matcher 类并调用其中 find 等方法进行匹配查找,代码量比较多,此处不再赘述。
下面看 String 的一个匹配校验的方法。
- public boolean matches(String regex) {
- return Pattern.matches(regex, this);
- }
显然,该方法内部调用的是 Pattern 的 matches 方法,
- public static boolean matches(String regex, CharSequence input) {
- Pattern p = Pattern.compile(regex);
- Matcher m = p.matcher(input);
- return m.matches();
- }
这是一个非常标准的对正则表达式的处理流程,首先编译(绑定)正则表达式字符串获取 Pattern 实例,然后调用 Pattern 的 matcher 方法获取 Matcher 实例,接着就可以利用 Matcher 实例完成大量工作。此处调用 matches 方法完成对已绑定的正则表达式和预处理字符串的匹配工作,返回值为 boolean。
最后看 String 的 ReplaceAll 方法:
- public String replaceAll(String regex, String replacement) {
- return Pattern.compile(regex).matcher(this).replaceAll(replacement);
- }
该方法实际上还是依赖的 Matcher 中的 replaceAll 方法,由于一个 Matcher 实例是同时绑定一个正则表达式和一个被匹配字符串的。所以在 Matcher 内部的 replaceAll 方法在进行搜索匹配的时候就无需传入额外参数。具体代码,大家可以自行查看,此处节约篇幅不再赘述。
三、常见正则表达式的案例
接下来我们主要从日常较为普遍使用的一些案例中来深刻理解上述所有内容。
1、Email 地址
通常我们的 Email 地址的格式主要是:
以上是腾讯 QQ 邮箱的要求,相对而言已算是较为复杂,接下来我么看如何实现它。首先,第一条要求:
- [ - ._a - z0 - 9A - Z] {
- 3,
- 18
- }
满足第二条要求:
- [a - zA - Z][ - ._a - z0 - 9A - Z] {
- 1,
- 16
- }
- /w/
满足第三个条件:
- ( ? ![ - 0 - 9a - zA - Z._] * (--|\.\. | __))[a - zA - Z][ - ._a - z0 - 9A - Z] {
- 1,
- 16
- }
- /w/
至于最后一个条件的匹配,我们使用否定顺序环视来实现,它要求右边界所有内容不能是如下的形式:0 个或者多个(英文、数字、减号、点或下划线)加上两个连续减号或者点或者下划线。也就是说,右边如果由多个字符或者一个减号,点或者下划线,那是没事的,可一旦出现连续的减号,点或者下划线,那么就将立马被否定顺序环视匹配,进而不满足条件结束。
其实上述对邮箱用户名的匹配算是比较严格的,一般用于匹配邮箱用户名的正则表达式则没这么严格,具体要求如下:
由于比较简单,此处直接写出结果:
- [ ^ -._][ - ._a - zA - Z0 - 9] {
- 0,
- 63
- }
2、手机号码
在看一个手机号码的正则表达式匹配情况,具体要求如下:
最终的表述结果为:
- 1[34578][0 - 9] {
- 9
- }
还有很多案例,此处为了使篇幅不至过长,到此为止。更多案例还有待大家自行测试。
本文如有错误,望大家指出,相互学习。
来源: http://www.cnblogs.com/yangming1996/p/7168353.html