阿瑞斯的BLOG

正则表达式学习总结

关键词
精确匹配 模糊匹配 位置匹配 重复出现 分组 捕获 反向引用 片段匹配 捕获的引用 邮箱验证 敏感词屏蔽 字符串修剪

心得

  1. 又叫规则表达式,它是用代码来准确表述一些我们语言上描述的规则,比如:用代码描述多个0,其中多个0就叫模式,规则

  2. 模式匹配是强大的,凡是你能用语言表述出来的规则,在正则表达式中都可以实现,而难点在于你是否深入正则表达式

  3. 谈正则肯定离不开字符串

  4. 正则表达式最难理解的地方在于等价这个概念,这个概念增加了理解难度,如果把等价都恢复成原始写法,自己书写正则就超级简单了,就像说话一样去写你的正则了

  5. 正则表达式除了匹配字符以外,还可以匹配位置

  6. 指定匹配的位置(实际上就是为了不断章取义)

  7. 复杂的规则往往就是指那些带了多个条件的正则表达式,比如位置条件,比如只匹配百分号前的数字,只匹配美元符号后的数字…这些

我们已经看到了,一个正则表达式中的许多元素才能够匹配字符串的一个字符.
例如: \s 匹配的只是一个空白符.还有一些正则表达式的元素匹配的是字符之间宽度为0的空间,而不是实际的字符例如: \b 匹配的是一个词语的边界,也就是处于一个/w字字符和一个\w非字字符之间的边界.像\b 这样的字符并不指定任何一个匹配了的字符串中的字符,它们指定的是匹配所发生的合法位置.有时我们称这些元素为正则表达式的.因为它们将模式定位在检索字符串中的一个特定位置.最常用的锚元素是 ^, 它使模式依赖于字符串的开头,而锚元素$则使模式定位在字符串的末尾.

例如:要匹配词 “javascript” ,我们可以使用正则表达式 /^ javascript $/. 如果我们想检索 “java” 这个词自身 (不像在 “javascript” 中那样作为前缀),那么我们可以使用模式 /\s java \s /, 它要求在词语java之前和之后都有空格.但是这样作有两个问题.第一: 如果 “java” 出现在一个字符的开头或者是结尾.该模式就不会与之匹配,除非在开头和结尾处有一个空格. 第二: 当这个模式找到一个与之匹配的字符时,它返回的匹配的字符串前端和后端都有空格,这并不是我们想要的.因此,我们使用词语的边界 \b 来代替真正的空格符 \s 进行匹配. 结果表达式是 /\b java \b/.
下面是正则表达式的锚字符:
字符 含义


^ 匹配的是字符的开头,在多行检索中,匹配的是一行的开头

$ 匹配的是字符的结尾,在多行检索中,匹配的是一行的结尾

\b 匹配的是一个词语的边界.简而言之就是位于字符\w 和 \w之间的位置(注意:[\b]匹配的是退格符)

\B 匹配的是非词语的边界的字符

正则表达式的特点

  1. 灵活性、逻辑性和功能性非常的强;

  2. 可以迅速地用极简单的方式达到字符串的复杂控制。

由于正则表达式主要应用对象是文本,因此它在各种文本编辑器场合都有应用,小到著名编辑器EditPlus,大到Microsoft Word、Visual Studio等大型编辑器,都可以使用正则表达式来处理文本内容。

规则总结

  1. 正则表达式中最小单位是字符

  2. 反斜杠(\ )总是对后面的字符起作用

  3. 大括号({})总是对前面的字符表达式起修饰作用

    PS: 关于字符与表达式的区别,用表达式更准确,因为如果存在分组的情况时,大括号作用的目标就是一个单元而非一个字符

  4. 问号,加号,星号(? + *)就是大括号的特殊情况,也就是等价写法

  5. 中括号([])不是对前面和后面起作用的,而是占了一个字符的位置

  6. 小括号()的作用很强大

    • 分组: 被小括号包裹的n个字符会被视为一个字符(专业术语:单元),这样可以方便? + *的整体匹配

      • 在完整模式中定义子模式:(场景:一堆图片要重命名,文件名是截屏图片xxx2016,我们要把2016改成2017,首先我们不能直接找2016,因为这样的话会找到别的带有2016字样的文件,所以我们肯定是先匹配所有的”截屏图片xxx2016”这个模式,也就是完整模式,然后通过这个模式下再去找我们要的核心:2016,最后再进行替换操作,而2016就是子模式)

      • 引用:用()分组以后,再后面用反斜杠+数字可以取得对前面小括号的引用,注意:引用的不是模式而是相同的文本

  7. |:竖线不单独对一个字符起作用,而是划分选项(专业术语:指定分组项),要么是左边的表达式,要么是右边的表达式

  8. ()是为了提取字符串,[]定义范围,{}定义长度

  9. 常用的特殊元字符以及在正则表达式种的行为
    正则表达式常用元字符

  10. 正则表达式种不能用空格匹配空格,需要用\s匹配空格

