正则表达式简明
前言
对于正则表达式,我们使用它的场景大致分类来讲有两种:
- 匹配字符
- 匹配位置
对于场景,具体到使用的Java API:
- 对字符串进行固定规则的校验,输出是否满足该正则表达式表示的规则,即匹配字符
matcher.matches()
; - 对字符串按照固定规则提取子串,可以按照组输出指定的子串,即匹配字符+分组
matcher.group()
; - 对字符串按照位置进行替换/插入,即匹配位置
matcher.replaceAll()
;
一 匹配字符
模糊匹配
正则表达式可以是一个没有任何规则的字符串,如:hello
,那它就只能匹配“hello”这一个字符串。模糊匹配可以针对待匹配字符串的可变子串做横向和纵向两种模糊匹配:
-
横向模糊匹配: 控制待匹配串的子串数量。基本实现为量词
{m,n}
,表示这个花括号前面的子串连续出现的次数,最少m次,最多n次。 例:abc{1,2}
代表:前两个字符必须是ab,第三个字符必须是c,最少出现1次,可以重复最多2次。上面的例子有没有可以达到同样效果的不同的表达式写法呢?
-
纵向模糊匹配: 控制待匹配串的子串可能出现的值。基本实现为字符组
[]
,中括号里为可能出现的字符。 例:a[bcd]e
代表:第一个字符必须为a,第二个字符可以为b、c、d中之一,第三个字符必须为e。那么什么是字符组呢? 如果要表示同一个位置的字符有几十个,这个表达式写出来岂不是特别长?
字符组
[abc] 字符组就是表示在当前位置可能出现的字符的集合。注意⚠️,虽然叫组,但是只表示匹配一个字符,不管这个组再长,一对中括号也只能匹配一个字符。
-
范围表示法 字符组里的字符如果是连续的,那它们可以被缩略成范围表示。如:
[1234567abcdefXYZ]
可以用范围表示法缩略为[1-7a-fX-Z]
。如果只想表示1、-、7这三个字符呢?
-
排除字符组 如果说字符组是白名单的话,那排除字符组就是黑名单。字符组的第一个字符为
^
表示不匹配这个字符组里的字符, 例:a[^bcd]e
代表:第一个字符必须为a,第二个字符只要不是b、c、d其他任意字符都可以,第三个字符必须为e。排除字符组也可以用范围表示法来表示。
-
一些常见字符组的简写
简写 字符组表示 说明 \d [0-9] 表示数字。d:digital \D [^0-9] 表示除数字以外的任意字符 \w [0-9a-zA-Z_] 表示数字、大小写字母和下划线。w:word \W [^0-9a-zA-Z_] 表示除数字、大小写字母和下划线外的任意字符 \s [ \t\v\n\r\f] 表示空白符。包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。s:space \S [^ \t\v\n\r\f] 表示除空白符以外的任意字符。 . [^\n\r\u2028\u2029] 表示通配符,即除换行符、回车符、行分隔符和段分隔符以外的任意字符。 如果要匹配任意字符怎么办? 正常情况下,
.
是完全够用的,分隔符换行符需要匹配的场景特别少见。如果一定要是所有字符呢?[\d\D]
、[\w\W]
、[\s\S]
、[^]
这四种都可以表示任意字符。但是请注意⚠️,根据实测,不同的正则表达式解析器对以上4种“任意字符”的表示方式并不完全支持,可能需要实际测试一下。比如Java中
Pattern.compile()
就是不支持[^]
这种表示方式的,字符组中的^
后面必须有东西,否则就会报错。还注意⚠️,
.
作为简写只在字符组以外生效,如果写成[.]
那它默认匹配的就是“点”这个字符本身,失去了代表通配符的功能,即转义成[\\.]
也没用。
量词
{m,n}
-
常见量词的简写
简写 量词表示 说明 {m,} {m,∞} 表示最少出现m次,最多不限。 {m} {m,m} 表示只能出现m次,不多不少。
注意最多最少一致也必须用花括号括起来,
不括的话会被当作匹配字符串处理。? {0,1} 表示不出现,或者只出现一次。 + {1,∞} 表示最少出现一次,最多不限。相当于第一条的m=1。 * {0,∞} 表示不出现,或者出现任意次,有没有,有几次都可以。 -
贪婪匹配和惰性匹配 贪婪匹配是正则默认的匹配方式。 在量词后面紧跟一个
?
代表惰性匹配模式。 量词本身不是也有?
吗? 什么叫贪婪匹配? 例:表达式:a.*b & a.*?b
待匹配字符串:aababa
; 由于Java的Matcher类find后并不能提取字符串,只有matches后才能提取字符串,不太好看到惰性匹配和贪婪匹配的区别,我们用replaceAll来操作。例:表达式:
(\d{2,5}).* & (\d{2,5}?).*
待匹配字符串:12345678abc
这里用提取分组的方式展现区别。
多选分支
多选分支属于字符组的一种,可以给待匹配的字符串分支选择。简单来说,类似于“枚举”,要么匹配a,要么匹配b。表示为a|b
。
例:表达式:good|goodbye
待匹配字符串:goodbye & good &goodbye!
我们给表达式添加分组,可以发现多选分支是惰性的。
二 匹配位置
基本规则下,我们只需要知道^
代表匹配开头,$
代表匹配结尾。
一般情况下,我们在整个表达式的开始写上^
,结束写上$
,代表我要匹配的字符串必须以啥开头,并且以啥结束。
例:表达式:a\d+b & ^a\d+b$
待匹配字符串:a123b & a123bab
在Java中,find
和match
的区别基本可以认为match是自带了^
和$
,所以可以认为这两个字符只影响find方法。但是不同的正则表达式解析器方法不同,为了你这段表达式的健壮性,一般推荐都加上。
三 分组
在一开始横向模糊匹配的部分,我们说量词是“表示这个花括号前面的子串连续出现的次数”,那这个子串有多长呢?实际上就是一个字符的长度。如果我想对一组字符重复多次呢——比如“goodgoodgood”这种情况?
例:表达式:good{3} & (good){3}
待匹配字符串:goodgoodgood
分组,表示为()
,可以把一些字符划成一堆,基本可以理解为数学表达式里的括号,括号右边的东西可以把括号里所有的东西当成一个整体。
分组和分支结合
(p1|p2)
分组可以让分支的表示更清晰,因此在用到分支的时候大多数都会也用到分组。
例:表达式:^Java is good|bad$ & ^Java is (good|bad)$
待匹配字符串:Java is bad & Java is bad
引用分组
这里把它称作提取子串更容易理解。对于匹配上的字符串来讲,被分组匹配上的字符串是可以提取出来的。
Java中
Matcher.group()
可以提取匹配上分组的字符串,Matcher.groupCount()
返回匹配上分组的数量。Matcher.group(0)
固定代表待匹配字符串本身,不计入Matcher.groupCount()
中。 例:表达式:\d{4}-\d{2}-\d{2} & \d{4}-\d{2}-\d{2}
待匹配字符串:2022-03-01