阅读精通正则表达式总结
# 精通正则表达式
本书的目的不是提供具体问题的解决办法,而是教会读者利用正则表达式来思考,解决遇到的各种问题。
正则表达式不是死板的教条,它更像是门艺术。
# 第1章 正则表达式入门
# 什么是正则表达式
完整的正则表达式由两种字符构成:特殊字符,如元字符*
和 普通文本字符。
类比日常语言,普通字符对应单词,特殊字符对应的就是语法。根据语言规则,按照语法把单词组合起来,就会得到能传达思想的文本。
完整的正则表达式由小的构建模块单元(building block unit)组成,每个单独的构建模块都很简单,不过因为它们能够以无穷多种方式组合,将它们结合起来实现特殊目标必须依靠经验。
# 行的起始^和 结束$
脱字符号^
代表一行的开始; 美元符号$
代表一行的结束。
脱字符号^
和美元符号$
的特别之处就在于,它们匹配的是一个位置,而不是具体的文本。
最好能养成按照字符来理解正则表达式的习惯:
- 不要这样:
^cat
匹配以cat开头的行 - 而要按照字符解读:
^cat
匹配的是以 c 作为一行的第一个字符,紧接一个 a,紧接一个 t 的文本。
这两种理解的结果并无差异,但按照字符来解读更易于明白新遇到的正则表达式的内部逻辑。
# 字符组 [ ]
字符组的表达式为[...]
,它的作用是列出在某处期望匹配的字符。
例子:搜索单词grey
或gray
[ea]
能够匹配到a
或e
,所以/gr[ea]y/
就是先找到g,跟着是一个r,然后是
一个a或者e,最后是一个y。
注意:
- 在字符组以外,普通字符(例如
/gr[ae]y/
中的 g 和 r)都有接下来是(and then)的意思,首先匹配 g,接下来是 r ······ - 在字符组内部,情况完全相反,字符组的内容是在同一个位置能够匹配的若干字符,它的意思是或
# 连字符
在字符组内部,连字符-
表示一个范围,而且允许多重范围,也允许随意组合字符范围和普通文本:
[0-9]
、[a-z]
:匹配一个数字、匹配一个小写字母[0-9a-fA-F]
:匹配一个数字、小写字母 a 到 f或大写字母 A 到 F 。顺序无所谓,即等价于[A-Fa-f0-9]
[0-9A-Z_!.?]
:匹配一个数字、大写字母、下画线、惊叹号、点号,或问号- 字符组开头的连字符只是一个普通的连字符号
在字符组内部,真正的特殊字符是连字符,在字符组外部的特殊字符在内部仅是普通字符。可以把字符组看作独立的微型语言。在字符组内部和外部,关于元字符的规定(哪些是元字符,以及它们的意义)是不同的
# 排除型字符组
排除型字符组的表达式为[^...]
,这个字符组匹配任何未列出的字符,也就是这里列出的是不希望匹配的字符。
脱字符号^
必须在第一个方括号之后,表示排除,否则它只是一个普通字符。
例子:
[^1-6]
匹配1到6以外的任何字符q[^u]
匹配字母 q 后面字母不是 u- 可以匹配到单词Iraqi、qasida
- 不可以匹配单词Iraq,因为一个字符组,即使是排除型字符组,也要匹配一个字符。如果Iraq后面又空格或者换行符,也可以匹配到。
请记住,排除型字符组表示:匹配一个未列出的字符,而不是不要匹配列出的字符。(match a character that's not listed,don't match what is listed)
# 任意字符 .
点号.
用来匹配一个任意字符。它是用来匹配任意字符的字符组的简写。
例子:搜索03/19/76、03-19-76或者03.19.76
这里,可以用一个明确的字符组[-./]
来匹配,也可以使用点号.
来匹配
const str = '03/19/76 03-19-76 03.19.76'
// 方法一
const regex1 = /03[-./]19[-./]76/g
// match1:['03/19/76', '03-19-76', '03.19.76']
const match1 = str.match(regex1)
// 方法二
const regex2 = /03.19.76/g
// match2:['03/19/76', '03-19-76', '03.19.76']
const match2 = str.match(regex2)
方法一更准确。方法二还可以匹配到03x19y76
或19 203319 7639
等很多其它结果。
那么应该如何选择?取决于你对需要检索文本的了解,以及你需要达到的准确程度。
一个重要但常见的问题是,写正则表达式时,我们需要在对欲检索文本的了解程度与检索精确性之间求得平衡。例如,如果我们知道,针对某个检索文本,/03.19.76/g
这个正则表达式基本不可能匹配不期望的结果,那么使用它就是合理的。要想正确使用正则表达式,清楚地了解目标文本是非常重要的。
# 多选结构 |
竖杠|
表示或,可以用它将把不同的子表达式组合成一个总的表达式,而这个总的表达式又能匹配任意的子表达式。
Rob
和Robert
是两个子表达式,Rob|Robert
就可以匹配其中任意一个正则表达式。在这样的组合中,子表达式被称为多选分支(alternative)。
例子:搜索单词grey
或gray
字符组方法:/gr[ea]y/
多选结构:
/grey|gray/
- 或使用括号
/gr(a|e)y/
。
**括号可以用来界定元字符的作用范围。**多选结构可以包括很多字符,但不能超越括号的界限。
其它例子:
/(First|1st) [Ss]treet/
,都是st
结尾,也可以把前面部分改成/(Fir|1)st/
const str = 'First Street | First street | 1st Street | 1st street' const regex1 = /(First|1st) [Ss]treet/g const match1 = str.match(regex1) console.log(match1) const regex2 = /(Fir|1)st [Ss]treet/g const match2 = str.match(regex2) console.log(match2)
^(From|Subject|Date):
,匹配以From:Subject:或者Date:开头的文本行
# 可选项元素 ?
问号?
表示之前紧邻的元素出现零次或一次。
例子:匹配7月4日,其中月份可写为July或Jul,日子可写为fourth、4th、4
正则表达式:/July? (fourth|4(th)?)/
,括号用于界定元字符的作用范围。
const str = 'July 4th | July fourth | July 4 | Jul 4th | Jul 4'
const regex1 = /July? (fourth|4(th)?)/g
const match1 = str.match(regex1)
console.log(match1)
# 重复出现元素 + *
加号+
表示之前紧邻的元素出现一次或多次。
星号*
表示之前紧邻的元素出现任意多次,或者不出现。
例子:匹配HR标签,并且可设置SIZE
const str = '<HR SIZE=14> <HR SIZE = 14 > <HR SIZE= 14 >'
const regex1 = /<HR +SIZE *= *[0-9]+ *>/g
const match1 = str.match(regex1)
console.log(match1)
- 使用空格加加号
/ +/
来匹配HR和SIZE之间的一个空格,空格加星号/ */
来匹配后面位置的任意多个空格。
# 量词
问号、加号、星号这三个元字符统称为量词。因为它们限定了所作用元素的匹配次数。
每个量词都规定了匹配成功至少需要的次数下限,以及尝试匹配的次数上 限。对某些量词来说,下限是0,对某些量词来说,上限是无穷大。
这里还有一种量词是区间量词,它能够使用元字符序列来自定义重复次数的区间,语法为...{min,max}
,如...{3, 12}
表示能够容许前面的元素重现次数在3到12之间。
量词 | 次数下限 | 次数上限 | 含义 |
---|---|---|---|
? | 无 | 1 | 单次可选 |
* | 无 | 无 | 任意次数均可 |
+ | 1 | 无 | 至少一次 |
{min, max} | min | max | 出现min到max次 |
# 括号及反向引用
括号有两种用途:
- 第一种是上面提到的,界定元字符的作用范围
- 第二种是记住它们包含的子表达式匹配的文本,称为捕获,然后可以用反向引用来调用捕获到的值。
反向引用是正则表达式的特性之一,它允许我们匹配与表达式先前部分匹配的同样文本。也就是允许我们调用之前捕获到的值。
在一个表达式中我们可以使用多个括号。再用\1
、\2
、\3
等来表示第一、第二、第三组括号匹配的文本。括号是按照开括号(
从左至右的出现顺序进行
的,所以/([a-z])(0-9)\1\2
中的\1
代表[a-z]
匹配的内容,而\2
代表[0-9]
匹配的内容。
例子:查询重复的单词正则
const str = 'hello hello world, this is a test test, not the theory'
const regex = /\b([a-z]+) +\1\b/ig
const match = str.match(regex)
console.log('match', match)
# 转义符
在字符组外部,使用斜杠加元字符时,如\.
,斜杠\
表示转义符,它作用的元字符会失去而特殊含义,成为普通字符。
特殊的元字符包括:( [ { \ ^ $ | ) ] } ? * + .
# 理解子表达式
子表达式指的是整个正则表达式中的一部分,通常是括号内的表达式,或者是由|
分隔的多选分支。
例如,在^(Subject|Date):
中,Subject|Date
通常被视为一个子表达式。其中的Subject
和Date
也算得上子表达式。而且,严格说起来,S
、u
、b
、j
这些字符,都算子表达式。1-6
这样的字符序列并不能算H[1-6] *
的子表达式,因为1-6
所属的字符组是不可分割的单元(unit)。但是,H
、[1-6]
、·*
都是H[1-6]·*
的子表达式。
与多选分支不同的是,量词(星号、加号和问号)作用的对象是它们之前紧邻的子表达式。所以mis+pell
中的+
作用的是s
,而不是mis
或者is
。当然,如果量词之前紧邻的是一个括号包围的子表达式,整个子表达式(无论多复杂)都被视为一个单元。
# 更多的例子
匹配程序中的标识符:标识符只包含字母、数字以及下划线,但不能以数字开头。
const regex = /[a-zA-Z_][a-zA-Z_0-9]*/ // 如果标识符长度有限制,如最长32个字符 const regex2 = /[a-zA-Z_][a-zA-Z_0-9]{0,31}/
匹配引号内的字符串。
// 用[^"]匹配除双引号之外的任何字符 const regex = /"[^"]*"/
匹配美元金额,可能包含小数
const str = 'Shirts $9.45, pants $12, shoes $20' const regex = /\$[0-9]+(\.[0-9][0-9])?/g const match = str.match(regex) console.log(match)
匹配时间,12小时制,如
09:17 am
、12:30 pm
,避免匹配不正确时间,如99:33 am
const str = '09:17 am 12:30 pm 99:33 am' const regex = /(1[012]|0[1-9]):[0-5][0-9] (am|pm)/g const match = str.match(regex) console.log(match)
匹配时间,24小时制,如
18:17
、09:45
# 第二章 入门示例拓展
# 匹配正负号的浮点数
匹配有正负号的浮点数(正号可省略),小数部分可有可无
const str = ['5.55', '-99','-0.23', '+90.9']
const regex = /^[+-]?[0-9]+(\.[0-9]*)?$/g
str.forEach(item => {
const match = item.match(regex)
console.log(match)
})
- 使用
[+-]?
处理正负号 - 使用
(\.[0-9]*)?
处理小数部分 - 开头和结尾的
^...$
不可省略,以保证匹配结果只包含数字
# 非捕获型括号
()
可以用来分组和捕获它包含的子表达式,(?:)
只会分组,并不捕获,称为非捕获型括号。
例子,捕获温度和单位。
const temperature = '39C'
// 不使用非捕获行括号,CF两端的括号排在第3位,它匹配的文本会保存在$3中
const regex1 = /^([+-]?[0-9]+(\.[0-9]*)?)([CF])$/
const match1 = temperature.replace(regex1, '温度:$1 单位: $3')
console.log('match1', match1)
// 使用非捕获行括号,CF两端的括号虽然排在第3位,但它匹配的文本会保存在$2中
const regex2 = /^([+-]?[0-9]+(?:\.[0-9]*)?)([CF])$/
const match2 = temperature.replace(regex2, '温度:$1 单位: $2')
console.log('match2', match2)
# \s的作用
元字符 "\s" 可以匹配一个空白,包括空格、制表符、换页符和换行符。
通常使用\s*
表示任意多个空白字符。
那么,上面温度的单位之间可以有空格,表示就是
/^([+-]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/
如果C或F接收不区分大小写,可以使用修饰符i
,表示就是
/^([+-]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i
代码如下:
const regex = /^([+-]?[0-9]+(?:\.[0-9]*)?)\s*([CF])$/i
const temperature1 = '39C'
const match = temperature1.replace(regex, '温度:$1 单位: $2')
console.log('match', match)
const temperature2 = '39 F'
const match2 = temperature2.replace(regex, '温度:$1 单位: $2')
console.log('match2', match2)
const temperature3 = '39 f'
const match3 = temperature3.replace(regex, '温度:$1 单位: $2')
console.log('match3', match3)
# 修正股票价格
修正价格,通常是保留小数点后两位数字,如果第三位不为零,也需要保留,去掉其他的数字。结果就是12.375000000392或者12.375会被修正为“12.375”,而37.500被修正为“37.50”。
const regex = /(\.\d\d[1-9]?)\d*/
console.log('12.375000000392'.replace(regex, '$1'))
console.log('12.375'.replace(regex, '$1'))
console.log('37.500'.replace(regex, '$1'))
最开始的\.
匹配小数点。接下来的\d\d
匹配开头的两位数字,
[1-9]?
」匹配可能跟在后面的非零数字。到这里,任何匹配的文本都是我们希望保留的,所以用括号把它保存到$1
中。
之后替换文本可以直接使用$1
。末尾的\d*
用来匹配其余的多余数字。
# 环视功能
环视与单词分界符\b
、锚点^
和$
相似,只匹配文本中特定位置,不匹配任何字符。
环视尝试匹配子表达式,在检查子表达式能否匹配的过程中,它们本身不会“占用”任何文本。
const str = '...by Jeffrey Friedl.';
const regex = /Jeffrey/;
const regex2 = /(?=Jeffrey)/;
// regex 可以匹配到 Jeffrey 本身
const match = str.match(regex);
console.log('match', match);
// regex2 匹配到右侧是Jeffrey标记的位置,即Jeffrey左侧
const match2 = str.match(regex2);
console.log('match2', match2);
/Jeffrey/
匹配结果:
/(?=Jeffrey)/
匹配结果:
顺序环视会检查子表达式能否匹配,但它只寻找能够匹配的位置,而不会真正“占用”这些字符。
const str = '...by Jeffrey Friedl.';
const str2 = '...by Thomas Jefferson.';
const regex = /(?=Jeffrey)Jeff/;
// 能匹配到Jeff
const match1 = str.match(regex);
console.log('match1', match1);
// 不能匹配到Jeff
const match2 = str2.match(regex);
console.log('match2', match2);
先匹配到右侧是Jefferey的位置,然后再匹配Jeff;str2中因为没有Jeffrey,而是Jefferson,虽然能匹配到Jeff,但不存在(?=Jeffrey)
能匹配的位置,所以整个表达式无法匹配。
把Jeffs
替换为Jeff's
:
const str = 'Jeffs and Jeffs';
// 方法1
const regex = /\bJeffs\b/g
const res1 = str.replace(regex, "Jeff's");
console.log('res1', res1);
// 方法2
const regex2 = /\b(Jeff)(s)\b/g
const res2 = str.replace(regex2, "$1'$2");
console.log('res2', res2);
// 方法3
const regex3 = /\bJeff(?=s\b)/g
const res3 = str.replace(regex3, "Jeff'");
console.log('res3', res3);
// 方法4
const regex4 = /(?<=\bJeff)(?=s\b)/g
const res4 = str.replace(regex4, "'");
console.log('res4', res4);
方法3:Jeff
匹配之后,接下来尝试的就是顺序环视。只有当s\b
在此位置能够匹配时(也就是Jeff
之后紧跟一个s
一个单词分界符)整个表达式才能匹配成功。但是,因为s\b
只是顺序环视子表达式的一部分,所以它匹配的s
不属于最终的匹配文本。**记住,Jeff
确定匹配文本,而顺序环视只是“选择”一个位置。**在此处使用顺序环视的唯一好处在于,它保证表达式不会匹配任意的情况。或者从另一个角度来说就是,它容许我们在只匹配Jeff
之前检查整个Jeffs
。
方法4: 实际上并没有匹配任何字符,只是匹配了我们希望插入撇号的位置。在这种情况下,我们并没有“替换”任何字符,而只是插入了一个撇号。
评价:
# 环视功能给数值插入逗号
一句话,The US population is 298444215,对于英语阅读者,298,444,215
看起来更自然。
如果手动添加,从左到右,每三个数字,如果左边还有数字的话,就加入一个逗号,但正则表达式一般都是从左向右工作的。
const str = 'The population of 29844215 is growing';
const str2 = 'The US population is 298444215';
const regex = /(?<=\d)(?=(?:\d{3})+\b)/g;
const res1 = str.replace(regex, ",");
console.log('res1', res1);
const res2 = str2.replace(regex, ",");
console.log('res2', res2);
首先逆序环视,(?<=\d)
从右到左匹配左边有数字的右侧位置,这时可以匹配到左侧有数字的位置,然后从这个位置再使用顺序环视(?=(?:\d{3})+\b)
从左到右匹配右边有三个数字,查看这个位置的右侧是否有三个数字,如果有,就插入一个逗号。
表达式中还使用了非捕获型括号(?:\d{}3)
,这样做的好处在于,见到这个正则表达式的人不会担
心与捕获型括号关联的$1
是否会被用到;而且它的效率更高,因为引擎不需要记忆捕获的文本。
# 第三章 正则表达式的特性和流派概览
# 程序中处理正则表达式的方式
一般来说,程序设计语言有3 种处理正则表达式的方式:
- 集成式(integrated):正则表达式是直接内建在语言之中的,Perl就是如此。
- 程序式(procedural)和面向对象式(object-oriented):正则表达式不属于语言的低级语法。相反,普通的函数接收普通的字符串,把它们作为正则表达式进行处理。由不同的函数进行不同的、关系到一个或多个正则表达式的操作。大多数语言(不包括Perl)采用的都是这两种方式之一,包括Java、.NET、Tcl、Python、PHP、Emacs、lisp和Ruby。当然,还有JavaScript。
# 集成式处理
Perl 会把正则表达式^Subject:(.*)
应用到$line
保存的文本中,如果能够匹配,则执行下面的程序段。其中,变量$1
代表括号内的子表达式匹配的文本,将它们赋值给$subject
。
if ($line =~ m/^Subject:(.*)/i) {
$subject = $1;
}
# 程序式和面向对象式
程序式处理和面向对象式处理非常相似。这两种方式下,正则功能不是由内建的操作符来提供,而是由普通函数(函数式)或构造函数及方法(面向对象式)来提供的。这种情况下,并没有专属于正则表达式的操作符,只有平常的字符串,普通的函数、构造函数和方法把这些字符串作为正则表达式来处理。
const str = 'Subject:hello';
const regex = /^Subject:(.*)/;
const m = str.match(regex);
let subject;
if (m[1]) {
subject = m[1];
}
console.log(1)
const regex2 = new Regex(/^Subject:(.*)/);
const m2 = str.match(regex2);
# 元字符
MDN正则表达式:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_expressions
元字符 | 作用 |
---|---|
\s | 任何空白字符,包括空格符、制表符、进纸符等 |
\S | 除\s以外的任何字符,等价于[^\s] |
\d | 数字,即0-9 |
\D | 除数字\d之外的任何字符,即[^0-9] |
\w | 匹配字母数字下划线,即[a-zA-Z0-9_] |
\W | 除\w以外的任何字符,也就是[^a-zA-Z0-9_] |
\b | 匹配一个词边界 |
\r | 回车符 |
\n | 换行符 |
\t | 水平制表符 |
\v | 垂直制表符 |
[\b] | 退格符 |
\e | Escape字符 |
# 修饰符
修饰符 | 作用 |
---|---|
g | 全局匹配 |
i | 不区分大小写 |
m | 多行搜索 |
s | 允许. 匹配换行符 |
u | 使用unicode码的模式进行匹配 |
y | 执行“粘性 (sticky )”搜索,匹配从目标字符串的当前位置开始。 |
# 第四章 正则表达式的匹配原理
# 正则表达式的零件
- 文字文本:例如a、\*、!、枝…
- 如果一个正则表达式只包含纯文本字符,例如
usa
,那么正则引擎会将其视为:一个u
,接着一个s
,接着一个a
。进行不区分大小写的匹配时的情况要复杂一点,因为b
能够匹配B,而B
也能匹配b
- 如果一个正则表达式只包含纯文本字符,例如
- 字符组、点号、Unicode属性及其他
- 通常情况下,这种匹配是比较简单的:无论字符组的长度是多少,它都只能匹配一个字符。
- 点号可以很方便地表示复杂的字符组,它几乎能匹配所有字符,所以它的作用也很简 单
- 其他的简便方式还包括
\w
、\W
和\d
- 捕获型括号
- 用于捕获文本的括号(而不是用于分组的括号)不会影响匹配的过程
- 锚点:例如,
^
、\z
、(?<=\d)
- 锚点可以分为两大类:简单锚点(^、$、\G、\b)和复杂锚点(例如顺序 环视和逆序环视)。简单锚点之所以得名,就在于它们只是检查目标字符串中的特 定位置的情况(^、\Z),或者是比较两个相邻的字符(\<、\b、…)。相反,复杂锚 点(环视)能包含任意复杂的子表达式,所以它们也可以任意复杂。
# 匹配基础
# 规则1:优先选择最左端(最靠开头)的匹配结果。
这条规则的由来是:匹配先从需要查找的字符串的起始位置尝试匹配。在这里,“尝试匹配(attempt)”的意思是,在当前位置测试整个正则表达式(可能很复杂)能匹配的每样文本。如果在当前位置测试了所有的可能之后不能找到匹配结果,就需要从字符串的第二个字符之前的位置开始重新尝试。在找到匹配结果以前必须在所有的位置重复此过程。只有在尝试过所有的起始位置(直到字符串的最后一个字符)都不能找到匹配结果的情况下,才会报告“匹配失败”。
如果要用「ORA」来匹配FLORAL,从字符串左边开始第一轮尝试会失败(因为「ORA」不能匹配FLO),第二轮尝试也会失败(「ORA」同样不能匹配LOR),从第三个字符开始的尝试能够成功,所以引擎会停下来,报告匹配结果。
使用fat|cat|belly|your
匹配 The dragging belly indicates that your cat is too fat. 结果是什么?
答案是:belly。
原因:尽管fat在最前面,而且正则表达式应该也能匹配到其它可能,但它们都不是最先出现的匹配结果,所以不会被选择。在下一轮尝试之前,正则表达式的所有可能都会尝试,也就是说,在移动之前,fat、cat、belly和yours都必须尝试。
const str = 'The dragging belly indicates that your cat is too fat.';
const regex = /fat|cat|belly|your/;
const m = str.match(regex);
console.log(m[0]);
# 规则2 标准量词是匹配优先的
标准匹配量词?
、*
、+
以及{min,max}
都是匹配优先(greedy)的。
简而言之,标准匹配量词的结果可能并非所有可能中最长的,但它们总是尝试匹配尽可能多的字符,直到匹配上限为止。
const str = 'March 1998';
const regex = /[0-9]+/;
const m = str.match(regex);
// ['1998', index: 6, input: 'March 1998', groups: undefined]
console.log(m);
上面的匹配到了所有数字,1匹配之后,实际上已经满足了成功的下限,但此正则表达式是匹配优先的,所以它不会停在此处,而会继续下去,继续匹配998,直到这个字符串的末尾。
# 过度优先匹配
来看一个正则表达式理解下这个规则:
正则表达式/^.*([0-9][0-9])/
,能够匹配一行字符的最后两位数字,如果有的话,然后将它们存储在$1 中。下面是匹配的过程:
.*
首先过度优先匹配整行,而[0-9] [0-9]
是必须匹配的,在尝试匹配行末的时候会失败,这样它会通知.*
:“嗨,你占的太多了,交出一些字符来吧,这样我没准能匹配。”匹配优先组件首先会匹配尽可能多的字符,但为了整个表达式的匹配,它们通常需要“释放”一些字符(抑制自己的天性)。当然,它们并不“愿意”这样做,只是不得已而为之。
当然,“交还”绝不能破坏匹配成立必须的条件,比如标准量词加号
+
的第一次匹配。
const str = 'about 24 characters long';
const regex = /^.*([0-9][0-9])/;
const m = str.match(regex);
// ['about 24', '24', index: 0, input: 'about 24 characters long', groups: undefined]
console.log(m);
上面代码的实际匹配过程如下:
.*
匹配整个字符串以后,第一个[0-9]
的匹配要求.*
释放一个字符g(最后的字符)。但是这并不能让[0-9]
匹配,所以.*
必须继续“交还”字符,接下来交还的字符是n。如此循环15次,直到.*
最终释放数字4为止。- 不幸的是,即使第一个
[0-9]
能够匹配4,第二个[0-9]
仍然不能匹配。为了匹配整个正则表达式,.*
必须再次释放一个字符,这次是2,由第一个[0-9]
匹配。 - 现在,4能够由第二个
[0-9]
匹配,所以整个表达式匹配的是about 24,$1
的值是24。
# 先来先服务
如果用/^.*[0-9]+/
来匹配Copyright 2003. ,括号会捕获到什么?
const str = 'Copyright 2003.';
const regex = /^.*([0-9]+)/;
const m = str.match(regex);
// ['Copyright 2003', 'Copyright 200', '3', index: 0, input: 'Copyright 2003.', groups: undefined]
console.log(m);
这个表达式本意是捕获整个数字2003,但结果并非如此。因为过度有限匹配,^.*
会首先匹配整行,为了满足[0-9]+
的匹配,.*
必须交还一些字符。在这个例子中,释放的字符是最后的3和点号,之后3能够由[0-9]
匹配。[0-9]
由 +
量词修饰,所以现在还只做到了最小的匹配可能,现在它遇到了.
,找不到其他可以匹配的字符。
与之前不同,此时没有“必须”匹配的元素,所以.
不会被迫交出0
。否则,[0-9]+
应当心存感激,接受匹配优先元素的馈赠,但请记住“先来先服务”原则。匹配优先的结构只会在被迫的情况下交还字符。所以,最终$1
的值是3。
注:这里所说的.*
必须继续“交还”或许会引起混淆,这么说是便于理解,而且跟实际结果一致。真相是由基本的引擎类型决定的,是DFA还是NFA。