起因
在平时工作时经常会用到正则表达式(以下简称正则),比如说校验输入格式,替换字符串等等,所以专门写一篇文章来记录一些常用的正则的写法及其实现思路。
什么是正则?为什么用它?
什么是正则?正则就是记录文本规则的代码,是对一串字符串的规则的描述。
为什么用它?因为正则可以帮助我们去描述一些复杂的规则,比如说对密码的限制。
API
首先我们先了解一下与正则相关的一些API,知道怎么去使用正则。
RegExp.prototype.test()
此方法接收一个字符串,判断字符串是否与正则表达式相匹配,返回true
或false
。
const reg = /^\d$/ |
RegExp.prototype.exec()
此方法接收一个字符串,执行一个搜索匹配,返回结果数组或null
。
const reg = /o+/ |
注意
在上面的代码中,我们可以发现一些不一样的地方,当正则表达式后面带上g
标志时结果会有些许的不同。这是因为当正则表达式带上g
标志时就代表开启了全局匹配,当每次成功匹配时,会同步更新reg
的lastIndex
属性,使得下一次搜索是从当前匹配的位置的下一位开始继续搜索的;而当不带g
标志时,就无法更新lastIndex
属性,从而进行多次搜索。同样的,test()
也会更新lastIndex
属性。
String.prototype.match()
此方法接收一个正则表达式(传入非正则类型会隐式的转换为正则类型),执行搜索匹配,返回结果数组或null
。
const reg = /o+/ |
注意
在上面的代码中,我们发现,当不开启全局匹配时,返回值与exec()
的返回值相同,而当开启全局匹配时,返回值是一个只包含所有匹配结果的数组。
String.prototype.matchAll()
此方法与match()
类似,但是参数接收的正则表达式必须设置了全局模式,否则会抛出异常。返回值是一个不可复用的迭代器。通过返回值中的done
判断是否迭代完成。
const reg = /o+/ |
String.prototype.search()
此方法接收一个正则表达式(传入非正则类型会隐式的转换为正则类型),执行搜索匹配,返回正则表达式在字符串中首次匹配项的索引或-1
。
const reg = /o+/ |
String.prototype.split()
此方法接收一个字符串或者正则表达式或者整数,使用指定的分隔符字符串将一个String
对象分割成子字符串数组,以一个指定的分割字串来决定每个拆分的位置。这里只讨论参数为正则表达式时的情况。
const reg = /o+/ |
String.prototype.replace()
此方法接收两个参数,第一个参数可以是一个字符串或者正则表达式,第二个参数可以是一个字符串或者函数,返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。这里只讨论第一个参数为正则表达式时的情况。
const reg = /o+/ |
当第二个参数为一个函数时,此函数返回值会作为替换字符串,同时接收以下几个参数。
变量名 | 含义 |
---|---|
match | 匹配的子串 |
$1,$2,… | 当一个参数为正则时,表达式中第n个小括号中匹配到的子表达式 |
offset | 匹配的子串相对于原字符串的偏移量,即子串开始的下标 |
string | 原字符串 |
'hello world!'.replace(/(o+).*(o+)/g, (match, $1, $2, offset, string) => { |
String.prototype.replaceAll()
此方法与replace()
类似,区别是默认替换所有满足条件的匹配项,参数接收的正则表达式必须设置了全局模式,否则会抛出异常。这里只讨论第一个参数为正则表达式时的情况。
const reg = /o+/ |
一些实例
下面将通过几个简单的常用的实例来学习如何写正则。
手机号校验
这里只针对国内11位手机号做处理
我们知道,手机号必须满足以下几点要求:
- 纯数字
- 11位
- 1开头
手机号前三位的区号也是一个校验的条件,但是这些号段可能还会不停的增加,所以这里不多加相应校验。
首先对于“纯数字”这个要求,可以使用/\d/
或者/[0, 9]/
来做校验。
/[0123]/ // 表示匹配其中任意一个字符,其中'[]'表示匹配的范围 |
其次对于“11位”这个要求,可以使用{11}
来加限制。
/\d{11}/ // 表示匹配精确的11位,其中'{}'表示用来匹配的长度 |
然后对于“1开头”这个要求,可以使用^1
来做校验。
/^1/ // 表示匹配以1开头的字符串 |
最后将所有的要求整合到一起就是/^1\d{10}$/
。这里要注意的是需要给最后面加上$
表示只匹配11位数字就结束了,不然对于以11位数字开头但长于11位的字符串也会校验成功。
const reg = /^1\d{10}$/ |
身份证号校验
身份证号码通常由15位数字或者17位数字加一位校验位组成,校验位可能是数字或者字符X。
- 15位纯数字
- 18位,17位纯数字加一位校验位
首先对于第一种形式,可以使用/^\d{15}$/
来做校验。
然后对于第二种形式,可以使用/^\d{17}(\d|X|x)$/
来做校验。
而对于这种有多种情况的,可以使用A|B
来实现。
/A|B/ // 表示匹配A或者B |
最后整合到一起就是/(^\d{15}$)|(^\d{17}(\d|X|x)$)/
。
const reg = /(^\d{15}$)|(^\d{17}(\d|X|x)$)/ |
日期格式校验
这是只做以下几种格式的校验。
- 2021/01/01
- 2021-01-01
- 2021.01.01
- 2021.1.1
下面分步来看。
- 第一步先看年月日。年份就是四位数字组成:
/\d{4}/
;一年有12个月,可以由1-12或者01-12表示,分别做校验就是:/0?[1-9]|1[0-2]/
。一个月有28-31天,同样可以由01-31或者1-31表示,分别做校验就是:/0?[1-9]|(1|2)[0-9]|3(?:0|1)/
。
注意:在日期这里,用到了3(?:0|1)
,这个表示匹配0|1
但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。等于是30|31
的简略表达。
- 第二步看连接符。连接符有三种形式,同时要保证两个连接符一致,这里可以通过捕获子表达式来使其保持一致。
捕获子表达式
前面我们知道了被包在()
中的就是一个子表达式,在正则表达式中,通过\num
来捕获子表达式。
/\num/ // 匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,'(.)\1' 匹配两个连续的相同字符。 |
在上述代码中,我们使用了\1
去捕获了子表达式([a-zA-Z])
,使得子表达式匹配到什么,\1
就是什么,也就是说二者保持一致。
我们可以使用捕获子表达式来实现一些要求几个部分一致的需求。
回到上面的连接符,知道了如何捕获子表达式,实现起来就很简单了:/^\d{4}([-/.])(0?[1-9]|1[0-2])\1(0?[1-9]|(1|2)[0-9]|3(?:0|1))$/
。这里需要注意的是表达式中用到了()
来区分不同的子表达式,所以在捕获的时候一定要注意是第几个表达式。
const reg = /^\d{4}([-/.])(0?[1-9]|1[0-2])\1(0?[1-9]|(1|2)[0-9]|3(?:0|1))$/ |
缺陷
上述的表达式有个缺陷就是不能判断日是否与月相对应,比如2021-02-31
,这个需要通过代码去实现。
金额格式校验
金额在输入的时候可能会是各式各样的,这里对以下几种方式做校验
- 1000,整数金额
- 1000.01,精确到分金额
- 1,000,3位分隔的整数金额格式
- 1,000.01,3位分隔的精确到分金额格式
下面分步来看。
第一步应该是一个不限制长度的数字:
/^[0-9]+$/
。第二步增加一个金额可以精确到分的校验:
/^[0-9]+(.[0-9]{1,2})?$/
。这里在上一步基础上增加了一个可以出现0或1次的(\.[0-9]{1,2})
,这个部分匹配的是以小数点开始的一到两位的小数部分。
注意:我在小数部分的校验用到了\.
,这是因为.
代表的是匹配除换行符(\n、\r)之外的任何单个字符。而当放置在[]
中时,.
代表的就是.
。
第三步写出一个必须输入
,
的校验,这里要明确,对于,
,必须从小数点或最右侧开始每三位一个,且不可出现在第一位,不可出现在小数位。也就是说我们要限制金额开头不可为,
,即/^[0-9]{1,3}/
(这里限制1-3位是因为,
分隔的就是3位,超过3位就必须加,
)。然后是包含,
的部分:/(,[0-9]{3})*/
,用到了’‘,是因为可能金额整数部分只有三位,恰好没有,
。合在一起就是一个必须输入,
的校验:`/^[0-9]{1,3}(,[0-9]{3})$/`。最后我们把第二步和第三步的合在一起就是:
/^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(\.[0-9]{1,2})?$/
。整体由()
区分成整数和小数两部分,小数部分可有可无;然后整数部分又由|
区分成不可输入,
和可以输入,
的两部分。
const reg = /^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(\.[0-9]{1,2})?$/ |
金额格式化
金额格式化是指将数字格式的转换为金额格式,即:1000.01 -> 1,000.01
。
这个的关键点在于找到插,
的地方,并且要排除小数位。首先,我们先学习几个新的知识点:正向肯定预查和正向否定预查。
正向肯定预查
正向肯定预查(
/(?=pattern)/
)在任何匹配pattern的字符串开始处匹配查找字符串,这是一个非获取匹配,预查不消耗字符。
举个栗子
const reg = /o(?=r)/g |
在上面的代码中,查找o
时忽略了下标为4的o
,而直接到了下标为7的o
,这是因为正则表达式的意思是全局查找后面紧跟r
的o
,只有下标为7的满足条件。为什么?
再看定义,“预查不消耗字符”,表示表达式中(?=r)
这一部分只是做一个限制,限制匹配的目标后面必须紧跟着r
,而真正匹配的只有前面的o
。
正向否定预查
正向否定预查(
/(?!pattern)/
),在任何不匹配pattern的字符串开始处匹配查找字符串,这是一个非获取匹配,预查不消耗字符。
举个栗子
const reg = /o(?!r)/g |
与正向肯定预查类似,不同的是它匹配的是后面没有紧跟r
的o
,所以只匹配到了下标为4的o
。
格式化
现在回到我们的问题,金额格式化,也就是整数部分右往左每三位加一个,
;这里就可以用到正向肯定预查,我们去找后面恰好有3的倍数位的位置(只有限制条件,没有实际匹配目标,匹配到的就是位置),也就是/(?=(\d{3})+)/
const reg = /(?=(\d{3})+)/g |
观察上面代码发现有两个问题,一个是首位也会被替换成,
,另一个是只要后面包含至少三位数字就一定会发生替换,并不是我们希望的恰好是3的倍数位。这肯定不是我们希望的。针对问题一,可以通过\B
限制不匹配边界。
/\b/ // 匹配一个单词边界 |
然后是问题二,这个需要用到正向否定预查,去限制即包含了3的倍数位的数字且不含多余的数字,也就是/\B(?=(\d{3})+(?!\d))/
。其中(?!\d)
表示后面还有数字就不匹配。
const reg = /\B(?=(\d{3})+(?!\d))/g |
可以发现已经达到我们想要的效果了。
然后我们扩展一下,学习一下与正向肯定/否定预查类似的反向肯定预查和反向否定预查。
反向肯定预查
反向肯定预查(
(?<=pattern)
),与正向肯定预查类似,只是方向相反。
反向肯定预查是给查询条件的左侧加限制条件。举个栗子
const reg = /(?<=w)o/g |
限制了o
的左侧必须是w
,所以只有下标为7的匹配成功了。
反向否定预查
反向否定预查(
(?<!pattern)
),与正向否定预查类似,只是方向相反。
举个栗子
const reg = /(?!w)o/g |
限制了o
的左侧必须不是w
,所以只有下标为4的匹配成功了。
密码校验
密码校验也会有一个难易程度,这里针对不同的强度分别做说明。
弱强度密码
限制条件:只能由字母、数字或者下划线组成,长度6-18位。
针对“只能由字母、数字或者下划线组成”这一限制条件,可以使用\w
来匹配。
/\w/ // 匹配字母、数字、下划线。等价于'[A-Za-z0-9_]' |
然后加一个长度限制就可以了:/^\w{6,18}$/
。
const reg = /^\w{6,18}$/ |
高强度密码
限制条件:至少同时包含大小写字母和数字以及特殊符号其中的三种,长度8-16位。
首先看一下匹配特殊字符的元字符。前篇介绍过的这里不再赘述。
/\cx/ // 匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。 |
下面分步来看。
- 第一步,先匹配一个非空字符串,长度8-16位。
const reg = /^\S{8,16}$/ |
- 第二步,分别限制其必输包含大写字母、小写字母、数字、特殊符号。
这里要用到正向肯定预查来限制必须包含某个规则。
比如说限制必须包含大写字母,可以这样写:/(?=[A-Z])/
,表示匹配目标后面必须跟一位大写字母。但是我们要明确两个点,一个是我们不确定有几位大写字母,而是我们不确定大写字母在哪一位。
对于第一点,其实可以不用去管他,我们只要去判断有没有,确保有一位存在即可。对于第二点,不确定位数,可以使用/\S*/
来匹配,表示前面有0到多位非空字符。合起来就是/(?=\S*[A-Z])/
。
// 必须包含大写字母 |
- 第三步,在第二步的基础上限制必须包含其中三种。
因为我们不确定用户输入的是哪三种,所以不同的情况我们都要加以考虑。
比如说对于“必须同时包含大小写字母和数字”这一类型。我们在上一步得到三种分别的限制条件的基础上,只需要将这三种合在一起,作为并且条件即可。而对于预查,按顺序一个接一个写,就是并集。
// 必须包含大小写字母和数字 |
然后我们将这几种情况合在一起就是
const reg = /^((?=\S*[A-Z])(?=\S*[a-z])(?=\S*[0-9]))|((?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*?()_ ]))|((?=\S*[a-z])(?=\S*[0-9])(?=\S*[!@#$%^&*?()_ ]))|((?=\S*[A-Z])(?=\S*[0-9])(?=\S*[!@#$%^&*?()_ ]))\S{8,16}$/ |
但是这个正则似乎过于长了。
一些笔试题
URL化
题目来源
题目描述
编写一种方法,将字符串中的空格全部替换为%20。假定该字符串尾部有足够的空间存放新增字符,并且知道字符串的“真实”长度。
示例1:
输入:“Mr John Smith ”, 13
输出:“Mr%20John%20Smith”
示例2:
输入:“ ”, 5
输出:“%20%20%20%20%20”
题解
我们可以使用\s
匹配到空白字符,然后使用replace()
将其全部替换为%20
。
同时注意题目中说的“知道字符串的‘真实’长度”,也就是说给到的输入可能长度会长于真实长度,需要先对其进行截取。
/** |