正则表达式学习

正则表达式学习

正则匹配作为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] 匹配一个空格键

小知识:

  1. Carriage return 字符表示的是返回当前行的开始,没有向下换行(有点像HOME建)。简称CR。转义字符为\r
  2. Line feed 表示换行符,向下换一行(ENTER建)。也叫newline,简称为LF或者NL。转义字符为\n。CRLF转义字符为\r\n
  3. 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标志,会附加groupsinputindex附加属性。
如果加了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表示匹配的字符相当于$&,而p1p2…则是捕获组,紧接着是当前匹配串的位置offset,以及源字符串string

5. split

split不仅可以用子串作为分隔符,也可以用正则作为分隔符。

练习实操

匹配类

1. 匹配颜色值-十六进制

#fff / #123 / #fafafa / #DDD / #F1F1F1

思路:第一个为#,后面接6位16进制数,或者3位缩写,大小写不敏感。

  1. 匹配#
/#/
  1. 匹配16进制数
/[a-f\d]/
  1. 匹配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),是为了优化性能。
  1. 最后
/#(?:[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。

  1. 匹配rgb()
/rgb\(\)/
  1. 值的限制。
/\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/
  1. 匹配r,g,b

注意到前面两个都是一个模式:x,数字加逗号。最后一个只有数字。我们把第二步的模式设为x

/(?:x,){2}x/
  1. 最后
/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
  • 至少有一个大写字母
  • 至少有一个小写字母
  • 至少有一个数字
  • 可以为大小写字母,数字,下划线,!,@

思路:
上面这些规则里,最难的是至少。我们怎么表示至少是这个例子的关键。

  1. 至少8个单字和特殊字符

/^ [\w!@]{8,} $/,需要注意的是这里我们要匹配整个字符串,所以我们加了^$表示整串匹配。从开头到结尾的每个字符都必须满足条件。

  1. 至少有一个数字

其实上一条以及匹配了所有的可行字符,只不过我们现在需要从中加几个条件。而加条件可以理解为断言,也就是我们可以将其转换为边界问题。

首先,字符串中必须要有一个数字。怎么表示呢?.*\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.*)就相当于是在字符串最开始的地方加了一个标识(断言),标识串里有没有数字。如果有,才能往后匹配。如果没有这个标识,就一个都匹配不了。

  1. 剩余

接下来是至少一个大写和小写,和上面一样,也是加断言的问题。

最后的结果是:

/ ^(?=.*\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是就👌了!

  1. 数字分组

问题来了,我们切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)

参考

上一篇 Flutter实战经验
下一篇 Performance API