起因
起因是一个朋友问怎么实现一个密码检查功能:
- 密码只能由大写字母,小写字母,数字构成;
- 密码不能以数字开头;
- 密码中至少出现大写字母,小写字母和数字这三种字符类型中的两种;
- 密码长度8-100位
然后他贴了写的代码:
$value = 'A1234567890a';
$rule = '/^[A-Z][A-Za-z]{7,100}|^[A-Z][A-Z0-9]{7,100}|^[a-z][A-Za-z]{7,100}|^[a-z][0-9a-z]{7,100}$/';
var_dump(preg_match($rule,(string)$value));
一看这变量名以$
开头,大概是php的,但我不怎么懂PHP,只看得懂中间的正则,猜最后一行是输出匹配结果。
这个正则显然是不满足上面说的条件的,|
的优先级比$
低,所以前面的表达式没有控制密码长度的作用。我依稀记得曾经学正则也看过相关的题目,这种复杂的正则检查是要用零宽断言。
然而我觉得零宽断言这东西写起来复杂,别人看更觉得复杂的东西,团队里面显然不是所有人都会去弄懂的。写这种复杂正则,以后需求变更,修改起来简直是噩梦,所以没怎么认真看,只知道有这么个东西,于是我就认真翻了一下相关的文章。
链接如下:
https://deerchao.net/tutorials/regex/regex.htm
这个是我觉得写得比较好的,内容很充实的一个文章了。
解决问题
对于这个需求,我们先完成第1
、2
和4
点,这三个比较简单,正则写出来如下
var regex = /^[A-Za-z][A-Za-z0-9]{7,99}$/
这个问题难写的就是第3
点,如果不用零宽断言,是要用很多个|
去做判断,非常长。
第三点是
密码中至少出现大写字母,小写字母和数字这三种字符类型中的两种;
换个说法就是
不能全是大写字母,不能全是小写字母,不能全是数字。
或者说
存在一个非大写字母,存在一个非小写字母,存在一个非数字。
第一种
对于第一种说法,可以用
var regex = /^(?![A-Z]*$)/; // 不能全是大写字母
var regex2 = /^(?![a-z]*$)/; // 不能全是小写字母
var regex3 = /^(?![0-9]*$)/; // 不能全是数字
这里的*
可以换成+
,只会在空字符串的时候有差异,对于我们上面的需求是没有差异的,因为限定了8-100的长度。
与前面的正则组合起来就是
var regex = /^(?![A-Z]*$)(?![a-z]*$)(?![0-9]*$)[A-Za-z][A-Za-z0-9]{7,99}$/;
第二种
对于第二种说法,可以用
var regex = /^(?=.*[^A-Z])/ // 存在一个非大写字母
var regex2 = /^(?=.*[^a-z])/ // 存在一个非小写字母
var regex3 = /^(?=.*[^0-9])/ // 存在一个非数字
这里*
不能替换成+
,为什么可以自己思考一下,我会给一反例证明替换有问题。
组合起来就是
var regex = /^(?=.*[^A-Z])(?=.*[^a-z])(?=.*[^0-9])[A-Za-z][A-Za-z0-9]{7,99}$/;
如果将所有的*
替换成+
,那么对于Aaaaaaaa
,正则匹配会失败,根据需求应该是成功。
第三种
什么?居然还有第三种?是的,当然还有第三种,和第四种。
刚刚我们写的断言,按照链接给的名字是零宽度正预测先行断言(?=exp)
和零宽度负预测先行断言(?!exp)
,当然名字其实不重要的,翻译过来的名字有很多种,不同的文章也不同,也不知道哪个是比较官方的。
所以我们可以用另外两种断言实现,分辨是零宽度正回顾后发断言(?<=exp)
和零宽度负回顾后发断言(?<!exp)
对于第三种和第四种,它是从后面检查前面的断言,这种断言据说
js是不支持的,但是chrome的引擎似乎是支持的,也不清楚是怎么回事。
用(?<!exp)
形式写第一种说法
var regex = /(?<!^[A-Z]*)$/; // 不能全是大写字母
var regex2 = /(?<!^[a-z]*)$/; // 不能全是小写字母
var regex3 = /(?<!^[0-9]*)$/; // 不能全是数字
同样的*
可以替换成+
,组合起来就是
var regex = /^[A-Za-z][A-Za-z0-9]{7,99}(?<!^[A-Z]*)(?<!^[a-z]*)(?<!^[0-9]*)$/
第四种
不废话了直接贴代码
var regex = /(?<=[^A-Z].*)$/ // 存在一个非大写字母
var regex2 = /(?<=[^a-z].*)$/ // 存在一个非小写字母
var regex3 = /(?<=[^0-9].*)$/ // 存在一个非数字
组合后如下,同样的不能把*
替换成+
,反例是aaaaaaaA
var regex = /^[A-Za-z][A-Za-z0-9]{7,99}(?<=[^A-Z].*)(?<=[^a-z].*)(?<=[^0-9].*)$/;
第五种、第六种……
组合有很多种,因为条件都说的很清楚了,四种断言是可以组合的,所以其实远不止以上说的四种情况,重要的是要掌握四种断言的本质。
结尾
本文只是一个引子,并不是说怎么学正则,学习正则可以参考上面给的链接,这里再给一次https://deerchao.net/tutorials/regex/regex.htm
警告:不要写过于复杂的正则,多人协作中,代码的可读性远比性能和炫技重要。