正则表达式

正则表达式是一种用来匹配字符串强有力的武器。Java内置了强大的正则表达式的支持。本章我们会详细介绍如何在Java程序中使用正则表达式。

正则表达式简介

在了解正则表达式之前,我们先看几个非常常见的问题:

  • 如何判断字符串是否是有效的电话号码?例如:010-1234567123ABC45613510001000等;
  • 如何判断字符串是否是有效的电子邮件地址?例如:test@example.comtest#example等;
  • 如何判断字符串是否是有效的时间?例如:12:3409:6099:99等。

一种直观的想法是通过程序判断,这种方法需要为每种用例创建规则,然后用代码实现。下面是判断手机号的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean isValidMobileNumber(String s) {
// 是否是11位?
if (s.length() != 11) {
return false;
}
// 每一位都是0~9:
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return true;
}

上述代码仅仅做了非常粗略的判断,并未考虑首位数字不能为0等更详细的情况。

除了判断手机号,我们还需要判断电子邮件地址、电话、邮编等等:

  • boolean isValidMobileNumber(String s) { … }
  • boolean isValidEmail(String s) { … }
  • boolean isValidPhoneNumber(String s) { … }
  • boolean isValidZipCode(String s) { … }

为每一种判断逻辑编写代码实在是太繁琐了。有没有更简单的方法?

有!用正则表达式。

正则表达式可以用字符串来描述规则,并用来匹配字符串。例如,判断手机号,我们用正则表达式\d{11}

1
2
3
boolean isValidMobileNumber(String s) {
return s.matches("\\d{11}");
}

使用正则表达式的好处有哪些呢?一个正则表达式就是一个描述规则的字符串,所以,只需要编写正确的规则,我们就可以让正则表达式引擎去判断目标字符串是否符合规则。

正则表达式是一套标准,它可以用于任何语言。Java标准库java.util.regex包内置了正则表达式引擎,在Java程序中使用正则表达式非常简单。

举个例子:要判断用户输入的年份是否是20##年,我们先写出规则如下:

一共有4个字符,分别是:200~9任意数字0~9任意数字

对应的正则表达式就是:20\d\d,其中\d表示任意一个数字。

把正则表达式转换为Java字符串就变成了20\\d\\d,注意Java字符串用\\表示\

最后,用正则表达式匹配一个字符串的代码如下:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
String regex = "20\\d\\d";
System.out.println("2019".matches(regex)); // true
System.out.println("2100".matches(regex)); // false
}
}

匹配规则

正则表达式的匹配规则是从左到右按规则匹配。我们首先来看如何使用正则表达式来做精确匹配。

对于正则表达式abc来说,它只能精确地匹配字符串"abc",不能匹配"ab""Abc""abcd"等其他任何字符串。

如果正则表达式有特殊字符,那就需要用\转义。例如,正则表达式a\&c,其中\&是用来匹配特殊字符&的,它能精确匹配字符串"a&c",但不能匹配"ac""a-c""a&&c"等。

要注意正则表达式在Java代码中也是一个字符串,所以,对于正则表达式a\&c来说,对应的Java字符串是"a\\&c",因为\也是Java字符串的转义字符,两个\\实际上表示的是一个\

1
String re2 = "a\\&c"; // 对应的正则是a\&c

如果想匹配非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字符
. 任意字符 ab&0
\d 数字0~9 0~`9`
\w 大小写字母,数字和下划线 azAZ0~`9_`
\s 空格、Tab键 空格,Tab
\D 非数字 aA&_,……
\W 非\w &@,……
\S 非\s aA&_,……

多个字符的匹配规则如下:

正则表达式 规则 可以匹配
A* 任意个数字符 空,AAAAAA,……
A+ 至少1个字符 AAAAAA,……
A? 0个或1个字符 空,A
A{3} 指定个数字符 AAA
A{2,3} 指定范围个数字符 AAAAA
A{2,} 至少n个字符 AAAAAAAAA,……
A{0,3} 最多n个字符 空,AAAAAA

复杂匹配规则

匹配开头和结尾

我们用^表示开头,$表示结尾。例如^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表示可以匹配ABCD

使用括号

我们想要匹配字符串learn javalearn phplearn go怎么办?一个最简单的规则是learn\sjava|learn\sphp|learn\sgo,但是这个规则太复杂了,可以把公共部分提出来,然后用(...)把子规则括起来表示成learn\s(java|php|go)