Q:什么是贪婪模式?

所谓贪婪模式,就是满足匹配条件的情况下,会尽可能多地匹配所搜索的字符串。例如,对于字符串 ‘oooooo’ ,’o+?’ 将匹配单个’o’,而 ‘o+’ 将匹配所有的o。

这是为什么?

这就是 ?号的特殊用法,当它放在其他量词(*, +, ?, {n}, {n,}, {n,m}) 后面,会取消贪婪模式,即尽可能少地进行匹配

正则表达式的应用场景

配合字符串使用

检查字符串中是否包含指定的模式,常用于:替换操作的第一步 | 邮箱验证

【1】模式:简单理解就是检查字符串中是否包含指定的值,这里用模式这个词更准确,因为它可以表示用于泛指,模糊匹配,范围更广,比如是否包含多个0,这句话用代码就不能用指定的值来表示,因为1个0也是0,多个0也是0

RegExp.test(String)返回布尔值true/false,在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的情况下,这个方法非常有用。

String.search(RegExp) 返回-1/位置

用指定的文本替换正则模式匹配到的文本,常用于:敏感词过滤,各种文本插值语法,高亮显示

String.replace(RegExp, String2) 这里返回的是一个新的字符串

其他的方法:

RegExp.exec(String) :返回字符串符合模式匹配的值,如果没有,返回null,一般这个方法用得少,检查是否有值用test,替换值用replace,对指定的部分替换用捕获组的方式。

String.match(RegExp) :如果具有选项g则找出所有匹配项,如果没有选项g,则会返回包含第一个匹配项的信息的数组,捕获组,整个匹配从0开始的索引,以及被解析的原始字符串。

如何用文字表达正则语法?

字符串;tel:086-0666-88810009999

原始正则:”^tel:[0-9]{1,3}-[0][0-9]{2,3}-[0-9]{8,11}$”

速记理解:匹配开始 “tel:普通文本”[0-9数字]{1至3位}”-普通文本”[0数字][0-9数字]{2至3位}”-普通文本”[0-9数字]{8至11位} 结束”

等价简写后正则写法:”^tel:\d{1,3}-[0]\d{2,3}-\d{8,11}$”

Q: 为什么 RegExp.$1 就指代了 /y+/?

描述:

为什么RegExp这个构造函数能知道我要的捕获组?我并没有以如下的方式获取捕获组,为什么就可以得到这个捕获组了呢?

1
2
3
let pattern = /(y+)/ // 定义一个模式
let matches = pattern.exec(fmt)
let captureGroup = matches[1] // 获取捕获组

如图:
正则疑惑
高程上是这么说的:

5.4.3 RegExp构造函数属性

