关于正则表达式,大家都要这样的经历:看完之后觉得懂了,但是过一段时间就忘记,自己写的时候更是到处查资料。鉴于这种情况,总结了自己关于正则表达式的学习心得,以供大家参考。本文将从正则表达式的基础概念讲起,紧接着总结跟正则表达式相关的一些常用方法,以及正则表达式的匹配方式。
1、基础概念
正则表达式(Regular Expression)是一种用来描述规则的表达式,其目的是为了根据特定的规则对字符串匹配,从而实现替换和搜索的功能。正则表达式的匹配其实不外乎两种:匹配字符(匹配内容)和匹配位置(断言匹配)。如何构建准确的正则表达式呢?
有2种方式可以创建一个正则表达式RegExp对象:字面量、构造函数。其中,pattern正则表达式的文本,flag 匹配标志,包含 g 、i、m 等
- 字面量:/pattern/flags
const regx = /abc/gi
- 构造函数: new RegExp(pattern[, flags])
const regx = new RegExp('adc', 'gi')
构造一个正则表达式,首先要有能代表匹配内容的特定字符,也就是元字符;其次,还要确定元字符的数目,这就需要限定符,也叫量词。如果需要获取匹配字符串中的局部内容时,还需要引入原子组(括号)的概念,针对复杂的正则表达式,还需要多选分支。
1.1、元字符
元字符也有叫字符组,元字符是构造正则表达式的一种基本元素,代表一种字符的可能。
元字符 | 说明 |
---|---|
. | 通配符,匹配除了少数字符(\n)之外的任意字符 |
\d | (digital),匹配数字,等价于 [0-9] |
\D | 匹配非数字,等价于 [^0-9] |
\w | (word)匹配字母或数字或下划线,等价于[a-zA-Z0-9_] |
\W | 匹配非单词字符,等价于 [^a-zA-Z0-9_] |
\s | (space)匹配空白符,等价于 [ \t\v\n\r\f] |
\S | 匹配非空白符,等价于 [^ \t\v\n\r\f]] |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
[abc] | 匹配 “a”、“b”、“c” 其中任何一个字符。 |
[^abc] | 匹配除了 “a”、“b”、“c” 之外的任何一个字符 |
1.2、限定符
有了元字符就能很方便的匹配对应的字符,而限定符(也叫量词)的作用就是确定元字符的数量,也即重复的次数。常用的元字符如下:
限定符 | 说明 |
---|---|
* | 重复零次或更多次, 等价于 {0,} |
+ | 重复一次或更多次,等价于 {1,n} |
? | 重复零次或一次,等价于 {0,1} |
{m} | 重复m次 |
{m,} | 至少重复出现m次 |
{m,n} | 连续出现 m 到 n 次 |
1.3、修饰符
修饰符对正则表达式匹配过程中进一步设置。
符号 | 说明 |
---|---|
g | 全局匹配,找到所有满足匹配的子串 |
i | 匹配过程中,忽略英文字母大小写 |
m | 多行匹配,把 ^ 和 $ 变成行开头和行结尾 |
1.4、原子组
原子组简单来讲就是通过括号提供分组,每一个括号都是一个子表达式。如下例所示,存在2个原子组(\d{1-2})、(\d{3-4}),他们分别代表匹配 1-2 数字、匹配 3-4 数组,由于正则表达式默认采用贪婪匹配(下文会有介绍),也就是尽可能多的匹配内容。因此最终的匹配结果是 617628,第一个原子组匹配的内容是 61, 第二个原子匹配的内容是 7628。实际操作过程中,原子组通常与字符串的 match 、正则的 exec 配合使用,这2个方法下文也会详细介绍。
// 原子组
const str = '61762828 176 2991 87132425'
const regx = /(\d{1,2})(\d{3,4})/
const result = str.match(regx)
// 0: "617628" value
// 1: "61" $1
// 2: "7628" $2
// groups: undefined
// index: 0
// input: "61762828 176 2991 87132425"
如果存在多个原子组,且有嵌套的情况下,如何确定原子组的排序啦? 采用的是深度优先的匹配方法,通俗的讲就是从左到右数左括号,第一个括号记 1 ,第二个记 2 ,依次类推。为什么计数从 1 开始,因为在 match 等方法的匹配中,第 0 个代表的是正则正则匹配的结果。
改造上述实例,在整个正则表达式上添加括号,从左到右数括号,原子组分别为 ((\d{1,2})(\d{3,4}))、(\d{1,2})、(\d{3,4})
// 原子组
const str = '61762828 176 2991 87132425'
const regx = /((\d{1,2})(\d{3,4}))/
const result = str.match(regx)
// 0: "617628" value
// 1: "617628" $1
// 2: "61" $2
// 3: "7628" $3
// groups: undefined
// index: 0
// input: "61762828 176 2991 87132425"
1.4.1、反向引用
反向引用就是对正则表达式里已有的分组引用。
例如,通过正则表达式匹配 html 标签,如下例所示,当前正则表达式对于 str2 的检测也是通过的,显然不能满足我们的要求。
const regx = /<\w*>.*<\/\w*>/
const str1 = '<div>哈哈</div>'
const str2 = '<div>哈哈</span>'
const result1 = regx.test(str1) // true
const result2 = regx.test(str2) // true
针对上述问题,原子组的反向引用就派上用场了。如下所示, \1 标识与第一个原子组 (\w*) 匹配的内容相同, 此时str2 就不能通过正则的检验。
const regx = /<(\w*)>.*<\/\1>/
const str1 = '<div>哈哈</div>'
const str2 = '<div>哈哈</span>'
const result1 = regx.test(str1) // true
const result2 = regx.test(str2) // false
1.4.2、原子组别名
原子组别名是对原子组重新命名,默认情况下原子组编号是从 1 递增。当存在多个原子的时候,原子组命名有极大的应用场景。原子组别名的语法如下:
原子命名:?<key>
原子组引用: \k<key>
其中 key 值为原子组的别名,前后保持一致。
例如将第一个分组命名为 tagName,在后续就可以通过 \k 对原子组进行反向引用。对原子组命名后,执行 match 方法在返回结果的 groups 字段中也有体现。
const regx = /<(?<tagName>\w*)>.*<\/(\k<tagName>)>/
const str1 = '<div>哈哈</div>'
const result1 = str1.match(regx)
// 0: "<div>哈哈</div>"
// 1: "div"
// 2: "div"
// groups: {tagName: 'div'}
// index: 0
// input: "<div>哈哈</div>"
// length: 3
1.4.3、非捕获组
上面使用的括号都会匹配他们匹配到的数据,以便后续引用,所以也可以称为捕获型分组和捕获型分支。如果想要括号最原始的功能,但不会引用它,也就是既不会出现在API引用里,也不会出现在正则引用里,可以使用非捕获组。
通俗讲:非捕获分组,只会匹配内容,但不是出现在反向引用中(不会出现在原子组编号中,即原子组编号时跳过当前括号),或者对应的一些 API (match、replace 等)的结果中。
(?:pattern)
如下例所示,有 2 个括号,但是第 2 个括号被设置为非捕获组,因此,在最终的结果中没有索引为 2 内容。
const regx = /<(\w*)>(?:.*)<\/\1>/
const str1 = '<div>哈哈</div>'
const result1 = str1.match(regx)
// 0: "<div>哈哈</div>"
// 1: "div"
// groups: undefined
// index: 0
// input: "<div>哈哈</div>"
// length: 2
1.5、多选分支
多选分支意味着正则表达式有多个子模式可以匹配,用 |(管道符)分隔,表示其中任何之一。
const regex = /good|nice/g; // 存在字符串 “good” 或者 “nice”
const string = "good idea, nice try.";
console.log( string.match(regex) );
// => ["good", "nice"]
下一个例子说明,正则表达式的分支匹配是惰性的,也即当前的子表达式匹配成功后,后边的就不再尝试。有点类似 js 的短路运算符。
const regex = /good|goodbye/g;
const string = "goodbye";
console.log( string.match(regex) );
// => ["good"]
const regex = /goodbye|good/g;
const string = "goodbye";
console.log( string.match(regex) );
// => ["goodbye"]
2、常用方法
对正则表达式的基础知识有一定认知之后,我们有必要了解与正则表达式相关的一些常用方法,其中 repalce、match是定义在 String 对象的原型上,exec、test 是定义在 RegExp 的对象原型上。
2.1、 String.prototype.replace
replace() 方法返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要调用的回调函数。
使用方式 :str.replace(regexp|substr, newSubStr|function)
上面这句话来自 MDN 中对 replace 方法的说明。通俗来说,字符串的replace方法接受2个参数,第一个参数是需要被替换的内容,可以是字符也可以是正则表达式;第二个参数即将被替换的内容,可以是字符串,也可以是函数返回值(返回字符串)。
主要针对第二个参数分 2 中情况讨论;
1. 第二个参数是字符串。
const str = 'abcddcba';
// 第一个参数、第二个参数都是字符串
const newStr1 = str.replace('a', 'A') // Abcddcba
// 第一个参数是正则,第二个参数都是字符串
const newStr2 = str.replace(/a/, 'A') // Abcddcba
// 第一个参数是正则,全局多次匹配,相当于 String.prototype.replaceAll
const newStr2 = str.replace(/a/g, 'A') // 'AbcddcbA'
2. 第二个参数是函数,此时主要针对第一个参数是正则表达式进行处理的。
const str = 'abc12345#$*%11111111111';
// p0为正则表达式匹配的内容,p1 表示第一个原子组匹配的内容,p2 为第二个原子匹配的内容
function replacer(p0, p1, p2, p3, offset, string) {
return [p1, p2, p3].join(' - ');
}
const newStr = str.replace(/([^\d]*)(\d*)([^\w]*)/, replacer); // 'abc - 12345 - #$*%11111111111'
此时需要重点关注的是 replacer 的入参,
1、第一个入参 p0, 代表正则表达式匹配的字符串,此处为 “abc12345#$*%”
2、p1, p2, p3 ... pn, 代表的是正则表达式中各个原子组匹配的值,当前实例中有 3 个原子组,因此代表原子组的入参也有 3 个,分别是 p1、p2、p3, 他们匹配的值分别为 “abc”、“12345”、“#$*%”
3、offset : 匹配子字符串在整个字符串表达式中位置;
4、string : 被匹配的原字符串,及当前实例中的 str
函数返回值,就是对应替换正则表达式匹配的内容,当前正则表达式匹配的是 “abc12345#KaTeX parse error: Undefined control sequence: \* at position 1: \̲*̲%",替换后变成 "abc -…*%”,因此最终返回 “abc - 12345 - #$*%11111111111”。
2.2、 String.prototype.match
match 方法检索返回一个字符串匹配正则表达式的结果。
使用方式 :str.match(regexp)
根据正则表达式是否使用了 g 标志,match 方法返回的结果有所不同,通过实例分别来看下。
1. 使用了 g 标志,则返回与正则表达式匹配的所有结果,不会返回每一个匹配的具体详情(例如原子组信息)
const str = 'aabcdaaaadcbaa';
const result = str.match(/(a+)/g) // ['aa', 'aaaa', 'aa']
2. 没有使用了 g 标志,则仅返回第一个完整匹配及其相关的捕获组信息。 其返回的数据是一个 Array,第一元素是完全匹配的结果(索引 0 ),第二个元素是第一个原子组匹配的结果(索引 1 ),以此类推;同时返回结果还有额外属性。
- groups : 命名的原子组统计;
- index :匹配的结果的开始位置
- input : 搜索的字符串
const str = 'aabcdaaaadcbaa';
const result = str.match(/(a+)/)
// 0: "aa"
// 1: "aa"
// groups: undefined
// index: 0
// input: "aabcdaaaadcbaa"
// length: 2 // array 的长度
如果你想要获得捕获组,并且设置了全局标志,有 2 种实现方式:
- 通过 RegExp.prototype.exec 方法,并配合 while 循环;
- 将 match 方法替换为 matchAll 方法。但是需要注意 matchAll 方法存在浏览器兼容性问题,具体可以查看:matchAll 兼容性
2.3、 RegExp.prototype.test
test 方法相对其他方法来说比较简单,来查看正则表达式与指定的字符串是否匹配,结果返回 boolean 值。
使用方式 :regexObj.test(str)
const str = 'hello world!';
const result = /^hello/.test(str); // true
当正则表达式设置了全局标志 g ,test() 的执行会改变正则表达式 lastIndex 属性。连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串。
lastIndex 是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始索引。
如果 lastIndex 大于字符串的长度,则 regexp.test 和 regexp.exec 将会匹配失败,然后 lastIndex 被设置为 0。
如果 lastIndex 等于或小于字符串的长度,则该正则表达式匹配从 lastIndex 位置开始的字符串。
– 如果 regexp.test 和 regexp.exec 匹配成功,lastIndex 会被设置为紧随最近一次成功匹配的下一个位置
– 如果 regexp.test 和 regexp.exec 匹配失败,lastIndex 会被设置为 0
const str = 'hello world hello world';
const regx = /hello/g
regx.test(str); // true
console.log(regx.lastIndex) // 5
regx.test(str); // true
console.log(regx.lastIndex) // 17
regx.test(str); // false
console.log(regx.lastIndex) // 0 匹配失败重新设置为0
2.4、 RegExp.prototype.exec
在上文中有提到过,如果设置了全局的标记 g,又想要匹配的捕获组详情,则可以通过 exec 方法配合 while 循环来实现。
exec() 方法在一个指定字符串中执行一个搜索匹配,返回一个结果数组或 null,其返回的格式总是包含捕获组的详情,即包含匹配结果、原子组匹配结果、groups 属性等,与 match 没有设置全局标记g 时返回的格式完成一致。
1. 没有设置全局标记 g ,则此时 exec 方法返回第一个匹配的内容, 且不改变 lastIndex 的值。
const str = 'aabcdaaaadcbaa';
const regx = /(a+)/
const result = regx.exec(str)
console.log(regx.lastIndex) // 0
// 0: "aa"
// 1: "aa"
// groups: undefined
// index: 0
// input: "aabcdaaaadcbaa"
// length: 2 // array 的长度
2. 设置全局标记 g ,则此时 exec 方法返回当前匹配的内容, 且将上次成功匹配后的位置记录在 lastIndex 属性中,这一点与 test 方法一致。
const str = 'aabcdaaaadcbaa';
const regx = /(a+)/g
const result = regx.exec(str)
console.log(regx.lastIndex) // 2
// 0: "aaaa"
// 1: "aaaa"
// groups: undefined
// index: 5
// input: "aabcdaaaadcbaa"
// length: 2 // array 的长度
const result = regx.exec(str)
console.log(regx.lastIndex) // 9
了解了 exec 的特性之后,就可以通过配合 while 循环实现全局匹配,并能得到每一次匹配成功后的捕获组详情。
const str = 'aabcdaaaadcbaa';
const regx = /(a+)/g
let result = null
while((result = regx.exec(str)) !== null ){
console.log(regx.lastIndex, result)
}
// 2 ['aa', 'aa', index: 0, input: 'aabcdaaaadcbaa', groups: undefined]
// 9 ['aaaa', 'aaaa', index: 5, input: 'aabcdaaaadcbaa', groups: undefined]
// 4 ['aa', 'aa', index: 12, input: 'aabcdaaaadcbaa', groups: undefined]
3、匹配方式
3.1、贪婪匹配
当正则表达式中包含能接受重复的限定符时,在使整个表达式能得到匹配的前提下,通常的行为是匹配尽可能多的字符,这种匹配方式叫做贪婪匹配。
const str = '61762828 176 2991 87132425'
const regx = /(\d{1,2})(\d{3,4})/g
const result = str.match(regx)
// "617628" 是前面的\d{1,2}匹配出了61,后面的匹配出了7628
// "2991" 是前面的\d{1,2}匹配出了2 ,后面的匹配出了991(满足匹配优先,再最大程度的贪婪)
// "871324"是前面的\d{1,2}匹配出了87,后面的匹配出了1324
贪婪匹配满足1个前提,2个原则:
前提: 满足匹配优先
原则: 1、最大程度贪婪;2、“先下手为强”。
针对第一点,在例子上 “2991”能够匹配就是先满足的优先匹配,然后再贪婪的。上述正则表达式可以理解为匹配数字 4-6 位,也就是说满足 4-6 的数字能被匹配,这是前提。然后再满足各个原子组尽可能的贪婪,如果第一个原子组贪婪得到 2 位,则后一个原子组无法匹配成功,这就无法满足匹配优先,因此第一个原子组的匹配 1 位,后一个原子组匹配 3 位。
针对第二点,来看下例。首先字符串 “12345” 能够匹配正则表达式,满足优先匹配,根据第二点,“先下手为强”,此时第一个原子组匹配的结果就是 3 位,后一个就是 2 位。
var string = "12345";
var regex = /(\d{1,3})(\d{1,3})/;
console.log( string.match(regex) );
// => ["12345", "123", "45", index: 0, input: "12345"]
3.2、惰性匹配
当正则表达式中包含能接受重复的限定符时,在使整个表达式能得到匹配的前提下,匹配尽可能少的字符,这种匹配方式叫做懒惰匹配。懒惰量词是在贪婪量词后面加个 ? 。
贪婪匹配满足1个前提,1个原则:
前提: 满足匹配优先
原则:尽可能的少
在下面的例子,前一个原子组是惰性匹配,后一个原子组是贪婪匹配。最终的匹配结果是 “1234”, 此时满足匹配优先的前提,同时第一个原子尽可能的少匹配,后一个原子组是尽可能多匹配。
const string = "12345";
const regex = /(\d{1,3}?)(\d{1,3})/;
// => ["1234", "1", "234", index: 0, input: "12345"]
在下面的例子,前一个原子是惰性匹配,后一个原子组是贪婪匹配,但因为正则表达式包含匹配开始位置和结束位置,尽管前一个是贪婪匹配,如果继续匹配 1 位,那就无法满足整个表达式匹配成功。但是为了满足优先匹配的前提,因此第一个原子组匹配的结果是 “12”,第二个原子组的匹配结果是 “345”
const string = "12345";
const regex = /^(\d{1,3}?)(\d{1,3})$/;
// => ['12345', '12', '345', index: 0, input: '12345', groups: undefined]
//知道你不贪、很知足,但是为了整体匹配成,没办法,也只能给你多塞点了。因此最后 \d{1,3}? 匹配的字
//符是 "12",是两个数字,而不是一个。
3.3、小结
不管是贪婪匹配还是惰性匹配,其前提都是优先满足匹配成功。很多时候为了满足优先匹配的前提,贪婪匹配也不一定能够匹配到最多的字符,同样的,为了满足优先匹配的前提,惰性匹配也不一定匹配的是最少的字符。
为什么会有上述这些现象,就需要了解一定的匹配原理 - 正则表达式回溯法,这个会在下一篇文章中详细介绍;
4、总结
通过这些文章的学习,你应该可以了解到正则表达式的一些基本知识,并通过配合正则表达式常用的方法,能够解决一些日常开发中的问题。但对于正则表达式的断言匹配,正则表达式的匹配原理 - 回溯法,以及如何优化正则表达式,这些内容将在下一个篇章中详细介绍,敬请关注。
参考文献:
- https://github.com/qdlaoyao/js-regex-mini-book/blob/master/JavaScript正则表达式迷你书(1.1版).pdf
- https://juejin.cn/post/6844903677119954958#heading-1
- https://juejin.cn/post/6844903680349585422#heading-0
- https://juejin.cn/post/7021672733213720613#heading-23
- https://www.bilibili.com/video/BV12J41147fC
- https://blog.csdn.net/ybdesire/article/details/78255427
- https://www.jianshu.com/p/fb3afbf8da10