再提一下上面PR的代码,那是一段在数字中间塞空格的代码,源自我的一篇介绍"安卓无障碍的开发"的文章。具体需求为:
val str = "abc1234def"
有这样一个字符串,希望处理之后,结果是:
abc1 2 3 4def
此时,如果不用正则表达式,直接编写代码处理,就需要判断某个字符是否为数字,它的下一个字符是否也为数字,然后再用StringBuilder拼接起来,还是比较麻烦的。而如果使用正则表达式,就非常简单:
val str = "abc1234def"
val regex = "(\\d)(?=\\d)".toRegex()
println(str.replace(regex,"$1 "))
// result: abc1 2 3 4def
可以看到,只需一个简单的正则表达式,就可以完成这个需求。这里涉及到捕获组和正向前瞻断言的知识,下面会介绍。
字符 | 作用 | 例子 |
---|---|---|
^ | 匹配一行的开头。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。如果在[]里面使用,表示匹配除了[]以外的字符。 | 用[^a-j]otlin匹配kotlin为true,如果去掉^为false |
$ | 匹配一行的结尾。如果设置了RegExp对象的Multiline属性,$ 还会与"\n"或"\r"之前的位置匹配 | \w+\d+$,匹配以数字结尾 |
| | 或 | a|b,匹配a或b |
[] | 在[]里面的任意字符。可以使用“-”表示范围 | a-zA-z:前者表示26个字母中的任意字符。0-9:表示0到9 10个数字中的任意字符。abc:表示abc三个字符中的任意字符 |
{} | 用于匹配次数,下面会列出相关用法 | none |
() | 用于捕获分组或限制操作符的作用范围 | (ab)+,匹配 ababab,ab出现了多次,所以 |
(?:pattern) | 用于非捕获组 | none |
. | 匹配任意单个字符,除了换行符。如果想要匹配"."字符,需要使用转义 | kotli.,可以匹配kotlin、kotlim等 |
\w | 匹配单字符,包括0-9、26个英文字母和下划线_ | kotli\w,匹配kotlin、kotlim等 |
\d | 匹配单个数字,等同于[0-9] | kotlin\d,匹配kotlin1、kotlin2等 |
\s | 匹配任意换行符 | \w\s\w,匹配a b |
\b | 匹配字符边界 | hello\b,匹配hello后面是否为边界。hello\b.*,匹配hello,world hello后面是否为边界。边界可以是没有字符,也可以是,.等字符 |
\uxxxx | 匹配16进制所表示的0xxxx字符 | \u4E00,匹配中文"一" |
{n} | n为非负整数,正好匹配n次 | \w{2},表示有2个字符 |
{n,} | n为非负整数,至少匹配n次 | \w{2,},表示至少有2个字符 |
{n,m} | n和m都为非负整数,匹配n到m次,n<=m | \w{2,3},表示有2到3个字符 |
? | 等同{0,1},表示零次或一次 | \w?,判断字符是否出现0到1次 |
* | 等同{0,},表示零次或多次 | \w*,该字符是否存在都无所谓。如,\d\w*,必须出现数字,但是否存在其他字符都行 |
+ | 等同{1,},表至少出现一次 | \d+\w*,如1months,出现了1,至于后面的months有没有,都行 |
大概就是这些,可能会有遗漏。
然后还有需要补充的:\d、\w、\s、\b这几个,如果将小写字母改为大写字母,就表示相反的意思。如\D表示数字以外的字符,^也有同样的作用,如[^\d]
上面提了这么多字符,来一些例子巩固一下
匹配手机号码
val str = "13111111111"
// 两种方式都可以[]里面的内容表示数字3到9,\\d{9}表示数字必须出现9次
// val regex = "1[3456789]\\d{9}".toRegex()
val regex = "1[3-9]\\d{9}".toRegex()
println(str.matches(regex))
匹配日期和时间
val str = "2022-12-31 00:00:00"
val regex = "\\d{4}-\\d{1,2}-\\d{1,2} \\d{2}:\\d{2}:\\d{2}".toRegex()
// 下面这个,其实就是让上面的 -\\d{1,2} 必须出现两次,可以算是另一种写法
// val regex = "\\d{4}(-\\d{1,2}){2} \\d{2}(:\\d{2}){2}".toRegex()
println(str.matches(regex))
Replace
val str = "abc123"
val regex = "\\d+".toRegex()
println(str.replace(regex, "str"))
// result: abcstr
val str = "abc123"
val regex = ".".toRegex()
println(str.replace(regex, "str"))
// result: strstrstrstrstrstr
上面是kotlin代码,如果是java,需要使用replaceAll方法,这个方法才能使用正则表达式,replace方法只能传一个字符串
匹配ipv4
val regex = "((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)".toRegex()
val str = "255.255.255.0"
// true
val str = "255.255.256.0"
// false
val str = "192.168.1.1"
// true
前面25开头的和后面25开头的规则是一样的,所以看一段就行
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
25[0-5]:表示250到255,”25[0-5]“过后又一个"|“符号,表示或。所以如果“25[0-5]”不匹配,就看“2[0-4][0-9]”。
2[0-4][0-9]:表示200到249。如果还是不匹配,剩下的就简单了。现在就只剩下[0,199]这个区间的数字。
[01]?[0-9][0-9]?:先是[0,1]?,表示是否存在0或1都可以。接着是[0-9][0-9]?。只有一个后面有问号,意思是,必须出现一个0到9的数字。
这样匹配之后,就限制了0到255的数字,并且至少有一位数字。然后还有\.,表示必须出现”.“。”."后面有{3},说明这个规则必须出现3次。然后就再来一段相同的规则,这样就完成了ipv4的匹配。
捕获组在正则匹配时,会将括号里面的内容保存起来。当开发者使用"$1"或"\1"时,就可以引用第index组的内容。捕获组的角标是从1开始的,角标0表示整个匹配的内容。
举个例子:
val str = "12months"
val regex = "(\\d+)"
// 在这个例子中,group0和group1都是12,因为\\d+匹配到的内容是12
val str = "a=b"
val regex = "(\\w+)=(\\w+)"
// 在这个例子中,group0是a=b,group1是a,group2是b
// 因为匹配整个a=b才符合"(\\w+)=(\\w+)"这个正则表达式,而months那里,只有12才是,后面的months不是
捕获组使用的例子:
val str = "12months"
val regex = "(\\d+)\\D*".toRegex()
println(str.replace(regex, "$1"))
// 12
// 如果将\\D*去掉
val str = "12months"
val regex = "(\\d+)".toRegex()
println(str.replace(regex, "$1 "))
// 12 months
$1就是将第一个组的内容取出来,所以这样就能够提取数字。捕获组还能在Pattern中使用,下面会介绍。
而下面的代码把\D*去掉,代码的逻辑就变成了在替换数字的内容,这里的例子是数字本身和一个空格,所以就变成了12 months
val str = "1231"
val regex = "(\\d)\\d*\\1".toRegex()
println(str.matches(regex))
// true
val str = ""
val regex = "<(\\w+)>\\1>".toRegex()
println(str.matches(regex))
// true
最后面的\1是用于判断是否存在于第一个组相同的数字,可以看到,第一个组是放在最开头,所以会匹配到1。此时\1就是代表1。(\d)后面还有\d*,被23匹配了。然后重新出现了数字1,所以就匹配成功。
接下来是一个提取日期的例子,顺便补充一点捕获组的知识
val str = "2022-12-31"
val regex = "(\\d{4})-(\\d{1,2})-(\\d{1,2})".toRegex()
println(str.matches(regex))
// true
可以看到,是否加了括号,不会影响正则表达式的使用,在此基础上,还能使用相同的表达式将日期提取出来。
val str = "2022-12-31"
val regex = "(\\d{4})-(\\d{1,2})-(\\d{1,2})".toRegex()
val year = str.replace(regex, "$1")
val month = str.replace(regex, "$2")
val day = str.replace(regex, "$3")
println("year: $year month: $month day: $day")
// year: 2022 month: 12 day: 31
结果是没有问题,只是过程有点麻烦,下面会使用Pattern优化这个问题,这里先不管。
再来一个案例,我有一篇博客,是写给EditText加后缀的。其实,我那篇博客写得代码,和我在项目中写得代码不一样,项目写代码反而有点烂,但里面就是用捕获组解决部分问题。
// 注意:是mont 1 ths,中间有一个1,需要将这个1和前面的12拼接在一起
val str = "12mont1hs"
val regex = "(\\d+)\\D*(\\d*)\\D*".toRegex()
println(str.replace(regex, "$1$2months"))
// 121months
如果用户在months这个单词中写了一个数字,我就将这个数字拿出来,并拼接到开头的数字,再设置到EditText里面。此时,就可以使用捕获组的形式,获取2个数字,并拼在一起,再补上后缀。
如果不使用正则表达式,想要实现这个功能还是比较麻烦的。
非捕获组
写了一堆,看怎么用
val str = "ababab"
val regex = "(?:ab)+".toRegex()
println(str.matches(regex))
// true
在这个例子中,如果去掉?:,结果也是true。那?:有什么用?
如果不使用?:,那(ab)在匹配完成之后,会将子表达式捕获,放到内存中。而如果使用了?:,就不会将子表达式捕获,也就不会放在内存中。
再举一个更为具体的例子:
val str = "ababab"
val regex = "(\\w)(\\w)".toRegex()
println(str.replace(regex,"$2"))
// bbb
// 而如果将第二个\\w改为?:\\w呢?
val str = "ababab"
val regex = "(\\w)(?:\\w)".toRegex()
println(str.replace(regex,"$2"))
// Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 2
将会运行报错,找不到第二组,说明group2不会被捕获,所以获取不到
Pattern方法
Matcher常用方法,注意,Matcher提到的方法都是非静态方法
上面提了这么多方法,其中:split、matches、replaceFirst和replaceAll方法,String本身也有提供, 并且在String的源码里面,也是使用这两个类的方法,所以不提供示例。其他大部分方法会提供代码示例:
lookingAt
val str = "123abc"
val regex = "\\d+"
val pattern = Pattern.compile(regex)
val matcher = pattern.matcher(str)
println(matcher.matches())
println(matcher.lookingAt())
matcher.reset("abc123")
println(matcher.lookingAt())
// false
// true
// false
可以看到,如果使用matches方法,会匹配失败。而如果使用lookingAt就能够匹配,因为这个字符串是数字开头。调用reset方法, 修改为字母开头,再次调用lookingAt方法,返回false。因为匹配到开头不是数字,所以就不继续匹配下去,直接返回false。
find、start、end、group、groupCount这些是成对存在的,所以放一起举例
// 没有使用捕获组的情况
val str = "a=b;c=d;e=f"
val regex = "\\w=\\w"
val pattern = Pattern.compile(regex)
val matcher = pattern.matcher(str)
while(matcher.find()){
println(matcher.group())
}
// a=b
// c=d
// e=f
// 注意:这里没有用到捕获组,所以不能使用matcher.group(1),如果这样用,会运行报错
而如果将给find传入1的参数,就会获取到第2个结果
if(matcher.find(1)){
println(matcher.group())
}
// c=d
// 注意,这里不能使用while(matcher.find(1)),否则会一直获取第2个结果,会导致循环一直在运行。
如果给regex套上捕获组:
val str = "a=b;c=d;e=f"
val regex = "(\\w)=(\\w)"
val pattern = Pattern.compile(regex)
val matcher = pattern.matcher(str)
while(matcher.find()){
println("group:${matcher.group()}, group1:${matcher.group(1)}, group2:${matcher.group(2)}")
}
// group:a=b, group1:a, group2:b
// group:c=d, group1:c, group2:d
// group:e=f, group1:e, group2:f
上面那个日期的例子,用Matcher就可以很方便的拿到:
val date = "2023-12-31"
val regex = "(\\d{4})-(\\d{1,2})-(\\d{1,2})"
val pattern = Pattern.compile(regex)
val matcher = pattern.matcher(date)
if(matcher.find()){
println("year:${matcher.group(1)}, month:${matcher.group(2)}, day:${matcher.group(3)}")
}
// year:2023, month:12, day:31
start和end:
while(matcher.find()){
println("group1 start:${matcher.start(1)}, end:${matcher.end(1)}")
println("group2 start:${matcher.start(2)}, end:${matcher.end(2)}")
}
// group1 start:0, end:1
// group2 start:2, end:3
// group1 start:4, end:5
// group2 start:6, end:7
// group1 start:8, end:9
// group2 start:10, end:11
这里如果调用start/end传入3,会运行报错,我就不提供代码了,自己试一下就知道了
group、start、end都有一个name的方法,这里举个例子说怎么用:
val numberGroupName = "number"
val wordGroupName = "word"
val str = "123, abc"
val regex = "(?<$numberGroupName>\\d+), (?<$wordGroupName>\\w+)"
val pattern = Pattern.compile(regex)
val matcher = pattern.matcher(str)
if(matcher.find()) {
val numberStart = matcher.start(numberGroupName)
val wordStart = matcher.start(wordGroupName)
val numberGroupText = matcher.group(numberGroupName)
val wordGroupText = matcher.group(wordGroupName)
println("numberStart:$numberStart, wordStart:$wordStart")
println("numberEnd:$numberEnd, wordEnd:$wordEnd")
println("numberGroupText:$numberGroupText, wordGroupText:$wordGroupText")
}
// numberStart:0, wordStart:5
// numberEnd:3, wordEnd:8
// numberGroupText:123, wordGroupText:abc
想要在捕获组里面为捕获组命名,可以使用(?
groupCount方法,上面这个例子中
println(matcher.groupCount())
// 2
注意,即使没用调用find方法,也可以调用groupCount方法
region相关方法
val str = "a=b;c=d;e=f"
val regex = "(\\w)=(\\w)"
val pattern = Pattern.compile(regex)
val matcher = pattern.matcher(str)
println("strLength:${str.length}, regionStart:${matcher.regionStart()}, regionEnd:${matcher.regionEnd()}")
matcher.region(2,10)
println("regionStart:${matcher.regionStart()}, regionEnd:${matcher.regionEnd()}")
while(matcher.find()){
println(matcher.group())
}
// strLength:11, regionStart:0, regionEnd:11
// regionStart:2, regionEnd:10
// c=d
可以看到,在设置region之前,regionEnd和str.length是一样,但设置之后,获取到的值就是设置的值,并且设置后还导致只find到一个值
reset方法,在上面这个的代码的基础上,调用reset方法
println("reset")
matcher.reset()
println("regionStart:${matcher.regionStart()}, regionEnd:${matcher.regionEnd()}")
while(matcher.find()){
println(matcher.group())
}
// reset
// regionStart:0, regionEnd:11
// a=b
// c=d
// e=f
调用reset之后,Matcher就恢复原来的样子了
Assertion用于在匹配过程中进行条件判断,Assertion是一种零宽度(zero-width)匹配,它不会消耗匹配的字符,仅用于确定匹配的位置是否满足特定条件。有什么用?
上面提到不会消耗匹配的字符,举一个例子就懂了,还是开头的那个例子
val str = "abc123def"
val regex = "(\\d)\\d".toRegex()
println(str.replace(regex, "$1 "))
// abc1 3def
使用这种方式替换之后,2就消失不见了,即使第2个\d没有使用捕获组。而如果使用Assertion,就不会有这个问题
几种Assertion的形式:
总结:前面与后面、能与不能的排列组合。名字看起来很唬人,而规则又很简单
例子:
// 正向现行断言还是上面的例子
// \\d(?=\\d),表示数字\d后面必须紧跟着数字\d
// 匹配到数字1,发现数字1后面是数字2,是匹配项,所以执行$1 ,在1后面加空格
// 匹配到数字2,发现数字2后面是数字3,是匹配项,所以执行$1 ,在2后面加空格
// 匹配到数字3,发现数字3后面是字母,不是匹配项,所以就不管
// 负向先行断言:?!pattern
val str = "abc123adef"
val regex = "(\\d)(?!\\D)".toRegex()
// val regex = "(\\d)(?![a-zA-Z])".toRegex()
println(str.replace(regex, "$1 "))
// abc1 2 3adef
只需将断言里面的表达式往反的修改,就能达到一样的效果
然后是后行断言
// 正向后行断言
val str = "abc123adef"
val regex = "(?<=\\d)(\\d)".toRegex()
println(str.replace(regex, " $1"))
// 匹配到数字1,发现数字1前面是字母,不是匹配项,所以不管
// 匹配到数字2,发现数字2前面是数字1,是匹配项,所以执行 $1,在2前面加空格
// 匹配到数字3,发现数字3前面是数字2,是匹配项,所以执行 $1,在3前面加空格
// 负向后行断言:?
val str = "abc123adef"
val regex = "(?.toRegex()
// val regex = "(?
println(str.replace(regex, " $1"))
// abc1 2 3adef
(?d)和(?u)不理解是什么意思,所以不提供代码
忽略大小写
val str = "aBC"
val regex = "(?i)abc".toRegex()
println(str.matches(regex))
// true
// 但这种做法存在问题,(?i)后面所有的字符串都会忽略大小写,如果后面还有def,并且不希望忽略大小写,这样就不行了,所有还有其他编写方式
// 使用(?i:str)编写
val str = "aBCdef"
val regex = "(?i:abc)dEf".toRegex()
println(str.matches(regex))
// false
val str = "aBCdef"
val regex = "(?i:abc)def".toRegex()
println(str.matches(regex))
// true
// 最后一种方式,使用(?-i)表示禁用忽略大小写
val str = "aBCdef"
val regex = "(?i)abc(?-i)def".toRegex()
println(str.matches(regex))
// true
注释模式
val input = "Hello,World!"
val regex = """
(?x) # 注释模式启用
(?i)hello # 不区分大小写匹配 hello
\W+ # 匹配非单词字符
(?-i)World # 区分大小写匹配 world
""".trimIndent()
val pattern = Pattern.compile(regex)
println(pattern.pattern())
val matcher = pattern.matcher(input)
if(matcher.find()) {
println(matcher.group())
}
// Hello, World
可以发现,可以找到符合这个正则表达式"(?i)hello\W+(?-i)World"的字符串
单行模式
val input = "Line 1\nLine 2\nLine 3"
val pattern = "Line.*"
val matcher = Pattern.compile(pattern, Pattern.DOTALL).matcher(input)
var count = 0
while(matcher.find()) {
println("count:${++count}")
println(matcher.group())
}
// count:1
// Line 1
// Line 2
// Line 3
虽然看起来打印了3行,但count只计算了一次,所以可以证明单行模式的"."将换行符视为一个普通字符。
多行模式
val input = "Line 1\nLine 2\nLine 3"
val pattern = "^Line.*"
val matcher = Pattern.compile(pattern, Pattern.MULTILINE).matcher(input)
while(matcher.find()) {
println(matcher.group())
}
// Line 1
// Line 2
// Line 3
从上面提到的各种用法可以看出,正则表达式不止能够用来匹配字符串,在处理字符串方面,也提供了很多用法。如果学会了这些,在开发中就可以使用正则表达式来解决复杂的问题,而不用编写复杂的代码来处理字符串。
不过内容这么多,也不可能都记下来,所以只要有一种"遇到字符串问题就思考能不能用正则表达式来解决"这样的思维,我觉得就已经够了。遇到问题时,再查一查怎么,能不能节省开发时间。如果能,就使用正则表达式来解决。
最后补充,正则表达式的知识体现非常庞大,这篇博客提到的这些,还不是正则表达式所有的内容,所以后面有时间的话,还会继续补充这篇博客。