正则表达式(Java 实现)
极速体验正则表达式威力
- 提取文章中所有的英文单词
- 提取文章中所有的数字
- 提取文章中所有的英文单词和数字
- 给你一个字符串(或文章),请你找出所有四个数字连在一起的字串
- 请验证输入的邮件,是否符合电子邮件格式
- 请验证输入的手机号,是否符合手机号格式
结论:正则表达式是处理文本的利器
正则表达式基本介绍
-
一个正则表达式,就是用某种模式去匹配字符串的一个公式。很多人因为正则表达式看上去比较古怪而且复杂所以不敢去使用。但是当你看完这篇文章并去加以练习后就会觉得这些复杂的表达式写起来还是相当简单的,而且,一旦你弄懂它们,你就能把数小时辛苦而且易错的文本处理工作缩短在几分钟(甚至几秒钟)内完成。
-
正则表达式:regular expression => RegExp
简单的说:正则表达式是对字符串执行模式匹配的技术。
几个简单的案例代码实现
1.提取文章中所有的英文单词
//假定,编写了爬虫,从百度页面得到如下文本
String content = "1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的" +
"静态网页能够“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通" +
"过网络传播并且能够跨平台运行。于是,世界各大IT企业为此纷纷投入了大量的" +
"人力、物力和财力。这个时候,Sun公司想起了那个被搁置起来很久的Oak,并且" +
"重新审视了那个用软件编写的试验平台,由于它是按照嵌入式系统硬件平台体系结" +
"构进行编写的,所以非常小,特别适用于网络上的传输系统,而Oak也是一种精简的" +
"语言,程序非常小,适合在网络上传输。Sun公司首先推出了可以嵌入网页并且可以" +
"随同网页在网络上传输的Applet(Applet是一种将小程序嵌入到网页中进行执行的技术)," +
"并将Oak更名为Java(在申请注册商标时,发现Oak已经被人使用了,再想了一系列" +
"名字之后,最终,使用了提议者在喝一杯Java咖啡时无意提到的Java词" +
"语)。5月23日,Sun公司在Sun world会议上正式发" +
"布Java和HotJava浏览器。IBM、Apple、DEC、Adobe、HP、Oracle、Netscape和微软" +
"等各大公司都纷纷停止了自己的相关开发项目,竞相购买了Java使用许可证,并为自己的产" +
"品开发了相应的Java平台";
Pattern pattern = Pattern.compile("[a-zA-Z]+");
Matcher matcher = pattern.matcher(content);
int no = 0;
while (matcher.find()) {
//匹配内容,文本,放到 m.group(0)
System.out.println("找到: " + (++no) + " " +matcher.group(0));
}
运行结果:
找到: 1 Oak
找到: 2 IT
找到: 3 Sun
找到: 4 Oak
找到: 5 Oak
找到: 6 Sun
找到: 7 Applet
找到: 8 Applet
找到: 9 Oak
找到: 10 Java
找到: 11 Oak
找到: 12 Java
找到: 13 Java
找到: 14 Sun
找到: 15 Sun
找到: 16 world
找到: 17 Java
找到: 18 HotJava
找到: 19 IBM
找到: 20 Apple
找到: 21 DEC
找到: 22 Adobe
找到: 23 HP
找到: 24 Oracle
找到: 25 Netscape
找到: 26 Java
找到: 27 Java
代码解读:
- 先创建一个 Pattern 对象 , 模式对象, 可以理解成就是一个正则表达式对象
Pattern pattern = Pattern.compile("[a-zA-Z]+");
- 创建一个匹配器对象
就是 matcher 匹配器按照 pattern (模式/样式), 到 content 文本中去匹配,找到就返回 true, 否则就返回 false
Matcher matcher = pattern.matcher(content);
- 可以开始循环匹配
int no = 0;
while (matcher.find()) {
//匹配内容,文本,放到 m.group(0)
System.out.println("找到: " + (++no) + " " +matcher.group(0));
}
关于 matcher.find() 和 matcher.group() 涉及到源码,此处的源码挺简单,建议阅读
会了这一个例子之后,掌握了正则表达式的三个步骤,做其它的匹配都是大同小异的,下面再举几个例子来巩固一下。
2.提取文章中所有的数字
注意,其它的代码都和上面的代码完全一样,只是正则表达式变了而已
Pattern pattern = Pattern.compile("[0-9]+");
运行结果:
找到: 1 1995
找到: 2 5
找到: 3 23
3.提取文章中所有的英文单词和数字
Pattern pattern = Pattern.compile("([0-9]+)|([a-zA-Z]+)");
运行结果:
找到: 1 1995
找到: 2 Oak
找到: 3 IT
找到: 4 Sun
找到: 5 Oak
找到: 6 Oak
找到: 7 Sun
找到: 8 Applet
找到: 9 Applet
找到: 10 Oak
找到: 11 Java
找到: 12 Oak
找到: 13 Java
找到: 14 Java
找到: 15 5
找到: 16 23
找到: 17 Sun
找到: 18 Sun
找到: 19 world
找到: 20 Java
找到: 21 HotJava
找到: 22 IBM
找到: 23 Apple
找到: 24 DEC
找到: 25 Adobe
找到: 26 HP
找到: 27 Oracle
找到: 28 Netscape
找到: 29 Java
找到: 30 Java
4.提取 IP 地址
String content = "私有地址(Private address)属于非注册地址,专门为组织机构内部使用。\n" +
"以下列出留用的内部私有地址\n" +
"A类 10.0.0.0--10.255.255.255\n" +
"B类 172.16.0.0--172.31.255.255\n" +
"C类 192.168.0.0--192.168.255.255";
Pattern pattern = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+");
\\d 表示一个任意的数字
运行结果:
找到: 1 10.0.0.0
找到: 2 10.255.255.255
找到: 3 172.16.0.0
找到: 4 172.31.255.255
找到: 5 192.168.0.0
找到: 6 192.168.255.255
底层实现(重要)
主要就是研究 matcher.find() 和 matcher.group()
通过一个例子来说明,即找到字符串里的连续4个数字
强烈建议:自己进行 debug,在第 16 行的位置加一个断点。用文字来叙述的话是不太好描述的,建议大家看完之后,自己进行 debug
String content = "1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月,Sun公司发布了" +
"第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的微型" +
"版),应用于移动、无线及有限资源的环境;J2SE(Java 2 Standard Edition,Java 2平台的" +
"标准版),应用于桌面环境;J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应" +
"用3443于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要的一个" +
"里程碑,标志着Java的应用开始普及9889 ";
//目标:匹配所有四个数字
//1. \\d 表示一个任意的数字
String regStr = "\\d\\d\\d\\d";
//2. 创建模式对象[即正则表达式对象]
Pattern pattern = Pattern.compile(regStr);
//3. 创建匹配器
//说明:创建匹配器matcher, 按照 正则表达式的规则 去匹配 content字符串
Matcher matcher = pattern.matcher(content);
//4.开始匹配
while (matcher.find()) {
System.out.println("找到: " + matcher.group(0));
}
}
代码是如何找到 1998 的?
matcher.find() 完成的任务:
- 根据指定的规则 ,定位满足规则的子字符串(比如 1998 )
- 找到后,将子字符串的开始的索引记录到 matcher 对象的属性 int[] groups 中,即记录 groups[0] = 0(因为 1998 中 1 的索引为 0),把该子字符串的结束的索引 + 1 的值记录到 groups[1] = 4。(为什么要记录索引 + 1呢?因为下面 matcher.group() 要调用 getSubSequence 进行截取字符串)
- 同时记录 oldLast 的值为 子字符串的结束的 索引 + 1 的值即 4, 即下次执行 find 时,就从 4 开始匹配
matcher.group(0) 分析:
首先看一下源码
public String group(int group) {
if (first < 0)
throw new IllegalStateException("No match found");
if (group < 0 || group > groupCount())
throw new IndexOutOfBoundsException("No group " + group);
if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
return null;
return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
}
这里我们只需要看第 8 行代码即可,注意我们传的参数的值是 0(因为 matcher.group(0)),再看第 8 行代码,很容易计算出 groups[group * 2] = groups[0] = 0,groups[group * 2 + 1] = groups[1] = 4
根据 groups[0] = 0 和 groups[1] = 4 的记录的位置,从 content 开始截取子字符串返回,就是 [0,4) 包含 0 但是不包含索引为 4 的位置(getSubSequence 的作用),再通过 toString() 返回,即 1998。
执行下一次循环,即先执行 matcher.find(),再执行 matcher.find() 。由于设定的字符串匹配规则,此时会定位到 1999 的位置。再次按照上面的规则来执行
-
根据指定的规则 ,定位满足规则的子字符串(比如 1999 )
-
找到后,将子字符串的开始的索引记录到 matcher 对象的属性 int[] groups;
groups[0] = 31,把该子字符串的结束的索引 + 1 的值记录到 groups[1] = 35。
-
同时记录 oldLast 的值为 子字符串的结束的 索引+1的值即 35, 即下次执行 find 时,就从 35 开始匹配
再次执行 matcher.group(0)
由于传的参还是 0,故由
return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
可知:groups[group * 2] = groups[0] = 31,groups[group * 2 + 1] = groups[1] = 35,故截取 [31,35)的字符串返回,即 1999
如果你有感觉到如果,不调用 matcher.group(0),而是调用 matcher.group(1),或者 matcher.group(2) 呢,那么说明你对正则表达式的理解更上一层了
什么是分组,比如 (\d\d)(\d\d) ,正则表达式中有() 表示分组,第1个()表示第1组,第2个()表示第2组…
String content = "1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月,Sun公司发布了" +
"第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的微型" +
"版),应用于移动、无线及有限资源的环境;J2SE(Java 2 Standard Edition,Java 2平台的" +
"标准版),应用于桌面环境;J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应" +
"用3443于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要的一个" +
"里程碑,标志着Java的应用开始普及9889 ";
String regStr = "(\\d\\d)(\\d\\d)";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到: " + matcher.group(0));
System.out.println("第1组()匹配到的值=" + matcher.group(1));
System.out.println("第2组()匹配到的值=" + matcher.group(2));
}
matcher.find() 完成的任务 (考虑分组)
-
根据指定的规则 ,定位满足规则的子字符串(比如(19)(98))
-
找到后,将 子字符串的开始的索引记录到 matcher 对象的属性 int[] groups 中,
2.1 groups[0] = 0 , 把该子字符串的结束的索引+1的值记录到 groups[1] = 4
2.2 记录1组()匹配到的字符串 groups[2] = 0 groups[3] = 2
2.3 记录2组()匹配到的字符串 groups[4] = 2 groups[5] = 4
2.4.如果有更多的分组…
- 同时记录 oldLast 的值为 子字符串的结束的 索引+1的值即 4, 即下次执行 find 时,就从 4 开始匹配
matcher.group():
return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();
调用 matcher.group(0) 时:由上面对 matcher.find() 的分析可知:groups[group * 2] = 0,groups[group * 2 + 1] = 4,由 getSubSequence 进行截取,,即返回 1998
调用 matcher.group(1) 时:由上面对 matcher.find() 的分析可知:groups[group * 2] = 0,groups[group * 2 + 1] = 2,由 getSubSequence 进行截取,,即返回 19
调用 matcher.group(2) 时:由上面对 matcher.find() 的分析可知:groups[group * 2] = 2,groups[group * 2 + 1] = 4,由 getSubSequence 进行截取,,即返回 98
运行结果:
找到: 1998
第1组()匹配到的值=19
第2组()匹配到的值=98
找到: 1999
第1组()匹配到的值=19
第2组()匹配到的值=99
找到: 3443
第1组()匹配到的值=34
第2组()匹配到的值=43
找到: 9889
第1组()匹配到的值=98
第2组()匹配到的值=89
小结:
如果正则表达式有 () 即分组,取出匹配的字符串规则如下:
- group(0) 表示匹配到的子字符串
- group(1) 表示匹配到的子字符串的第一组字串
- group(2) 表示匹配到的子字符串的第2组字串
- … 但是分组的数不能越界,比如只有两个分组,但是却调用 matcher.group(3)
正则表达式语法
元字符
如果想要灵活的运用正则表达式,必须了解其中各种元字符的功能,元字符从功能上大致分为:
- 限定符 ---- 限定字符的个数
- 选择匹配符 ----
- 分组组合和反向引用符
- 特殊字符
- 字符匹配符
- 定位符
元字符-转义号 \\
\\ 符号
说明:在我们使用正则表达式去检索某些特殊字符的时候,需要用到转义符号,否则检索不到结果,甚至会报错。
需要用到转移字符的有:.*+()$/?[]^{}
String content = "abc$(a.bc(123( )";
//匹配( => \\(
String regStr = "\\(";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到 " + matcher.group(0));
}
元字符-字符匹配符
表中出现的 ?,*,+ 等在元字符-限定符中有解释
符号 | 含义 | 实例 | 解释 |
---|---|---|---|
[ ] | 可接收的字符列表 | [efgh] | e、f、g、h 中的任意 1 个字符 |
[ ^ ] | 不接收的字符列表 | [^abc] | 除 a、b、c 之外的任意 1 个字符,包括数字和特殊符号 |
- | 连字符 | A-Z | 任意单个大写字母 |
符号 | 含义 | 示例 | 说明 | 匹配输入 |
---|---|---|---|---|
. | 匹配除 \n 以外的任何字符 | a…b | 以 a 开头,b 结尾,中间包括 2 个任意字符的长度为 4 的字符串 | aaab、aefb、a35b、a#*b |
\\d | 匹配单个数字字符,相当于 [0-9] | \\d{3}(\\d)? | 包含 3 个或 4 个数字的字符串 | 123、9876 |
\\D | 匹配单个非数字字符,相当于 [^0-9] | \\D(\\d)* | 以单个非数字字符开头,后接任意个数字字符串 | a、A342 |
\\w | 匹配单个数字、大小写字母字符,相当于 [0-9a-zA-Z] | \\d{3}\\w{4} | 以 3 个数字字符开头的长度为7的数字字母字符串 | 234abcd、12345Pe |
\\W | 匹配单个非数字、大小写字母字符,相当于 [^0-9a-zA-Z] | \\W+\\d{2} | 以至少 1 个非数字字母字符开头,2 个数字字符结尾的字符串 | #29,#?@10 |
元字符-选择匹配符
在匹配某个字符串的时候是选择性的,即:既可以匹配这个,又可以匹配那个,这时需要用到选择匹配符号
符号 | 含义 | 示例 | 解释 |
---|---|---|---|
| | 匹配"|"之前或之后的表达式 | ab|cd | ab 或者 cd |
String content = "zhu朱 最好的猪";
String regStr = "zhu|朱|猪";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到 " + matcher.group(0));
结果:
找到 zhu
找到 朱
找到 猪
元字符-限定符
用于指定其前面的字符和组合项连续出现多少次
符号 | 含义 | 示例 | 说明 | 匹配输入 |
---|---|---|---|---|
* | 指定字符重复 0 次或 n 次(无要求)零到多 | (abc)* | 仅包含任意个 abc 的字符串,等效于\w* | abc、abcabcabc |
+ | 指定字符重复 1次或 n 次(至少一次)1到多 | m+(abc)* | 以至少1个 m 开头,后接任意个 abc 的字符串 | m、mabc、mabcabc |
? | 指定字符重复 0 次或 1 次(最多一次)0到1 | m+abc? | 以至少1个m开头,后接ab或abc的字符串 | mab、mabc、mmmab、mmabc |
{n} | 只能输入 n 个字符 | [abcd]{3} | 由 abcd 中字母组成的任意长度为 3 的字符串 | abc、dbc、adc |
{n,} | 指定至少 n 个匹配 | [abcd]{3,} | 由 abcd 中字母组成的任意长度不小于 3 的字符串 | aab、dbc、aaabdc |
{n,m} | 指定至少 n 个但不多于 m 个匹配 | [abcd]{3,5} | 由 abcd 中字母组成的任意长度不小于 3 ,不大于 5 的字符串 | abc、abcd、aaaaa、bcdab |
注意:Java 匹配默认贪婪匹配,即尽可能匹配多的
元字符-定位符
定位符,规定要匹配的字符串出现的位置,比如在字符串的开始还是在结束的位置,这个也是相当有用的,必须掌握。
符号 | 含义 | 示例 | 说明 | 匹配输入 |
---|---|---|---|---|
^ | 指定起始字符 | 1+[a-z]* | 以至少 1 个数字开头,后接任意个小写字母的字符串 | 123、6aa、555edf |
$ | 指定结束字符 | 2\\-[a-z]+$ | 以 1 个数字开头后接连字符"-",并以至少1 个小写字母结尾的字符串 | 1-a |
\\b | 匹配目标字符串的边界 | zhu\\b | 这里说的字符串的边界指的是子串间有空格,或者是目标字符串的结束位置 | zhuyierzuihaodezhu zzzhu |
\\B | 匹配目标字符串的非边界 | zhu\\B | 和\\b 的含义刚刚相反 | zhuyierzuihaodezhu zzzhu |
应用实例
手机号码
要求: 必须以 13,14,15,18 开头的 11 位数 , 比如 13588889999
String regStr = "^1[3|4|5|8]\\d{9}$";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if(matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
url 地址
思路:
- 先确定 url 的开始部分 https:// | http:
- .然后通过 ([\\w-]+.)+[\\w-] 匹配
www.bilibili.com
String regStr = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/%.#]*)?$";
头的 11 位数 , 比如 13588889999
String regStr = "^1[3|4|5|8]\\d{9}$";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if(matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
url 地址
思路:
- 先确定 url 的开始部分 https:// | http:
- .然后通过 ([\\w-]+.)+[\\w-] 匹配
www.bilibili.com
String regStr = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/%.#]*)?$";