所谓正则表达式的技巧

Published on:
Tags: 正则 ruby

今天下午,看到一道正则的题目:最少包含数字和字母, 只能包含._-@这四个特殊字符,其他的字符不允许使用。

我一看, 这不就是某些网站在输入密码时候为了让用户选择安全强度比较高的密码而做的过滤吗? 还比上面那个题目上多了一个条件: 密码最少要求8位,最大32位。

 第一次想简单了:

/\d+|[a-zA-Z]+|[_.@-]+/

这个是没有考虑到题目中「最少包含数字和字母」这个条件, 这个条件的意思是:字符中最少的组合是数字+字符,也就是说数字和字符必须同时存在,单有数字或单有字母,是不能通过验证的。

否定匹配

这样的话就有点难度了。 正则表达式中, 没有直接表示「且」这种同时存在的概念。想表达「且」,有一个办法就是使用否定匹配(?!),比如:

"abcdsss".match(/^(?!abcd).*?$/)
=> nil
"acbcdsss".match(/^(?!abcd).*?$/)
=> #<MatchData "acbcdsss">

但是用在这个题目上,还不太适合,因为你需要找出一个否定条件:「除数字和字母之外的任意字符都不匹配」。 除数字和字母之外的任意字符,太多了, 而且,这个条件也把题目中允许的那四个特殊字符给否定了。

所以这种方法排除。

环视

经过思考, 我确定了一种方法,是可以判断这种情况的, 那就是判断: 「数字旁边是字母, 字母旁边是数字, 字母和数字之间可能有特殊字符,包括允许和不允许的」。 那么,只有通过「环视」功能,才可以做出以上的判断,于是得出如下正则表达式:

# 数字旁边是字母,字母旁边是数字

/[0-9a-zA-Z]*(?<=[a-zA-Z])[0-9a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/

"1234fsfsfd123123sdfs".match(/[0-9a-zA-Z]*(?<=[a-zA-Z])[a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/)
=> #<MatchData "234fsfsfd123123sdfs">


"111".match(/[0-9a-zA-Z]*(?<=[a-zA-Z])[a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/)
=> nil

"abcd".match(/[0-9a-zA-Z]*(?<=[a-zA-Z])[a-zA-Z]*(?=[0-9])[a-zA-Z0-9]*/)
=> nil

看起来靠谱, 思路应该是没错了,可是。。。 这只符合数字和字母的组合, 数字和字母之间还可能有任意的特殊符合,包括允许的和不允许的。最终,沿着这个思路,我得出了下面这段很长很笨的正则:

"sfdsf_@ssf11_@11".match(/(?<must>[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[0-9])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[@_-])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?=[a-z])[0-9a-zA-Z]*(?<spec>[@_.-]*))|(?<must>[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[a-z])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?<=[@_-])[0-9a-zA-Z]*(?<spec>[@_.-]*)(?=[0-9])[0-9a-zA-Z@_-]*(?<spec>[@_.-]*))/)

=> #<MatchData

 "sfdsf_@ssf11_@11"
 must:nil
 spec:nil
 spec:nil
 spec:nil
 spec:nil
 must:"sfdsf_@ssf11_@11"
 spec:""
 spec:"_@"
 spec:"_@"
 spec:"">

我使用了命名捕获组,must代表必须匹配, spec代表允许匹配的特殊字符。但是这个答案还不是最终的,因为它不完美,它不能满足所有的字符组合。

一定还有更简单的方法。

奇技淫巧

最终是看了一位正则高手给出的答案(也许有更简单的):

(?=^.*?[0-9])(?=^.*?[A-Za-z])^[0-9a-zA-Z_.@\-]{8,32}$

这个非常简洁,思路跟我那个笨办法是一样的,采用了环视,通过位置的判断达到数字与字母「且」的组合, 其中简单的技巧在于:

^.*?

这四个常用的正则元字符, 单个的意思,大家都知道, 但是组合在一起,非常强大。

"ssfds".match(/^.*/)
=> #<MatchData "ssfds">


"ssfds".match(/^.*?/)
=> #<MatchData "">

我们都知道.*的组合是非常强大的, 匹配任意字符, 但是加了问号「?」, 就不一样了。

? (问号),有两层意思:

  1. 匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。
  2. 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。也就是懒惰模式。

所以, (^.*?)这个技巧,就是使用了「懒惰模式」, 意思是「匹配任意字符0次,也就是忽视任意字符」。 回到我们的正则表达式中:

(?=^.*?[0-9]) # 正向环视 ,匹配一个右边包含数字(忽视数字之前的任意字符)的位置

(?=^.*?[A-Za-z]) #正向环视, 匹配一个右边包含大小写字母(忽视字母之前的任意字符)的位置

^[0-9a-zA-Z_.@\-]{8,32}$ # 在匹配到上面的位置之后, 再匹配位置右边是否存在8-32位长的包含数字、字母、四位被允许的特殊字符组合。

这就是这个表达式的含义。 (.*?) 这样的用法,估计很多人知道,您看完之后,也许感觉很简单, 但是如果看了我上面的笨办法之后,你应该可以感受得到,对于不经常写正则表达式的人来说,这样的技巧太重要了,也非常值得我们学习。

这就是所谓的技巧,也许你早已知道,但你却不知道何时何地如何使用。

最后再推荐一个在线调试正则的工具:https://www.debuggex.com/

Comments

comments powered by Disqus