前言
正则表达式在我们日常开发或者是日常工作中十分常见,它能够很快地帮助我们定位到
符合特定规则
的文本内容,也常常用于判断某些文本是否合法(比如邮箱、手机号码的格式等等)。学好正则表达式可以让我们在工作中事半功倍,如果你对它还不熟悉的话,希望这篇文章能够给你参考。
一、正则表达式的语法
(一)普通字符
字母、数字、汉字、下划线、以及没有特殊定义的标点符号,都属于“普通字符”。正则表达式中的普通字符,在匹配到目标文本中与之相同的一个字符。
举个例子,我们可以用正则表达式为:aa
的式子,来判断目标文本中是否存在aa
字符串
(二)简单的转义字符
语法 | 含义 |
---|---|
\n | 换行符 |
\t | 制表符 |
\ | 表示\本身 |
\^,\$,\.,\(,\),\{,\},\?,\+,\*,|,\ [,\] | 匹配这些字符本身 |
有关换行符和制表符(即按Tab键所生成的空格)的正则匹配,我们应该是用的最多的了。
(三)标准字符集合
标准字符集合是正则表达式提供给我们的可以用来匹配多种字符的表达式。
语法 | 含义 |
---|---|
\d | 任意一个数字,0-9中的任意一个 |
\w | 任意一个字母或下划线,也就是AZ,az,0~9,_中的任意一个 |
\s | 包括空格/制表符、换行符等空白字符中的其中任意一个 |
. | 小数点可以匹配任意一个字符,如果要匹配包括"\n"在内的所有字符,一般用[\s\S] |
标准字符集合相对于普通字符来说,具备更为强大的通配性,比如想匹配包含两个数字的话,只需要\d{2}
就可以匹配出来了({2}为量词,相当于匹配两次,后面会讲到)
注意,标准字符集的使用要区分大小写,大写是相反的意思
也就是说,\d表示一个0-9的数字,\D表示的就是任何一个不是0-9的字符
(四)自定义字符集合
- [ ]方括号匹配方式,可以匹配方括号中任意一个字符
正则表达式允许我们自己根据需要来灵活地定义满足条件的字符集,比如说满足3,4,5中任何一个字符就匹配,我们可以用[345]进行定义。
举例 | 含义 |
---|---|
[ab5@] | 匹配"a"或"b"或"5"或"@" |
[^ abc] | 匹配"a","b","c"之外 的任意一个字符 |
[f-k] | 匹配"f"~"k"之间的任意一个字符 |
[^ A-F0-3] | 匹配"A""F",03之间的任意一个字符 |
231 424 4424ds788
31e3422r4 rer3
13411r42eg
133ddgf3344
-- 匹配7-8或等于@的字符
[7-8@]
-- 匹配非数字字母字符
[^A-Za-z0-9]
- 正则表达式的特殊符号,被包含到中括号中,则失去特殊含义,
^
,-
除外。(比如说.
就是只一个小数点,不再是代表任意一个字符) - 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。比如:
- [\d.-+]将匹配:数字,小数点,
+
和-
- [\d.-+]将匹配:数字,小数点,
(五)量词
量词是用于修饰匹配次数的特殊符号,比如说现在我们想文本中匹配满足5个数字的结果,我们可以写成\d\d\d\d\d
,但是这样很繁琐,可以使用量词{5}
表示匹配5次,也就是\d{5}
量词匹配还可以次数比较灵活,具体见下面
语法 | 含义 |
---|---|
{n} | 表达式重复n次 |
{m,n} | 表达式最少重复m次,最多重复n次(也就是匹配次数的区间范围) |
{m,} | 表达式至少重复m次 |
? | 匹配表达式0次或者1次,相当于{0,1} |
+ | 表达式至少重复1次,相当于{1,} |
* | 表达式重复0次或任意次,相当于{0,} |
量词中还存在着贪婪和非贪婪两个模式
- 贪婪模式为匹配字符越多越好,默认
- 非贪婪模式为匹配字符越少越好(需要手动在量词后面加上一个
?
号)
对贪婪模式的概念不清楚的话可以看一下下面的例子
(六)字符边界
字符边界和之前的匹配方式有着较大的不同,后者匹配的是字符,而字符边界匹配的是符合某种条件的位置
语法 | 含义 |
---|---|
^ | 从字符串开始的地方匹配 |
$ | 从字符串结束的地方匹配 |
\b | 匹配一个单词边界 |
简单的说,就是字符边界可以让我们根据位置来快速找到是否符合条件的数据
a123
b123
c321
d123a
-- 匹配以a开头的结果
^a
-- 匹配以字母开头的结果
^\w
-- 匹配以小写字母结尾的结果
[a-z]$
特别注意:字符边界使用的^
和自定义字符集使用的^
含义完全不同,后者的话是用于排他性匹配的。
(七)正则表达式的匹配模式(了解)
-
IGNORECASE
忽略大小写模式- 匹配时忽略大小写
- 默认情况下,正则表达式区分大小写
-
SINGLELINE
单行模式- 整个文本看做一个字符串,只有一个开头,一个结尾
- 是小数点
。
可以匹配包含换行符\n
在内的任意字符
-
MULTILINE
多行模式- 每行都是 一个字符串
- 在指定了
MULTILINE
之后,如果需要仅匹配字符串开始和结束为止,可以使用\A
和\Z
(八)选择符和分组
表达式 | 作用 |
---|---|
|分支结构 | 左右两边表达式之间“或”关系,匹配左边或者右边 |
( )捕获组 | (1)在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰(2)取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到(3)每一对括号会分配一个编号,使用()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 |
(?:Expression)非捕获组 | 一些表达式中,不得不使用(),但又不需要保存()中的子表达式匹配的内容,这时可以用非捕获组来低效使用()带来的副作用 |
举例说明一下:
1. 分支结构
a123
b123
c321
d123a
-- [a-c] | [1-3]{3} 表示满足a-c或者满足三位1-3的数字就可以匹配。所以一共有8组匹配结果
a、123、b、123、c、321、123、a
2、捕获组,就是捕获不同的括号匹配到的内容。我们可以用它来实现一些复杂的正则匹配
aa123
bb123
cb321
d123aabbb
-- 匹配aa、bb、cc的所有文本
([a-c])\1
解释一下, [a-c]表示匹配到a到c的所有字母, \1捕获到第一个捕获组的内容
当第一个捕获组捕获到a的时候,\1表示的就是a,以此类推,就可以捕获到所有复数的字母了。
如果有三个相同字母的情况也很简单,([A-Za-z])\1\1
3、反捕获组
利用捕获组进行反向引用的本质,是将捕获组捕获到的内容存在内存中,方便反向引用的时候使用,但是当捕获组太多的时候会占用太多的内存,这个时候我们就可以使用反捕获组进行标识啦
- 反向引用(
\nnn
)- 每一对()会分配一个编号,使用()的捕获根据左括号的顺序从1开始自动编号
- 通过反向引用,可以对分组已捕获的字符串进行引用
(九)预搜索(零宽断言)
- 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是
零宽度
- 这个位置应该符合某个条件。判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。是对位置的匹配
- 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是零宽度的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果而言的。
表达式 | 作用 |
---|---|
(?=exp) | 断言自身出现的位置后面可以匹配表达式exp |
(?<=exp) | 断言自身出现的位置前面可以匹配表达式exp |
(?!exp) | 断言自身出现的位置后面不可以匹配表达式exp |
(?<!exp) | 断言自身出现的位置前面不可以匹配表达式exp |
乍看一下,字符边界和预搜索很像,二者都能根据字符的位置去匹配文本
但两者实际上还是有所不同的,最主要的区别在于是否零宽
比如我们现在想在下面的文本中匹配开头两位为a-c的字母,后面是1-3的数字,使用字符边界可以这样写^([a-c]{2})[1-3]+
而使用零宽断言的话,写法和结果是这样的
还有一定要注意的,无论使用零宽断言还是字符边界,前后匹配的字符要和正则的一致。(也就是说,如果是匹配前面的字符,表达式要写在前面,否则写后面)
二、在Java程序中使用正则表达式
- 类Pattern
- 正则表达式的编译表现形式
- Pattern p = Pattern.compile(r,int); //建立并启用正则表达式
- 类Matcher
- 通过解释Pattern对character sequence执行匹配操作的引擎
- Matcher m = p.matcher(str); // 匹配字符串
1、Matches 判断字符串是否满足正则要求
package com.xiaoming;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PatternTest {
private static final String STR1="aabbcc123aabbcc1234@12345.com";
private static final String STR2="aabbcc123aabbcc12";
public void matches(String patternRule){
//定义表达式对象
Pattern pattern = Pattern.compile(patternRule);
// 创建Matcher匹配对象
Matcher m1 = pattern.matcher(STR1);
Matcher m2 = pattern.matcher(STR2);
boolean result1 = m1.matches();
boolean result2 = m2.matches();
System.out.println("STR1中对正则"+patternRule+"的匹配结果是:"+result1);
//STR1中对正则[a-c1-3]+的匹配结果是:false
System.out.println("STR2中对正则"+patternRule+"的匹配结果是:"+result2);
//STR2中对正则[a-c1-3]+的匹配结果是:true
}
public static void main(String[] args) {
PatternTest patternTest = new PatternTest();
// 判断文本中是否满足正则的要求
patternTest.matches("[a-c1-3]+");
}
}
所以,matches
方法很适合用来直接校验某些文本是否满足正则格式,可以用来校验邮箱和手机格式是否正确。
2、Find和Group 查找和匹配符合正则要求的下一个序列
public static void main(String[] args) {
PatternTest patternTest = new PatternTest();
// 判断文本中是否满足正则的要求
//patternTest.matches("[a-c1-3]+");
List<String> result = patternTest.findAndGroup("[\\d]+");
System.out.println(result);
// 可以看到,我们把每个符合正则的子序列截取了出来[123, 1234, 12345]
}
private List<String> findAndGroup(String patternRule) {
List list = new ArrayList<String>();
Pattern pattern = Pattern.compile(patternRule);
Matcher matcher = pattern.matcher(STR1);
while (matcher.find()){
String str1 = matcher.group();
list.add(str1);
}
return list;
}
3、ReplaceAll 替换掉所有和正则匹配的结果
private static final String STR1="aabbcc123aabbcc1234@12345.com";
public void replaceAll(String patternRule){
String result = STR2.replaceAll(patternRule, "!");
System.out.println(result);
// aabbcc!!!aabbcc!!
}
public static void main(String[] args) {
PatternTest patternTest = new PatternTest();
patternTest.replaceAll("[\\d]");
}
比较常见的方式是,使用这个方法来去除用户填写数据中存在的空格。str.replaceAll("\\s","");
4、split 根据正则将文本进行分割
private static final String STR1="aabbcc123aabbcc1234@12345.com";
public static void main(String[] args) {
PatternTest patternTest = new PatternTest();
patternTest.split("\\d+");
}
public void split(String patternRule){
List<String> list = Arrays.asList(STR1.split(patternRule));
// 将文本按照正则进行截断
System.out.println(list);
// 结果是 [aabbcc, aabbcc, @, .com]
}
我们常常用某个符号拼装一些数据后,传到某些方法中再集中做数据拆分。就比如将用户的多个爱好用$拼接起来后,再通过split
方法将一个个爱好单独拎出来。
三、补充一些实用的正则案例
案例一 给指定格式的文本前后加固定文本内容
对于下面的文本,我们希望在每行数字的前后都加上单引号,且每行数字的最后需要加一个逗号。
123456
456789
123456
步骤一:统一处理数字后面的新增字符串
通过观察,我们可以发现我们可以通过2个规律来捕捉到对应的文本,①换行符 ②每行数字都是6位数。因为这个场景比较简单,选择换行符就能满足我们的需要,我们将所有\r\n
的地方替换为',\r\n
就能处理完每行数字后面的字符串内容。
步骤二:统一处理数字前面新增的字符串
使用零宽断言来定位位置,在匹配到的位置上面插入'
即可
PS:选择\r\n
替换为',\r\n,
其实也是一个不错的选择,只需要首位自己手动补一下符号就行
当然了,这里我们还可以使用反捕获组来更快的解决这个问题,只需要根据每行数字写一下正则来抓取,再根据反捕获组\1
直接调整每行的内容,更优雅。
案例二:过滤出指定格式的字符串内容
假如说给定以下文本,我们希望将USER_ID:XXXXXX
的所有文本提取出来,可以怎么实现呢?
Jan 8, 2023 @ 23:50:59.054 WARN [3212023] - [88db89e4-209c-48b1-b6b1-a722e13b1b88] - {auth.XXXXUserExitImpl.authenticate:100} - [鐧诲綍鐧诲綍] - [USER_ID:608899] -> SSO 鐧诲綍鐧诲綍鐧诲綍?[10.19.140.165] 鐧诲綍鐧诲綍鐧诲綍?[Sun Jan 08 23:50:58 CST 2023] 鐧诲綍:[0.151]
Jan 8, 2023 @ 23:41:24.213 WARN [4493547] - [d9322ff3-e0d1-4f00-9927-c11f22240eb0] - {auth.XXXXUserExitImpl.authenticate:100} - [鐧诲綍鐧诲綍] - [USER_ID:788890] -> SSO 鐧诲綍鐧诲綍鐧诲綍?[10.127.14.45] 鐧诲綍鐧诲綍鐧诲綍?[Sun Jan 08 23:41:23 CST 2023] 鐧诲綍:[0.066]
从文本的内容来看,我们很容易就能提取出USER_ID:\d{6}
的正则表达式,那么接下来我们只需要对将该文本前后的内容删除掉就能得到结果。可以使用下面两条正则来分别匹配前后的文本,用空白字符串替换即可。
(?<=USER_ID:\d{6}).*
.+(?=USER_ID:\d{6})