JavaScript正则表达式
正则简介
参考资料
正则表达式(regular expression),字面意思是描述规则的表达式,这里的规则指搜索字符串(即在字符串中搜索子串)的规则,你可以用正则描述你搜索字符串子串时候的规则,通过正则搜索时候会按照描述的规则进行匹配。
为什么我们需要搜索字符串子串(即正则匹配)呢?
- 判断一个字符串中是否有数字【验证】
- 将字符串以数字为分隔符分成多段【切分】
- 把“2021-12-16”中的年月日提取出来【提取】
- 把字符串中的数字都替换成16进制【替换】
我们在日常业务开发中经常会处理字符串,而在处理字符串时候可能需要按照某种规则搜索字符串子串。\
正则语法
通常我们使用自然语言描述我们查找字符串子串的需求时候会如何表达呢?
- abab【精确匹配】
- 3个数字、4个字母【元字符】
- 连续2个ab【量词】
- 以a开头、以b结尾【位置】
- 1个1~5的数字【字符组】
- 大小写无关【修饰符】
- 尽量多匹配/尽量少匹配【贪婪/惰性】
- 3个数字重复2次【分组反向引用】
- 3个数字或2个字母【分支】
可以看到我们在描述搜索的子串的规则时候,会有不同的需求(精确匹配、模糊匹配、重复次数、尽量多/尽量少匹配)等等,这些需求正则语法都支持通过特定的语法表达。
匹配位置和匹配字符
正则表达式不是匹配字符就是匹配位置
因此当我们根据需求写正则时候,先将需求转化为匹配字符/匹配正则的描述,然后根据描述写正则。
精确匹配
正则表达式中可以使用具体字符来构造要匹配的子串
/abc/
修饰符
i(ignore)
不区分大小写
g(global)全局匹配
默认正则从左到右匹配,匹配到第一个返回
增加g修饰后,会将所有匹配的都记录下来,直到字符串结束
m(multiline)多行匹配
^和$匹配行首和行位,如果不加m修饰,整个作为一行,如果加了m修饰,换行符\n分割为多行。通常使用m修饰都会配合g一起使用。
'1\n'.match(/1$/)
'1\n'.match(/1$/m)
'1\n2'.match(/^2/)
'1\n2'.match(/^2/m)
元字符、元字符转义
元字符是拥有特殊含义的字符,每个元字符代表一类字符(数字、字母)或者位置(开头、结尾、单词边界)
字符组
当需要表达一个在某个集合内的字符时候,例如“1个1~5的数字”,语法是中括号[1,2,3,4,5],也可以用范围[1-5],范围表示ASCii码对应的码点范围,如果范围不对(下限在ASCii中小于上限)则会报错。
量词
元字符指拥有特殊含义的字符,量词指用来修饰字符、元字符和分组的标志符。
元字符和量词都是正则的重要组成部分,这是从语法结构来进行分类的。从使用场景的角度,更好地划分方式是分为匹配位置和匹配字符。
匹配位置
位置指字符之间的部分,包括开头和结尾。字符之间只有一个位置,位置和位置不会相邻。
- 开头^
- 结尾$
- 单词边界 \b
- 非单词边界 \B
- (?=p)后面匹配到p的位置
- (?!p)后面没有匹配到p的位置
- (?<=p)前面匹配到p的位置
- (?<!p)前面未匹配到p的位置
连续的位置匹配子表达式,在正则匹配时候是“与”的关系。
'abc'.match(/(?=b)(?<=a)/)
'abc'.match(/(?<=a)(?=b)/)
注意这个括号不是分组
'abc'.match(/(?=b)(?<=a)(b)/) // 可以看到位置没有作为匹配子表达式
贪婪、惰性
贪婪匹配指尽可能多地匹配,默认是贪婪匹配
'aaaa'.match(/a+/g)
'aaaa'.match(/(a+)(a+)/g) // 第一个括号先尽可能多匹配,然后后面的再匹配
'aaaa'.match(/(a+)+/g) // 括号内量词先贪婪匹配,然后外面量词再匹配
惰性匹配尽可能少地匹配,在量词后面加“?”即可实现惰性匹配
'aaaa'.match(/a+?/g) // ["a", "a", "a", "a"]
'aaaa'.match(/(a+?)(a+?)/g) // ["aa", "aa"]
'aaaa'.match(/(a+?)+?/g) // ["a", "a", "a", "a"]
分支
分支相当于编程语言中的或运算
/(a|b)c/g.test('ac') // true
/(a|b)c/g.test('bc') // true
分组
分组有很多作用(作为整体进行匹配、缓存分组、反向引用),可以将多个子表达式打包,这样可以让量词作用于多个子表达式组成的整体。
分组的使用
/(ab)+/g
正则表达式匹配到之后,会缓存匹配到的每个分组,用于引用、替换和提取
提取
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
\
引用
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
string.match(regex); // 或者regex.test(string);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
替换
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result);
// => "06/12/2017"
反向引用
可以在正则内部引用前面匹配到的分组,因此叫“反向引用”。
var regex = /\d{4}(-|/|.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
非捕获分组
如果不希望分组被捕获,可以使用这种语法
(?:p)
正则回溯原理
正则匹配的大致过程是(假定使用了g修饰符)
- 先取出正则的子表达式进行匹配(默认贪婪匹配)
- 如果能够匹配上,则对下一个子表达式进行匹配(可能是下个元字符,也可能是加上量词,例如/(a+){2,}/g 中,“(a+)”是一个子表达式,先对其匹配,如果匹配成功则继续对(a+){2,}进行匹配;再比如/ab/g,先匹配a,匹配上的话继续匹配b)
- 如果匹配不上,则开始回溯,回溯到上一个状态继续匹配。如果回溯到第一个子表达式也没有匹配,则从下一个字符开始匹配。为什么需要回溯呢?因为有些模糊匹配可以有多种匹配方法,只有尝试过所有的情况都没有匹配成功,才能认为匹配失败。例如/ab{1,3}c/g.test('abbc'),{1,3}可以是1个~3个。
- 如果正则所有子表达式匹配完,则匹配成功,这时候如果字符串没有结束,则继续匹配(如果没有g修饰符,则停止)
- 如果字符串结束,正则未把所有子表达式匹配完,则失败
正则使用回溯算法尝试所有可能情况进行匹配。回溯过程可能比较消耗性能。
正则性能优化
尽量精确
正则表达式在匹配时候,如果表达式比较模糊就会存在多个状态,就可能会出现很多回溯情况。因此应该尽量精确,减少可能的状态,从而减少回溯
`"abc"de`.match(/".*?"/g) // 由于惰性匹配,因此匹配到第二个引号之前会进行回溯。
`"abc"de`.match(/"[^"]"/g) // 没有回溯
使用非捕获分组
因为分组可能会被缓存,如果不需要对分组进行提取、引用、反向引用等操作,则不需要捕获分组。
独立出确定字符
/a+/ // bad
/aa*/ // good
减少分支匹配损耗
提取分支公共部分,可以减少回溯损耗
/^abc|^def/ // bad
/^(?:abc|def)/ // good
/red|read/ // bad
/rea?d/ // good
阅读正则
正则语法结构中的组成元素。
字符字面量、字符组、量词、锚字符、分组、选择分支、反向引用。
在阅读正则表达式时候,先拆分分支,再对每个分支分析,每个分支从左至右,划分成一个个的子表达式,根据元字符+量词理解子表达式描述的规则,子表达式中还可能有分支,再递归地拆分和分析不同分支即可。
正则编程
test、split、search、match、replace\
验证
// test返回是否成功匹配到子串
/\d+/g.test('1234'); // true
检索
// search返回匹配到的子串的index
'1234abc5678'.search(/\d+/g) // 1
提取
match方法匹配到则返回匹配结果,否则返回null。
在全局模式下match返回所有匹配到的子串数组。
在非全局模式下,match返回一个数组,第一个元素是匹配到的子串,后面元素是分组。返回的结果中还包括输入字符串、匹配到的子串的index等信息。
在全局检索模式下,match() 即不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果您需要这些全局检索的信息,可以使用 RegExp.exec()。
'1a2 3b4'.match(/(\d+)(\w+)(\d+)/g) // ["1a2", "3b4"]
'1a2 3b4'.match(/(\d+)(\w+)(\d+)/) // ["1a2", "1", "a", "2", index: 0, input: "1a2 3b4", groups: undefined]
替换
"2017-06-12".replace(/(\d{4})-(\d{2})-(\d{2})/, function ($0, $1, $2, $3) {
return `${$2}/${$1}/${$3}`;
})
// "06/2017/12"
总结
正则表达式中的主要组成元素包括 字符字面量、字符组、量词、锚字符、分组、选择分支、反向引用。
这里需要注意一些符合,它们在不同场景有不同含义
括号的作用
- 分组
- 非捕获
- 位置
问号的作用
- 惰性
- 量词
- 位置