「正则表达式」全解

1,040 阅读7分钟

我正在参加「掘金·启航计划」

前言

谈起正则表达式每次都是去看看文档,看了就忘,大部分实际用的时候都是谷歌一下答案,那么为了提升对正则表达式的理解,就让我们一起通过案例再次学习吧!!!

注明:以下知识内容是对 现代 JavaScript 教程-正则篇的笔记 ,一切请以官方为准,这里只是做了简单的笔记。

强烈推荐!!!

除此之外,额外加了一些小知识。

正则测试网站: 链接

正则轨道图网站:链接

修饰符

g 全局

i 不区分大小写

m 多行模式

s 启用 “dotall” 模式,允许点 . 匹配换行符 \n

u 开启完整的 Unicode 支持,该修饰符能够正确处理代理对

y 粘滞模式,在文本中的确切位置搜索

let str = "we we W  WE we dWEd dWE dwed wed ddwe"
console.log( str.match(/we/gi) )  // 表示全局搜索 we 不区分大小写
// /we/i/ 表示匹配第一个 we, 没有匹配项,不会收到一个空数组,而是会收到 null
// 如果希望保底为数组
console.log( str.match(/we/i ) || [] )

// 替换
console.log( "we we W  WE we dWEd dWE dwed wed ddwe".replace(/we/i, "hone") )

// 找到至少一个匹配项则返回 true,否则返回 false
/we/i.test(str)  // true

Screen Shot 2022-10-03 at 7.51.36 PM.png

字符类

正则含义
/\d/数字:0-9
/\s/空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v\f 和 \r
/\w/「单字」字符:拉丁字母或数字或下划线 _。非拉丁字母(如西里尔字母或印地文)不属于 \w

Screen Shot 2022-10-03 at 8.13.24 PM.png

let str = "+7(903)-123-45-67" 
console.log( str.match(/\d/) ) // 7  匹配第一个
console.log( str.match(/\d/g) ).join('') // 查找所有,最终结果 79031234567
str.replace(/\D/g, "") // 查找非数字并删除,最终结果 79031234567

反向类

正则含义
/\D//d 以外的任何字符
/\S//s 以外的任何字符
/\W//w 以外的任何字符

点(.)匹配「任何字符」

. 它与除换行符之外的任何字符匹配, 默认情况下,点与换行符 \n 不匹配

Screen Shot 2022-10-03 at 8.25.06 PM.png

写成 /h./s, 就可以匹配任何字符

Screen Shot 2022-10-03 at 8.55.10 PM.png

对于空格,平时容易忽略

// 以下两种都可以匹配到
console.log("1 - 2".match(/\d - \d/))
console.log("1 - 2".match(/\d/s-\s\d/))

⚠️注意:在正则表达式中,所有字符都很重要。

Unicode:修饰符 "u" 和类 \p{...}

Unicode 属性 \p{…}

现代JS教程详细链接

// \p{L} 表示任何语言中的一个字母
let str = "A ბ ㄱ"; alert( str.match(/\p{L}/gu) ) // A,ბ,ㄱ 
console.log( str.match(/\p{L}/g) ); // null(没有匹配项,因为没有修饰符 "u")

修饰符 u 表示启用正则表达式中对 Unicode 的支持。

  1. 4 个字节长的字符被以正确的方式处理:被看成单个字符,而不是 2 个 2 字节长的字符。
  2. Unicode 属性可以被用于查找:\p{…}

锚点:字符串开始 ^ 和末尾 $

插入符号 ^ 匹配文本开头,而美元符号 $ 则匹配文本末尾

// /^hone/ 的意思是字符串开始,紧接着就是 ^hone
/^hone/.test("hone age 18")   
// /18$/ 是否以18结尾
/18$/.test("hone age 18")   

测试完全匹配

// 所对应的匹配项必须正好在文本 '^' 的开头之后开始,并且结尾 '$' 必须紧跟其后
"12:24".test(/^\d\d:\d\d$/)  // 可以匹配到

