# 正则基础
正则是、短小精悍、能力强的高精简武器,所以以下是使用该高精尖武器的步骤与练习方法
TIP
正则表达是要么匹配模式,要么匹配字符,要么匹配位置
下面请先欣赏 Vue2.0 中对该武器的使用:
// 小甜点,看出来他们用来完成哪些功能了吗?答案会在后面揭晓,对后面看 Vue 模板编译与解析部分非常有帮助
onRE = /^@|^v-on:/
dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
stripParensRE = /^\(|\)$/g
dynamicArgRE = /^\[.*\]$/
argRE = /:(.*)$/
bindRE = /^:|^\.|^v-bind:/
propBindRE = /^\./
modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
slotRE = /^v-slot(:|$)|^#/
lineBreakRE = /[\r\n]/
whitespaceRE = /[ \f\t\r\n]+/g
invalidAttributeRE = /[\s"'<>\/=]/
// 这个是否可以优化?前面用到懒惰模式,后面用贪婪模式?这里的的优点?
forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
// text 部分的解析
defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g
// 构建出需要的正则
const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
// filter 部分的
validDivisionCharRE = /[\w).+\-_$\]]/
// 高级部分
// 匹配函数定义
fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
// 匹配函数调用部分
fnInvokeRE = /\([^)]*?\);*$/
// 表达式属性访问路径匹配
simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 正则基础知识
- 源字符串,都有字符和位置【从0开始逐个匹配】
- 横向匹配----长度不固定;纵向模糊匹配----不确定的字符[xxxx]
- 占有字符:子表达式匹配到的是字符内容,而非位置,且在最终结果中;互斥的,即一个字符同一时间只能由一个子表达式匹配
- 零宽度:子表达式匹配到的是仅仅是位置,或者匹配的内容并不保存到最终结果中;非互斥,即可以同时由多个零宽度的子表达式匹配。如:^,
- 匹配位置----^、$、\b、\B,正向环视(?=p)与(?!p)、反向环视(?<=);
- \b 单词边界,即\W与\w 直接的位置,包括\w 和^,\w 和$直接的位置,剩下的都是\B
- p 为一个子模式,(?=p)匹配的是 p 前面的位置;
- p 为一个子模式,(?!p)匹配的是前面不是 p 的位置
- 正向环视(?=p)与(?!p)、反向环视(?<=)与(?<!):环视更多的是作为
限制条件使用;不消耗字符,也就是说,在一个匹配发生后,匹配字符之后立即开始下一次匹配的搜索,而不是从包含环视的字符之后开始 - (?=) 正向环视,匹配当前位置,右边是否有相应限定的字符出现。(?=p) p是子模式,即p前面的位置;(?!p) 不是p前面的位置;如
/^(?=D)[E-F]+$/- 位置开始位置0;
- 零宽度子表达式之间不互斥,即同一个位置可以由多个零宽度子表达式匹配,所以环视从位置0开始尝试匹配,右侧为字符D;才为匹配成功;
- 因为上个匹配只进行匹配位置,结果不保存到最终结果,如果上个匹配成功的位置为0,那么下面也是从0位置开始匹配。
- (?<) 反向环视,(?<p) p后面的位置;(?!<p) 不是p后面的位置
// 环视相关
/^(?:\/(?=$))?$/i, /^\/dialog(?:\/(?=$))?$/i
/\((?!\?)/g
/tom(?=(and))\1jerry/
/(?=(.+.+))\1+X/
/\d{1,3}(?=(\d{3})+$)/g
/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{4,8}$/ //只是限定了前面或者后面匹配的规则,而不占用匹配的字符。
/(?<=\s)\d+(?=\s)/ //匹配两边是空白符的数字,不包括空白符
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 正则相关关键词讲解
- 分组捕获与非捕获:将需要捕获的内容放入一对小括号内();捕获将相关匹配存储到一个
临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。 - 非捕获:(?:)需要分组,但不捕获;这种分组正则表达式引擎不会捕获它所匹配的内容即不会为非捕获型分组
分配组号,也就是不放在我们的内存当中,这样也能提高我们的性能(?:exp)不捕获reg?<name>exp匹配exp, 并将捕获文本放到名称为name的组里
- 对捕获的引用:$1, $2... 或 RegExp.$2 != express,其注意事项
- 不是表示引用,而是表示\和 0时,使用(?:\1)0或\1(?:0)
- 引用不存在的分组:如\2,即是对 2的转义
- 分组后面有量词时,则捕获的是最后一次的匹配,如:/(\d)+ \1/.test("12345 5")
- 反向引用\1...\10,即在正则表达式中引用前面的正则子表达式
- 贪婪「*, +,{2,}」与 懒惰「*? 或 +?」
- 常用表达量词:\w,\d,\s, \S, \r, \n, \t ...
- \b: 单词与空格之间,一边为\w,一边为非\w
- \B: 左右两边都为\w与\W之间的位置,\w与^之间的位置,\w与$之间的位置
- \b 单词边界 \f 换页符 \n换行符 \r 回车符 \t 制表符 \v 垂直制表符 \x20 空白符 \x0A(16) \011(8) posix字符类
- 常用捕获位置量词:^, $, \b, \B ...
- 字符组与排查字符:[字符组],[^不包含的字符组]
- 分支:
| - 回溯在不同情况下的表现情况:
- 贪婪模式下:模糊度过高,直接匹配到比较考结束位置了,下一个匹配项无法匹配,则需要向前回溯,如下面横向对比的例子-----使字符组匹配尽量精确,不要范围过大
- 惰性模式下:匹配过少,后面的匹配不了,前一个匹配要再想后匹配,直到后一个匹配成功或者整体匹配失败
- 分支结构:多条匹配道路,第一个使整体匹配成功的道路,匹配不成功也要进行多次回溯
- 修饰符---g, i, m多行匹配,只影响^和$,二者变成行的概念,即行开头和行结尾
- 构建正则时考虑的点
- 准确性,匹配预期的字符串
- 准确性,不匹配非预期的字符串
- 可读性和可维护性
- 提取正则的公共部分,使用反向引用,不断的提取优化
- 效率
- 是否需要复杂的正则,是否可以拆分成几个,分段匹配
- 常用的字符组:[\u4e00-\u9fa5]
// 基本款
/^\/\*[^/]\*\*\/$/ // 多行注释
/^\/\/[^\n]\*/ // 单行注释
/^\x20\*\/\/[^0-9\n]\*/
/?!^a/ //不以a开始
/(?=[a-zA-Z]\d|\d[a-zA-Z]|[a-z][A-Z]|[A-Z][a-z])[\da-zA-Z]{6,12}/g
1
2
3
4
5
6
2
3
4
5
6
# 特殊属性的意义
- input,
RegExp.$_整个待匹配字符串 - leftContext, RegExp['$`'] 上次匹配之前的子字符串
- lastMatch,
RegExp['$&']最后匹配的字符串 - multiline,
RegExp['$\*']是否所有表达式都使用多行模式的布尔值 - lastParen,
RegExp['$+']最后匹配的分组
# 回溯与优先匹配
- 回溯:正则表达式的强大功能中心,它使得表达式强大、灵活、可以匹配非常复杂的模式。同时这种强大需要付出一定的代码;回溯是影响表达式引擎性能的单个最重要的因素。
- 优先匹配:
- 如果匹配到一个位置,需要做
尝试匹配或者跳过匹配这样的选择的时候,对于量词匹配,引擎会优先作出进行尝试行为,而忽略量词优先的时候则进行跳过尝试匹配。如ab?c 匹配abc时,b存在备选状态,如果匹配成功则放弃备选状态;如果匹配的ac则先进行匹配尝试,另一种状态放入到备选状态,如果尝试匹配失败,则进行回溯; - 放弃量词优先:ab??c,这时先放弃量词优先,跳过了的匹配,先匹配后面的。
- 如果匹配到一个位置,需要做
- 线性比较, 非可先限定符不会进行回溯, 如{2}, 如果不包含可选限定符或替换构造,则正则能近线性时间运行。
- 使用可选限定或替换构造的回溯
- 嵌套的可靠限定符的回溯
- 控制回溯:非回溯子表达式,后行断言, 先行断言
- 多选结构,尽量让匹配成功可能性大的情况放在前边,多选状态在每个位置多出多个备选状态,以便回溯 a|b|c|d
# 优化策略
- 不需要捕获的地方使用(?:expression) 。
- 如果括号是非必须的,请不要加括号。
- 不要滥用字符数组,比如[.],请直接用. 。
- 使用合适的位置匹配,如^ $ ,这会加速定位。
- 从两次中提取必须元素,如:x+写成xx*,a{2,4}写成aa{0,2}。
- 提取多选结构开头的相同字符,如the|this 改成th(?:e|is)。(如果你的正则引擎不支持这么使用就改成th(e|is));尤其是锚点,一定要独立出来,这样很多正则编译器会根据锚点进行特别的优化: ^123|^abc 改成^(?:123|abc)。同样的$也尽量独立出来。
- 多选结构后边的一个表达式放入多选结构内,这样能够在匹配任何一个多选结构的时候在不退出多选结构的状态下查看后一匹配,匹配失败的更快。这种优化需要谨慎使用。
- 忽略优先匹配和优先匹配需要你视情况而定。如果你不确定,请使用匹配优先,它的速度是比忽略优先快的。
- 拆分较大正则表达式成一个个小的正则表达式,这是非常有利于提高效率的。
- 模拟锚点,使用合适的环视结构来预测合适的开始匹配位置,如匹配十二个月份,可以先预查首字符是否匹配:(?=JFMASOND)(?:Jan|Feb|...|Dec)。这种优化请根据实际情况使用,有时候环视结构开销可能更大。
- 很多情况下使用固化分组和占有优先量词能够极大提高速度。
- 避免像(this|that)*这样的几乎无尽的匹配。上边提到的 (...+)*也类似。
- 如果能简单的匹配大幅缩短目标字符串,可以进行多次正则匹配,经过实践十分有效。
# 性能提升
- 优化尝试(比较)次数与回溯次数
- 减少回溯次数【减少循环查找同一个字符次数】
- 测试与优化工具:regexbuddy
- 使用正确的边界匹配器(^、$、\b、\B等),限定搜索字符串位置
- 使用具体的元字符、字符类(\d、\w、\s等) ,[^]少用”.”字符
- 使用正确的量词(+、*、?、{n,m}),如果能够限定长度,匹配最佳
- 使用非捕获组、原子组,减少没有必要的字匹配捕获用(?😃
- 进行分组匹配 | + g模式
# 正则表达式运行过程与优化
- 编译
- 设定起始位置
- 尝试匹配
- 若匹配失败则返回前一步重新匹配
- 返回匹配成功失败的结果
- 常用优化手段:
- 精确字符组匹配范围
- 当不需要分组或反向引用时,使用非捕获型分组
- 独立出确定字符,加快判断是否匹配失败,进而加快移位的速度
- 提取分支公共部分,建设分支个数
TIP
使用 search, test, match, exec, split, replace 进行正则验证,其中 search与 match 会把字符串参数转换成正则,所以要加转义
# 正则例子
/<div[^>]+>[^<]*(?=<p)[^>]+>[^<]*(?=<\/p>)/ //找到 div 内的 p 标签
/^([01][0-9]|[2][0-3]):[0-5][0-9]$/ // 时:分
/^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/ //时:分:秒
/^[0-9]{4}-(0[1-9]|[1][0-2])-(0[1-9]|[12][0-9]|3[01])$/ 日期
/[^\\:*<>|"?\r\n/]/ //不包含特殊字符
/([^\\:*<>|"?\r\n/]+\\)*/
/([^\\:*<>|"?\r\n/]+)?/
"12345678".replace(/(?=(\d{3})+$)/g, ","); //后面跟随三个数字,没有排除开始位
"123456789".replace(/(?!^)(?=(\d{3})+$)/g, ",");
"12345678 123456789".replace(/(?!\b)(?=(\d{3})+\b)/g, ","); //用空格时的数字
/(?<=\d)(?=(\d{3})+$)/g;
Num.toFixed(2).replace(/\B(?=(\d{3})+\b)/, ",").replace(/^/,"$$ ");
IPv4 /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/
/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/ //必须包含一个字符(数字) + 密码长度 6-12 位数字或字母
/(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/ //必须包含小写字母与数字 6-12 位数字或字母
/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/ //密码长度 6-12 位数字或字母,即 不能全是数字 或 不能全是大写或小写字母
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
横向例子对比:
/id=".*"/ //贪婪模式,会持续匹配到最后一个“结束
/id=".*?"/ //惰性匹配,存在回溯次数过多的问题
/id="[^"]*"/ //用否定字符组匹配
1
2
3
4
5
2
3
4
5
# 优化的例子:缩短了引擎从开始工作到反馈匹配结果(成功/失败)的时间
/^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/
/^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/ //提取公共部分
/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/ //优化:
// 实数
/^[+-]?(\d+\.\d+|\d+|\.\d+)$/
/^([+-])?(\d+\.\d+|\d+|\.\d+)$/
/^[+-]?(\d+)?(\.)?\d+$/
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
写个函数,判断一个字符串是否为手机靓号,手机靓号条件:有3个连续相同的数字如 '111' 或者 有4个依次递增1的数字 '1234'【编程】
疑问:[\d\D]表示的范围 与[\s\S] 整体表示的范围是否一致?
/(?=.*[0-9])^/两个锚点在一起,其的作用?
# 一段简单的 Vue1.0 的 text 解析的代码
正则的综合运用
export function parseText (text) {
// ...
var tokens = []
var lastIndex = tagRE.lastIndex = 0
var match, index, html, value, first, oneTime
while (match = tagRE.exec(text)) {
index = match.index
if (index > lastIndex) {
tokens.push({
value: text.slice(lastIndex, index)
})
}
html = htmlRE.test(match[0])
value = html ? match[1] : match[2]
first = value.charCodeAt(0)
oneTime = first === 42 // *
value = oneTime
? value.slice(1)
: value
tokens.push({
tag: true,
value: value.trim(),
html: html,
oneTime: oneTime
})
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push({
value: text.slice(lastIndex)
})
}
cache.put(text, tokens)
return tokens
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35