正则表达式是一种用来匹配字符串强有力的武器。Java内置了强大的正则表达式的支持。本章我们会详细介绍如何在Java程序中使用正则表达式。
正则表达式简介
在了解正则表达式之前,我们先看几个非常常见的问题:
- 如何判断字符串是否是有效的电话号码?例如:
010-1234567
,123ABC456
,13510001000
等; - 如何判断字符串是否是有效的电子邮件地址?例如:
test@example.com
,test#example
等; - 如何判断字符串是否是有效的时间?例如:
12:34
,09:60
,99:99
等。
一种直观的想法是通过程序判断,这种方法需要为每种用例创建规则,然后用代码实现。下面是判断手机号的代码:
1 |
|
上述代码仅仅做了非常粗略的判断,并未考虑首位数字不能为0
等更详细的情况。
除了判断手机号,我们还需要判断电子邮件地址、电话、邮编等等:
- boolean isValidMobileNumber(String s) { … }
- boolean isValidEmail(String s) { … }
- boolean isValidPhoneNumber(String s) { … }
- boolean isValidZipCode(String s) { … }
- …
为每一种判断逻辑编写代码实在是太繁琐了。有没有更简单的方法?
有!用正则表达式。
正则表达式可以用字符串来描述规则,并用来匹配字符串。例如,判断手机号,我们用正则表达式\d{11}
:
1 |
|
使用正则表达式的好处有哪些呢?一个正则表达式就是一个描述规则的字符串,所以,只需要编写正确的规则,我们就可以让正则表达式引擎去判断目标字符串是否符合规则。
正则表达式是一套标准,它可以用于任何语言。Java标准库java.util.regex
包内置了正则表达式引擎,在Java程序中使用正则表达式非常简单。
举个例子:要判断用户输入的年份是否是20##
年,我们先写出规则如下:
一共有4个字符,分别是:2
,0
,0~9任意数字
,0~9任意数字
。
对应的正则表达式就是:20\d\d
,其中\d
表示任意一个数字。
把正则表达式转换为Java字符串就变成了20\\d\\d
,注意Java字符串用\\
表示\
。
最后,用正则表达式匹配一个字符串的代码如下:
1 |
|
匹配规则
正则表达式的匹配规则是从左到右按规则匹配。我们首先来看如何使用正则表达式来做精确匹配。
对于正则表达式abc
来说,它只能精确地匹配字符串"abc"
,不能匹配"ab"
,"Abc"
,"abcd"
等其他任何字符串。
如果正则表达式有特殊字符,那就需要用\
转义。例如,正则表达式a\&c
,其中\&
是用来匹配特殊字符&
的,它能精确匹配字符串"a&c"
,但不能匹配"ac"
、"a-c"
、"a&&c"
等。
要注意正则表达式在Java代码中也是一个字符串,所以,对于正则表达式a\&c
来说,对应的Java字符串是"a\\&c"
,因为\
也是Java字符串的转义字符,两个\\
实际上表示的是一个\
。
1 |
|
如果想匹配非ASCII字符,例如中文,那就用\u####
的十六进制表示,例如:a\u548cc
匹配字符串"a和c"
,中文字符和
的Unicode编码是548c
。
匹配任意字符
精确匹配实际上用处不大,因为我们直接用String.equals()
就可以做到。大多数情况下,我们想要的匹配规则更多的是模糊匹配。我们可以用.
匹配任意一个字符。
例如,正则表达式a.c
中间的.
可以匹配一个任意字符,例如,下面的字符串都可以被匹配:
"abc"
,因为.
可以匹配字符b
;"a&c"
,因为.
可以匹配字符&
;"acc"
,因为.
可以匹配字符c
。
但它不能匹配"ac"
、"a&&c"
,因为.
匹配一个字符且仅限一个字符。
匹配数字
如果我们只想匹配0
~`9之间的数字,可以用
\d匹配。例如,正则表达式
00\d`可以匹配:
"007"
,因为\d
可以匹配字符7
;"008"
,因为\d
可以匹配字符8
。
它不能匹配"00A"
,"0077"
,因为\d
仅限单个数字字符。
匹配常用字符
用\w
可以匹配一个字母、数字或下划线,w的意思是word。例如,java\w
可以匹配:
"javac"
,因为\w
可以匹配英文字符c
;"java9"
,因为\w
可以匹配数字字符9
;。"java_"
,因为\w
可以匹配下划线_
。
它不能匹配"java#"
,"java "
,因为\w
不能匹配#
、空格等字符。
匹配空格字符
用\s
可以匹配一个空格字符,注意空格字符不但包括空格
,还包括tab字符(在Java中用\t
表示)。例如,a\sc
可以匹配:
"a c"
,因为\s
可以匹配空格字符"a c"
,因为\s
可以匹配tab字符\t
。
它不能匹配"ac"
,"abc"
等。
匹配非数字
用\d
可以匹配一个数字,而\D
可以匹配一个非数字。例如,00\D
可以匹配:
"00A"
,因为\D
可以匹配非数字字符A
;"00#"
,因为\D
可以匹配非数字字符#
。
00\d
可以匹配的字符串"007"
,"008"
等,00\D
是不能匹配的。
类似的,\W
可以匹配\w
不能匹配的字符,\S
可以匹配\s
不能匹配的字符,这几个正好是反着来的。
重复匹配
我们用\d
可以匹配一个数字,例如,A\d
可以匹配"A0"
,"A1"
,如果要匹配多个数字,比如"A380"
,怎么办?修饰符*
可以匹配任意个字符,包括0个字符。我们用A\d*
可以匹配:
A
:因为\d*
可以匹配0个数字;A0
:因为\d*
可以匹配1个数字0
;A380
:因为\d*
可以匹配多个数字380
。
修饰符+
至少可以匹配至少一个字符。我们用A\d+
可以匹配:
A0
:因为\d+
可以匹配1个数字0
;A380
:因为\d+
可以匹配多个数字380
。
但它无法匹配"A"
,因为修饰符+
要求至少一个字符。
修饰符?
可以匹配0个或1个字符。我们用A\d?
可以匹配:
A
:因为\d?
可以匹配0个数字;A0
:因为\d?
可以匹配1个数字0
。
但它无法匹配"A33"
,因为修饰符?
超过1个字符就不能匹配了。
如果我们想精确指定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个字符。
小结
单个字符的匹配规则如下:
正则表达式 | 规则 | 可以匹配 |
---|---|---|
A |
指定字符 | A |
\u548c |
指定Unicode字符 | 和 |
. |
任意字符 | a ,b ,& ,0 |
\d |
数字0~9 | 0 ~`9` |
\w |
大小写字母,数字和下划线 | a z ,A Z ,0 ~`9, _` |
\s |
空格、Tab键 | 空格,Tab |
\D |
非数字 | a ,A ,& ,_ ,…… |
\W |
非\w | & ,@ ,中 ,…… |
\S |
非\s | a ,A ,& ,_ ,…… |
多个字符的匹配规则如下:
正则表达式 | 规则 | 可以匹配 |
---|---|---|
A* |
任意个数字符 | 空,A ,AA ,AAA ,…… |
A+ |
至少1个字符 | A ,AA ,AAA ,…… |
A? |
0个或1个字符 | 空,A |
A{3} |
指定个数字符 | AAA |
A{2,3} |
指定范围个数字符 | AA ,AAA |
A{2,} |
至少n个字符 | AA ,AAA ,AAAA ,…… |
A{0,3} |
最多n个字符 | 空,A ,AA ,AAA |
复杂匹配规则
匹配开头和结尾
我们用^
表示开头,$
表示结尾。例如^A\d{3}$
,可以匹配"A001"
、"A380"
。
匹配指定范围
如果我们规定一个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
。
或规则匹配
用|
连接的两个正则规则是或规则,例如,AB|CD
表示可以匹配AB
或CD
。
使用括号
我们想要匹配字符串learn java
、learn php
和learn go
怎么办?一个最简单的规则是learn\sjava|learn\sphp|learn\sgo
,但是这个规则太复杂了,可以把公共部分提出来,然后用(...)
把子规则括起来表示成learn\s(java|php|go)
。
小结
复杂匹配规则主要有:
正则表达式 | 规则 | 可以匹配 |
---|---|---|
^ | 开头 | 字符串开头 |
$ | 结尾 | 字符串结束 |
[ABC] | […]内任意字符 | A,B,C |
[A-F0-9xy] | 指定范围的字符 | A ,……,F ,0 ,……,9 ,x ,y |
[^A-F] | 指定范围外的任意字符 | 非A ~`F` |
AB|CD|EF | AB或CD或EF | AB ,CD ,EF |
分组匹配
我们前面讲到的(...)
可以用来把一个子规则括起来,这样写learn\s(java|php|go)
就可以更方便地匹配长字符串了。实际上,(...)
还有一个重要作用,就是分组匹配。
我们来看一下如何用正则匹配区号-电话号
这个规则,利用前面的知识:
1 |
|
虽然这个正则匹配规则很简单,但是往往匹配成功后,下一步是提取区号和电话号,分别存入数据库。于是问题来了:如何提取匹配的子串?
当然可以用String提供的indexOf()
和substring()
这些方法,但它们从正则匹配的字符串中提取子串没有通用性。正确的方法是用(...)
先把要提取的规则分组,把上述正则表达式变为:
1 |
|
那么匹配后,如何按括号提取子串呢?
现在我们没法用String.matches()
这样简单的判断方法了,必须引入java.util.regex
,用Pattern
对象匹配,匹配后获得一个Matcher
对象,如果匹配成功,就可以直接从Matcher.group(index)
返回子串。
1 |
|
要特别注意,Matcher.group(index)
方法的参数用1表示第一个子串,2表示第二个子串。如果我们传入0,会得到什么呢?答案是整个正则匹配到的字符串。
Pattern
我们在前面的代码中用到的正则表达式代码是String.matches()
方法,而我们在分组提取的代码中用的是java.util.regex
包里面的Pattern
类和Matcher
类。实际上这两种代码本质上是一样的,因为String.matches()
方法内部调用的就是Pattern
和Matcher
类的方法。
但是反复使用String.matches()
对同一个正则表达式进行多次匹配效率较低,因为每次都会创建出一样的Pattern
对象。完全可以先创建出一个Pattern
对象,然后反复使用,就可以实现编译一次,多次匹配。
使用Matcher
时,必须首先调用matches()
判断是否匹配成功,成功后,才能调用group()
提取子串。利用提取子串的功能,我们很容易地就获得了区号和电话号两部分字符串。
非贪婪匹配
介绍非贪婪匹配之前,我们先看一个简单的问题。给定一个字符串表示的数字,判断该数字末尾0的个数。例如:
"123000"
:3个0
"10100"
:2个0
"1001"
:0个0
可以很容易地写出该正则表达式:(\d+)(0*)
。我们期望的分组匹配结果是:
input | \d+ |
0* |
---|---|---|
123000 | “123” | “000” |
10100 | “101” | “00” |
1001 | “1001” | “” |
但实际上的分组匹配结果是:
input | \d+ |
0* |
---|---|---|
123000 | “123000” | “” |
10100 | “10100” | “” |
1001 | “1001” | “” |
仔细观察上述结果,实际上它是完全合理的,因为\d+
确实可以匹配一个及以上的数字字符。这是因为正则表达式默认使用贪婪匹配:任何一个正则,它总是尽可能多地向后匹配。所以,\d+
总是会把后面的0包含进来。
要让\d+
尽量少匹配,让0*
尽量多匹配,我们就必须让\d+
使用非贪婪匹配。在规则\d+
后面加个?
即可表示非贪婪匹配。改写后的正则表达式为(\d+?)(0*)
。
因此,给定一个规则,加上?
后就变成了非贪婪匹配。
我们再来看个例子。我们再来看这个正则表达式(\d??)(9*)
,注意\d?
表示匹配0个或1个数字,后面第二个?
表示非贪婪匹配,因此,给定字符串"9999"
,匹配到的两个子串分别是""
和"9999"
,因为对于\d?
来说,可以匹配1个9
,也可以匹配0个9
,但是因为后面的?
表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了0个9
。
搜索和替换
分割字符串
使用正则表达式分割字符串可以实现更灵活的功能。String.split()
方法传入的正是正则表达式。如果我们想让用户输入一组标签,然后把标签提取出来,因为用户的输入往往是不规范的,这时,使用合适的正则表达式,就可以消除多个空格、混合,
和;
这些不规范的输入,直接提取出规范的字符串。
搜索字符串
我们来看一个例子。
1 |
|
我们获取到Matcher
对象后,不需要调用matches()
方法(因为匹配整个串肯定返回false),而是反复调用find()
方法,在整个串中搜索能匹配上\\wo\\w
规则的子串,并打印出来。这种方式比String.indexOf()
要灵活得多,因为我们搜索的规则是3个字符:中间必须是o
,前后两个必须是字符[A-Za-z0-9_]
。
替换字符串
使用正则表达式替换字符串可以直接调用String.replaceAll()
,它的第一个参数是正则表达式,第二个参数是待替换的字符串。
1 |
|
反向引用
如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个<b>xxxx</b>
,这个时候,使用replaceAll()
的时候,我们传入的第二个参数可以使用$1
、$2
来反向引用匹配到的子串。
1 |
|
它实际上把任何4字符单词的前后用<b>xxxx</b>
括起来。实现替换的关键是<b>$1</b>
,它用匹配的分组子串([a-z]{4})
替换了$1
。