RegExp构造函数包含一些属性(这些属性在其他语言中被看成静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名
RegExp 构造函数属性

当然,除了这些属性以外,还有9个用于存储捕获组的构造函数属性,访问这些属性的语法是 RegExp.$1、 RegExp.$2、 RegExp.$3…… RegExp.$9,在调用exec()或test()方法时,这些属性会自动被填充。

从上面已经找到了我们想要的答案:

  1. RegExp构造函数包含一些静态属性

  2. 静态属性的值基于所执行的最近一次正则表达式操作而而变化,比如exec()或test()

  3. RegExp.$1访问到的就是最近一次正则表达式操作的捕获组内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let text = "this has been a short summer"
let pattern = /(.)hort/g
if(pattern.test(text)) {
console.log('获取最近一次要匹配的字符串: ' + RegExp.$_)
console.log('获取最近一次的匹配项: ' + RegExp.lastMatch)
console.log('获取最近一次匹配的捕获组: ' + RegExp.lastParen)
console.log('获取字符串最近一次匹配之前的文本:' +RegExp.leftContext)
console.log('是否所有表达式都使用了多行文本: ' + RegExp.multline)
console.log('获取字符串最近一次匹配之后的文本: ' + RegExp.rightContext)
console.log('通过RegExp获取捕获组: ' + RegExp.$1)
console.log('通过RegExp获取捕获组: ' + RegExp.$2)
}
// 获取最近一次要匹配的字符串: this has been a short summer
// 获取最近一次的匹配项: short
// 获取最近一次匹配的捕获组: s
// 获取字符串最近一次匹配之前的文本:this has been a
// 是否所有表达式都使用了多行文本: undefined
// 获取字符串最近一次匹配之后的文本: summer
// 通过RegExp获取捕获组: s
// 通过RegExp获取捕获组: // 因为没有第二个捕获组,所有没有填充值

模式对象的实例属性

尤其注意实例属性和静态属性的区别

通过这些属性可以取得有关模式的各种信息

  • 是否设置了 g 修饰符? => pattern.global
  • 是否设置了 i 修饰符? => pattern.ignoreCase
  • 是否设置了 m 修饰符? => pattern.multiline
  • 获取正则表达式的字符串表示 => pattern.source
  • 查找/设置开始搜索下一个匹配项的字符位置 => pattern.lastIndex

    • 注意,这里从字面理解起来可能会有困惑,最后的索引? 指的是下次开始搜索匹配项的起始字符位置

    • 注意,这个行为仅仅在具有修饰符的模式对象中有用,比如g,y(es6新增),在没有修饰符的模式对象中会忽略这个变化,即获取/设置不具有修饰符的模式对象的lastIndex属性,会被忽略.

    • 注意,只有调用exec()test()这些模式对象的方式 lastIndex 属性才会发生改变;调用字符串的方法,比如match方法,lastIndex 属性不会发生改变.

ES6对正则表达式的修复和增强

修复1

原本( ES5 )的正则表达式无法对码位(code point)大于 0FFFF 的字符进行匹配,会匹配不上,因为对于码位大于 0FFFF 的字符需要用2个码位(或者说字符)去表示一个字符(指码位大于0FFFF 的字符),但是正则表达式却不知道这回事,它依然会认为这2个码位表示2个字符而不是码位大于 0FFFF 的字符,这是因为正则表达式默认操作模式是编码单元(code unit)操作模式.

用2个码位(编码单元)表示1个字符(或者说一个码位)的术语就叫代理对

那么 ES6 怎么修复这个问题呢?
ES6引入了修饰符u, 含义为开启Unicode 模式,如此一来模式对象就不会视代理对为2个字符了.其实这里应该不少人和自己一样有个疑问:我怎么知道什么时候2个字符是映射一个单字符而不是2个单字符?这里在自己查阅资料后得到了理解,即代理对的定义:如果第一个值来自于高代理区(D800–DBFF),并且第二个值来自于低代理去( DC00-DFFF),那么这就是一个代理对,即2个字符映射一个辅助平面(supplementary plane)或者说码位大于 0*FFFF 的字符.

1
2
3
4
let text = '𠮷';
/^\uD842/.test('\uD842\uDFB7') // true
/^\uD842/u.test('\uD842\uDFB7') // false
/^\uD842/.test(text) // true

第二行代码说明了 JavaScript 正则表达式对象默认的操作模式-编码单元(也可以叫不认识代理对模式),\uD842\uDFB7即代理对表示汉字”吉”,但是正则表达式并不认识代理对,仍把它当成2个字符,所以第二行结果是true(当成单字符匹配),而使用修饰符u后,正则表达式就会开启 Unicode 模式,即拥有识别代理对的能力,目标字符串如果存在代理对就会有类似添加小括号的行为(便于理解),即(\uD842\uDFB7),显然此时模式\uD842并不匹配(\uD842\uDFB7),所以第三行代码结果为false。第四行的结果为 true, 可以看出对于模式匹配字符串,字符串首先会默认转换为编码单元再进行模式匹配.

以上示例,我们学到了什么?

  • 模式匹配的时候,字符串会转换成对应的编码单元

  • 使用了u修饰符,则会开启 Unicode 模式,此时如果字符串中存在代理对,会被正确识别

  • 使用了u修饰符,并不影响模式本身,即/\^\uD842/u/\^\uD842/仍然匹配\uD842这个字符,只是如果这个字符在目标字符串中不表示自身,而是作为代理对的一部分,这两种模式就会存在是否正确识别的差异.

修复2 正则表达式的复制

增强1 粘连修饰符 y

粘连(sticky)修饰符,何为粘连修饰符?这就不得不提到 g 修饰符了,如果默认的不带 g 修饰符,每次匹配会重新从字符串开始的位置匹配,我称之为无状态匹配.但是 g 修饰符会从上一次匹配成功的下一个位置开始,即剩余字符串开始匹配,而 y 修饰符和 g 修饰符类似,也是如此,不同之处在于,g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let s = 'aaa_aa_a';
let pattern1 = /a+/;
let pattern2 = /a+/g;
let pattern3 = /a+/y;
// 第一次匹配
pattern1.exec(s); //['aaa']
pattern2.exec(s); //['aaa']
pattern3.exec(s); //['aaa']
// 第二次匹配
pattern1.exec(s); //['aaa']
pattern2.exec(s); //['aa']
pattern3.exec(s); //null

第一次匹配的结果都一样,因为都是从目标字符串的开头进行匹配,而第二次匹配的结果却各不相同,因为不带修饰符的模式匹配是无状态匹配,即每次都从目标字符串的开头重新匹配,而带有修饰符 g 和 y 的则会记住上次匹配完时的位置,下次匹配时候从剩余字符串的开头继续匹配,而 y 修饰符返回为 null 是因为 y 暗含了\^语义,而 g 修饰符则没这个语义,只要在剩余字符串中能匹配到模式即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// y 修饰符与lastIndex属性
const REGEX = /a/y;
// 指定从2号位置开始匹配
REGEX.lastIndex = 2;
// 不是粘连,匹配失败
REGEX.exec('xaya') // null
// 指定从3号位置开始匹配
REGEX.lastIndex = 3;
// 3号位置是粘连,匹配成功
const match = REGEX.exec('xaxa');
match.index // 3
REGEX.lastIndex // 4

从上面这2个示例,我们学到了什么?

  • y修饰符暗含了\^的语义.

  • y修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都有效。
  • 模式对象的lastIndex属性可以影响模式匹配时从目标字符串的哪个位置开始匹配.

增强2 flags 属性

在 ES5中,我们无法直接获取一个模式对象的修饰符,那么在 ES5中我们是如何做的呢?

注意:模式对象的source属性只能获得模式的文本而不能获得修饰符

1
2
3
4
5
6
7
8
9
function getFlags(re) {
let text = re.toString()
return text.substring(text.lastIndexOf('/')+1, text.length);
}
// toString()的返回值为 "/ab/g"
let re = /ab/g;
console.log(getFlags(re)); // 'g'

为了使获取修饰符的过程更简单, ES6新增了一个flags属性,它与source属性都是只读的原型属性访问器,对其只定义了 getter 方法,访问这个属性会返回所有应用于当前模式对象的修饰符字符串.

增强3 sticky 属性

与y修饰符相匹配,ES6 的正则对象多了sticky属性,表示是否设置了y修饰符。

1
2
var r = /hello\d/y;
r.sticky // true

正则表达式的先行(xíng)断言(lookahead)和后行断言(lookbehind)

什么是先行断言呢?

如同^代表开头,$代表结尾,\b代表单词边界一样,先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为“零宽”。所谓位置,是指字符串中(每行)第一个字符的左边、最后一个字符的右边以及相邻字符的中间(假设文字方向是头左尾右)。

全称是零宽先行断言,因为本身不消耗字符

”先行断言“指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。”先行否定断言“指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/。

先行断言示例: 如果 re(待匹配的模式) 后面的字符是 gular, 则匹配这个 re

例如对”a regular expression”这个字符串,要想匹配regular中的re,但不能匹配expression中的re,可以用”re(?=gular)”,该表达式限定了re右边的位置,这个位置之后是gular,但并不消耗gular这些字符,将表达式改为”re(?=gular).”,将会匹配reg,元字符.匹配了g,括号这一砣匹配了e和g之间的位置。

先行否定断言的匹配: 如果re(待匹配的模式) 后面的字符不是regex和regular, 则匹配这个 re

1
/re(?!g)/

即匹配时会限制模式后面的位置不能出现字符 g
否定和非否定的区别在于该位置之后的字符

(?=y) 又叫 正向先行断言, (?!y)又叫负向先行断言

如何理解先行和后行?

如何记忆先行形式

(?)是先行断言和后行断言的标识符,代表不占用位置,不消耗字符,?可以理解成询问
(?=pattern)询问是