正则表达式学习
正则匹配作为JS语言中的一个功能。常常出现在我们的表单验证,字符格式化等场景中,也是面试考核的常客。虽然网上的正则一大推,可以直接拿来用,但我们还是有必要学习下正则的基本用法。毕竟它对于字符串匹配处理真的很方便,同时也不会让你倒在简单正则面试题上。
本篇文章注重介绍正则基础字符及用法,属于正则使用入门。最后通过一个个小例子,一步一步的讲解如何匹配出我们需要的结果。
话不多说,先是了解正则的基础知识。
特殊字符
转义符
普通字符转义为特殊字符
特殊字符如果是字母
组成的话,一般在前面加个\
标识其为特殊字符。
比如w
,正则/w/
匹配的是w
这个字母。而正则/\w/
却不一样,它匹配的是一个单字字符(字母、数字或者下划线)
。并且一般大写和小写是相反的规则,比如正则/\W/
匹配的是一个非单字字符
。
特殊字符转义为普通字符
而像.
,/
之类的特殊含义字符想要匹配其原字符,也需要在前面加上\
进行转义。
比如.
,本来匹配的是除换行符之外的任何单个字符
,在下面这句话中"You are good."
我们就想查找一下这个句号.
。这时候就需要用到转义/\./
,在这里它匹配的就是上面这句话的标点.
这个字符。
断言类(Assertions)
断言类字符一般表示匹配的边界符合的条件。
边界类断言
字符 | 含义 |
---|---|
^ |
匹配输入的开头。当有多行标识m 时,可匹配换行符后的开头。 |
$ |
匹配输入的结束。当有多行标识m 时,可匹配换行符后的开头。 |
\b |
匹配单词的边界(border),一般匹配单词字母和非字母(可以是换行符,输入开始,结束)之间的位置。 |
\B |
和\b 相反,前后字符都是字母,或者前后字符都不是字母 |
其他断言
字符 | 含义 |
---|---|
x(?=y) |
向前断言,x紧贴在y前面时,匹配x。也就是说紧贴在y前面 作为条件。 |
x(?!y) |
向前否定断言,x不紧贴y前面时,匹配x。 |
(?<=y)x |
向后断言,x紧贴在y后时,匹配x。紧贴在y后面 作为条件。 |
(?<!y)x |
向后否定断言,x不紧贴在y后时,匹配x。 |
字符类(Character Classes)
用于匹配各种类型的字符。
字符 | 含义 |
---|---|
. |
匹配除行终止符之外的任何单个字符 |
\d |
匹配任何数字(digit) |
\D |
匹配任何非数字 |
\w |
匹配数字字母下划线 |
\W |
和\w 相反 |
\s |
匹配空白字符,包括空格,tab,换行符,换页符 |
\S |
和\s 相反 |
\t |
匹配一个水平tab字符 |
\r |
匹配一个CR |
\n |
匹配一个换行符 |
\v |
匹配一个垂直换行符 |
\f |
匹配一个换页符 |
[\b] |
匹配一个空格键 |
小知识:
- Carriage return 字符表示的是返回当前行的开始,没有向下换行(有点像HOME建)。简称
CR
。转义字符为\r
。- Line feed 表示换行符,向下换一行(ENTER建)。也叫newline,简称为
LF
或者NL
。转义字符为\n
。CRLF转义字符为\r\n
。- Form feed 表示换页符。转义字符为
\f
。简称FF
。
组和范围(Groups and Ranges)
用于匹配多个字符
字符 | 含义 |
---|---|
x|y |
匹配x或者y |
[xyz] |
字符集,匹配xyz中的任意一个。也有[a-z] 的形式,表示所有小写字母。但是如果- 在第一个或者最后一个出现,表示的是- 字符。 |
[^xyz] |
匹配除了xyz之外的任意字符。这里的^ 表示取反。 |
(x) |
捕获组: 匹配x并记住匹配项。 |
\n |
表示最后第n个捕获的引用。比如/(\w)b\1/,这里的\1 引用的就是\w 所匹配的内容。 |
(?<Name>x) |
具名捕获组: 匹配"x"并将其存储在返回的匹配项的groups属性中,该属性位于 |
(?:x) |
非捕获组。可以优化捕捉组的性能。 |
量词(Quantifiers)
同样用于匹配多个字符,更适用于重复规则。
字符 | 含义 |
---|---|
x* |
匹配 x 0次或多次 |
x+ |
匹配 x 至少1次 |
x? |
匹配 x 0次或1次 |
x{n} |
匹配 x n次 |
x{n,} |
匹配 x 至少n次 |
x{n,m} |
匹配 x 至少n次,至多m次 |
非贪婪
像*
,+
这样的量词都是贪婪的,会匹配尽可能多的字符。但是加上?
就会变成非贪婪,只要找到匹配就会停止。
字符串转义正则转义符
这标题是不是有点晕?当使用RegExp进行构造正则时,由于\
是字符串的转义标识,所以我们需要对正则字符串做转义处理。
比如我们想要的到/\w/
,使用new RegExp('\w')
只会得到/w/
。因为"\w" === "w"
。
所以我们需要先转义字符串。new RegExp("\\w")
,告诉字符串处理器不要处理\
。这样最后的到就是/\w/
啦!
常用正则标志
在使用正则的时候我们通常还会搭配标志来使用。标志表示了正则匹配的部分规则/配置。
标志 | 说明 |
---|---|
g |
全局搜索 |
i |
不区分大小写 |
m |
多行搜索,影响$^ |
正则相关方法
1. exec
该方法返回一个数组或者null(没有匹配)。
返回的数组中:[0]表示匹配的值,[1]…[n]表示捕获的分组。
属性index
表示匹配的字符的位置索引。input
表示原字符串。
2. test
查看正则表达式与指定的字符串是否匹配。返回布尔值。
注意:
当设置了全局标志g
时,每一次调用test都可能改变regex.lastIndex
,有可能引起判断和我们想的不一样。
var regex = /foo/g;
// regex.lastIndex is at 0
regex.test('foo'); // true
// regex.lastIndex is now at 3
regex.test('foo'); // false
// regex.lastIndex is now at 0
regex.test('foo'); // true
JS字符串相关方法
1. match
未匹配时返回null
,匹配时返回数组。
如果不加g
标志,会附加groups
,input
,index
附加属性。
如果加了g
标志,则会返回一个数组,包含所有的匹配项。
"Hello world!".match(/\w/)
// ["H", index: 0, input: "Hello world!", groups: undefined]
2. matchAll
在看了match
方法之后你可能有疑问,那我想看每一个匹配项的捕获组咋办?matchAll
就是为这个而生的。他的正则参数必须要加上g
标志,不然会报错。
3. search
返回匹配到的第一个字符的起始位置,未找到返回-1。findIndex
的正则版
4. replace
replace() 方法返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要调用的回调函数。如果pattern是字符串,则仅替换第一个匹配项。
原字符串不会改变。
str.replace(regexp|substr, newSubStr|function)
第一个参数是匹配模式,第二个参数是替换项。值得注意的是,第二个参数可以是字符串,也可以是函数。
- 当其为字符串时,有几个特殊字符可用于表示某些匹配项。
变量名 | 代表的值 |
---|---|
$$ |
插入一个 “$”。 |
$& |
插入匹配的子串。 |
$` |
插入当前匹配的子串左边的内容。 |
$' |
插入当前匹配的子串右边的内容。 |
$n |
假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从1开始 |
- 当传入函数时
function (match, p1, p2, p3, ... offset, string) {}
match
表示匹配的字符相当于$&
,而p1
,p2
…则是捕获组,紧接着是当前匹配串的位置offset
,以及源字符串string
。
5. split
split
不仅可以用子串作为分隔符,也可以用正则作为分隔符。
练习实操
匹配类
1. 匹配颜色值-十六进制
#fff / #123 / #fafafa / #DDD / #F1F1F1
思路:第一个为#,后面接6位16进制数,或者3位缩写,大小写不敏感。
- 匹配
#
/#/
- 匹配16进制数
/[a-f\d]/
- 匹配6位或3位16进制数
注意到这里6是3的一倍,也就是重复一次关系。
/(?:[a-f\d]{3}){1,2}/
解释:
[a-f\d]
是一个字符集合,也可表示为[a-f0-9]
。[a-f\d]{3}
表示这个集合的字符要连在一起出现3次。(?:[a-f\d]{3}){1,2}
表示连续3个16进制字符
这组
字符需要出现1或者2次。这里用到了非捕获组(?:x)
,是为了优化性能。
- 最后
/#(?:[a-f\d]{3}){1,2}\b/gi
解释:\b
表示匹配到字符串后面不应再跟其他字母了,gi
表示全局搜索以及大小写不敏感,可以匹配多个以及#Fff
这样的表示。
2. 匹配颜色值-RGB
rgb(1,2,3) / rgb(255,255,255) / rgb( 0, 02, 0)
思路:重要的是匹配括号里面的内容,注意最大值255,允许空格,前导0。
- 匹配
rgb()
/rgb\(\)/
- 值的限制。
/\s*0*(?:\d|\d\d|1\d\d|2[0-4]\d|25[0-5])\s*/
保证了值的大小限制0-255,也允许了前后的空格和前导0。
我们还可以简化一下。把200以下的数字一起表示。
/\d|\d\d|1\d\d/ ==> /1?\d?\d/
- 匹配
r,g,b
注意到前面两个都是一个模式:x,
数字加逗号。最后一个只有数字。我们把第二步的模式设为x
/(?:x,){2}x/
- 最后
/rgb\((?:x,){2}x\)/gi
==>
/rgb\((?:\s*0*(?:1?\d?\d|2[0-4]\d|25[0-5])\s*,){2}\s*0*(?:1?\d?\d|2[0-4]\d|25[0-5])\s*\)/gi
3. 密码规则
需要匹配正确的密码格式:
- 长度至少为8
- 至少有一个大写字母
- 至少有一个小写字母
- 至少有一个数字
- 可以为大小写字母,数字,下划线,!,@
思路:
上面这些规则里,最难的是至少
。我们怎么表示至少是这个例子的关键。
- 至少8个单字和特殊字符
/^
[\w!@]{8,}
$/
,需要注意的是这里我们要匹配整个字符串,所以我们加了^
和$
表示整串匹配。从开头到结尾的每个字符都必须满足条件。
- 至少有一个数字
其实上一条以及匹配了所有的可行字符,只不过我们现在需要从中加几个条件。而加条件可以理解为断言,也就是我们可以将其转换为边界问题。
首先,字符串中必须要有一个数字。怎么表示呢?.*\d.*
!断言就那几个式子,我们来看看向前查找
的结果:/(?=.*\d.*)/g
a b 1
^ ^ ^
==>
[]
如果它前面没有加要匹配的字符,那么它所指向的是字符间的空隙!
这时候如果我们在后面跟一个字符b
:/(?=.*\d.*)b/g
————
a | b | 1
^ | ^ | ^
————
==>
["b"]
这里,匹配的空隙加上"b",发现有一个符合的,所以会成功匹配一个字符"b"。
结合我们的题目,我们只需要一个表示这个字符串里有数字的标识就可以了,并且它需要出现在最开始的位置,因为我们想要匹配的是整个字符串。所以我们还需要加上^
:/^(?=.*\d.*)b/g
我们后面跟的也不是"b",而是至少8个单字加特殊字符:/^(?=.*\d.*)[\w!@]{8,}$/g
这里的^(?=.*\d.*)
就相当于是在字符串最开始的地方加了一个标识(断言),标识串里有没有数字。如果有,才能往后匹配。如果没有这个标识,就一个都匹配不了。
- 剩余
接下来是至少一个大写和小写,和上面一样,也是加断言的问题。
最后的结果是:
/
^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])
[\w!@]{8,}$
/g
替换类
做这类转换的时候,我们得想清楚我们需要把原字符的哪些部分做转换。也就是我们需要匹配哪些子串。
1. 数字千分位格式化
1,000 / 1,253,512 / 124,125,125.234
思路:
我们先来看看整数。比如12345678
,我们需要咋切呢,12,345,678
。
这里我们很快发现一个规律:(1-3个数字), (3个数字),(3个数字) ... (边界/非数字)
然后将我们匹配到的每个组后面加个分隔符,
。是8是就👌了!
- 数字分组
问题来了,我们切123,123,12
这样好切,/(\d{3})+/g
就好了。但是反过来咋办呢?
说到反,我们应该会联想到正向查找x(?=y)
,就是说在后面是y
的情况下匹配x
。
怎么理解呢,就是说我们可以决定只有在后面全是3个一组的数字的时候,再匹配前面剩余的字符。
比如/\d*
(?=
(\d{3})+
)
/g
,这里用到正向查找,表示我们匹配的是后面跟着若干个3个一组数字
的一连串数字
。
"1234567890"
==>
["1234567"]
这里匹配的是1234567
,因为\d*
是贪婪的,它会匹配尽可能多的字符。
但其实我们不想让它匹配这么多,因为最前面的那组只能是1到3个数字。
1,234 / 12,345 / 123,456 / 1,234,567
对吧,所以我们给第一组加个限制:
/\d{1,3}
(?=
(\d{3})+
)
/g
1234567890
==>
["123","456","7"]
这次匹配的结果呢,又和我们想的不一样了,咋肥事啊?
这就要提到之前我们学习test
方法举的一个例子了。
var regex = /foo/g;
// regex.lastIndex is at 0
regex.test('foo'); // true
// regex.lastIndex is now at 3
regex.test('foo'); // false
当有g
标志的时候,每一次匹配,正则的lastIndex
都会更新,表示它前面的子串都被匹配过了。接下来的匹配要以lastIndex
为起点。
我们来模拟一下/\d{1,3}(?=(\d{3})+)/g
匹配过程:
1234567890
// 第一次
123 (456) (789) 0
\d{1,3} (\d{3})+
// 第二次
456 (789) 0
\d{1,3} (\d{3})+
// 第三次
7 890
\d{1,3} (\d{3})+
所以结果是匹配到了123 456 7
。那么了解到这个情况后,我们就知道了,(\d{3})+
匹配完后,后面有可能还跟着尾巴。这不行,我们需要匹配完后后面就没数啦!所以我们还需要加上\b
,表示匹配的串需要处于边界的位置。
那么就得到:
/\d{1,3}
(?=
(\d{3})+
\b
)
/g
这下就OK啦!
1234567890
==>
["1","234","456"]
到目前为止我们完成了整数的匹配。那么如果加上小数,会出现什么情况呢?
66666.12345
==>
["66","12"]
它不仅匹配的整数部分,还顺带匹配了小数部分。当然这是我们不想要的,我们不想匹配小数点后的数字。
听听这句话,小数点后的不匹配。也就是说匹配项的前面不能跟小数点。前面不能跟
,那不就是往后否定查找
((?<!y)x
)吗?
我们可以写出(?<!\.\d*)
,表示前面不能是.
加数字。就是说你不能在.
后面进行匹配查找。那我们组合在一起:
/
(?<!\.\d*)
(\d{1,3}(?=(\d{3})+\b))
/g
1234567890.000000
==>
["1","234","456"]
完美!最后一步大家都懂,用replace
方法将每个匹配项加个后缀,
。
兼容性
并不是每个浏览器的每个版本都支持所有的这些特性。所以我们在使用的时候还需要考虑到兼容性的问题。
- IE,Safari不支持向后查找
(?<y)x
/(?<!y)x
。 - IE,Firefox for Android不支持具名捕获组
(?<name>x)
。