简述
前瞻和后顾是正则中比较难以理解的两个概念,也有人称作为正向零宽断言和负向零宽断言,还有叫做顺序环视和逆序环视。有很多种叫法,但是每一种叫法都让人难以理解。本文通过一张图示,带领大家彻底搞明白这个让人难以理解的概念。同时也会分享几个应用示例给大家,帮助大家以后写出更加简练的正则。
上面说的几种叫法,有前后、正反、顺逆几个关键字,这些表示的都是方向。本文最关键的地方就是要告诉大家这个方向到底是什么方向。相信大家看图也就知道了,这个方向代表的是正则沿着字符串匹配检测的方向。
从正在匹配检测的位置开始,其左侧为已经被匹配检测的位置和字符,右侧为还未被匹配检测的位置和字符。正则匹配的方向是从左往右的,所以我个人觉得叫左视和右视会更合适一些,因为左右这样的描述是绝对的,而前后、正反、顺逆这样的描述都是相对的。如果把这个概念理解了,那么对前后、正反、顺逆的概念也就清晰了。
解读
有了方向的概念后,我们再来对名称来进行定义解读。
前瞻
(?=exp)
:从一个位置往匹配方向的前方看,或者往匹配方向的正向看,或者顺着匹配方向看。去看这个位置 前方(右侧) 的字符是否等于exp。
后顾
(?<=exp)
:从一个位置往匹配方向的后方看,或者往匹配方向的反向看,或者逆着匹配方向看。去看正位置 后方(左侧) 的字符是否等于exp。
前瞻和后顾只会去匹配位置,不会匹配文本,不同的叫叫法实际都是表达的同一个意思,看大家怎么理解比较方便就怎么记名字。
与前瞻和后顾相似的还有负前瞻和负后顾,这里的负我们可以理解为非,就是不等于,相应的只要把前瞻和后顾表达中的=
换成!
即可。
负前瞻
(?!exp)
:从一个位置往匹配方向的前方看,或者往匹配方向的正向看,或者顺着匹配方向看。去看 前方(右侧) 的字符是否不等于exp。
负后顾
(?<!exp)
:从一个位置往匹配方向的后方看,或者往匹配方向的反向看,或者逆着匹配方向看。去看 后方(左侧) 的字符是否不等于exp。
额外说明:等于和不等于的叫法是为了更好的理解匹配含义,后顾和负后顾在某些语言或者环境下不支持,使用时请谨慎验证支持情况。
示例
1、检测字符串是否包含abc
其实如果只是单纯的检测某个字符串是否包含abc
?用indexOf()
最好,不过这里还是分享一个特殊正则校验的思路给大家。
如果只是写成/[abc]/
的话,这个集合内部是或
的关系。这只意味着字符串包含a
、b
、c
这三个中的任意一个,不能检测出字符串是否包含连续的abc
。
那我们要从另外一个角度去分析,找出字符串的任意一个位置右边是abc
即可(也可以用后顾找出一个位置左边是abc
,不过后顾支持率不好),我们可以利用前瞻的特性来实现这个正则/(?=abc)/
:
/(?=abc)/.test("cbacbac") //本字符串中不包含连续的abc,结果返回false
/(?=abc)/.test("cbacbabc") //本字符串中包含连续的abc,结果返回true
2、检测字符串是否包含连续两个相同的字符
good
、tomorrow
等有包含两个连续相同的字符,good
包含oo
、tomorrow
包含rr
,这样的校验用indexOf
是完成不了的,因为不能确定哪个字符连续出现了。这个例子是在第1个的基础上来的,因此我们直接修改第一个案例的表达式:
- 把
abc
换成一个任意字符:/(?=
\w)/
- 把替换后的这个任意字符用分组捕获到,并反向引用这个分组:
/(?=(\w)
\1)/
/(?=(\w)\1)/.test("good") //本字符串包含连续相同的字符oo,结果返回true
/(?=(\w)\1)/.test("tomorrow") //本字符串包含连续相同的字符rr,结果返回true
/(?=(\w)\1)/.test("cbacbabc") //本字符串中不包含连续的字符,结果返回false
3、检测字符串必须包含大写字符、小写字符和数字
做这个检测其实可以拆分为三个:包含大写字符、包含小写字符、包含数字,三个条件同时满足就通过。
//对字符串进行三个条件的校验,都满足时返回true
function test(str) {
return [/[A-Z]/,/[a-z]/,/\d/].every(reg => reg.test(str))
}
//或者对字符串进行三个条件的校验,只要一个不满足就返回false
function test(str) {
return ![/[A-Z]/,/[a-z]/,/\d/].some(reg => !reg.test(str))
}
test("aB123") //满足条件 结果返回true
test("ab123") //不满足条件 结果返回false
不过这样有个缺点,就是如果字符串中还包含其他的特殊字符也会校验通过。如果我们希望字符串只能由大写、小写以及数字组成,且每一种都要包含。应该怎么校验呢?
我们可以简单的分析一下隐藏的条件:
- 字符串包含大写、小写和数字,意味着从初始位置往后到结束都不能只由其中的一种或者两种来组成,即不能都是
[A-Za-z]
、[A-Z\d]
、[a-z\d]
,我们想到可以用负前瞻(匹配一个位置右边不能是啥) - 字符串只能由大写字符、小写字符和数字来组成
[A-Za-z\d]
- 大写、小写和数字每种都要包含,字符串长度最低是3
用正则去一步一步实现:
- 初始位置到结束都不能全是
[A-Za-z]
:/^(?![A-Za-z]+$)/
- 到结束也不能是
[A-Z\d]
,用或|
:/^(?!([A-Za-z]+
|[A-Z\d]+)$)/
- 到结束也不能是
[a-z\d]
,再用或|
:/^(?!([A-Za-z]+|[A-Z\d]+
|[a-z\d]+)$)/
- 只能由
[A-Za-z\d]
来组成:/^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)
[A-Za-z\d]/
- 字符串最低长度得是3:
/^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)[A-Za-z\d]
{3,}/
- 到结束都要满足条件:
/^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)[A-Za-z\d]{3,}
$/
var reg = /^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)[A-Za-z\d]{3,}$/
reg.test("ab123") //不满足条件,没有包含大写 结果返回false
reg.test("aB123") //满足条件 结果返回true
reg.test("aB12!") //不满足条件,包含了感叹号 结果返回false
如果想对长度有要求可以再加上限制,比如要求长度在6-10位:
长度限制:
/^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)[A-Za-z\d]
{6,10}$/
var reg = /^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)[A-Za-z\d]{6,10}$/
reg.test("aB123") //不满足条件,长度不够 结果返回false
reg.test("aBc123") //满足条件 结果返回true
reg.test("aBcD1234567") //不满足条件,长度超出 结果返回true
这里给大家留个题目:匹配8-16位且不低于三种由大写、小写、数字、特殊字符组成的文本。特殊字符的范围为了简便定义为:
!?@
,大家可以把自己的答案留在评论区。
彩蛋
作为程序员,奔着能省一些代码就省一些代码的原则,[A-Za-z\d]
多少看起来有点冗余了,因为我们都知道\w
表示大写
、小写
、数字
和_
,\W
则表示取反。我们只是不需要\w
中的_
,因此我们可以利用负负得正的效果[^\W] === \w
,再把_
排除掉即可:
再简单点:
/^(?!([A-Za-z]+|[A-Z\d]+|[a-z\d]+)$)
[^\W_]{6,10}$/
类似这样的操作还有,匹配一个大于0且小于32的正整数:
正常的正则为:
/^([123456789]|[12]\d|3[01])$/
简单点的为:
/^(
[^\D0]|[12]\d|3[01])$/
所以写正则比读正则要简单的多,同样校验能力的一个正则表达式可能有很多不同的写法,读起来很费力。 如果大家有正则表达式的需求,也可以在评论区留言,我会的尽量帮大家写出来。