常用类解析:正则表达式 regex
前言
正则表达式不仅仅是Java的技术,在任何一门编程语言中都会存在,是一种通用的IT技术,其理念和用法在任何编程语言中基本一致,除了有一些由于语言不同而导致的一些语法不同正则表达式,主要用于匹配(查找 替换 计数)字符串中的数据的,也叫做文本匹配技术。
一、正则表达式
经常会需要编写代码来验证用户输人,比如验证输人是否是一个数字,是否是一个全部小写的字符串,或者社会安全号。如何编写这种类型的代码呢?一个简单而有效的做法是使用正则表达式来完成这个任务。
正則表达式(regular expression,简写为 regex) 是一个字符串,用来描述匹配一个字符串集合的模式。对于字符串处理来说,正则表达式是一个强大的工具。可以使用正则表达式来匹配、替换和分割字符串。
比如:给定一个字符串"13088889090",如何判断它是手机号?
public boolean isPhoneNumber(String s) {
if (s.length() != 11) {
return false;
}
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
对于上述问题的求解而言,我们只解决了长度的问题,内容必须是数字字符的问题,但是,对于手机号这个特殊的数字而言
第1位能否为0?前3位表示对应公司(联通 移动 电信)123 109 110??? 前3位它不是真随机的?就现在这两个问题而言,如果再去用代码做的话,会比较复杂一些;
比如:对于给定给一个字符串s,如何判断该字符串是邮箱地址呢?
public boolean isEmail(String s) {
......
}
“zh@xianoupeng.com” “346530074@qq.com” "H_LaK_123@haha.xixi.lala.gov"
为了每一种判断逻辑都要编写代码实现是太繁琐了,正则表达式就是出来解决这些问题的!
如果用正则表达式,去匹配手机号的话
public boolean isPhoneNumber(String s) {
//确定了长度为11 并且也确定了必须是数字字符
return s.matches("\\d{11}");
}
从目前的角度而言,问题的解决确实变简单了,代码可读性的难度就上来了
在Java标准库中java.uitl.regex包里内置了正则表达式引擎,在Java程序中使用正则非常方便,使用的正则表达式就是一个描述规则的字符串,所以,我们只需要编写正确的规则,我们就可以让正则表达式引擎去判断目标字符串是否符合规则即可。
比如:判断一个年份 "20##"年,我们写出规则如下:
一个4个字符:2,0 ,0~9任意一个数字 ,0~9任意一个数字;
对应的正则表达式:20\d\d
其中\d
表示的是任意一个数字的意思;
但是在Java中,正则表达式要用字符串来表示,"20\d\d"这样写是不对的,因为在Java中 " \ “是转义字符,最终应该这么写"20\\d\\d”。
二、匹配规则
正则表达式的匹配规则是从左到右按规则匹配的。我们首先来看如何用正则表达式进行精确匹配:
对于正则表达式 abc 来说,它只能精确地匹配字符串 “abc” ,不能匹配 “ab” , “Abc” , “abcd” 等其他任何字符串。
如果正则表达式有特殊字符,那就需要用 \ 转义。例如,正则表达式 a\&c ,其中 \& 是用来匹配特殊字符 & 的,它能精确匹配字符串 “a&c” ,但不能匹配 “ac” 、 “a-c” 、 “a&&c” 等。
要注意正则表达式在Java代码中也是一个字符串,所以,对于正则表达式 a\&c 来说,对应的Java字符串是 “a\\&c” ,因为 \ 也是Java字符串的转义字符,两个 \\ 实际上表示的是一个 \ :
String regex01 = "20\\d\\d";
System.out.println("2019".matches(regex01));
System.out.println("2100".matches(regex01));
System.out.println("20AB".matches(regex01));
String regex02 = "abc";
System.out.println("abc".matches(regex02));
System.out.println("ab".matches(regex02));
String regex03 = "a\\&c";
System.out.println("a&c".matches(regex03));
System.out.println("ac".matches(regex03));
如果想匹配非ASCII字符,例如中文,那就用 \u#### 的十六进制表示,例如: a\u548cc 匹配字符串 “a和c” ,中文字符 和 的Unicode编码是 548c 。
String regex04 = "a\\u548cc";
System.out.println("a和c".matches(regex04));
System.out.println("a哈c".matches(regex04));
三、匹配任意字符
精确匹配实际上用处不大,因为我们直接用 String.equals() 就可以做到。大多数情况下,我们想要的匹配规则更多的是模糊匹配。我们可以用 “ . ”匹配一个任意字符。
例如,正则表达式 a.c 中间的 . 可以匹配一个任意字符,例如,下面的字符串都可以被匹配:
- “abc” ,因为 . 可以匹配字符 b ;
- "a&c" ,因为 . 可以匹配字符 & ;
- “acc” ,因为 . 可以匹配字符 c 。
但它不能匹配 “ac” 、 “a&&c” ,因为 . 匹配一个字符且仅限一个字符。
String regex05 = "a.c";
System.out.println("abc".matches(regex05));//true
System.out.println("a&c".matches(regex05));//true
System.out.println("acc".matches(regex05));//true
System.out.println("ac".matches(regex05));//false
System.out.println("a&&c".matches(regex05));//false
四、匹配数字
用 . 可以匹配任意字符,这个口子开得有点大。如果我们只想匹配 0 ~ 9 这样的数字,可以用 \d 匹配。例如,正则表达式 00\d 可以匹配:
- “007” ,因为 \d 可以匹配字符 7 ;
- “008” ,因为 \d 可以匹配字符 8 。
它不能匹配 “00A” , “0077” ,因为 \d 仅限单个数字字符。
String regex06 = "Demo\\d\\d\\.java";
System.out.println("Demo11.java".matches(regex06));//true
System.out.println("Demo123.java".matches(regex06));//false
System.out.println("Demo.java".matches(regex06));//false
System.out.println("Demo12.avi".matches(regex06));//false
五、匹配常用字符
用 \w 可以匹配一个字母、数字或下划线,w的意思是word。例如, java\w 可以匹配:
- “javac” ,因为 \w 可以匹配英文字符 c ;
- “java9” ,因为 \w 可以匹配数字字符 9 ;
- “java_” ,因为 \w 可以匹配下划线 _ 。
它不能匹配 “java#” , "java " ,因为 \w 不能匹配 # 、空格等字符。
String regex07 = "java\\w";
System.out.println("java_".matches(regex07));//true
System.out.println("javac".matches(regex07));//true
System.out.println("java".matches(regex07));//false
System.out.println("java!".matches(regex07));//false
六、匹配空格字符
用 \s 可以匹配一个空格字符,注意空格字符不但包括空格 " “`,还包括tab字符(在Java中用制表符”\t"表示)。 例如,a\sc可以匹配:
- “a c” ,因为 \s 可以匹配空格字符
" "
; - “a c” ,因为 \s 可以匹配tab字符 \t 。
它不能匹配 “ac” , “abc” 等。
String regex08 = "a\\sc";
System.out.println("a c".matches(regex08));//true
System.out.println("a c".matches(regex08));//false
System.out.println("ac".matches(regex08));//false
System.out.println("a c".matches(regex08));//false
System.out.println("a\tc".matches(regex08));//true
七、匹配非数字
用 \d 可以匹配一个数字,而 \D 则匹配一个非数字。例如, 00\D 可以匹配:
- “00A” ,因为 \D 可以匹配非数字字符 A ;
- “00#” ,因为 \D 可以匹配非数字字符 # 。
00\d 可以匹配的字符串 “007” , “008” 等, 00\D 是不能匹配的。
类似的, \W 可以匹配 \w 不能匹配的字符, \S 可以匹配 \s 不能匹配的字符,这几个正好是反着来的。
所以说:
\d
任意一个数字\D
任意一个非数字;\s
任意一个空格\S
任意一个非空格;\w
任意一个数字字母下划线\W
任意一个非数字字母下划线
String regex09 = "\\d\\w\\W\\D";
System.out.println("1234".matches(regex09));//false
System.out.println("9w!c".matches(regex09));//true
八、重复匹配
我们用 \d 可以匹配一个数字,例如, A\d 可以匹配 “A0” , “A1” ,如果要匹配多个数字,比如 “A380” ,怎么办?
修饰符 * 可以匹配任意个字符,包括0个字符。我们用 A\d* 可以匹配:
- A :因为 \d* 可以匹配0个数字;
- A0 :因为 \d* 可以匹配1个数字 0 ;
- A380 :因为 \d* 可以匹配多个数字 380 。
String regex10 = "A*"; //连续0个或多个A
System.out.println("A".matches(regex10));
System.out.println("Animal".matches(regex10));
System.out.println("A123123".matches(regex10));
System.out.println("AAAAAAA".matches(regex10));
String regex11 = "A\\d*";
System.out.println("A12313123123".matches(regex11));
System.out.println("A".matches(regex11));
System.out.println("Aasdhjahsjdk1123".matches(regex11));
System.out.println("".matches(regex11));
修饰符 + 可以匹配至少一个字符。我们用 A\d+ 可以匹配:
- A0 :因为 \d+ 可以匹配1个数字 0 ;
- A380 :因为 \d+ 可以匹配多个数字 380 。
但它无法匹配 “A” ,因为修饰符 + 要求至少一个字符。
String regex12 = "A\\d+";
System.out.println("A12313123123".matches(regex12));
System.out.println("A".matches(regex12));
System.out.println("Aasdhjahsjdk1123".matches(regex12));
System.out.println("".matches(regex12));
修饰符 ? 可以匹配0个或一个字符。我们用 A\d? 可以匹配:
- A :因为 \d? 可以匹配0个数字;
- A0 :因为 \d+ 可以匹配1个数字 0 。
但它无法匹配 “A33” ,因为修饰符 ? 超过1个字符就不能匹配了。
String regex13 = "A\\d?";
System.out.println("A12313123123".matches(regex13));
System.out.println("A".matches(regex13));
System.out.println("A1".matches(regex13));
System.out.println("".matches(regex13));
如果我们想精确指定n个字符怎么办?用修饰符 {n} 就可以。 A\d{3} 可以精确匹配:
- A380 :因为 \d{3} 可以匹配3个数字 380 。
如果我们想指定匹配n~m个字符怎么办?用修饰符 {n,m} 就可以。 A\d{3,5} 可以精确匹配:
- A380 :因为 \d{3,5} 可以匹配3个数字 380 ;
- A3800 :因为 \d{3,5} 可以匹配4个数字 3800 ;
- A38000 :因为 \d{3,5} 可以匹配5个数字 38000 。
如果没有上限,那么修饰符 {n,} 就可以匹配至少n个字符。
System.out.println("A123".matches("A\\d{3}"));
System.out.println("A123".matches("A\\d{3,5}"));
System.out.println("A12345".matches("A\\d{3,5}"));
System.out.println("A123".matches("A\\d{4,}"));
比如匹配一个座机号 010-33167854 3个数字-8个数字 \d{3}\-\d{8}
。
System.out.println("010-12345678".matches("\\d{3}\\-\\d{8}"));
System.out.println("0101-2345678".matches("\\d{3}\\-\\d{8}"));
九、正则表达式小总结
正则表达式 规则 可以匹配
A 固定字符A "A"
\u548c 指定Unicode字符 "和"
. 任意一个字符 "a" "1" " "
\d 一个数字字符0~9 "0"~"9"
\w 一个数字或字母或下划线
\W 非\w
\D 非\d
\s 一个空格字符(\t ' ')
\S 非\s
A* A有任意个 "" "A" "AA" "AAA"
A+ A至少一个 "A" "AA" "AAA"
A? A0个或1个 "" "A"
A{3} A出现3次 "AAA"
A{3,5} A出现3~5次 "AAA" "AAAA" "AAAAA"
A{2,} A出现至少2次 "AA" "AAA" "AAAA"
A{0,3} A出现0~3次 "" "A" "AA" "AAA"
十、匹配开头和结尾
用正则表达式进行多行匹配时,我们用 ^ 表示开头,$ 表示结尾。例如, ^A\d{3}$ ,可以匹配 “A001” 、 “A380” 。
String regex01 = "^Demo\\w*\\.java$";
System.out.println("Demo01.java".matches(regex01));//true
十一、匹配指定范围
如果我们规定一个7~8位数字的电话号码不能以 0 开头,应该怎么写匹配规则呢? \d{7,8} 是不行的,因为第一个 \d 可以匹配到 0 。
使用 […] 可以匹配范围内的字符,例如, [123456789] 可以匹配 1 ~ 9 ,这样就可以写出上述电话号码的规则: [123456789]\d{6,7} 。
把所有字符全列出来太麻烦, […] 还有一种写法,直接写 [1-9] 就可以。
要匹配大小写不限的十六进制数,比如 1A2b3c ,我们可以这样写: [0-9a-fA-F] ,它表示一共可以匹配以下任意范围的字符:
- 0-9 :字符 0 ~ 9 ;
- a-f :字符 a ~ f ;
- A-F :字符 A ~ F 。
如果要匹配6位十六进制数,前面讲过的 {n} 仍然可以继续配合使用: [0-9a-fA-F]{6} 。
[…] 还有一种排除法,即不包含指定范围的字符。假设我们要匹配任意字符,但不包括数字,可以写 [^1-9]{3} :
- 可以匹配 “ABC” ,因为不包含字符 1 ~ 9 ;
- 可以匹配 “A00” ,因为不包含字符 1 ~ 9 ;
- 不能匹配 “A01” ,因为包含字符 1 ;
- 不能匹配 “A05” ,因为包含字符 5 。
String regex02 = "[2-9]\\d{5}";
System.out.println("345678".matches(regex02));//true
System.out.println("123456".matches(regex02));//false
String regex03 = "[0-9a-fA-F]{4}";
System.out.println("12ab".matches(regex03));//true
System.out.println("wc99".matches(regex03));//false
十二、逻辑或匹配规则
用 | 连接的两个正则规则是或 规则,例如, AB|CD 表示可以匹配 AB 或 CD 。
String regex04 = ".*java|c|python|go.*";
System.out.println("I like java".matches(regex04));//true
System.out.println("I like c++".matches(regex04));//false
十三、使用括号
现在我们想要匹配字符串 learn java 、 learn php 和 learn go 怎么办?一个最简单的规则是 learn\sjava|learn\sphp|learn\sgo ,但是这个规则太复杂了,可以把公共部分提出来,然后用 (…) 把子规则括起来表示成 learn\s(java|php|go) 。
String regex05 = "learn\\s(java|php|go)";
System.out.println("learn java".matches(regex05));//true
System.out.println("learn c++".matches(regex05));//false
复杂匹配规则主要有:
十四、分组匹配
我们前面讲到的 (…) 可以用来把一个子规则括起来,这样写 learn\s(java|php|go) 就可以更方便地匹配长字符串了。
实际上 (…) 还有一个重要作用,就是分组匹配。
我们来看一下如何用正则匹配 区号-电话号
码这个规则。利用前面讲到的匹配规则,写出来很容易:
\d{3,4}-\d{6,8}
虽然这个正则匹配规则很简单,但是往往匹配成功后,下一步是提取区号和电话号码,分别存入数据库。于是问题来了:如何提取匹配的子串?
当然可以用 String 提供的 indexOf() 和 substring() 这些方法,但它们从正则匹配的字符串中提取子串没有通用性,下一次要提取 learn\s(java|php) 还得改代码。
正确的方法是用 (…) 先把要提取的规则分组,把上述正则表达式变为 (\d{3,4})- (\d{6,8}) 。
现在问题又来了:匹配后,如何按括号提取子串?
现在我们没办法用 String.matches() 这样简单的判断方法了,必须引入 java.util.regex 包,底层实际上先创建一个关于正则表达式规则的对象Pattern, 然后规则对象Pattern对数据进行匹配,生成一个匹配对象Matcher,匹配的结果:都在Matcher里,有一个方法Matcher中的**mathces()**才是最终判断字符串是否匹配规则的结果boolean,如果匹配成功,就可以直接从 Matcher.group(index) 返回子串:
Pattern p = Pattern.compile("(\\d{3,4})-(\\d{6,8})");
Matcher m = p.matcher("010-12345678");
if (m.matches()) {
//获取的结果是整个电话号
System.out.println(m.group(0));
//获取的第一个部分 区号
System.out.println(m.group(1));
//获取的第二个部分 电话
System.out.println(m.group(2));
}
Pattern
我们在前面的代码中用到的正则表达式代码是 String.matches()
方法,而我们在分组提取的代码中用的是 java.util.regex
包里面的 Pattern
类和 Matcher
类。实际上这两种代码本质上是一样的,因为 String.matches()
方法内部调用的就是 Pattern
和 Matcher
类的方法。
但是反复使用 String.matches()
对同一个正则表达式进行多次匹配效率较低,因为每次都会创建出一样的 Pattern
对象。完全可以先创建出一个 Pattern
对象,然后反复使用,就可以实现编译一次,多次匹配:
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
pattern.matcher("010-12345678").matches(); // true
pattern.matcher("021-123456").matches(); // true
pattern.matcher("022#1234567").matches(); // false
// 获得Matcher对象:
Matcher matcher = pattern.matcher("010-12345678");
if (matcher.matches()) {
String whole = matcher.group(0); // "010-12345678", 0表示匹配的整个字符串
String area = matcher.group(1); // "010", 1表示匹配的第1个子串
String tel = matcher.group(2); // "12345678", 2表示匹配的第2个子串
System.out.println(area);
System.out.println(tel);
}
}
}
使用Matcher时,必须首先调用matches()判断是否匹配成功,匹配成功后,才能调用group()提取子串。
利用提取子串的功能,我们轻松获得了区号和号码两部分。
正则表达式用
(...)
分组可以通过Matcher
对象快速提取子串:
group(0)
表示匹配的整个字符串;group(1)
表示第1个子串;group(2)
表示第2个子串,以此类推。
十五、非贪婪匹配
在介绍非贪婪匹配前,我们先看一个简单的问题:
给定一个字符串表示的数字,判断该数字末尾 0 的个数。例如:
"123000"
:3个0
"10100"
:2个0
"1001"
:0个0
可以很容易地写出该正则表达式:(\d+)(0*)
,Java代码如下:
Pattern p1 = Pattern.compile("(\\d+)(0*)");
Matcher m1 = p1.matcher("123000");
if (m1.matches()) {
System.out.println("group1 = " + m1.group(1));
System.out.println("group2 = " + m1.group(2));
}
然而打印的第二个子串是空字符串" "
。
实际上,我们期望分组匹配结果是:
但实际的分组匹配结果是这样的:
仔细观察上述实际匹配结果,实际上它是完全合理的,因为 \d+
确实可以匹配后面任意个 0
。
这是因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配,因此,\d+
总是会把后面的 0
包含进来。
要让 \d+
尽量少匹配,让 0*
尽量多匹配,我们就必须让 \d+
使用非贪婪匹配。在规则 \d+
后面加个 ?
即可表示非贪婪匹配。 我们改写正则表达式如下:
Pattern p1 = Pattern.compile("(\\d+?)(0*)");
Matcher m1 = p1.matcher("123000");
if (m1.matches()) {
System.out.println("group1 = " + m1.group(1));
System.out.println("group2 = " + m1.group(2));
}
结果:
因此,给定一个匹配规则,加上 ?
后就变成了非贪婪匹配。
我们再来看这个正则表达式 (\d??)(9*)
,注意 \d?
表示匹配0
个或1
个数字,后面第二个 ?
表示非贪婪匹配,因此,给定字符串"9999"
,匹配到的两个子串分别是""
和 "9999"
, 因为对于\d?
来说,可以匹配1个9
,也可以匹配0个 9
,但是因为后面的 ?
表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了0个 9
。
十六、分割字符串
使用正则表达式分割字符串可以实现更加灵活的功能。 String.split()
方法传入的正是正则表达式。我们来看下面的代码:
System.out.println(Arrays.toString("a b c".split("\\s")));
System.out.println(Arrays.toString("a b c".split("\\s")));
System.out.println(Arrays.toString("a b c".split("\\s+")));
System.out.println(Arrays.toString("a , b ;; c ,,;; d , e".split("[\\,\\;\\s]+")));
结果:
如果我们想让用户输入一组标签,然后把标签提取出来,因为用户的输入往往是不规范的,这时,使用合适的正则表达式,就可以消除多个空格、混合 ,
和 ;
这些不规范的输入,直接提取出规范的字符串。
十七、搜索字符串
使用正则表达式还可以搜索字符串,我们来看例子:
String s = "the quick brown fox jumps over the lazy dog";
Pattern p2 = Pattern.compile("\\wo\\w");
Matcher m2 = p2.matcher(s);
while (m2.find()) {
//Matcher的start和end是从左到右每一个匹配到的字符串在原字符串中的起始位置
String sub = s.substring(m2.start(),m2.end());
System.out.println(sub);
}
结果:
我们获取到 Matcher
对象后,不需要调用 matches()
方法(因为匹配整个串肯定返回false),而是反复调用 find()
方法,在整个串中搜索能匹配上\\wo\\w
规则的子串,并打印出来。这种方式比 String.indexOf()
要灵活得多,因为我们搜索的规则是3个字符:中间必须是o
,前后两个必须是字符 [A-Za-z0-9_]
。
十八、替换字符串
使用正则表达式替换字符串可以直接调用 String.replaceAll()
,它的第一个参数是正则表达式,第二个参数是待替换的字符串。我们还是来看例子:
String s1 = "The quick\t\t brown fox jumps over the lazy dog.";
s1 = s1.replaceAll("\\s+", " ");
System.out.println(s1); // "The quick brown fox jumps over the lazy dog."
上面的代码把不规范的连续空格分隔的句子变成了规范的句子。可见,灵活使用正则表达式可以大大降低代码量。
十九、反向引用
如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个 xxxx
,这个时候,使用replaceAll()
的时候,我们传入的第二个参数可以使用 $1
、$2
来反向引用匹配到的子串。例如:
String s1 = "the quick brown fox jumps over the lazy dog";
s1 = s1.replaceAll("\\s([a-z]{4})\\s","<h1>$1</h1>");//<h1>lazy</h1>
System.out.println(s1);
String s2 = "010-123456 0101-33445566 010-98712345";
//"010-123456" => "(010)-[123456]"
s2 = s2.replaceAll("(\\d{3,4})-(\\d{6,8})","($1)-[$2]");
System.out.println(s2);
结果:
它实际上把任何4字符单词的前后用xxxx
括起来。实现替换的关键就在于 " $1 "
,它用匹配的分组子串 ([a-z]{4})
替换了$1
。