Regex
引言
正则表达式(Regular Expression)是使用单个字符串来描述、匹配一系列符合某个句法规则的一串字符。我在之前的学习工作中,遇到正则表达式,往往都是直接搜索相关问题的结果,或者尝试用其他方法解决。也尝试过学习相关的规则,但是过一段时间不用之后,往往会生疏乃至忘记。这篇文章尝试着理解并消化正则表达式相关的知识。
字符与位置
正则表达式是匹配模式,不是匹配字符,就是匹配位置
匹配字符
特殊字符
正则表达式除了匹配常规的 a-z,A-Z,0-9 等字符外,还存在着一些特殊字符,如下所示
字符 | 记忆方法 |
---|---|
\t | tab |
\v | vertical tab |
\f | form feed |
\r | return |
\n | new line |
字符组
虽然叫字符组,但是只匹配字符组中的某个字符,比如[abc]
只匹配 a,b,c 中的一个。对应的如果不想匹配某几个字符,则需要在字符组最前面加上**脱字符(^),形如[^abc],则表示匹配除 a,b,c 之外的其他字符。使用连字符(-)**则可以匹配连续的一段字符,形如 a-z,0-9。另外有如下相关的简写形式来表示对应的字符区间。
正则表达式 | 匹配区间 | 记忆方法 |
---|---|---|
\d | [0-9] | digit |
\D | [^0-9] | opposite to \d |
\w | [0-9a-zA-Z_] | word**(注意有下划线_)** |
\W | [^0-9a-za-z_] | opposite to \w |
\s | [\t\v\n\r\f] | space character |
\S | [^\t\v\n\r\f] | opposite to \s |
. | [^\n\r\u2028\u2029] | 匹配除换行符之外的所有字符 |
循环与重复
在很多情况下,我们需要匹配零到多个重复的字符,在这个情况下掌握**{}**以及对应的简写方式就显得尤为重要。以下是完整的语法:
1 | {m} 匹配m次 |
贪婪匹配与惰性匹配
正常情况下的字符串匹配都是贪婪匹配的,也就是在当前条件下尽可能多的匹配字符。但是有些情况下贪婪匹配不是一件好事,需要我们使用惰性匹配来规避掉这个问题,处理方法就是在循环语法后加上**?**,这样就可以尽可能少的匹配对应的字符了。所有惰性匹配情况如下:
1 | {m,n}? |
多选分支
使用管道符(|)可以实现或逻辑,具体形式如(p1|p2|p3)。例如/good|bad/可以匹配 good 或者 bad 中的一个字符。
匹配位置
匹配位置这里我认为是正则里的一个弯,绕过了这个弯,才可以看到更多。正则里的位置代表的是字符串中每个相邻字符中间的位置,我们可以想象成为一个虚拟位置,这个位置中的字符串为空字符串,这样就可以更好的理解位置这个概念。在 ES5 与 ES6 中,一共有 8 个锚字符,分别是:
1 | ^ 匹配开头,在多行模式下匹配行开头 |
下面分别介绍这几个锚字符的具体含义
^与$
这两个比较好理解,分别匹配的是开头与结尾
\b 与\B
b 是边界 boundary 单词的首字母缩写。\b 匹配包括的位置是\w 与\W 之间的位置,\w 与^之间的位置,\w 与$之间的位置,例如:
1 | let str = '[lover] taylor swift album_1'; |
老规矩,\B 是与\b 相反的,具体匹配的位置是\W 与\W 之间的位置,\w 与\w 之间的位置,\W 与^之间的位置,\W 与$之间的位置,例如:
1 | let str = '[lover] taylor swift album_1'; |
(?=p)与(?!p)
先行断言与先行否定断言,注意需要搭配括号使用。这里的 p 代表的是一个匹配规则,匹配的是符合该规则的字符前面的位置,比如(?=b)则代表字符串 b 前面的位置,例如:
1 | let str = 'boy'; |
?!p 则是?=p 的反例,例如:
1 | let str = 'loop'; |
(?<=p)与(?<!p)
后行断言与后行否定断言。有了先行断言相关的知识,那么后行断言就比较好理解了。后行断言匹配的是符合规则 p 的字符后面的位置。
需要注意的是浏览器的兼容性问题,后行断言在 ES5 环境下是不支持的,但是可以用:
1 | var str = 'apple people'; |
类似于这种方式进行模拟,但是带来的结果是求得结果的过程显得冗长和啰嗦,在 ES6 中则直接支持该语法。
实际应用
现在有如下需求,将诸如 1234567 的一串数字按照每三位进行隔开,中间加上逗号,变成如 1,234,567 这种形式。
比较常见且简单的做法是用Number('1234567').toLocaleString('en-US')
这种方法直接可以得到结果,那么用正则如何做到呢?
首先我们尝试给最后三位字符前添加一个逗号,对应的正则比较容易写出来:
1 | let str = '1234567'; |
之后考虑每对应三个数字之前的逗号,则\d{3}
对应的加上一个加号就可以匹配到
1 | let str = '1234567'; |
但是这样会出现一个问题,在三的倍数的数量的数字字符下,会在字符串的开头多加一个逗号,所以需要把对应的这个开头的逗号去掉,即不匹配开头的位置(?!^)
1 | let str = '123456789'; |
这样就可以写出符合需求的正则了。
JavaScript 中正则表达式的相关 API
RegExp.prototype.test()
返回一个布尔值,表示当前的正则是否能匹配参数中的字符串
这个方法比较简单,需要注意的是,当正则表达式使用了 g 修饰符时,表示全局搜索,会匹配多个结果。这时每次使用 test 方法时都会从上次结束的位置开始向后进行匹配
1 | let regex = /x/g; |
RegExp.portotype.exec()
返回对应参数字符串匹配正则之后的结果,返回值类型为 Array 或者 null
注意:如果正则表达式中包含圆括号(组匹配),那么返回的数组会包含多个成员,例如:
1 | let regex = /\b(\d+)\b/; |
返回的结果中,第一个值代表匹配的整体结果,第二个是组匹配的结果,如果正则中有多个组匹配,则在数组之后追加之后的组进行组匹配后的结果。此外,返回的数字还包括两个属性 index 与 input,其中 index 代表整体匹配成功时的开始位置,input 则代表输入的原始字符串。
另外,和 test 方法一样,exex 方法在全局模式下(加上 g 修饰符),会匹配多个结果,每次调用 exec 方法也会从上次结束的位置开始向后进行匹配。还是上面的例子:
1 | let regex = /\b(\d+)\b/g; |
由上述代码可以看出,在使用 exec 时,经常需要配合使用 while 循环
String.prototype.match()
返回一个数组,数组包含所有匹配的子字符串,没有匹配结果时返回 null
1 | let regex = /x/g; |
String.prototype.search()
返回第一个满足正则的匹配结果在整个字符串中的位置,如果不能匹配则返回-1
1 | let regex = /y/g; |
String.prototype.replace()
替换匹配正则的值。接受两个参数,第一个为相应的正则,第二个为需要替换的内容
1 | let regex = /\d+/g; |
replace 方法的第二个参数可以使用美元符号$,用来指代所替换的内容,例如:
1 | console.log('xbrave blog'.replace(/(\w+)\s(\w+)/, '$2 $1')); //result: 'blog xbrave' |
replace 方法的第二个参数也可以是一个函数,返回的值会替换为对应的匹配内容,例如:
1 | 'x and y'.replace(/x|y/g, function (str) { |
作为 replace 方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。
String.prototype.split()
按照对应的正则分割目标字符串,返回一个分割结果组成的数组
1 | 'x189y'.split(/\d+/); //result: ['x','y'] |
总结
正则表达式算是一种学习曲线比较陡峭,但是理解贯通之后收益较大的知识。在实际运用中,诸如 ejs 之类的涉及到字符串处理的方面用处比较多,也可以很方便的抽象出对应的字符串模板。因此对于一些复杂字符串的分割与匹配,我们可以使用正则表达式这一利器帮助我们实现相应的功能。但是需要注意的是,由于正则表达式的性能问题与多解性,在比较复杂的字符串匹配的情况下,写好一个准确性与效率达到平衡的正则表达式不是一件简单的事情,需要我们多思考,多实践,这样才能逐渐掌握这一知识。