锚点 ^ 和 $ 属于测试,它们的宽度为零。

什么字符串可以匹配模式 ^$? 答:空字符串是唯一的匹配项:它开始并立即结束

锚点 ^ $ 的多行模式,修饰符 "m"

m 启用多行模式修饰符 ,可以匹配每一行的^$

Screen Shot 2022-10-03 at 9.31.27 PM.png 默认情况下,锚点 ^ 仅匹配文本的开头,在多行模式下,它匹配行的开头

搜索行的末尾 $

\d$ 寻找每行的最后一个数字。

查找每行以数字开头,中间匹配重复出现1次或者更多次数字,并且以数字结尾的匹配字符 Screen Shot 2022-10-03 at 9.34.39 PM.png

寻找新的一行

可以使用 \d\n 来寻找新的一行

Screen Shot 2022-10-03 at 9.42.34 PM.png

Screen Shot 2022-10-03 at 9.42.07 PM.png

词边界:\b

遇到 \b 时,它会检查字符串中的位置是否是词边界

Screen Shot 2022-10-03 at 9.50.05 PM.png ⚠️注意: 词边界 \b 不适用于非拉丁字母

转义,特殊字符

转义

我们想要找到一个 + 号,,要将特殊字符用作常规字符,那么就需要在前面加上:\+

Screen Shot 2022-10-03 at 9.55.53 PM.png

  • 要在字面意义上搜索特殊字符 [ \ ^ $ . | ? * + ( ),我们需要在它们前面加上一个反斜杠 \(“转义它们”)。

集合和范围 [...]

集合

[…]  表示查找满足的 "任意一个" 。

Screen Shot 2022-10-03 at 10.00.47 PM.png

范围

方括号也可以包含 字符范围

Screen Shot 2022-10-03 at 10.03.12 PM.png

字符类是某些字符集合的简写:

  • \d —— 和 [0-9] 相同
  • \w —— 和 [a-zA-Z0-9_] 相同
  • \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 Unicode 空格字符相同

排除范围

[^…] "排除"范围匹配

  • [^aeyo] —— 匹配除了 'a''e''y' 或 'o' 之外的任何字符。
  • [^0-9] —— 匹配除了数字之外的任何字符,与 \D 作用相同。
  • [^\s] —— 匹配任何非空格字符,与 \S 作用相同
  • [^] —— 表示任意字符,即对空值取反,得到全集

[…] 中的转义

在方括号中,我们可以使用绝大多数特殊字符而无需转义:

  • 符号 . + ( ) 无需转义。
  • 在开头或结尾(未定义范围)的连字符 - 不会被转义。
  • 插入符号 ^ 仅在开头会被转义(表示排除)。
  • 右方括号 ] 总是会被转义(如果我们需要寻找那个符号)

如果你为了“以防万一”转义了它们,这也不会有任何问题

范围和修饰符 “u”

如果集合中有代理对(surrogate pairs),则需要标志 u 才能使它们正常工作。

console.log( '𝒳'.match(/[𝒳𝒴]/u) ) // 𝒳

// 忘记写 u
'𝒳'.match(/[𝒳-𝒴]/); // Error: Invalid regular expression

量词 +, *, ? 和 {n}

{n}

在一个字符(或一个字符类,或 [...] 等)后附加一个量词,用来指出我们具体需要的数量。

\d{5} 表示 5 位数,与 \d\d\d\d\d 相同。

确切的位数:{5}

Screen Shot 2022-10-03 at 10.15.57 PM.png

范围:{2,5},匹配 2-5 个

Screen Shot 2022-10-03 at 10.17.05 PM.png

\d{2,} 查找大于2的数字

Screen Shot 2022-10-03 at 10.18.45 PM.png

缩写

+

代表 一个或多个,与 {1,} 相同。 Screen Shot 2022-10-03 at 10.20.55 PM.png Screen Shot 2022-10-03 at 10.26.42 PM.png

代表 零个或一个,与 {0,1} 相同。换句话说,它使得符号变得可选。 Screen Shot 2022-10-03 at 10.25.11 PM.png

