正则表达式
在写了leetcode 125题判断是否是有效的回文字符串时用到了正则表达式,借此来复习一下,题解如下:
class Solution {
public boolean isPalindrome(String s) {
if (s == null || s.length() == 0)
return true;
String replace = s.trim().replaceAll("[^0-9a-zA-Z]","").toLowerCase();
String s1 = new StringBuffer(replace).reverse().toString();
return replace.equals(s1);
}
}
正则表达式是一套标准,它可以用于任何语言。Java标准库的java.util.regex包内置了正则表达式引擎,在Java程序中使用正则表达式非常简单。
正则表达式可以用字符串来描述规则,并用来匹配字符串。例如,判断手机号,我们用正则表达式\d{11}:
public class RegexTest {
public static void main(String[] args) {
String p1 = "15200000000";
String p2 = "1234567890";
System.out.println(isValidMobilePhoneNumber(p1)); // true
System.out.println(isValidMobilePhoneNumber(p2)); //false
}
public static boolean isValidMobilePhoneNumber(String s) {
return s.matches("\\d{11}");
}
}
matches 库函数如下:
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
数字表达式
举个例子:要判断用户输入的年份是否是20##年,我们先写出规则如下:
一共有4个字符,分别是:2,0,09任意数字,09任意数字,对应的正则表达式就是:20\d\d,其中\d表示任意一个数字,把正则表达式转换为Java字符串就变成了20\d\d,注意Java字符串用\表示\。
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
}
匹配规则
正则表达式的匹配规则是从左到右按规则匹配。如果正则表达式有特殊字符,那就需要用\转义。例如,正则表达式a&c,其中&是用来匹配特殊字符&的,它能精确匹配字符串"a&c",但不能匹配"ac"、“a-c”、"a&&c"等。
要注意正则表达式在Java代码中也是一个字符串,所以,对于正则表达式a&c来说,对应的Java字符串是"a\&c",因为\也是Java字符串的转义字符,两个\实际上表示的是一个\:
匹配人意字符 – “.”
例如,正则表达式a.c中间的.可以匹配一个任意字符,例如,下面的字符串都可以被匹配:
- “abc”,因为.可以匹配字符b;
- “a&c”,因为.可以匹配字符&;
- “acc”,因为.可以匹配字符c。
但它不能匹配"ac"、“a&&c”,因为.匹配一个字符且仅限一个字符。
!!! 匹配数字 – “\d” digital
如果我们只想匹配0~9这样的数字,可以用\d匹配。例如,正则表达式00\d可以匹配:
- “007”,因为\d可以匹配字符7;
- “008”,因为\d可以匹配字符8。
它不能匹配"00A",“0077”,因为\d仅限单个数字字符。
匹配非数字 – “\D”
用\d可以匹配一个数字,而\D则匹配一个非数字。例如,00\D可以匹配:
- “00A”,因为\D可以匹配非数字字符A;
- “00#”,因为\D可以匹配非数字字符#。
00\d可以匹配的字符串"007","008"等,00\D是不能匹配的。
!! 匹配常用字符 – “\w” word
用\w可以匹配一个字母、数字或下划线,w的意思是word。例如,java\w可以匹配:
- “javac”,因为\w可以匹配英文字符c;
- “java9”,因为\w可以匹配数字字符9;。
- “java_”,因为\w可以匹配下划线_。
它不能匹配"java#","java ",因为\w不能匹配#、空格等字符。
\W可以匹配\w不能匹配的字符
!!! 匹配空格字符 – “\s” space
用\s可以匹配一个空格字符,注意空格字符不但包括空格,还包括tab字符(在Java中用\t表示)。例如,a\sc可以匹配:
- “a c”,因为\s可以匹配空格字符;
- “a c”,因为\s可以匹配tab字符\t。
它不能匹配"ac","abc"等。
\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个或一个字符。我们用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\d{3}$,可以匹配"A001"、“A380”。
如果我们规定一个78位数字的电话号码不能以0开头,应该怎么写匹配规则呢?\d{7,8}是不行的,因为第一个\d可以匹配到0。使用[…]可以匹配范围内的字符,例如,[123456789]可以匹配19,这样就可以写出上述电话号码的规则:[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。
public class Main {
public static void main(String[] args) {
String re = "java|php";
System.out.println("java".matches(re)); //true
System.out.println("php".matches(re)); //true
System.out.println("go".matches(re)); //false
}
}
使用括号
现在我们想要匹配字符串learn java、learn php和learn go怎么办?一个最简单的规则是learn\sjava|learn\sphp|learn\sgo,但是这个规则太复杂了,可以把公共部分提出来,然后用(…)把子规则括起来表示成 learn\s(java|php|go)
分组匹配 Matcher.group(index)
我们来看一下如何用正则匹配区号-电话号码这个规则。利用前面讲到的匹配规则,写出来很容易:
\d{3,4}\-\d{6,8}
虽然这个正则匹配规则很简单,但是往往匹配成功后,下一步是提取区号和电话号码,分别存入数据库。于是问题来了:如何提取匹配的子串?
当然可以用String提供的indexOf()和substring()这些方法,但它们从正则匹配的字符串中提取子串没有通用性,下一次要提取learn\s(java|php)还得改代码。
正确的方法是用(…)先把要提取的规则分组,把上述正则表达式变为 “(\d{3,4})\-(\d{7,8})”。
现在问题又来了:匹配后,如何按括号提取子串?
现在我们没办法用String.matches()这样简单的判断方法了,必须引入java.util.regex包,用Pattern对象匹配,匹配后获得一个Matcher对象,如果匹配成功,就可以直接从Matcher.group(index)返回子串:
Pattern p = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
Matcher m = p.matcher("010-12345678");
if(m.matches()) {
// 第一个字串
String s1 = m.group(1);
String s2 = m.group(2);
System.out.println(s1);
System.out.println(s2);
} else {
System.out.println("匹配失败");
}
要特别注意,Matcher.group(index)方法的参数用1表示第一个子串,2表示第二个子串。如果我们传入0会得到什么呢?答案是010-12345678,即整个正则匹配到的字符串。
使用Matcher时,必须首先调用matches()判断是否匹配成功,匹配成功后,才能调用group()提取子串。
利用提取子串的功能,我们轻松获得了区号和号码两部分。
非贪婪匹配 – 尽可能少的匹配
在介绍非贪婪匹配前,我们先看一个简单的问题:
给定一个字符串表示的数字,判断该数字末尾0的个数。例如:
- “123000”:3个0
- “10100”:2个0
- “1001”:0个0
可以很容易地写出该正则表达式:(\d+)(0*),Java代码如下:
// 贪婪匹配
Pattern pattern = Pattern.compile("(\\d+)(0*)");
Matcher matcher = pattern.matcher("1230000");
if (matcher.matches()) {
String g1 = matcher.group(1);
String g2 = matcher.group(2);
System.out.println(g1); // 1230000
System.out.println(g2); // 空
}
然而打印的第二个子串是空字符串""。
仔细观察上述实际匹配结果,实际上它是完全合理的,因为\d+确实可以匹配后面任意个0。
这是因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配,因此,\d+ 总是会把后面的0包含进来。
要让 \d+ 尽量少匹配,让 0* 尽量多匹配,我们就必须让 \d+ 使用非贪婪匹配。在规则 \d+ 后面加个 ? 即可表示非贪婪匹配。我们改写正则表达式如下:
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d+?)(0*)");
Matcher matcher = pattern.matcher("1230000");
if (matcher.matches()) {
System.out.println("group1=" + matcher.group(1)); // "123"
System.out.println("group2=" + matcher.group(2)); // "0000"
}
}
}
因此,给定一个匹配规则,加上?后就变成了非贪婪匹配。
我们再来看这个正则表达式(\d??)(9*),注意\d?表示匹配0个或1个数字,后面第二个?表示非贪婪匹配,因此,给定字符串"9999",匹配到的两个子串分别是"“和"9999”,因为对于\d?来说,可以匹配1个9,也可以匹配0个9,但是因为后面的?表示非贪婪匹配,它就会尽可能少的匹配,结果是匹配了0个9。
// 贪婪匹配
Pattern pattern = Pattern.compile("(\\d??)(9*)");
Matcher matcher = pattern.matcher("99999");
if (matcher.matches()) {
String g1 = matcher.group(1);
String g2 = matcher.group(2);
System.out.println("g1: " + g1); // 1230000
System.out.println("g2: " + g2); // 空
} else {
System.out.println("匹配失败");
}
结果如下:
g1:
g2: 99999
小结
- 正则表达式匹配默认使用贪婪匹配,可以使用?表示对某一规则进行非贪婪匹配。
- 注意区分?的含义:\d??
!!! 搜索和替换
使用正则表达式分割字符串可以实现更加灵活的功能。String.split()方法传入的正是正则表达式。我们来看下面的代码:
"a b c".split("\\s"); // { "a", "b", "c" }
"a b c".split("\\s"); // { "a", "b", "", "c" }
"a, b ;; c".split("[\\,\\;\\s]+"); // { "a", "b", "c" }
搜索字符串
String s = "the quick brown fox jumps over the lazy dog.";
Pattern pattern = Pattern.compile("\\wo\\w");
Matcher m = pattern.matcher(s);
while (m.find()) {
String sub = s.substring(m.start(), m.end());
System.out.println(sub);
}
结果如下:
row
fox
dog
我们获取到Matcher对象后,不需要调用matches()方法(因为匹配整个串肯定返回false),而是反复调用find()方法,在整个串中搜索能匹配上\wo\w规则的子串,并打印出来。这种方式比String.indexOf()要灵活得多,因为我们搜索的规则是3个字符:中间必须是o,前后两个必须是字符[A-Za-z0-9_]。
替换字符串
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+", " ");
System.out.println(r); // "The quick brown fox jumps over the lazy dog."
}
}
结果如下:
The quick brown fox jumps over the lazy dog.
反向应用
如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个xxxx,这个时候,使用replaceAll()的时候,我们传入的第二个参数可以使用$1、$2来反向引用匹配到的子串。例如:
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字符单词的前后用xxxx括起来。实现替换的关键就在于" $1 ",它用匹配的分组子串([a-z]{4})替换了$1。