介绍

正则表达式(regular expression),常简写为regex,用简单字符串来描述、匹配文中全部匹配指定格式的字符串。人话讲就是根据一些规则制定一个字符串,然后你可以用这个字符串来筛选满足规则的字符串。许多程序设计语言都支持用正则表达式操作字符串,这里主要介绍正则表达式在Java中的运用。

不同编程语言的正则表达式引擎有所不同,这里提供一个链接,里面详细介绍了不同语言对各种特性的支持程度。

快速使用

先说明一下\的使用。

在Java普通字符串中,反斜杠\本身就是转义字符,比如\n被转义为”换行符”,又比如\\被转义为\。而正则表达式也有自己的语法,它也使用反斜杠作为转义字符,比如\d表示“匹配一个数字”。

那么二者结合起来呢🧐。以"\\d"为例。编译器看到字符串"\\d"会根据字符串规则将其转换为两个字符,一个\,一个d。接下来正则表达式引擎会对其进行解析,最终生效的正则模式就是\d。可以这样理解:正则表达式需要 \d 来匹配数字。但在Java字符串里,一个 \ 需要写成 \\。所以,要把正则的 \d 放到Java字符串里,就变成了 \\d

到底需不需要两个\\,idea会给你答案。

java.util.regex包是Java标准库中用于支持正则表达式操作的包,主要涉及到PatternMatcher这两个类的操作。这里有个简单的例子:

1
2
3
4
5
6
7
8
9
10
String pattern = "java\\d";
String text1 = "java1";
String text2 = "javaBad";

Pattern p = Pattern.compile(pattern);

Matcher m = p.matcher(text1);
System.out.println(m.matches());//true
m = p.matcher(text2);
System.out.println(m.matches());//false

先调用Pattern类的静态方法compile(参数为正则表达式)生成一个实例对象,通过调用该对象的matcher方法(参数为待匹配文本)生成一个Matcher实例。接下来就有很多方法供你选择,这里我调用的是matches方法来输出布尔值,在例子中体现为字符串Java后面能否匹配上数字。Matcher类里还有个find方法也很常见,下文会提到。

匹配规则详解

简单匹配

为方便演示,接下来的示例代码使用String类的matches方法,该方法底层原理仍然是PatternMatcher这两个类的使用,后面有详细说明。下面示例参考廖雪峰菜鸟教程

匹配任意字符:.可以匹配除\r\n之外的任何单个字符。如a.c可以匹配abc但不能匹配abbcac

匹配数字:\d匹配 0~9 的数字,同样只匹配一个字符。匹配非数字:\D匹配非数字。

匹配常用字符:\w可以匹配一个字母、数字或下划线

匹配空格字符:\s可以匹配任何空白字符,包括空格、制表符、换页符等。与[\f\n\r\t\v]等效。\W\S\D同样是反着来的。

重复匹配:

*可以匹配任意个字符,包括0个字符。

+可以匹配至少一个字符。比如A\d+可以匹配A11111A0。但不能匹配A,因为至少一个字符。

?可以匹配0个或一个字符。

如果想精确指定n个字符,使用{n},比如A\d{3}可以匹配到A123。指定匹配n~m个字符,用{n,m}, 例如A\d{3,5}可以精确匹配A123 A1234 A12345{n,}表示可以匹配至少n个字符。m和n为非负整数,其中n <= m。再举一个例子:o{2}和Bob中的一个o不匹配,而匹配food中的两个o。不同表达式可能是等效的,比如o{0,1}o?

来个综合点的例子:假如电话号码规则如下:34位数字表示区位,78位数字表示电话,中间用-连接。答案:\\d{3,4}-\\d{7,8}。对于连字符-,一般情况下只是一个普通字符,不需要进行转义,当然写上两个反斜杠也是对的,idea会给出提示移除多余的反斜杠。

1
2
3
4
5
String pattern = "\\d{3,4}-\\d{7,8}";//不知道需不需要写\?idea会给你的答案
String text1 = "0123-123456";
String text2 = "010-1234567";
System.out.println(text1.matches(pattern));//false
System.out.println(text2.matches(pattern));//true

复杂匹配:

匹配开头和结尾:^匹配输入字符串开始的位置,$匹配输入字符串结束的位置。他们俩的作用是将匹配过程限制在整个字符串上,避免了在子串中成功匹配的情况。其实matches()方法的行为已经隐含了^...$锚点的效果,而find()方法则没有。matches方法尝试将整个输入序列与模式匹配,而find方法会在输入序列中查找下一个与模式匹配的子序列。仔细品味这两个方法的名字,你也许会理解。