*

代表 零个及以上,与 {0,} 相同。也就是说,字符可以出现任何次数或者不出现。 Screen Shot 2022-10-03 at 10.26.05 PM.png

贪婪量词和惰性量词

Screen Shot 2022-10-03 at 10.36.06 PM.png

Screen Shot 2022-10-04 at 7.39.56 PM.png

贪婪搜索、惰性模式 具体过程

捕获组 (...)

  • 方法 str.match 仅当不带修饰符 g 时返回捕获组。
  • 方法 str.matchAll 始终返回捕获组

Screen Shot 2022-10-04 at 7.49.03 PM.png

Screen Shot 2022-10-04 at 7.49.20 PM.png

// 域名搜索
"site.com my.site.com".match(/(\w+\.)+\w+/g) // site.com my.site.com

// 电子邮件
"my@mail.com @ his@site.com.uk".match(/[-.\w]+@([\w-]+\.)+[\w-]+/g) // my@mail.com, his@site.com.uk

嵌套组

let str = '<span class="my">'
let regexp = /<(([a-z]+)\s*([^>]*))>/
let result = str.match(regexp) 
alert(result[0]) // <span class="my"> 
alert(result[1]) // span class="my" 
alert(result[2]) // span 
alert(result[3]) // class="my"

可选组

'a'.match(/a(z)?(c)?/)

Screen Shot 2022-10-04 at 7.58.46 PM.png

命名组

// 这里的 <year> <month> <day> 就是对每个数组进行命名
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";

// 匹配的组在 .groups 属性中
let groups = str.match(dateRegexp).groups;

alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30

替换中的捕获组

$n 中的 n 是组号

let str = "John Bull"; 
let regexp = /(\w+) (\w+)/;  // 这里分成两组

// $1 表示 John,$2 表示 Bull
alert( str.replace(regexp, '$2, $1') ); // Bull, John

// 命名的括号 引用为 $<name>
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g; 
let str = "2019-10-30, 2020-01-01"; 

alert( str.replace(regexp, '$<day>.$<month>.$<year>') ); // 30.10.2019, 01.01.2020

非捕获组

通过在组的开头添加 ?: 来排除编号。

let str = "Gogogo John!"; 

// ?: 从捕获组中排除 'go' 
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp); 
alert( result[0] ); // Gogogo John(完整的匹配项) 
alert( result[1] ); // John 
alert( result.length ); // 2(在数组中没有其他数组项)

模式中的反向引用:\N\k<name>

按编号反向引用:\N

let str = `He said: "She's the one!".`; 

let regexp = /(['"])(.*?)\1/g;

alert( str.match(regexp) ); // "She's the one!"

代码执行过程:正则表达式引擎会找到第一个引号 (['"]) 并记住其内容。那是第一个捕获组。

在模式中 \1 表示“找到与第一组相同的文本”,在我们的示例中为完全相同的引号。

与此类似,\2 表示第二组的内容,\3 —— 第三分组,依此类推。

  • 注意:如果我们在捕获组中使用 ?:,那么我们将无法引用它。用 (?:...) 捕获的组被排除,引擎不会记住它。
  • 注意:在替换字符串中我们使用美元符号:$1,而在模式中 —— 使用反斜杠 \1

按命名反向引用:\k<name>

要引用命名的捕获组,我们可以使用:\k<name>

let str = `He said: "She's the one!".`; 
let regexp = /(?<quote>['"])(.*?)\k<quote>/g;

alert( str.match(regexp) ); // "She's the one!"

选择 (OR)

用竖线 | 表示

  • gr(a|e)y 等同于 gr[ae]y
  • gra|ey 表示 gra 或 ey

前瞻断言与后瞻断言

前瞻断言

x(?=y) 仅在后面是 Y 时匹配 X

"1 turkey costs 30€".match(/\d+(?=€)/) // 30

否定的前瞻断言

X(?!Y),意思是“搜索 X,但前提是后面没有 Y

"1 turkey costs 30€".match(/\d+(?!€)/) // 1

后瞻断言

  • 肯定的后瞻断言:(?<=Y)X,匹配 X,仅在前面是 Y 的情况下。
  • 否定的后瞻断言:(?<!Y)X,匹配 X,仅在前面不是 Y 的情况下。
"1 turkey costs $30".match(/(?<=\$)\d+/) // 30
"1 turkey costs $30".match(/(?<!\$)\b\d+/g)) // 1

捕获组

"1 turkey costs 30€".match(/\d+(?=(€))/) // 30, €
"1 turkey costs $30".match(/(?<=(\$))\d+/) // 30, $

灾难性回溯

现在JS教程 灾难性回溯

粘性修饰符 "y",在位置处搜索

y 修饰符让我们能够在源字符串中的指定位置进行搜索

let str = 'let varName = "value"'; 
let regexp = /\w+/y; 
regexp.lastIndex = 3; 
alert( regexp.exec(str) ); // null(位置 3 有一个空格,不是单词) 
regexp.lastIndex = 4; alert( regexp.exec(str) ); // varName(在位置 4 的单词)

正则表达式和字符串的方法

str.match(regexp)

在字符串 str 中查找 regexp 的匹配项。

  1. 如果 regexp 不带有修饰符 g,则它以数组的形式返回第一个匹配项,其中包含捕获组和属性 index(匹配项的位置)、input(输入字符串,等于 str
  2. 如果 regexp 带有修饰符 g,则它将返回一个包含所有匹配项的数组,但不包含捕获组和其它详细信息。
  3. 如果没有匹配项,则无论是否带有修饰符 g,都将返回 null

str.matchAll(regexp)

它主要用来搜索所有组的所有匹配项。

与 match 相比有 3 个区别:

  1. 它返回一个包含匹配项的可迭代对象,而不是数组。我们可以用 Array.from 将其转换为一个常规数组。
  2. 每个匹配项均以一个包含捕获组的数组形式返回(返回格式与不带修饰符 g 的 str.match 相同)。
  3. 如果没有结果,则返回的是一个空的可迭代对象而不是 null

str.split(regexp|substr, limit)

alert('12, 34, 56'.split(/,\s*/)) // 数组 ['12', '34', '56']

str.search(regexp)

方法 str.search(regexp) 返回第一个匹配项的位置,如果没找到,则返回 -1

str.replace(str|regexp, str|func)

用于搜索和替换的通用方法

// 用冒号替换连字符 
alert('12-34-56'.replace("-", ":")) // 12:34-56

str.replaceAll(str|regexp, str|func)

这个方法与 str.replace 本质上是一样的,但有两个主要的区别:

  1. 如果第一个参数是一个字符串,它会替换 所有出现的 和第一个参数相同的字符串​,​而 replace 只会替换 第一个
  2. 如果第一个参数是一个没有修饰符 g 的正则表达式,则会报错。带有修饰符 g,它的工作方式与 replace 相同。

replaceAll 的主要用途是替换所有出现的字符串。

regexp.exec(str)

regexp.exec(str) 方法返回字符串 str 中的 regexp 匹配项。与以前的方法不同,它是在正则表达式而不是在字符串上调用的。

它的行为取决于正则表达式是否具有修饰符 g

如果没有修饰符 g,则 regexp.exec(str) 会返回与 第一个匹配项,就像 str.match(regexp) 那样。这种行为并没有带来任何新的东西。

但是,如果有修饰符 g,那么:

  • 调用 regexp.exec(str) 会返回第一个匹配项,并将紧随其后的位置保存在属性 regexp.lastIndex 中。
  • 下一次这样的调用会从位置 regexp.lastIndex 开始搜索,返回下一个匹配项,并将其后的位置保存在 regexp.lastIndex 中。
  • ……以此类推。
  • 如果没有匹配项,则 regexp.exec 返回 null,并将 regexp.lastIndex 重置为 0

regexp.test(str)

方法 regexp.test(str) 查找匹配项,然后返回 true/false 表示是否存在。