CcbeanBlog CcbeanBlog
首页
  • 前端文章

    • JavaScript
    • HTML+CSS
    • Vue
    • React
  • 系列笔记

    • React使用学习
    • Vue2源码探究
  • Node文章

    • 基础
    • 问题
    • 框架
  • 系列笔记

    • 数据结构与算法
  • 构建工具文章

    • webpack
  • 系列笔记

    • Webpack5使用学习
  • MySQL
  • Linux
  • 网络
  • 小技巧
  • 杂记
  • 系列笔记

    • Protobuf Buffers
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Ccbean

靡不有初,鲜克有终
首页
  • 前端文章

    • JavaScript
    • HTML+CSS
    • Vue
    • React
  • 系列笔记

    • React使用学习
    • Vue2源码探究
  • Node文章

    • 基础
    • 问题
    • 框架
  • 系列笔记

    • 数据结构与算法
  • 构建工具文章

    • webpack
  • 系列笔记

    • Webpack5使用学习
  • MySQL
  • Linux
  • 网络
  • 小技巧
  • 杂记
  • 系列笔记

    • Protobuf Buffers
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

    • JS基础
    • JS的执行环境及作用域
    • JS中原型对象的性质
    • 关于JSON
    • 阅读JavaScript高级程序设计总结
    • 项目搭建规范的配置
    • 阅读精通正则表达式总结
      • 第1章 正则表达式入门
        • 什么是正则表达式
        • 行的起始^和 结束$
        • 字符组 [ ]
        • 连字符
        • 排除型字符组
        • 任意字符 .
        • 多选结构 |
        • 可选项元素 ?
        • 重复出现元素 + *
        • 量词
        • 括号及反向引用
        • 转义符
        • 理解子表达式
        • 更多的例子
      • 第二章 入门示例拓展
        • 匹配正负号的浮点数
        • 非捕获型括号
        • \s的作用
        • 修正股票价格
        • 环视功能
        • 环视功能给数值插入逗号
      • 第三章 正则表达式的特性和流派概览
        • 程序中处理正则表达式的方式
        • 集成式处理
        • 程序式和面向对象式
        • 元字符
        • 修饰符
      • 第四章 正则表达式的匹配原理
        • 正则表达式的零件
        • 匹配基础
        • 规则1:优先选择最左端(最靠开头)的匹配结果。
        • 规则2 标准量词是匹配优先的
        • 过度优先匹配
        • 先来先服务
        • NFA和DFA
  • HTML+CSS

  • Vue

  • React

  • TypeScript

  • 系列笔记

  • 前端
  • JavaScript
ccbean
2022-09-29
目录

阅读精通正则表达式总结

# 精通正则表达式

本书的目的不是提供具体问题的解决办法,而是教会读者利用正则表达式来思考,解决遇到的各种问题。

正则表达式不是死板的教条,它更像是门艺术。

# 第1章 正则表达式入门

# 什么是正则表达式

完整的正则表达式由两种字符构成:特殊字符,如元字符* 和 普通文本字符。

类比日常语言,普通字符对应单词,特殊字符对应的就是语法。根据语言规则,按照语法把单词组合起来,就会得到能传达思想的文本。

完整的正则表达式由小的构建模块单元(building block unit)组成,每个单独的构建模块都很简单,不过因为它们能够以无穷多种方式组合,将它们结合起来实现特殊目标必须依靠经验。

# 行的起始^和 结束$

脱字符号^代表一行的开始; 美元符号$代表一行的结束。

脱字符号^和美元符号$的特别之处就在于,它们匹配的是一个位置,而不是具体的文本。

最好能养成按照字符来理解正则表达式的习惯:

  • 不要这样:^cat匹配以cat开头的行
  • 而要按照字符解读:^cat匹配的是以 c 作为一行的第一个字符,紧接一个 a,紧接一个 t 的文本。

这两种理解的结果并无差异,但按照字符来解读更易于明白新遇到的正则表达式的内部逻辑。

精通正则表达式1-01

# 字符组 [ ]

字符组的表达式为[...],它的作用是列出在某处期望匹配的字符。

例子:搜索单词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。当然,如果量词之前紧邻的是一个括号包围的子表达式,整个子表达式(无论多复杂)都被视为一个单元。

# 更多的例子

  1. 匹配程序中的标识符:标识符只包含字母、数字以及下划线,但不能以数字开头。

    const regex = /[a-zA-Z_][a-zA-Z_0-9]*/
    
    // 如果标识符长度有限制,如最长32个字符
    const regex2 = /[a-zA-Z_][a-zA-Z_0-9]{0,31}/
    
  2. 匹配引号内的字符串。

    // 用[^"]匹配除双引号之外的任何字符
    const regex = /"[^"]*"/
    
  3. 匹配美元金额,可能包含小数

    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)
    
  4. 匹配时间,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)
    
  5. 匹配时间,24小时制,如18:17、09:45

    精通正则表达式1-02

# 第二章 入门示例拓展

# 匹配正负号的浮点数

匹配有正负号的浮点数(正号可省略),小数部分可有可无

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、锚点^和$相似,只匹配文本中特定位置,不匹配任何字符。

精通正则表达式1-03

环视尝试匹配子表达式,在检查子表达式能否匹配的过程中,它们本身不会“占用”任何文本。

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/匹配结果:

精通正则表达式1-04

/(?=Jeffrey)/匹配结果:

精通正则表达式1-05

顺序环视会检查子表达式能否匹配,但它只寻找能够匹配的位置,而不会真正“占用”这些字符。

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)能匹配的位置,所以整个表达式无法匹配。

精通正则表达式1-06

把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。

精通正则表达式1-07

方法4: 实际上并没有匹配任何字符,只是匹配了我们希望插入撇号的位置。在这种情况下,我们并没有“替换”任何字符,而只是插入了一个撇号。

精通正则表达式1-08

评价:

精通正则表达式1-09

# 环视功能给数值插入逗号

一句话,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 种处理正则表达式的方式:

  1. 集成式(integrated):正则表达式是直接内建在语言之中的,Perl就是如此。
  2. 程序式(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)”搜索,匹配从目标字符串的当前位置开始。

# 第四章 正则表达式的匹配原理

# 正则表达式的零件

  1. 文字文本:例如a、\*、!、枝…
    • 如果一个正则表达式只包含纯文本字符,例如usa,那么正则引擎会将其视为:一个u,接着一个s,接着一个a。进行不区分大小写的匹配时的情况要复杂一点,因为b能够匹配B,而B也能匹配b
  2. 字符组、点号、Unicode属性及其他
    • 通常情况下,这种匹配是比较简单的:无论字符组的长度是多少,它都只能匹配一个字符。
    • 点号可以很方便地表示复杂的字符组,它几乎能匹配所有字符,所以它的作用也很简 单
    • 其他的简便方式还包括\w、\W和\d
  3. 捕获型括号
    • 用于捕获文本的括号(而不是用于分组的括号)不会影响匹配的过程
  4. 锚点:例如,^、\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。

# NFA和DFA

编辑 (opens new window)
上次更新: 2023/07/05, 23:08:46
项目搭建规范的配置
HTML和CSS基础学习

← 项目搭建规范的配置 HTML和CSS基础学习→

最近更新
01
项目搭建规范的配置
07-15
02
Vite的使用
07-03
03
Rollup的使用
06-30
更多文章>
Theme by Vdoing | Copyright © 2018-2023 Ccbeango
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式