前言
作为一名程序员,你肯定是知道正则表达式的。作为计算机领域最伟大的发明之一,正则表达式简单、强大,它可以极大地提高我们工作中的文本处理效率。现在各大操作系统、编程语言、文本编辑器也都已经支持正则表达式。
但经常在⽹上看到许多⼈抱怨正则表达式【难学】,发现大家和我之前的做法⼀样:⽤到什么功能,就去⽹上搜⼀个例⼦来改改,能跑通就满意。⾄于这例⼦到底如何构成的,⾃⼰是不是都懂了,其实⼼⾥没底,能⼤概看懂五六分,就已经很满⾜了。
但是这样治标不治本。有点像挖井,每次挖到⼀点⽔就满⾜了,根本不关⼼挖没挖到含⽔层。结果就是每次要喝⽔的时候,你都得重新打⼀眼井。那么对于正则表达式,我们有没有可能打出⼀⼝【永不⼲涸的深井】?当然有,那就需要 ⼀次性多投⼊点时间,由表及里,由术及道。一旦掌握了方法,之后就会简单很多了 。
如果每天花一刻钟,坚持一个礼拜,从了解到熟悉,从熟悉到理解,达到【不忘】的阶段。多投⼊时间很好理解,但什么叫掌握⽅法呢?深⼊正则表达式概念思维层⾯。不要盯着正则字符和表达式皱眉,⽽要把真正的【规律】给找出来。也正因为这样,我们才需要⼀次性多投⼊点时间。
概述/简介
正则表达式,英文是 Regular Expression,简称RE。顾名思义,正则其实就是一种描述 文本内容组成规律的表示方式
(pattern)。
简单来说,正则是一个非常强大的文本处理工具,它的应用极其广泛。我们可以利用它来校验数据的有效性,比如表单输入是不是符合要求;也可以从文本中提取想要的内容,比如从网页中抽取数据;还可以用来做文本内容替换,从而得到我们想要的内容。
而在前端 Javascript 中,正则对象是 RegExp
。 它通常应用于 RegExp
对象的 exec
和 test
方法,以及 String
对象的 match
、matchAll
、replace
、search
、split
等方法中。
那么接下来让我们一起学习前端正则表达式~
初识正则表达式
RegExp 声明定义
正则表达式定义有两种方式:
- 一种是使用
字面量
,以斜杠表示开始和结束 - 一种是使用
RegExp
构造函数,分别接收两个参数:正则表达式/文本、修饰符
js
// 字面量 定义
const reg1 = /abc/i
// RegExp 构造
const reg2 = new RegExp('abc', 'i')
const reg3 = new RegExp(/abc/, 'i')
上面写法是等价的。它们主要区别是第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且前者比较便利和直观。在实际应用中,基本上都采用字面量定义正则表达式。不过在后者在动态生成正则表达式中更具独特的优势。
RegExp 实例属性
正则对象的实例属性分成两类
-
一类是与修饰符相关,用于了解设置了什么修饰符(后面会更详细的说明)
RegExp.prototype.flags
:返回已经设置的所有修饰符RegExp.prototype.dotAll
:表示设置了s
修饰符,其意是.
允许匹配换行符RegExp.prototype.sticky
:表示设置了y
修饰符, 其意搜索是否是粘黏RegExp.prototype.global
:表示设置了g
修饰符, 其意是否全局查找匹配RegExp.prototype.unicode
:表示设置了u
修饰符,识别 Unicode 代码点RegExp.prototype.multiline
:表示设置了m
修饰符,其意是否多行搜索RegExp.prototype.ignoreCase
:表示设置了i
修饰符, 其意是否忽略大小写RegExp.prototype.hasIndices
:表示设置了d
修饰符,为每个捕获组子字符串提供索引
js(/foobar/gimsy).flags // gimsy
-
一类是与修饰符无关属性
-
RegExp.prototype.lastIndex
返回整数,表示下一次开始搜索的位置。该属性可读写,但只在进行连续搜索时有意义(如上个例子中设置了修饰符g或y时)
-
RegExp.prototype.source
返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
js(/\w\d/g).lastIndex // 0 (/\w\d/g).source // '\\w\\d'
-
RegExp 实例方法
-
RegExp.prototype.test()
返回布尔值,表示当前模式是否能匹配参数字符串。如果正则表达式带有
g
或y
修饰符时,则每次都从上一次结束的位置开始向后匹配。但是如果未匹配,则lastIndex
属性重置为0
js/^cats/.test('cats and dogs') // true /^dogs/.test('cats and dogs') // false
-
RegExp.prototype.exec()
返回匹配结果,如果未发现匹配返回
null
,如果匹配则返回一个数组,成员是匹配的子字符串,此外返回的数组还包含以下属性input
:整个原字符串。index
:模式匹配成功的开始位置(从0开始计数)groups
:如果有命名分组,则返回一个对象 { [分组名称]: 匹配字符串 },否则 undefined
其他说明:
-
如果正则表示式包含圆括号(即含有"组匹配"),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的
length
属性等于组匹配的数量再加1。 -
如果正则表达式加上
g
或y
修饰符,则可以使用多次exec()
方法,下一次搜索的位置从上一次匹配成功结束的位置开始。但是如果未匹配,则lastIndex
属性重置为0
。这个模式与test()
方法一致。
js
const str = '_aa_aba_abba_'
const reg = /a(?<name>b+)?a/g
reg.exec(str)
/** 执行结果
* {
* 0: "aa"
* 1: undefined
* groups: {name: undefined}
* index: 1
* input: "_aa_aba_abba_"
* length: 2
* }
*/
reg.exec(str)
/** 执行结果
* {
* 0: "aba"
* 1: "b"
* groups: {name: "b"}
* index: 4
* input: "_aa_aba_abba_"
* length: 2
* }
*/
reg.exec(str)
/** 执行结果
* {
* 0: "aba"
* 1: "bb"
* groups: {name: "bb"}
* index: 8
* input: "_aa_aba_abba_"
* length: 2
* }
*/
reg.exec(str)
/** 执行结果
* null
*/
String 实例方法
-
String.prototype.match()
字符串的
match
方法与正则对象的exec
方法类似,匹配成功返回一个数组,匹配失败返回null
。但是如果正则表达式带有g
修饰符,则该方法与正则对象的exec
方法行为不同,会一次性返回所有匹配成功的结果。jsconst str = '_aa_aba_abba_' const reg1 = /a(?<name>b+)?a/ const reg2 = /a(?<name>b+)?a/g str.match(reg1) /** 执行结果 * { * 0: "aa" * 1: undefined * groups: {name: undefined} * index: 1 * input: "_aa_aba_abba_" * length: 2 * } */ str.match(reg2) /** 执行结果 * ['aa', 'aba', 'abba'] */
-
String.prototype.split()
字符串对象的
split
方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。该方法接受两个参数,第一个参数是正则表达式表示分隔规则,第二个参数是返回数组的最大成员数。js('a-aa-aaa').split(/-/) // ['a', 'aa', 'aaa'] ('a-aa-aaa').split(/-/, 2) // ['a', 'aa'] // 如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。 ('a-aa-aaa').split(/(-)/) // ['a', '-', 'aa', '-', 'aaa'] ('a-aa-aaa').split(/(-)/, 2) // ['a', '-']
-
String.prototype.search()
字符串对象的
search
方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1
js('a-aa-aaa').search(/aa/) // 2 ('a-aa-aaa').search(/aaaa/) // -1
-
String.prototype.replace()
字符串对象的
replace
方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。-
正则表达式如果不加
g
修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。js'a-aa-aaa'.replace('a', 'b') // "b-aa-aaa" 'a-aa-aaa'.replace(/a/, 'b') // "b-aa-aaa" 'a-aa-aaa'.replace(/a/g, 'b') // "b-bb-bbb"
-
replace
方法的第二个参数可以使用美元符号$
,用来指代所替换的内容。$&
:匹配的子字符串- ``$```:匹配结果前面的文本
$'
:匹配结果后面的文本$n
:匹配成功的第n
组内容,n
是从1开始的自然数, 支持 1~99$$
:指代美元符号$
js'2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, '$&') // "2023-09-05/hello world - lin" '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, '$`') // "2023-09-05/2023-09-05/ - lin" '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, "$'") // "2023-09-05/ - lin - lin" '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, "$1") // "2023-09-05/hello - lin" '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, "$$") // "2023-09-05/$ - lin"
-
replace
方法第二个参数还可以是一个函数,将每个匹配内容替换为函数返回值。作为一个替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应参数)另外最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。如果正则表达式存在命名分组,则最后一个参数原字符串后面还会再多个命名分组匹配的参数
js'2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, function($0, $1, $2, $3, $4) { console.log(`$0: ${$0}`) console.log(`$1: ${$1}`) console.log(`$2: ${$2}`) console.log(`$3: ${$3}`) console.log(`$4: ${$4}`) return $0 }) // // console.log: // $0: hello world // $1: hello // $2: world // $3: 11 // $4: 2023-09-05/hello world - lin //
-
了解正则表达式
在初识正则表达式中,我们已经熟悉了在前端 Javascript 中正则表达式的声明和定义、正则表达式的实例属性/方法、正则表达式不同修饰符各自作用,以及通过 RegExp/String 对象的实例方法的进行使用。那么接下来,我们逐步梳理并了解正则表达式中的术语和概念(字面量字符 、元字符 、预定义模式 、量词符 、修饰符等),希望大家对正则表达式基础知识和概念有更深的了解。
字面量字符
大部分字符在正则表达式中,就是字面的含义,比如 /a/
匹配 a
,/b/
匹配 b
。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的 a
和 b
),那么它们就叫做"字面量字符"(literal characters)。
预定义模式
预定义模式指的是某些常见模式的简写方式。
\d
匹配0-9之间的任一数字,相当于[0-9]
。\D
匹配所有0-9以外的字符,相当于[^0-9]
。\w
匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
。\W
除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
。\s
匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
。\S
匹配非空格的字符,相当于[^ \t\r\n\v\f]
。\b
匹配词的边界。\B
匹配非词边界,即在词的内部。
js
// \b 的例子
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false
// \B 的例子
/\Bworld/.test('hello world') // false
/\Bworld/.test('hello-world') // false
/\Bworld/.test('helloworld') // true
元字符
除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做"元字符"(metacharacters)
-
点字符
.
匹配除回车(
\r
)、换行(\n
) 、行分隔符(\u2028
)和段分隔符(\u2029
)以外的所有字符 -
位置字符
^
和$
分别表示字符串的开始位置和字符串的结束位置。当设置了
multiline
修饰符时,^
和$
还会匹配每行行首和行尾 -
选择符
|
在正则表达式中表示"或关系"(OR),即
cat|dog
表示匹配cat
或dog
-
字符类
表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如
[xyz]
表示x
、y
、z
之中任选一个匹配。其中有两个字符在字符类中有特殊含义:-
脱字符(^)
如果方括号内的第一个字符是
[^]
,则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]
表示除了x
、y
、z
之外都可以匹配 -
连字符(-)
某些情况下,对于连续序列的字符,连字符(
-
)用来提供简写形式,表示字符的连续范围。比如,[abc]
可以写成[a-c]
,[0123456789]
可以写成[0-9]
,同理[A-Z]
表示26个大写字母。
js/^[abc]/.test('world') // false /^[abc]/.test('apple') // true /^[^abc]/.test('world') // true /^[^abc]/.test('apple') // false /a-z/.test('b') // false /[a-z]/.test('b') // true
-
-
转义符
正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配
+
,就要写成\+
。正则表达式中需要反斜杠转义的,一共有12个字符:^
、.
、[
、$
、(
、)
、|
、*
、+
、?
、{
和\
。需要特别注意的是,如果使用RegExp
方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。js/1+2/.test('1+2') // false /1\+2/.test('1+2') // true (new RegExp('1\+2')).test('1+2') // false (new RegExp('1\\+2')).test('1+2') // true
-
特殊字符
正则表达式对一些不能打印的特殊字符,提供了表达方法。
\n
匹配换行键。\r
匹配回车键。\t
匹配制表符 tab(U+0009)。\v
匹配垂直制表符(U+000B)。\f
匹配换页符(U+000C)。\xhh
匹配一个以两位十六进制数(\x00
-\xFF
)表示的字符。\uhhhh
匹配一个以四位十六进制数(\u0000
-\uFFFF
)表示的 Unicode 字符。
量词符
量词符用来设定某个模式出现的次数。精确匹配次数时,使用大括号({}
)表示。{n}
表示恰好重复n
次,{n,}
表示至少重复n
次,{n,m}
表示重复不少于n
次,不多于m
次。
?
问号表示某个模式出现0次或1次,等同于{0,1}
*
星号表示某个模式出现0次或多次,等同于{0,}
+
加号表示某个模式出现1次或多次,等同于{1,}
js
/^\d{3}$/.test('123') // true
/^\d{3}$/.test('1234') // false
/^\d{3,}$/.test('123') // true
/^\d{3,}$/.test('1234') // true
/^\d{2,3}$/.test('123') // true
/^\d{2,3}$/.test('1234') // false
修饰符
-
RegExp.prototype.dotAll
表示设置了
s
修饰符,其作用是特殊字符.
应匹配字符串中的下述行终结符- U+000A 换行符"
\n
") - U+000D 回车符("
\r
") - U+2028 行分隔符(line separator)
- U+2029 段分隔符(paragraph separator)
js(/foo.bar/).dotAll // false (/foo.bar/s).dotAll // true (/foo.bar/).test('foo\nbar') // false (/foo.bar/s).test('foo\nbar') // true
- U+000A 换行符"
-
RegExp.prototype.global
表示设置了
g
修饰符,其意是全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。js(/foo\w+/).global // false (/foo\w+/g).global // true ('foobar\nfootball').match(/foo\w+/) // ['foobar'] ('foobar\nfootball').match(/foo\w+/g) // ['foobar', 'football']
-
RegExp.prototype.multiline
表示设置了
m
修饰符,其作用^
和$
会匹配每行行首和行尾,即^
和$
会识别换行符\n
。js(/^football/).multiline // false (/^football/m).multiline // true (/^football/).test('basketball\nfootball') // false (/^football/m).test('basketball\nfootball') // true
-
RegExp.prototype.ignoreCase
表示设置了
i
修饰符, 意味着在字符串进行匹配时,应该忽略大小写js(/^football/).ignoreCase // false (/^football/i).ignoreCase // true (/^football/).test('Football') // false (/^football/i).test('Football') // true
-
RegExp.prototype.unicode
表示设置了
u
修饰符,开启了多种 Unicode 相关的特性,任何 Unicode 代码点的转义都会被解释。js(/\u{61}/).unicode // false (/\u{61}/u).unicode // true (/\u{61}/).test('a') // false (/\u{61}/u).test('a') // true
-
RegExp.prototype.sticky
表示设置了
y
修饰符, 仅从正则表达式的lastIndex
属性表示的索引处为目标字符串匹配,并且不会尝试从后续索引匹配。如果表达式同时指定了sticky
和global
,其将会忽略global
标志。js/** * a) 仅从正则 lastIndex 属性指向的索引为目标字符串开始进行匹配(/foo/y 感觉更像 /^foo/y) * b) 一个表达式同时指定了 `sticky` 和 `global`,将会忽略 `global` 标志 * c) 从下面例子注意 sticky 和 global 修饰符的区别 */ const str1 = 'foobarfootball' const reg1 = /foo/y const reg2 = /^foo/g // ------- sticky: reg1 --------- reg1.lastIndex // 0 reg1.test(str1) // true reg1.lastIndex // 3 reg1.lastIndex // 3 reg1.test(str1) // false, 因为索引指向第3个字符, /^foo/y.test('barfootball') reg1.lastIndex // 0, 因为没匹配,所有重置 reg1.lastIndex 为 0 reg1.lastIndex = 6 // 设置 lastIndex 索引为 6 reg1.test(str1) // true, 因为索引指向第6个字符, /^foo/y.test('football') reg1.lastIndex // 9 // ------- global: reg2 --------- reg2.lastIndex // 0 reg2.test(str1) // true reg2.lastIndex // 3 reg2.lastIndex // 3 reg2.test(str1) // false reg2.lastIndex // 0, 因为没匹配,所有重置 reg2.lastIndex 为 0 reg2.lastIndex = 6 // 设置 lastIndex 索引为 6 reg2.test(str1) // false, 注意与sticky的区别,'g'修饰的正则表达式中^只能匹配全文的开头字符 reg2.lastIndex // 0, 因为没匹配,所有重置 reg2.lastIndex 为 0
深入正则表达式
在上一个小节中,我们一起熟悉了正则表示式的一些术语和概念,对正则的基础知识有了进一步的了解。那在这个小节中,我们一起学习正则表达式中 分组匹配(分组匹配 、捕获分组引用 、命名捕获分组 、非捕获分组 、零宽断言 )和正则匹配模式(贪婪模式 、非贪婪模式 、独占模式)。正是部分使得正则表达式晦涩难懂,也正是这些使得正则表达式高效而又强大。
分组匹配
分组和编号
括号在正则中可以 用于分组 ,被括号括起来的部分 「子表达式」会被保存成一个 子组。那分组和编号的规则是怎样的呢?其实很简单,第几个括号就是第几个分组。那如果括号存在嵌套括号呢,也很简单,可以数左括号计数,哈哈。
例如 这里有个日期 2023-09-06 12:00:01,我们使用正则匹配日期和时间来验证下分组规则。
js
const date = '2023-09-06 12:00:01'
const regex = /((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2}))/
date.replace(regex, ($0, $1, $2, $3, $4, $5, $6, $7, $8) => {
console.log(`$0: ${$0}`) // 匹配的文本
console.log(`$1: ${$1}`) // 第一个分组捕获
console.log(`$2: ${$2}`) // 第二个分组捕获
console.log(`$3: ${$3}`) // 第三个分组捕获
console.log(`$4: ${$4}`) // 第四个分组捕获
console.log(`$5: ${$5}`) // 第五个分组捕获
console.log(`$6: ${$6}`) // 第六个分组捕获
console.log(`$7: ${$7}`) // 第七个分组捕获
console.log(`$8: ${$8}`) // 第八个分组捕获
return $0
})
//
// console.log:
// $0: 2023-09-06 12:00:01
// $1: 2023-09-06
// $2: 2023
// $3: 09
// $4: 06
// $5: 12:00:01
// $6: 12
// $7: 00
// $8: 01
//
命名捕获分组
前面我们讲了分组编号,但由于编号得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,可能导致编号发生变化 ,因此提供了 命名分组(named grouping) ,这样和数字相比更容易辨识,不容易出错。命名分组的格式为 (?<分组名>...)
。
还是这个日期例子 2023-09-06 12:00:01,我们使用正则匹配日期和时间来验证下命名分组。
js
const date = '2023-09-06 12:00:01'
const regex = /(?<date>(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})) (?<time>(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2}))/
date.replace(regex, '$<year>年$<month>月$<day>日 $<hour>点$<minute>分$<second>秒')
// result:
// '2023年09月06日 12点00分01秒'
date.replace(regex, ($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) => {
console.log(`$0: ${$0}`) // 匹配的文本
console.log(`$1: ${$1}`) // 第一个分组捕获
console.log(`$2: ${$2}`) // 第二个分组捕获
console.log(`$3: ${$3}`) // 第三个分组捕获
console.log(`$4: ${$4}`) // 第四个分组捕获
console.log(`$5: ${$5}`) // 第五个分组捕获
console.log(`$6: ${$6}`) // 第六个分组捕获
console.log(`$7: ${$7}`) // 第七个分组捕获
console.log(`$8: ${$8}`) // 第八个分组捕获
console.log(`$9: ${$9}`) // 匹配位置索引
console.log(`$10: ${$10}`) // 整个文本
console.log(`$11:`, $11) // 命名分组匹配对象
return $0
})
//
// console.log:
// $0: 2023-09-06 12:00:01
// $1: 2023-09-06
// $2: 2023
// $3: 09
// $4: 06
// $5: 12:00:01
// $6: 12
// $7: 00
// $8: 01
// $9: 0
// $10: 2023-09-06 12:00:01
// $11: {
// date: "2023-09-06",
// year: "2023",
// month: "09",
// day: "06",
// time: "12:00:01",
// hour: "12",
// minute: "00",
// second: "01",
// }
//
编号捕获引用
刚刚我们在 String.prototype.replace()
方法中,通过$1
、$2
的方式引用了正则表达式中的分组匹配项,对于命名分组的,replace 第二个参数作为函数,其函数参数最后一个参数也返回了命名分组匹配的结果。
那在正则表达式内部是否可以引用分组捕获的匹配项呢?答案是可以的。在知道了分组引用的编号(number)后,我们就可以使用 反斜扛 + 编号
,即 \number
的方式来进行引用
js
const reg = /^<(\w+)>.+(<\/\1>)$/
const html = `<html><header></header><body></body></html>`
html.replace(reg, '$1') // 'html'
html.replace(reg, '$2') // '</html>'
命名捕获引用
那如果是命名分组匹配 (?<name>)
, 那么在正则表达式内部通过 \k<name>
引用,详见如下
js
const reg = /^<(?<tag>\w+)>.+(<\/\k<tag>>)$/
const html = `<html><header></header><body></body></html>`
html.replace(reg, '$<tag>') // 'html'
html.replace(reg, '$2') // '</html>'
非捕获分组
在括号里面的会保存成子组 ,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它 ,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用 (?:)
不保存子组。
js
const reg = /^<(?:\w+)>.+(<\/html>)$/
const html = `<html><header></header><body></body></html>`
html.replace(reg, '$1') // '</html>', 因为 (?:\w+) 不捕获,所以 $1 指向 (<\/html>)
零宽断言
在使用正则表达式时,有时我们需要捕获的内容前后必须是特定内容,但又不捕获这些特定内容的时候,零宽断言就起到作用了。零宽断言正如它的名字一样,是一种零宽度的匹配,简单理解,零宽断言匹配不会改变 lastIndex
的索引值。
零宽断言有正向(positive)、负向(negative)、先行(lookahead)、后行(lookbehind),一共有4种组合形式:
零宽断言类型 | 零宽断言表达式 | 零宽断言描述 | 零宽断言标签 |
---|---|---|---|
零宽正向先行断言 | (?=exp) | 匹配某位置,该位置后面内容应匹配表达式 exp,【JS支持】 | lookahead positive |
零宽负向先行断言 | (?!exp) | 匹配某位置,该位置后面内容不匹配表达式 exp,【JS支持】 | lookahead negative |
零宽正向后行断言 | (?<=exp) | 匹配某位置,该位置前面内容应匹配表达式 exp,【JS支持】 | lookbehind positive |
零宽负向后行断言 | (?<!exp) | 匹配某位置,该位置前面内容不匹配表达式 exp,【JS支持】 | lookbehind negative |
-
先行(lookahead)/ 后行(lookbehind)
先行断言,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为先行。
后行断言,引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。
-
正向(positive)/ 负向(negative)
正向就表示匹配括号中的表达式,负向表示不匹配。
js
// 零宽正向先行断言
(/Hello (?=World)/).test('Hello World') // true
(/Hello (?=World)/).test('Hello RegExp') // false
// 零宽负向先行断言
(/Hello (?!World)/).test('Hello World') // false
(/Hello (?!World)/).test('Hello RegExp') // true
// 零宽正向后行断言
(/(?<=Hello) RegExp/).test('Hello RegExp') // true
(/(?<=Hello) RegExp/).test('Welcome RegExp') // false
// 零宽负向后行断言
(/(?<!Hello) RegExp/).test('Hello RegExp') // false
(/(?<!Hello) RegExp/).test('Welcome RegExp') // true
匹配模式
这一节我们讲一下正则中的三种模式,贪婪匹配、非贪婪匹配和独占模式(JS不支持)。
这些模式会改变正则中量词的匹配行为,比如匹配一到多次;在匹配的时候,匹配长度是尽可能长还是要尽可能短呢? 如果不知道贪婪和非贪婪匹配模式,我们写的正则很可能是错误的,这样匹配就达不到期望的效果。
由于本节内容和量词相关的元字符密切相关,所以我们先来回顾一下正则表达式中表示量词的元字符
贪婪匹配(Greedy)
我们来看一下贪婪匹配。在正则中,表示次数的量词 默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。
首先,我们来看一下在字符串 aaabb 中使用正则 a*
的匹配过程。
字符串 | aaabb |
---|---|
下标 | 012345 |
匹配 | 开始 | 结束 | 说明 | 匹配内容 |
---|---|---|---|---|
第 1 次 | 0 | 3 | 到第一个字母b发现不满足,输出 aaa | aaa |
第 2 次 | 3 | 3 | 匹配剩下b发现匹配不上,输出空字符串 | 空字符串 |
第 3 次 | 4 | 4 | 匹配剩下b发现匹配不上,输出空字符串 | 空字符串 |
第 4 次 | 5 | 5 | 匹配剩下空字符串,输出空字符串 | 空字符串 |
非贪婪匹配(Lazy)
正则表达式默认是贪婪的,那么如何将贪婪模式变成非贪婪模式呢?我们可以 在量词后面加上英文的问号 (?
) 即可,正则就变成了 a*?
对比 /'.+'/
和 /'.+?'/
的区别
js
("'Hello Tom', 'Welcome to China'").match(/'.+'/) // ["'Hello Tom', 'Welcome to China'"]
("'Hello Tom', 'Welcome to China'").match(/'.+?'/) // ["'Hello Tom']
独占模式匹配(JS不支持
)
独占模式虽然在 Javascript 中不支持,但对理解贪婪模式和非贪婪模式匹配却很有帮助,所以也在这里进行讲解。
不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式,独占模式,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。
你可能会问,那什么是回溯呢?我们来看一些例子
-
例如下面贪婪模式下的正则:
jsregex = "xy{1,3}z" text = "xyyz"
在匹配时,
y{1,3}
会尽可能长地去匹配,当匹配完 xyy 后,由于 y 要尽可能匹配最长,即三个,但字符串中后面是个 z 就会导致匹配不上,这时候正则就会 向前回溯,吐出当前字符 z,接着用正则中的 z 去匹配。 -
如果我们把这个正则改成 非贪婪模式:
jsregex = "xy{1,3}?z" text = "xyyz"
由于
y{1,3}?
代表匹配 1 到 3 个 y,尽可能少地匹配 。匹配上一个 y 之后,也就是在匹配上 text 中的 xy 后,正则会使用 z 和 text 中的 xy 后面的 y 比较,发现正则 z 和 y 不匹配,这时正则就会 向前回溯,重新查看 y 匹配两个的情况,匹配上正则中的 xyy,然后再用 z 去匹配 text 中的 z,匹配成功。
我们比对理解了贪婪模式 和 非贪婪模式 ,那么如何理解独占模式呢?它和贪婪模式很像,独占模式会尽可能多地去匹配,但如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。
自测/实践
到这里我们已经全面梳理并学习了正则表达式基础知识和相关API的应用。下面我列举一些需要正则表达式实现的例子,感兴趣的朋友们可以尝试实践下:
转换日期格式
js
// 原日期 2023-09-06 15:15:28
//
// a) 转换成 2023年09月06日 15时15分秒
//
提取 div标签
js
// 内容: '<html><head></head><body><div><div>innner content</div></div></body></html>'
//
// a) 提取: '<div><div>innner content</div></div>'
// b) 提取: '<div>innner content</div>'
// c) 提取: 'innner content'
//
转换 glob
语法
将 glob
语法转换成 RegExp 对象
glob 通配符 | glob 描述 | 例子 | 匹配 | 不匹配 |
---|---|---|---|---|
* |
匹配任意数量的任何字符,包括无 | Law* |
Law ,Laws ,Lawyer |
GrokLaw ,La ,aw |
? |
匹配任何 单个 字符 | ?at |
Cat ,cat ,Bat ,bat |
at |
[abc] |
匹配括号中给出的一个字符 | [CB]at |
Cat ,Bat |
cat ,bat |
[a-z] |
匹配括号中给出的范围中的一个字符 | Letter[0-9] |
Letter0 ,Letter1 ...Letter9 |
Letters ,Letter ,Letter10 |
[!abc] |
匹配括号中未给出的一个字符 | [!C]at |
Bat ,bat ,cat |
Cat |
[!a-z] |
匹配不在括号内给定范围内的一个字符 | Letter[!3-5] |
Letter1 ... |
Letter3 ...Letter5 , Letterxx |
{a..z} |
匹配括号中给出的一个字符,等同于[abc] | {CB}at |
Cat ,Bat |
cat ,bat |
{start..end} |
会匹配连续范围的字符 | d{a..d}g |
dag ,dbg ,dcg ,ddg |