正则表达式
学习内容来自B站韩顺平老师的Java基础课
正则表达式
可以快速方便的匹配字符串的内容,匹配内容可以通过特有规则的 pattern 指定
- 正则表达式就是用某种模式(pattern)去匹配字符串的一个公式
- 虽然看上去古怪,但是学起来并不算复杂
- 学会后可以大量缩短文本处理工作的耗时
比如从给定的文本中找出所有四个数字连在一起的子串,并且这四个数字中,第一个与第四个相同,第二个与第三个相同,比如 1221,3443
如果使用传统方法,只能通过遍历字符串,并且遍历的同时记录连续数字个数,然后再做相应判断,比较麻烦
而使用正则表达式,就可以通过指定 pattern 快速匹配出想要的内容
或者说验证邮箱、手机号的格式,也可以指定 pattern 快速进行判断
底层实现分析
比如在一串文本中找到所有四个数字连在一起的子串:
public static void main(String[] args) {
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" +
"平台的企业版),应用于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要" +
"的一个里程碑,标志着Java的应用开始普及。";
// 匹配所有四个数字
// 1. \\d 表示一个任意的数字
String regStr = "\\d\\d\\d\\d";
// 2. 创建 Pattern 对象
Pattern pattern = Pattern.compile(regStr);
// 3. 创建匹配器
// 说明:创建匹配器 matcher,按照 regStr 指定的规则去匹配 content 字符串
Matcher matcher = pattern.matcher(content);
// 4. 开始匹配
// 找到就返回 true,否则返回 false
// 匹配到的内容放入 matcher.group(0)
while (matcher.find()) {
System.out.println("找到:" + matcher.group(0));
}
}
其中matcher.find() 完成的任务有:
- 根据指定的规则,定位满足要求的字符串(比如 1998 )
- 找到后,将子串索引记录到 matcher 对象的属性 int[] groups 中(比如 1998 )。开始索引记录到 groups[0],即 groups[0] = 0;结束索引 +1 后记录到 groups[1] 中,即 groups[1] = 4
- 同时记录 oldLast 的值为 groups[1] 的值,用来作为下次执行 find() 方法的匹配开始位置
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();
}
上述可以概括为返回 [groups[0], groups[1]) 之间的子串,类似 subString 方法
那么为什么是 group(0),这个 0 又代表什么意思?
对上述例子稍作修改,在 pattern 中加上两对小括号,如下:
这样做相当于对正则表达式进行分组,有多少对括号就分成多少组,那么现在使用 matcher.find() 方法完成的任务有:
- 根据指定的规则,定位满足要求的字符串(比如 1998 )
- 找到后,将子串索引记录到 matcher 对象的属性 int[] groups 中
- 比如 1998,开始索引记录到 groups[0],即 groups[0] = 0; 结束索引 +1 后记录到 groups[1] 中,即 groups[1] = 4
- 考虑分组,对于子串 1998,记录第 1 组() 匹配的字符串 19 groups[2] = 0,groups[3] = 2
- 考虑分组,对于子串 1998,记录第 2 组() 匹配的字符串 98 groups[4] = 2,groups[5] = 4
- 如果有更多的分组,以此类推
- 同时记录 oldLast 的值为 groups[1] 的值,用来作为下次执行 find() 方法的匹配开始位置
即分组后,groups 中 0 和 1 索引记录的仍为匹配到的子串的首尾索引,往后的位置依次记录分组对应的索引
比如:
public static void main(String[] args) {
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" +
"平台的企业版),应用于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要" +
"的一个里程碑,标志着Java的应用开始普及。";
// 匹配所有四个数字
// 1. \\d 表示一个任意的数字
String regStr = "(\\d\\d)(\\d\\d)";
// 2. 创建 Pattern 对象
Pattern pattern = Pattern.compile(regStr);
// 3. 创建匹配器
// 说明:创建匹配器 matcher,按照 regStr 指定的规则去匹配 content 字符串
Matcher matcher = pattern.matcher(content);
// 4. 开始匹配
// 找到就返回 true,否则返回 false
// 匹配到的内容放入 matcher.group(0)
/**
* matcher.find() 完成的任务:
* 1. 根据指定的规则,定位满足要求的字符串(比如 1998 )
* 2. 找到后,将子串索引记录到 matcher 对象的属性 int[] groups 中
* 开始索引记录到 groups[0],即 groups[0] = 0
* 结束索引 +1 后记录到 groups[1] 中,即 groups[1] = 4
* (考虑分组) 2.1 groups[0] = 0,groups[1] = 4
* (考虑分组) 2.2 对于子串 1998,记录第 1 组() 匹配的字符串 19 groups[2] = 0,groups[3] = 2
* (考虑分组) 2.3 对于子串 1998,记录第 2 组() 匹配的字符串 98 groups[4] = 2,groups[5] = 4
* (考虑分组) 如果有更多的分组,以此类推
* 3. 同时记录 oldLast 的值为 groups[1] 的值,用来作为下次执行 find() 方法的匹配开始位置
*
* 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();
* }
*
* 1. 上述方法可以概括为返回 [groups[0], groups[1]) 之间的子串,类似 subString 方法
*
*/
while (matcher.find()) {
System.out.println("找到:" + matcher.group(0));
System.out.println("第 1 组 () 匹配到的值:" + matcher.group(1));
System.out.println("第 2 组 () 匹配到的值:" + matcher.group(2));
}
}
输出结果:
找到:1998
第 1 组 () 匹配到的值:19
第 2 组 () 匹配到的值:98
找到:1999
第 1 组 () 匹配到的值:19
第 2 组 () 匹配到的值:99
正则表达式语法
正则表达式由各种元字符组成,元字符从功能上大致分为:
- 限定符
- 选择匹配符
- 分组组合和反向引用符
- 特殊字符
- 字符匹配符
- 定位符
转义符
首先需要知道转义符为 \\
- 当需要使用正则表达式去检索某些特殊字符的时候,需要用到转移符号,否则检索不出结果,甚至会报错
就像使用 ( 去匹配 abc($abc(123( 会报错
在 ( 前加上 \ 即可
注意:在 Java 的正则表达式中,两个 \ 表示其它语言中的一个 \
需要用到转义符的字符有:
. * + ( ) $ / \ ? [ ] ^ { }
字符匹配符
其中
- \\d{3} 等价于 \\d\\d\\d
除此之外,还有:
-
\\s
匹配任意空白字符(空格、制表符) -
\\S
匹配任意非空白字符,与上一个相反 -
[abcd]
表示匹配 abcd 中的任意一个字符 -
[^abcd]
表示匹配不是 abcd 的任意一个字符
Java 的正则表达式默认区分大小写,如何实现不区分大小写?
-
(?i)abc
表示 abc 都不区分大小写 -
a(?i)bc
表示 bc 不区分大小写 -
a((?i)b)c
表示只有 b 不区分大小写 - 也可以在 Pattern 的 compile 方法中加上参数 Pattern.CASE_INSENSIVE,如
Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSIVE);
选择匹配符
在匹配某个字符时可以是选择性的,既可以匹配这个,又可以匹配那个。通俗理解就是逻辑表达式中的或操作,符号也和或一样,为 |
限定符
用于指定前面的字符和组合项连续出现多少次,比如前面 \\d{3} 等价于 \\d\\d\\d
注意:
- Java 匹配是贪婪匹配,如果指定为
a{3 ,4}
后,待匹配文本为 aaaa456,那么会匹配到 4 个 a,即 aaaa,而不是 3 个 a - 同理,如果指定为
a{3 ,4}
后,待匹配文本为 aaaaaaa789,那么会先匹配到 4 个 a,即 aaaa,再匹配到 3 个 a,即 aaa;而如果待匹配文本为 aaaaaa678,那么只会匹配到 4 个 a,即 aaaa -
+
表示匹配出现大于等于 1 次的,如果指定为1+
,待匹配文本为 1111456,那么会直接匹配到 1111 -
*
和?
同理
定位符
分组
例子:
public static void main(String[] args) {
String content = "jieruigou NN GGG1237gou 9987gou";
String regStr = "(?<g1>\\d\\d)(?<g2>\\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("第 1 个分组内容(通过组名):" + matcher.group("g1"));
System.out.println("第 2 个分组内容:" + matcher.group(2));
System.out.println("第 2 个分组内容(通过组名):" + matcher.group("g2"));
}
}
输出结果:
找到:1237
第 1 个分组内容:12
第 1 个分组内容(通过组名):12
第 2 个分组内容:37
第 2 个分组内容(通过组名):37
找到:9987
第 1 个分组内容:99
第 1 个分组内容(通过组名):99
第 2 个分组内容:87
第 2 个分组内容(通过组名):87
非捕获分组
例子 1:(?:pattern)
的使用
public static void main(String[] args) {
String content = "hi杰瑞狗 jerry杰瑞dog 杰瑞队长hello";
// 以下两种写法等价
// String regStr = "杰瑞狗|杰瑞dog|杰瑞队长";
String regStr = "杰瑞(?:狗|dog|队长)";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到:" + matcher.group(0));
}
}
输出结果
找到:杰瑞狗
找到:杰瑞dog
找到:杰瑞队长
例子 2:(?=pattern)
的使用
public static void main(String[] args) {
String content = "hi杰瑞狗 jerry杰瑞dog 杰瑞队长hello";
// 以下两种写法等价
// String regStr = "杰瑞狗|杰瑞dog|杰瑞队长";
// String regStr = "杰瑞(?:狗|dog|队长)";
String regStr = "杰瑞(?=dog|队长)";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到:" + matcher.group(0));
}
}
输出结果:
找到:杰瑞
找到:杰瑞
(?!pattern)
就相当于(?=pattern)
反过来的效果,(?=pattern)
匹配到的(?!pattern)
都匹配不到
注意:
- 非捕获分组不能使用 matcher.group(1)
非贪心匹配
之前提过 Java 默认的是贪婪匹配,非贪婪匹配可以通过加 ?
来实现
应用实例
汉字验证
public void isCharacter() {
String content = "杰瑞狗";
// 汉字的编码范围
String regStr = "^[\u0391-\uffe5]+$";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
}
邮编验证(不完全)
// 要求:六位数字
public void isMailCode() {
String content = "411320";
// 汉字的编码范围
String regStr = "^[1-9]\\d{5}$";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
}
QQ 号验证
// 要求:1 - 9 开头的数(5位~10位)
public void isQQId() {
String content = "411320";
// 汉字的编码范围
String regStr = "^[1-9]\\d{4,9}$";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
}
手机号验证
// 要求:1开头,第二位是 3 4 5 8 其中一个,共 11 位
public void isPhoneNumber() {
String content = "15966667777";
// 汉字的编码范围
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 验证
public void isURL() {
String content = "https://www.bilibili.com/video/BV1fh411y7R8?p=894&spm_id_from=pageDriver";
/**
* 分析思路
* 1. 开头可能有 https:// 或者 http://
* 2. 域名由 数字、字母、下划线、- 组成
* 3. 之后的路径由 \ 开头,然后跟上字母、数字以及一些字符组成
*/
String regStr = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/.%#]*)?$"; // [.?*] 中括号中的字符表示匹配字符本身
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println("满足格式");
} else {
System.out.println("不满足格式");
}
}
常用类
Pattern
pattern 对象是一个正则表达式对象,没有公共构造器
若想创建 Pattern 对象,就需要调用它的静态方法 compile() 返回 Pattern 对象
该方法需要接受一个正则表达式作为它的第一个参数,如:
Pattern pattern = Pattern.compile("^1[3|4|5|8]\\d{9}$");
Pattern 类还有其他方法,如
- matches,用来验证输入字符串是否满足给定要求
public void testMatches() {
String content = "hello jerry hello, gougougou";
String regStr = "hello";
// 若正则表达式能整体匹配给定文本,返回 true,否则返回 false
boolean matches = Pattern.matches(regStr, content);
System.out.println(matches ? "整体匹配成功" : "整体匹配失败");
}
就像上面应用实例的各种验证,其实可以使用 matches 方法
事实上,该方法底层仍然是调用 Matcher 类的 matches 方法
Matcher
Matcher 对象是对输入字符串进行解释和匹配的引擎,与 Pattern 类一样,Matcher 也没有公共构造方法,需要调用 Pattern 对象的 matcher 方法获得 Matcher 对象
Matcher matcher = pattern.matcher(content);
Matcher 类的常用方法有:
例子 1:
public void testMethod() {
String content = "hello jerry dog jack jessi hello jim hello";
String regStr = "hello";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("=========");
// 当前匹配到的子串的开始索引,相当于 groups[0]
System.out.println(matcher.start());
// 当前匹配到的子串的结束索引 + 1,相当于 groups[1]
System.out.println(matcher.end());
System.out.println("找到:" + content.substring(matcher.start(), matcher.end()));
}
// 整体匹配方法,校验某个字符串是否满足某个规则
System.out.println("整体匹配=" + matcher.matches());
}
输出结果:
=========
0
5
找到:hello
=========
27
32
找到:hello
=========
37
42
找到:hello
整体匹配=false
例子 2:
请将字符串 “hello jerry dog jack jessi hello jim hello” 中的 jerry 换成 杰瑞狗
public void testExchange() {
String content = "hello jerry dog jack jessi hello jerry jim hello";
String regStr = "jerry";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
// replaceAll 方法会返回替换后的新字符串,并不修改原来的字符串
String newContent = matcher.replaceAll("杰瑞狗");
System.out.println("newContent = " + newContent);
}
输出结果:
newContent = hello 杰瑞狗 dog jack jessi hello 杰瑞狗 jim hello
PatternSyntaxException
PatternSyntaxException 是一个非强制异常类,用来表示正则表达式中的语法错误
反向引用
重新回到一开始提到的问题:
- 从给定的文本中找出所有四个数字连在一起的子串,并且这四个数字中,第一个与第四个相同,第二个与第三个相同,比如 1221,3443
可以发现之前介绍的内容不能完成这个功能,所以需要介绍新方法–反向引用
首先需要明确三个概念
- 分组:
可以用小括号将正则表达式包起来,每个包起来的内容都可以视为一个分组 - 捕获:
把正则表达式中子表达式/分组匹配的内容保存到内存中,分组默认以数字编号区分,也可以进行显示命名。默认情况下,分组 0 为整个正则表达式的匹配结果,然后按从左至右将分组分为 1、 2、 以此类推 - 反向引用:
小括号的内容被捕获后,可以在这个括号之后(右侧)被使用,从而写出功能更为复杂的正则表达式,这就称为反向引用。反向引用既可以在正则表达式内部引用(通过 \\分组号 引用),也可以在外部引用(通过 $分组号 引用)
那么现在可以通过反向引用上面提出的问题:
public static void main(String[] args) {
String content = "jerry dog3443 dog1234 jerry 1221 hello";
String regStr = "(\\d)(\\d)\\2\\1";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到=" + matcher.group(0));
}
}
再来一个例子:
在字符串中检索编号,形式为:12321-333444111,即以五位数开头,然后接一个 -
,之后接一个九位数,要求每三位要相同
public void testNum() {
String content = "jerry12321-444555999 dog3443 dog1234 jerry 1221 hello";
String regStr = "\\d{5}-(\\d)\\1{2}(\\d)\\2{2}(\\d)\\3{2}";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到=" + matcher.group(0));
}
}
结巴去重案例
把类似:”我…我要…学学学学…java编程!“
通过正则表达式修改成”我要学java编程!“
public static void main(String[] args) {
String content = "我...我要...学学学学...java编程!";
// 去掉所有的 .
Pattern pattern = Pattern.compile("\\.");
Matcher matcher = pattern.matcher(content);
content = matcher.replaceAll("");
System.out.println(content);
// 去掉重复的字,方法一
// 先用 (.)\\1+ 匹配连续相同的字
pattern = Pattern.compile("(.)\\1+");
matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到=" + matcher.group(0));
}
// 再用反向引用 $1 替换匹配到的内容
String newContent = matcher.replaceAll("$1");
System.out.println("newContent = " + newContent);
// 去掉重复的字,方法二
content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
System.out.println("content = " + content);
}
输出结果:
我我要学学学学java编程!
找到=我我
找到=学学学学
newContent = 我要学java编程!
content = 我要学java编程!
在 String 类中使用正则表达式
替换
String 类的 replaceAll(String regex, String replacement) 方法,可以直接使用正则表达式进行替换
例子:
public static void main(String[] args) {
// 将下面文本中的 JDK1.3、JDK1.4 替换成 JDK
String content = "2000年5月,JDK1.3、JDK1.4和J2SE1.3相继发布,几周后其获得了" +
"Apple公司Mac OS X的工业标准的支持。2001年9月24日,J2EE1.3发布。2002" +
"年2月26日,J2SE1.4发布。自此Java的计算能力有了大幅提升,与J2SE1.3相比," +
"其多了近62%的类和接口。在这些新特性当中,还提供了广泛的XML支持、安全套接字" +
"(Socket)支持(通过SSL与TLS协议)、全新的I/OAPI、正则表达式、日志与断言" +
"。2004年9月30日,J2SE1.5发布,成为Java语言发展史上的又一里程碑。为了表示" +
"该版本的重要性,J2SE 1.5更名为Java SE 5.0(内部版本号1.5.0),代号为“Ti" +
"ger”,Tiger包含了从1996年发布1.0版本以来的最重大的更新,其中包括泛型支持、" +
"基本类型的自动装箱、改进的循环、枚举类型、格式化I/O及可变参数。";
// 使用 String 类的方法
content = content.replaceAll("JDK1\\.3|JDK1\\.4", "JDK");
System.out.println(content);
}
判断
String 类的 matches(String regex) 方法
例子:
判断给定的手机号是否是 138 / 139 开头
public void testMatches() {
String content = "13866666666";
if (content.matches("13(8|9)\\d{8}")) {
System.out.println("符合要求!");
} else {
System.out.println("不符合要求!");
}
}
分割
String 类的 split(String regex) 方法
例子:
按照 - + # ~ 分割字符串
public void testSplit() {
String content = "我+是-一~个#字-符+串";
String[] split = content.split("~|\\+|-|#");
for (String s : split) {
System.out.println("s = " + s);
}
}