匹配指定范围:[xyz]匹配包含的任一字符,比如[abc]匹配plain中的 a。[a-z]

匹配 a 到 z 范围内的任何小写字母。[^a-z]匹配任何不在 a 到 z 范围内的字符,取补集的意思,和前面的\b\B类似。[0-9][A-Z]同样理解。

如果要匹配6位十六进制数,可以这样写:[0-9a-fA-F]{6}

或规则匹配:AB|CD表示可以匹配 AB 或 CD。(z|f)ood匹配 zood 或 food,当然这个正则表达式也可以写成[zf]ood

**分组匹配:**字面意思,通过()将表达式分组处理,可以配合Matcher类的group方法使用。

1
2
3
4
5
6
7
8
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); // 010
System.out.println(g2); // 12345678
}

非贪婪匹配:

1
2
3
4
5
6
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+可以匹配到后面的数字。正则表达式默认使用贪婪匹配,以\d+为例,后面有多少数字就匹配多少(只要连续),这样0*就匹配到空字符串。如果想让\d+少匹配,可以写成(\\d+?)(0*)?表示非贪婪匹配,这样输出就变成了"123" "0000"

不能简单的把非贪婪匹配认为最少匹配,觉得输出应该是1230000。非贪婪匹配是在保证后面表达式都能匹配上的前提下尽量少匹配。引擎保证的是整体成功优先,我认为可以是一种平衡吧,这里不做过多解释,因为更深层的原理我也不懂。

这里的?和前面提到的?不一样。(\d??)(9*)\d?表示匹配0个或1个数字,后面的?表示非贪婪匹配。如果给定字符串"9999",匹配到的两个子串分别为"" "9999"

实战演练

**匹配邮箱:**这里假设字符@前可以出现数字、英文字母、下划线和中划线,字符@后是域名格式,长度不限。

先分析邮箱名称部分,只能出现数字、英文字母和下划线、中划线,那么可以这样写[0-9a-zA-Z_-],也可以选择\w,这里我选择第二种。又因为不止一个字符,所以加个+。变成\w+

然后分析域名部分,域名一般是weixin.qq.com这种类型,也就是**.**.**。可以以第一个英文句点为分界线将其拆解为两部分。一部分是**一部分是.**的复制粘贴。第一部分依然可以这样写\w+,一个.**这样写\.\w+,多个.**这样写(.\w+)+

经过分析,答案就是\w+@\w+(.\w+)+,放到java中,需要写成\\w+@\\w+(.\\w+)+,还是那句话:到底需不需要写反斜杠,idea会给你答案。

如果你在浏览器搜索“正则表达式邮箱匹配”,你可能会得到很多答案,当你不确定时,多去尝试。

拓展

Matcher类除了提到的matches方法和find方法,还有start``replaceAll等,可以点击链接了解更多。这里我想介绍一下PatternMatcher这两个类出现的其他地方,比如上文提到的String.matches()方法。

为什么说String.matches()方法底层原理仍然是PatternMatcher这两个类的使用。ctrl+鼠标左键点开方法源码即可发现:

1
2
3
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}

发现调用的是Pattern类的静态方法matches,同时传进去两个参数,一个正则表达式,一个待匹配字符串。接着点进去matches方法

1
2
3
4
5
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}

然后就会发现这跟之前的示例代码没啥两样,只是compilematcher这两个方法的参数不再是字符串常量,而是传进来的参数regexinput

再说一个例子,就是String.split()方法。简单说一下:这个方法最终调用的是String类的private String[] split(String regex, int limit, boolean withDelimiters) {...}这个方法,可以看到传进去一个正则表达式作为参数,这个方法的最后几行代码是这样的

1
2
3
4
Pattern pattern = Pattern.compile(regex);
return withDelimiters
? pattern.splitWithDelimiters(this, limit)
: pattern.split(this, limit);

根据参数withDelimiters的布尔值来确定调用对象pattern的哪个方法,其实这两个方法最终调用的还是一个方法,兜兜转转还是回到了Pattern类和Matcher类。

这篇博客就到这里,错误不可避免,欢迎指正。我会持续更新,欢迎收藏我的网站

参考文章: