image

感谢

本文参考《正则表达式迷你书》

分组和分支结构

分组

括号可以提供分组的功能。/a+/, 标示a出现多次。/(ab)+/, 则是将ab作为一组,表示ab出现多次。

分组引用

使用括号可以实现数据提取和替换操作。我们以匹配日期(yyyy-mm-dd)为例


// 无括号版本
var reg1 = /\d{4}-\d{2}-\d{2}/

image


// 有括号版本
var reg2 = /(\d{4})-(\d{2})-(\d{2})/

image

正则引擎在匹配的过程中,会存储每一个分组匹配到的数据

提取分组数据

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同理

image


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

image

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)

匹配过程的拆分

image

有回溯的匹配


var reg = /ab{1,3}c/
var str = 'abbc'

// 匹配是有回溯的
reg.test(str)

匹配过程的拆分

image

匹配b字符的量词是{1,3}。第5步的时候, 我们需要匹配第3个b的时候, 结果匹配到了c, 匹配发送了错误, 但是量词时{1,3}, b{1,3}已经匹配完成了。所以会到之前的匹配状态(第6步), 回退状态的过程就被称为回溯。

为什么.(通配符)是非常影响效率的?


var reg = /".*"/
var str = '"abc"de'

image

.*表示任意字符的任意长度, 它会匹配到字符串的结尾的部分。然后在逐步的回溯。直到'"'匹配'"', 所以说通配符是非常低效的。我们在这个例子中可以使用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'

image

为了满足整体的匹配, 只能给只需要匹配1个的惰性量词, 匹配两个字符。 形成回溯。

分支结构

当/分支1|分支2|分支3/, 当分支1不满足会尝试分支2,分支2不满足会尝试分支3。这种尝试也是回溯的一种

正则表达式的拆分

操作符的优先级

操作符号 操作符 优先级
转义符 \ 1
括号和方括号 (...)、(?:...)、(?=...)、(?!...)、[...] 2
量词限定符 {m}、{m,n}、{m,}、?、*、+ 3
位置和序列 ^、$、元字符、一般字符 4
管道符(竖杠) 5

var reg = /ab?(c|de*)+|fg/
  1. (c|de*)是一个整体
  2. de*中, e作为一个整体
  3. c|de*中,c和de是不同的整体
  4. ab?(c|de)+|fg中, ab?(c|de)+和fg作为不同的整体

image

注意要点

匹配字符串整体问题


// ^abc 和 bcd$ 是两个不同的整体
var reg1 = /^abc|bcd$/

// 可以通过括号把(abc|bcd)作为同一个整体
var reg2 = /^(abc|bcd)$/

image

image

量词连缀问题

/^[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多页


  1. "
  2. +?

已注销
518 声望187 粉丝

想暴富