感谢
本文参考《正则表达式迷你书》
分组和分支结构
分组
括号可以提供分组的功能。/a+/, 标示a出现多次。/(ab)+/, 则是将ab作为一组,表示ab出现多次。
分组引用
使用括号可以实现数据提取和替换操作。我们以匹配日期(yyyy-mm-dd)为例
// 无括号版本
var reg1 = /\d{4}-\d{2}-\d{2}/
// 有括号版本
var reg2 = /(\d{4})-(\d{2})-(\d{2})/
正则引擎在匹配的过程中,会存储每一个分组匹配到的数据
提取分组数据
match方法
match接受一个正则表达式作为参数。如果正则表达式中有g标示, 将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。如果没有使用g标示,则仅返回第一个完整匹配及其相关的捕获组。
var regex = /(\d{4})-(\d{2})-(\d{2})/g
var string = "2017-06-12 2017-06-12"
// ["2017-06-12", "2017-06-12"]
// 返回与完整正则表达式匹配的所有结果, 不含分组的结果
string.match(regex)
var regex = /(\d{4})-(\d{2})-(\d{2})/
var string = "2017-06-12 2017-06-12"
// 只返回第一个完整匹配和其分组
// ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12 2017-06-12", groups: undefined]
string.match(regex)
exec方法
exec方法接受一个字符串作为参数。如果exec匹配失败将会返回null, 如果匹配成功exec方法将会返回一个数组。
返回的数组将完全匹配成功的文本作为第一项,而分组匹配的结果在数组位置0的后面。返回的结果同时拥有index属性, 标示了匹配字符为于原始字符的索引。input属性则是原始的字符串。
注意当正则对象是否包含g返回结果是不一样的。如果正则中含有g标示, 那么正则对象的lastIndex(下一次匹配开始的位置), 会更新。而如果不含有g, 正则的lastIndex不会更新。
var regex = /(\d{4})-(\d{2})-(\d{2})/
var string = "2017-06-12 2017-06-12"
// ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12 2017-06-12", groups: undefined]
// regex.lastIndex === 0
regex.exec(string)
// ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12 2017-06-12", groups: undefined]
// regex.lastIndex === 0
regex.exec(string)
var regex = /(\d{4})-(\d{2})-(\d{2})/g
var string = "2017-06-12 2017-06-12"
// ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12 2017-06-12", groups: undefined]
regex.exec(string)
// ["2017-06-12", "2017", "06", "12", index: 11, input: "2017-06-12 2017-06-12", groups: undefined]
// regex.lastIndex === 21
regex.exec(string)
RegEx
我们也可以通过RegEx全局的构造函数来获取分组匹配的结果。构造函数的全局属性$1到$9存储了分组匹配的结果。
var regex = /(\d{4})-(\d{2})-(\d{2})/g
var string = "2017-06-12 2017-06-12"
// 任何正则操作即可
regex.exec(string)
// "2017"
RegExp.$1
替换分组数据
我们可以通过replace方法配合分组匹配的结果, 实现替换分组数据
replace
str.replace(regexp|substr, newSubStr|func)
- regexp 一个正则表达式对象,该正则所匹配的内容会被replace的第二个参数(或者函数的返回值)替换掉
- substr 当str中含有substr, 会被第二个参数替换, 仅有第一个匹配项会被替换
- newSubStr 用于替换掉第一个参数在原字符串中的匹配部分的字符串。该字符串中可以内插一些特殊的变量名(见下表)
- func 函数的返回值将替换掉第一个参数匹配到的结果
变量名 | 含义 |
---|---|
$1, $2, $3…… | 第n个分组匹配的结果 |
$` | 插入当前匹配的子串左边的内容 |
$' | 插入当前匹配的子串右边的内容 |
$& | 当前匹配的子串 |
// "?loHel?"
'?Hello?'.replace(/(Hel)(lo)/, '$2$1')
// "?ello?ello?"
// H被替换为ello?
'?Hello?'.replace(/H/, "$'")
// "??ello?"
// H被替换为?
'?Hello?'.replace(/H/, "$`")
指定一个函数作为参数
函数的返回值作为替换字符串。 (注意:上面提到的特殊替换参数在这里不能被使用。) 另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式,那么这个方法将被多次调用,每次匹配都会被调用。
// 函数每匹配到一次, 就会被调用
'?????'.replace('/?/g', function () {
return '?'
})
函数的参数 | 含义 |
---|---|
match | 当前匹配的子串 |
p1, p2, …… pn | 匹配的分组数据 |
示例
将'2019-01-01'替换为01/01/2019
var str = '2019-01-01'
var reg = /(\d{4})-(\d{2})-(\d{2})/
// 01/01/2019
var result = str.replace(reg, '$2/$3/$1')
// 01/01/2019
result = str.replace(reg, function (match, year, month, day) {
return `${month}/${day}/${year}`
})
反向引用
我们现在有一个需求, 使用一个正则表达式匹配如下格式的?日期字符串
2019-01-01, 2019.01.01, 2019/01/01, 并且前后的分割符号需要保持一致
var reg = /\d{4}(\/|\.|-)\d{2}(\/|\.|-)\d{2}/
? 上面的正则达到了最基本的目的, 但是我们还没有解决前后分割符需要保持一致的问题。
这时我们可以到反向引用, 1表示第一个分组所匹配到的具体的字符, 2, 3同理
var reg = /\d{4}(-|\/|\.)\d{2}\1\d{2}/
括号嵌套
var regex = /^((\d)(\d(\d)))\1\2\3\4$/
var string = "1231231233"
// 分组一
// \d\d\d \1对应 123
// 分组二
// \d \2 对应 1
// 分组三
// \d\d \3 对应 23
// 分组四
// \d \4 对应3
10
10表示第10个分组呢?还是1和0?
10表示的是第十个分组
引用不存在的分组
引用不存在的分组不会报错。例如2, 2分组不存在, 2匹配的就是字符串"2"
如果分组后面添加量词
分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配
var str = '12345'
var reg = /\d+/
var reg1 = /(\d)+/
// ["12345", index: 0, input: "12345", groups: undefined]
str.match(reg)
// ["12345", index: 0, input: "12345", groups: undefined]
str.match(reg1)
// \1 应当是匹配的最后一次结果
var regex = /(\d)+ \1/;
// false
regex.test("12345 1")
// true
regex.test("12345 5") );
非捕获括号
之前的括号都被称为捕获型的分组, 捕获到的组的数据会在API中引用。如果只想使用括号最原始的功能,不在API和反向引用中使用可以使用非捕获括号。
var str = 'HelloWorld'
var reg1 = /(?:Hello)(?:World)/
var reg2 = /(Hello)(World)/
// ["HelloWorld", index: 0, input: "HelloWorld", groups: undefined]
// 捕获组的数据不会引用
reg1.exec(str)
// ["HelloWorld", "Hello", "World", index: 0, input: "HelloWorld", groups: undefined]
reg2.exec(str)
示例
实现trim功能
第一种思路是匹配空格, 然后替换空格为空字符串
var reg = /^\s+|\s+$/g
var str = ' reg '
str.replace(reg, '')
第二种思路是使用分组把内容提取出来, 进行替换。
var str = ' reg '
var reg = /^\s+(.*)\s+$/g
str.replace(reg, '$1')
如果使用量词, 我们需要使用惰性匹配, 因为(.)s$中(.)的部分会匹配除了最后一个空格之外的其他空格
var str = ' reg '
var reg = /^\s*(.*)\s*$/g
str.replace(reg, '$1')
将每个单词的首字母转换成大写
var str = 'hello world'
// Hello World
// 我们匹配单词边界后的第一个字母
str.replace(/\b\w{1}/g, function (word) {
return word.toLocaleUpperCase()
})
匹配成对标签
我们可以使用反向引用, 我们将前面的标签作为分组用括号包起来, 1将会是分组匹配的结果。我们这样就能保证前后两个标签的类型是一致的
var reg = /<([^>]+)>\w+<\/\1>/
// true
reg.test('<p>123</p>')
// false
reg.test('<p>123</div>')
4月3号更新 ??????
回溯
没有回溯的匹配
var reg = /ab{1,3}c/
var str = 'abbbc'
// 匹配是没有回溯的
reg.test(str)
匹配过程的拆分
有回溯的匹配
var reg = /ab{1,3}c/
var str = 'abbc'
// 匹配是有回溯的
reg.test(str)
匹配过程的拆分
匹配b字符的量词是{1,3}。第5步的时候, 我们需要匹配第3个b的时候, 结果匹配到了c, 匹配发送了错误, 但是量词时{1,3}, b{1,3}已经匹配完成了。所以会到之前的匹配状态(第6步), 回退状态的过程就被称为回溯。
为什么.(通配符)是非常影响效率的?
var reg = /".*"/
var str = '"abc"de'
.*表示任意字符的任意长度, 它会匹配到字符串的结尾的部分。然后在逐步的回溯。直到'"'匹配'"', 所以说通配符是非常低效的。我们在这个例子中可以使用1代替通配符。
常见的回溯形式
贪婪量词
b{1, 3}会首先尝试匹配bbb, 然后bb, 最后是b。如果还不行则匹配失败。贪婪量词从多往少尝试的过程就是回溯的过程。如果多个贪婪量词在一起时, 前面的贪婪量词会优先匹配。
var str = '12345'
var reg = /(\d{1,3})(\d{2,3})/
// true
reg.test(str)
// ["12345", "123", "45", index: 0, input: "12345", groups: undefined]
reg.exec(str)
惰性量词
惰性量词依然可能存在回溯的情况
var reg = /^\d{1,3}?\d{1,3}$/
var str = '12345'
为了满足整体的匹配, 只能给只需要匹配1个的惰性量词, 匹配两个字符。 形成回溯。
分支结构
当/分支1|分支2|分支3/, 当分支1不满足会尝试分支2,分支2不满足会尝试分支3。这种尝试也是回溯的一种
正则表达式的拆分
操作符的优先级
操作符号 | 操作符 | 优先级 | |
---|---|---|---|
转义符 | \ | 1 | |
括号和方括号 | (...)、(?:...)、(?=...)、(?!...)、[...] | 2 | |
量词限定符 | {m}、{m,n}、{m,}、?、*、+ | 3 | |
位置和序列 | ^、$、元字符、一般字符 | 4 | |
管道符(竖杠) | 5 |
var reg = /ab?(c|de*)+|fg/
- (c|de*)是一个整体
- de*中, e作为一个整体
- c|de*中,c和de是不同的整体
- ab?(c|de)+|fg中, ab?(c|de)+和fg作为不同的整体
注意要点
匹配字符串整体问题
// ^abc 和 bcd$ 是两个不同的整体
var reg1 = /^abc|bcd$/
// 可以通过括号把(abc|bcd)作为同一个整体
var reg2 = /^(abc|bcd)$/
量词连缀问题
/^[abc]{3}+$/会产生错误?, 量词无法对量词使用。我们可以使用括号/^([abc]{3})+$/, 将([abc]{3})作为一个整体。
元字符转义问题
在正则表达式中, 元字符因为具有特殊的含义。所以如果需要匹配元字符本身, 必须对元字符转义。
正则表达式中的元字符, ^、$、.、*、+、?、|、、/、(、)、[、]、{、}、=、!、:、-
字符组中的元字符
假设我们需要匹配一组字符, ^, +, ?
2的含义并不是一组字符而是反义字符组。我们必须对^符号进行转义。修改为[^+?]才符合要求。
匹配字符组本身
如果我们需要匹配{2,4}或者[123]呢?
我们可以对两个括号进行转义或者只对一个括号转义, 使它无法构成字符组。修改为{2,4}, 或者{2,4}即可。
案例
IPV4
var reg = /^((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])$/
这个正则看似很复杂,但我们根据操作符的优先级,我们可以分为((...).){3}, (...)这两部分。而((...).)中里面的括号其实是4个分支语句。可以继续拆分。(0{0,2}d|0?d{2}|1d{2}|2[0-4]d|25[0-5])内容可以分成5个分支。
- 0{0,2}d 匹配1位数, 并补0
- 0?d{2} 匹配2位数, 并补0
- 1d{2} 匹配100-199
- 2[0-4]d 匹配200-249
- 25[0-5] 匹配250-255
正则表达式的构建
书中本章节更多的是介绍, 是否应当能使用正则, 和是否有必要使用正则,以及正则效率的问题。有兴趣的同学可以直接去看书。但是我觉得本章中,介绍了两个正则的例子?还是很值得思考?的?
匹配固定电话 ☎️
电话的格式如下055188888888,0551-88888888,(0551)88888888我们该如何通过一个正则进行匹配, 我们将书写正则过程拆分成几步。
我们首先书写✍️3种正则分别匹配3种格式的电话号码
var reg1 = /^0\d{2,3}[1-9]\d{6,7}$/
var reg2 = /^0\d{2,3}-[1-9]\d{6,7}$/
var reg3 = /^\(0\d{2,3}\)[1-9]\d{6,7}$/
接下来我们使用或者分割符把3个正则链接为1一个正则表达式
var reg = /^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}$/
我们提交公共的部分
var reg = /^(0d{2,3}|0d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/
公共部分的第一个分支和第二个分支只差别一个-符号,可以对-使用量词?,还可以进一步简化
var reg = /^(0d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/
匹配浮点数
如果通过一个正则匹配1.23、+1.23、-1.23 10、+10、-10 .2、+.2、-.2
我们将这些例子分为两种类型, "1.23"、"+1.23"、"-1.23"(有整数位有小数位), "10"、"+10"、"-10"(只有整数位), ".2"、"+.2"、"-.2"(只有小数位)
我们先写三个正则匹配三种类型
var reg1 = /^[-+]?\d+\.\d+$/
var reg2 = /^[-+]?\d+$/
var reg3 = /^[-+]?\.\d+/
我们合并这三种正则
var reg = /^[-+]?\d+\.\d+$|^[-+]?\d+$|^[-+]?\.\d+/
简化正则
var reg = /^[-+]?(\d+\.\d+|\d+|\.\d+)$/
正则表达式编程
⚠️ 相关API注意要点
match返回结果的格式问题
match接受一个正则表达式作为参数。如果正则表达式中有g标示, 将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
如果没有使用g标示,则仅返回第一个完整匹配及其相关的捕获组。
exec比match更??
当正则没有g时,使用match返回的信息比较多。但是有g后,match就没有关键的信息index了(只会返回全部匹配的内容)。
而exec方法即使正则包含g,它也能接着上一次匹配后继续匹配。reg的lastIndex会更新(如果不含g不会更新), exec可以配合while使用。
var str = '2019.01.01'
var reg1 = /\./
var reg2 = /\./g
// [".", index: 4, input: "2019.01.01", groups: undefined]
reg1.exec(str)
// 0
reg1.lastIndex
// [".", index: 4, input: "2019.01.01", groups: undefined]
reg2.exec(str)
// 5
reg2.lastIndex
// [".", index: 4, input: "2019.01.01", groups: undefined]
str.match(reg1)
// 0
reg1.lastIndex
// [".", "."]
str.match(reg2)
// 0
reg2.lastIndex
修饰符 g,对exec和test的影响
字符串的方法, 每次匹配后不会影响正则对象的lastIndex属性的值
exec和test方法, 在全局匹配下会影响正则对象的lastIndex, 如果找不到了lastIndex会重置为0
exec和test方法, 如果正则没有g, lastIndex不会改变
结语
?看到这里也结束了, 大家如果有兴趣可以看原书, 书本身是开源免费的, 很精简80多页
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。