正则中关于环视(lookaround)的小例子

刚开始学正则表达式时,环视(lookaround)经常会给初学者造成一定的困扰。但如果能抓住其中的要点,那么这种困惑就会立刻消失。

环视(lookaround)其实分为两个部分:前瞻(lookahead)和后视(lookbehind)。

注:这里的翻译是基于我个人的理解,其它地方可能还有别的叫法

引言

我们在学一个东西时,我们需要去考虑一下它的使用场景,如果没有一个明确的目的的话,学到的知识也不会很深刻。

考虑这样一个场景,我们在做用户登录或注册校验时,会去判断用户输入的密码是否合法,比如,现在对于用户输入的密码,我们有如下几点要求:

  • 至少有 6 个字符
  • 包含一个小写字母
  • 包含一个大写字母
  • 包含一个数字

如果不使用正则表达式的话,估计大部分人都会用 if 语句去逐个判断每个要求是否符合。这样的代码初学时也没有少写过,不能说这样写的方式有什么不对,但总归是没有那么优雅。

那么有没有更好的解决方案呢?可能大部分人也会想到用正则表达式,但如果对正则表达式的了解并不是很深入的话,面对这样的需求,可能会遇到这样一个问题:怎么判断字符串中至少含有一个大小写字母和数字?如果你动手去写的话,你就会发现你没法保证写出来的表达式不考虑顺序。

环视(lookaround)的简单讲解

那么如何解决这个问题?如何能够通过正则表达式,在不考虑字符出现顺序的情况下判断密码是否至少包含一个大小写字母和数字?

这就将用到我们接下来要介绍的关于环视(lookaround)的两个部分:前瞻(lookahead)和后视(lookbehind)。

当我们在使用前瞻(lookahead)和后视(lookbehind)时,正则表达式在处理字符串的过程中,是不会在字符串上移动的,也就是说我们可以使用这种技术或者说手段来提前判定字符串是否符合一些情况。

那么,在继续深入讲解之前,我们先来学习一下 4 种环视(lookaround)的写法,这里假设你已经具备了基本的正则表达式语法知识。

环视(lookaround) 名称 做了什么
(?=foo) 前瞻(lookahead) 判断紧跟在字符串中当前位置后面的内容是否 foo
(?!foo) 否定前瞻(negative lookahead) 判断紧跟在字符串中当前位置后面的内容是否不是 foo
(?<=foo) 后视(lookbehind) 判断紧跟在字符串中当前位置前面的内容是否 foo
(?<!foo) 否定后视(negative lookbehind) 判断紧跟在字符串中当前位置前面的内容是否不是 foo

注:上面的 foo 可以替换为正则表达式,功能将会更强大

环视(lookaround)的简单示例

理解不了上面的介绍?那么为了能先让读者简单理解四种环视的写法的作用,我先讲个简单例子,现在先假设这个当前的字符串是 foobarbarfoo

例子 描述
bar(?=bar) 匹配第一个 bar(因为第一个 bar 后面紧跟着一个 bar
bar(?!bar) 匹配第二个 bar(因为第二个 bar 后面没有紧跟着一个 bar
(?<=foo)bar 匹配第一个 bar(因为第一个 bar 前面紧跟着 foo
(?<!foo)bar 匹配第二个 bar(因为第二个 bar 前面没有紧跟着 foo

在上面的例子中,都着重强调了 “紧跟着” 这几个字,这是相对于括号之外的那个 bar 字符串而言的,即要判断紧挨着这个字符串 bar 的前后是否符合要求。

解决问题

现在环视(lookaround)的概念以及简单的示例已经介绍完了,那么再回到我们开头讲的那个例子:如何用正则表达式去判断用户输入的密码是否符合要求?

我们先把几个需求逐步解决。首先第一个:至少有 6 个字符。这个很好解决,保证密码内容是由大小写字母及数字组成的,并且长度至少为 6。

1
^[A-Za-z0-9]{6,}$

简单解释一下吧,^ 用来匹配开头,$用来匹配结尾,[A-Za-z0-9]表示要匹配的内容是由大小写字母及数字组成的,{6,}表示长度至少为6。

那么第二个要求:包含一个小写字母。前面我们在描述那几个例子时,都强调了要紧跟在字符串当前位置,那么可能有读者会困惑,既然都这样要求了,那么就无法不强调一个先后顺序了,但是事实是,我们可以通过修改表达式来达到这个目的,修改原来的正则表达式,满足现在的要求:

1
^(?=.*[a-z])[A-Za-z0-9]{6,}$

与之前用字符串的写法不同,这里使用了一个表达式: .*[a-z].用来匹配任何字符,*即匹配 0 个到多个字符,[a-z]即表示小写字母,这个写法表示,后面的字符串中至少要有一个小写字母,而小写字母的前面有什么,不需要考虑,即只要满足一堆字符串后面有一个小写字母即可。注意这里只是一个判断,正则表达式在环视扫描时,不会在字符串上移动,如果没有符合要求的字符,那么就会结束扫描。

当然这是贪婪(greedy)模式的写法,也可以使用懒惰(lazy)模式的写法: (?=.*?[a-z]),即在 * 后面加一个 ? ,这样只要有第一个符合要求的字符出现,就会停止匹配,然后继续扫描。如果你不理解什么是贪婪(greedy)模式和懒惰(lazy)模式,可以先跳过这一段,文末会有个简单的解释。

后面两个要求:包含一个大写字母和包含一个数字。原理同第二个要求,这里就直接给出最终实现了:

1
^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])[A-Za-z0-9]{6,}$

值得注意的是,(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])这三个表达式,会先后进行扫描判断,只要有不符合的就停止,匹配失败。正则表达式在扫描时不会在字符串上移动,所以这三个表达式的写法并没有顺序。

以上,就解决了我们本文开头提出的问题,有了正则表达式,你就可以使用你喜欢或者你正在用的编程语言去进行尝试了。

本文的灵感来自 CodeWarsRegex Password Validation

解决方案也可以参考 我的实现

关于贪婪(greedy)模式和懒惰(lazy)模式

  • 贪婪意味着匹配最长的字符串
  • 懒惰意味着匹配最短的字符串

举例来说,给定一个字符串 InnoFang

  • 对贪婪模式来说,正则表达式为 I.*n,匹配文本输出为 InnoFang
  • 对懒惰模式来说,正则表达式为 I.*?n,匹配文本输出为 InnoFang

两者在写法上的区别,就是懒惰模式相比贪婪模式会在诸如 *+? 等限定匹配数量的符号后面加一个 ?

参考

Author: Inno Fang
Link: http://innofang.github.io/2018/07/12/%E6%AD%A3%E5%88%99%E4%B8%AD%E5%85%B3%E4%BA%8E%E7%8E%AF%E8%A7%86%EF%BC%88lookaround%EF%BC%89%E7%9A%84%E5%B0%8F%E4%BE%8B%E5%AD%90/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-ND 4.0 unless stating additionally.