小结

复杂匹配规则主要有:

正则表达式 规则 可以匹配
^ 开头 字符串开头
$ 结尾 字符串结束
[ABC] […]内任意字符 A,B,C
[A-F0-9xy] 指定范围的字符 A,……,F0,……,9xy
[^A-F] 指定范围外的任意字符 A~`F`
AB|CD|EF AB或CD或EF ABCDEF

分组匹配

我们前面讲到的(...)可以用来把一个子规则括起来,这样写learn\s(java|php|go)就可以更方便地匹配长字符串了。实际上,(...)还有一个重要作用,就是分组匹配。

我们来看一下如何用正则匹配区号-电话号这个规则,利用前面的知识:

1
\d{3,4}-\d{6,8}

虽然这个正则匹配规则很简单,但是往往匹配成功后,下一步是提取区号和电话号,分别存入数据库。于是问题来了:如何提取匹配的子串?

当然可以用String提供的indexOf()substring()这些方法,但它们从正则匹配的字符串中提取子串没有通用性。正确的方法是用(...)先把要提取的规则分组,把上述正则表达式变为:

1
(\d{3,4})-{\d{6,8}}

那么匹配后,如何按括号提取子串呢?

现在我们没法用String.matches()这样简单的判断方法了,必须引入java.util.regex,用Pattern对象匹配,匹配后获得一个Matcher对象,如果匹配成功,就可以直接从Matcher.group(index)返回子串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.regex.*;

public static void main(String[] args) {
Pattern p = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
Matcher m = p.matcher("010-12345678");
if (m.matches()) {
String g1 = m.group(1);
String g2 = m.group(2);
System.out.println(g1);
System.out.println(g2);
} else {
System.out.println("匹配失败!");
}
}

要特别注意,Matcher.group(index)方法的参数用1表示第一个子串,2表示第二个子串。如果我们传入0,会得到什么呢?答案是整个正则匹配到的字符串。

Pattern

我们在前面的代码中用到的正则表达式代码是String.matches()方法,而我们在分组提取的代码中用的是java.util.regex包里面的Pattern类和Matcher类。实际上这两种代码本质上是一样的,因为String.matches()方法内部调用的就是PatternMatcher类的方法。

但是反复使用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
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.regex.*;

public class Main {
public static void main(String[] args) {
String s = "the quick brown fox jumps over the lazy dog.";
Pattern p = Pattern.compile("\\wo\\w");
Matcher m = p.matcher(s);
while (m.find()) {
String sub = s.substring(m.start(), m.end());
System.out.println(sub);
}
}
}

我们获取到Matcher对象后,不需要调用matches()方法(因为匹配整个串肯定返回false),而是反复调用find()方法,在整个串中搜索能匹配上\\wo\\w规则的子串,并打印出来。这种方式比String.indexOf()要灵活得多,因为我们搜索的规则是3个字符:中间必须是o,前后两个必须是字符[A-Za-z0-9_]

替换字符串

使用正则表达式替换字符串可以直接调用String.replaceAll(),它的第一个参数是正则表达式,第二个参数是待替换的字符串。

1
public class Main {    public static void main(String[] args) {        String s = "The     quick\t\t brown   fox  jumps   over the  lazy dog.";        String r = s.replaceAll("\\s+", " "); // \s匹配空格字符和tab字符,把不规范的连续空格分隔的句子变成了规范的句子        System.out.println(r); // "The quick brown fox jumps over the lazy dog."    }}

反向引用

如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个<b>xxxx</b>,这个时候,使用replaceAll()的时候,我们传入的第二个参数可以使用$1$2来反向引用匹配到的子串。

1
public class Main {    public static void main(String[] args) {        String s = "the quick brown fox jumps over the lazy dog.";        String r = s.replaceAll("\\s([a-z]{4})\\s", " <b>$1</b> ");        System.out.println(r);// the quick brown fox jumps <b>over</b> the <b>lazy</b> dog.    }}

它实际上把任何4字符单词的前后用<b>xxxx</b>括起来。实现替换的关键是<b>$1</b>,它用匹配的分组子串([a-z]{4})替换了$1

加密与安全 单元测试
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×