文章目录
匹配规则
如果想匹配非ASCII字符,例如中文,那就用\u####
的十六进制表示,例如:a\u548cc
匹配字符串"a和c"
,中文字符“和”的Unicode编码是548c
匹配任意字符
精确匹配实际上用处不大,因为我们直接用String.equals()就可以做到。大多数情况下,我们想要的匹配规则更多的是模糊匹配。我们可以用.匹配一个任意字符。
例如,正则表达式a.c
中间的.
可以匹配除\n
以外任意一个字符,例如,下面的字符串都可以被匹配:
- “abc”,因为.可以匹配字符b;
- “a&c”,因为.可以匹配字符&;
- “acc”,因为.可以匹配字符c。
但它不能匹配"ac"、“a&&c”,因为.
匹配一个字符且仅限一个字符。
- 要匹配包括 ‘\n’ 在内的任何字符,请使用像 ‘[.\n]’ 的模式。
- 类似地,可以使用
\s\S
匹配绝大部分字符,范围比.
还广
匹配数字
用.
可以匹配任意字符,这个口子开得有点大。如果我们只想匹配 0~9 这样的数字,可以用\d
匹配。例如,正则表达式00\d
可以匹配:
- “007”,因为
\d
可以匹配字符7; - “008”,因为
\d
可以匹配字符8。
它不能匹配"00A",“0077”,因为\d
仅限单个数字字符。
- 给出一串数字"0123456789",
\d\d{5}
匹配012345
,因为\d{5}
匹配5个数字,加上最前面的\d
,一共6个数字;(\d\d){5})
匹配0123456789
共10个数字
匹配常用字符
用\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 个或一个字符。我们用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个字符。
复杂匹配规则
正则表达式 | 规则 | 可以匹配 |
---|---|---|
^ | 开头 | 字符串开头 |
$ | 结尾 | 字符串结尾 |
b | 字符边界 | \bchapter[1-9][0-9]?\b 可以匹配chapter1、chapter2、…chapter10…chapter19 |
B | 匹配非单词边界 | er\B 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。 |
[ABC] | […]内任意字符 | A,B,C |
[A-F0-9 xy] | 指定范围的字符 | A,……,F,0,……,9,x,y |
[^A-F] | 指定范围外的任意字符 | 非A~F |
(?< name >exp) | 匹配exp | 捕获文本到名称为name的组里,也可以写成(?'name’exp) |
(?:exp) | 匹配exp | 不捕获匹配的文本,也不给此分组分配组号,不进行存储供以后使用。industr(?:y|ies) 就是一个比 industry|industries 更简略的表达式。 |
(?=exp) | 零宽断言(正向预查) | 匹配exp前面的位置,也是非获取匹配,\w+(?=\d+) 能够匹配section1 中的section |
(?<=exp) | 零宽断言(反向预查) | 匹配exp后面的位置 |
(?!exp) | 零宽断言 | 匹配后面跟的不是exp的位置 |
(?<!exp) | 零宽断言 | 匹配前面跟的不是exp的位置 |
示例
[^\\\/\^] 除了(\)(/)(^)之外的所有字符,注意转义符前加`\`
^[0-9]{1,}$ 或 ^[0-9]+$ 所有的整数
^\-{0,1}[0-9]{0,}\.[0-9]{0,}$ 或 ^\-?[0-9]*\.?[0-9]*$ 所有小数
零宽断言
接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b
,^
,$
那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧:
(?=exp)
也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp
。比如\b\w+(?=ing\b)
,匹配以 ing
结尾的单词的前面部分(除了ing
以外的部分),如查找I'm singing while you're dancing.
时,它会匹配sing
和 danc
。
(?<=exp)
也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp
。比如(?<=\bre)\w+\b
会匹配以 re
开头的单词的后半部分(除了 re
以外的部分),例如在查找reading a book
时,它匹配 ading
下面这个例子同时使用了这两种断言:(?<=\s)\d+(?=\s)
匹配以空白符间隔的数字(再次强调,不包括这些空白符)
零宽度负预测先行断言(?!exp)
,断言此位置的后面不能匹配表达式 exp
。例如:\d{3}(?!\d)
匹配三位数字,而且这三位数字的后面不能是数字;
同理,我们可以用(?<!exp)
,零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp
:(?<![a-z])\d{7}
匹配前面不是小写字母的七位数字。
matches和lookingAt方法
matches
要求整个序列都匹配,而lookingAt
不要求。
lookingAt
方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。
public class RegexMatches
{
private static final String REGEX = "foo";
private static final String INPUT = "fooooooooooooooooo";
private static final String INPUT2 = "ooooofoooooooooooo";
private static Pattern pattern;
private static Matcher matcher;
private static Matcher matcher2;
public static void main( String args[] ){
pattern = Pattern.compile(REGEX);
matcher = pattern.matcher(INPUT);
matcher2 = pattern.matcher(INPUT2);
System.out.println("lookingAt(): "+matcher.lookingAt());//true
System.out.println("matches(): "+matcher.matches());//false
System.out.println("lookingAt(): "+matcher2.lookingAt());//false
}
}
分组匹配
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Test {
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 s1 = m.group(1);
String s2 = m.group(2);
System.out.println(s1);//010
System.out.println(s2);//12345678
}else {
System.out.println("匹配失败");
}
}
}
要特别注意,Matcher.group(index)
方法的参数用1
表示第一个子串,2
表示第二个子串。如果我们传入0
会得到什么呢?答案是010-12345678
,即整个正则匹配到的字符串。
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()
提取子串。
利用提取子串的功能,我们轻松获得了区号和号码两部分。
非贪婪匹配
在介绍非贪婪匹配前,我们先看一个简单的问题:
给定一个字符串表示的数字,判断该数字末尾0的个数。例如:
- “123000”:3个0
- “10100”:2个0
- “1001”:0个0
可以很容易地写出该正则表达式:(\d+)(0*)
,Java代码如下:
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)); // "1230000"
System.out.println("group2=" + matcher.group(2)); // ""
}
}
}
仔细观察上述实际匹配结果,实际上它是完全合理的,因为\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。
搜索和替换
分割字符串
使用正则表达式分割字符串可以实现更加灵活的功能。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" }
如果我们想让用户输入一组标签,然后把标签提取出来,因为用户的输入往往是不规范的,这时,使用合适的正则表达式,就可以消除多个空格、混合,和;这些不规范的输入,直接提取出规范的字符串。
搜索字符串
使用正则表达式还可以搜索字符串,我们来看例子:
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");//“\\w”表示任意数字、字母和下划线,“\\wo\\w”表示三个字母,中间字母是“o”
Matcher m = p.matcher(s);
while (m.find()) {
String sub = s.substring(m.start(), m.end());
System.out.println(sub);//row和dog
}
}
}
我们获取到Matcher
对象后,不需要调用 matches()
方法(因为匹配整个串肯定返回false),而是反复调用find()
方法,在整个串中搜索能匹配上\\wo\\w
规则的子串,并打印出来。这种方式比String.indexOf()
要灵活得多,因为我们搜索的规则是3个字符:中间必须是o,前后两个必须是字符[A-Za-z0-9_]
。
替换字符串
使用正则表达式替换字符串可以直接调用String.replaceAll()
,它的第一个参数是正则表达式,第二个参数是待替换的字符串。我们还是来看例子:
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."
}
}
上面的代码把不规范的连续空格分隔的句子变成了规范的句子。可见,灵活使用正则表达式可以大大降低代码量。
反向引用
([a-z]{2})\1
可以匹配gogo
toto
这种;(.)\\1(.)\\2||(..)\\1
匹配安安静静
这种
反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式;也就是说,反向引用的是由之前表达式决定的具体文本,而不是符合某种规则的未知文本。
如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个<b>xxxx</b>
,这个时候,使用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字符单词的前后用<b>xxxx</b>
括起来。实现替换的关键就在于" <b>$1</b> "
,它用匹配的分组子串([a-z]{4})
替换了$1
。