关于正则表达式, 你要了解的都在这里了

887 阅读7分钟

正则表达式一直是我心中的痛, 一是没有系统的学习方法, 二是总觉得并不是什么时候都能用上. 但是我相信大多数不太懂正则表达式的人, 都跟我有一样的想法. 要用上的时候, 又发现它十分重要. 好在我发现了一本开源书JavaScript正则表达式, 篇幅不长, 却非常体系化. 十分推荐大家好好读一下.😝

今天靠这篇文章一起来系统地梳理一下正则表达式的方方面面, 让你对正则表达式有一个整体的认识, 能够在平时的业务开发中用上, 甚至在看别人的项目和源码的时候, 不至于眉头紧锁

总览(插图)

正则匹配.png

匹配

正则表达式: 要么匹配字符, 要么匹配位置

匹配字符

模糊匹配

横向模糊

允许匹配长度不固定, 可以有多种情况

量词: {n, m} , 意思是表示某个字符可以出现最少n次, 最多m次.

  • 如图, 正则会横向去匹配满足条件的字符
纵向模糊匹配

纵向模糊

匹配到一个字符时, 可以是多种不同的字符, 也可以是多种可能

  • 如图, 这个字符可以是a, b, c.
纵向模糊匹配

匹配字符组

纵向模糊

[abcdefg] : 匹配任意一个字符

范围表示法 -

[a-g]: 表示从字母a到g的任意一个字符

如果需要匹配符号-, 则需要转义 \-

范围表示法的部分简写
  • digit(数字): \d 表示 [0-9]

  • word(单词): \w 表示 [0-9a-zA-Z] 数字、大小写字母和下划线

  • space(空白符) \s 表示 空白符 如: 空格、水平制表符、垂直制表符、换行符、回车符、换页符。

如果将上述符号改为大写, 则表示非。\D 则表示非数字,[^0-9]

排除字符组

  • ^: [^abc] 则表示不匹配abc中的任意一个

修饰符

修饰符的使用是可以叠加的

  • g全局匹配: 在目标字符串中按顺序找到满足匹配模式的所有子串, g修饰符, 也会对一些api产生影响, 稍后再进行阐述. 如果没有加全局修饰符, 则在找到第一个符合条件的字符串后就会退出.

  • m多行匹配

  • i忽略字母大小写

  • uUnicode模式: 启用Unicode匹配

  • sdotAll模式: 表示元字符., 匹配任何字符

let str = 'I\nLove\nYou' // 注意存在换行符 👈

str.replace(/^|$/, '#') // 输出: '#I\nLove\nYou' 非全局, 仅匹配一次

str.replace(/^|$/g, '#') // 输出: '#I\nLove\nYou#'

str.replace(/^|$/gm, '#') // 输出: '#I#\n#Love#\n#You#' 多行模式下有了行的概念, 所以按行查询

匹配位置

上述匹配都是找到对应的字符, 但是正则还可以匹配位置

什么是位置?

相邻的字符之间的位置, 既是本文中提到的位置

微信截图_20210914131226.png

正则边界

在多行模式(g)下, ^, $表示为开头语结尾

  • ^: 此符号不仅有表示非, 也有表示开头字符的意思

    在带有修饰符的情况下, ^表示匹配开头, 在纵向匹配内, 则代表.

    如:

    /[^123]/ 表示非123字符中的一个.

    /^123/ 以123字符开头.

  • $: 表示匹配字符结尾的意思

    如: /123$/ 带有该符号, 则表示匹配字符要求以123结尾

单词边界

b的是 boundary 的简写

\b: 表示为单词的边界, 具体就是 \w\W 之间的位置,也包括 \w^ 之间的位置,和 \w$ 之间的位置

举个栗子:

首先,\w的意思是 [0-9a-zA-Z], 基于这点来看一个例子

let str = `a*bc`

str.replace(/\b/g, '#') // replace 先简单地理解为替换就可以了

// 输出 "#a#*#bc#"
  • 我们现在匹配的是位置, 所以它在所有边界处都插入了#。 在 bc 之间, 由于这个位置两边都是单词, 它并不是单词的边界, 所以不会匹配到

\B: 表示为非单词的边界

let str = `a*bc`

str.replace(/\b/g, '#') // replace 先简单地理解为替换就可以了

// 输出 "a*b#c"
  • 注意插入的位置, 就可以很好理解了。 非单词的边界, 那就是两个单词之间

指定边界

以下两者匹配的都是模式 p 前的位置, p 可以是单个字符, 可以是一个分组

  • (?=p): p 前面的位置

举个栗子: 在 a 的前面插入一个字符 #

let str = `123abc`

str.replace(/(?=a)/g, '#')

// "123#abc"
  • (?!p): 非 p 前面的位置
let str = `123abc`

str.replace(/(?!a)/g, '#')

// "#1#2#3a#b#c#"

量词

在纵向匹配中, 我们可以根据{m, n} 去决定至多至少应该匹配到多少次, 对于这种匹配模式也有简写

  • ?(有吗?)

    等价于 {0, 1}表示出现或者不出现

  • +(得先有一个)

    等价于{1, } 表示至少出现一个, 并且可以有任意个

  • *(随意)

    等价于{0, } 表示出现任意次, 有可能也不出现

匹配模式

贪婪匹配模式

在满足条件的基础上,尽可能地多匹配

var regex = /\d{2,5}/g; // 匹配连续的数字至少2到5个
var string = "123 1234 12345 123456"; 
console.log( string.match(regex) ); // => ["123", "1234", "12345", "12345"]

惰性匹配模式

只要满足条件就不再往下匹配, 在量词后面加上一个?,就可以转变为惰性匹配

  • 假设我们要获取一个节点的id
let str = '<div id="container" class="main"></div>'

let reg = /id=".*"/

str.match(reg) // 输出: [id="container" class="main"]
  • . 为通配符, 也就是任意字符都能够被匹配到, *是至任意字符可以出现任意次, 并且该字符是贪婪的, 他就会尝试尽可能多的匹配, 这就导致了它匹配的是最后的双引号

image.png

  • 转为惰性匹配
let str = '<div id="container" class="main"></div>'

let reg = /id=".*?"/ // 👈 加一个问号

str.match(reg) // 输出: [id="container"]
  • 由于在匹配到 container 后的双引号就可以满足条件了, 所以不再继续往下匹配 image.png

分组

() 通过括号将括号内的正则表达式视为一个整体

捕获括号

在分组结构中, 存放在括号内的正则表达式匹配到的内容, 会被捕获存储起来, 我们称之为引用 那么, 怎么拿到被捕获到的内容呢?

  • 通过正则构造函数的属性去访问
let str = 'abc-abcd-abcde'

str.match(/(\w{3})-(\w{4})-(\w{5})/g)

// 以正则从左到右出现的分组排序
RegExp.$1 // abc
RegExp.$2 // abcd
RegExp.$3 // abcde

当然, 也并非一定通过这种形式, 如文章一开始举例的一样,在replace函数中, 也可以直接使用分组捕获的内容, 另外除了数字类型的简写, 还有$_等形式的简写

全名简写说明
input$_最后搜索的字符串
lastMatch$&最后匹配的文本
lastParen$+最后匹配的捕获组
leftContext$`input字符串中出现在lastMatch前面的文本
rightContext$'input字符串出现在lastMatch后面的文本

但是需要注意的是, RegExp的构造函数属性并不被推荐使用, JavaScript高级程序设计中的一句话

微信图片_20210924125956.png

非捕获括号

(?:p):只匹配不捕获

const str = 'abc123abc'
const reg = /(\d+)/g

str.match(reg)
RegExp.$1 // 👈 '123'

const reg = /(?:\d+)/g // 非捕获
// or
const reg = /\d+/g


str.match(reg)
RegExp.$1 // 👈 ''

分支

分支结构其实就是通过 | (管道符), 对匹配模式进行拆分, 当成 JavaScript 中的 来理解即可.

如: (p1|p2|p3) 其中p1, p2, p3 就是子模式

APIs

string.match(regex) 匹配

如果正则带有g符号, 则返回的是一个匹配字符串数组, 而不是一个标准的匹配格式

const str = '123-ABC-123'
const reg1 = /ABC/
const reg2 = /ABC/g

str.match(reg1) // ['ABC', index: 4, input: '123-ABC-123', groups: undefined]
str.match(reg2) // ['ABC']
  • 捕获
const str = '123-ABC-123'
const reg = /(ABC)/

str.match(reg) //  ['ABC', 'ABC', index: 4, input: '123-ABC-123', groups: undefined]

注: 需要注意的一点是, match会将传入的字符串参数转为一个正则. (如下方的例子, 参数被误认为一个通配符. 这可能跟初始目的不符)

let str = '12.4'

str.match('.') // ['1', index: 0, input: '12.4', groups: undefined]

string.search(regex)

返回第一次匹配成功的索引, 否则则返回 -1

注: search方法与match类似, 会做一次隐式转换, 所以要注意上述问题

regex.exec(string)

功能与match相似, 但是比起match要更加强大, 在带有全局表示g的情况下, exec也会返回一个标准的匹配格式. 并且exec方法是有状态的(会记住上次匹配的位置)

const str = '123-ABC-456-EFG'
const reg = /\d{3,}/g

reg.exec(str) // ['123', index: 0, input: '123-ABC-123-EFG-456', groups: undefined]
reg.lastIndex // 3
// 第二次执行
reg.exec(str) // ['456', index: 8, input: '123-ABC-123-EFG-456', groups: undefined]
reg.lastIndex // 11
// 第三次执行
reg.exec(str) // null
reg.lastIndex // 0
// 此时再次执行 就会开始下一轮循环

regex.test(string)

如果带有全局修饰符g, test方法同样是"有状态"的

let reg = /a/g
let str = 'abc-abc-abc'

console.log(reg.test(str), reg.lastIndex)
// true 1
console.log(reg.test(str), reg.lastIndex)
// true 5
console.log(reg.test(str), reg.lastIndex)
// true 9

string.replace(regex, <function | string>)

replace方法虽叫做替代, 但是其实他非常强大, 因为我们可以假借替换之名做很多其实事情- 我 replace的第二个参数可以是一个回调函数, 也可以是一个字符串. 如果是一个回调函数则可以接收到5个参数

  • match 匹配到的内容
  • $1~$9 捕获到的分组(这个参数存在的多少, 与你设置的捕获分组数量有关)
  • index 当前索引
  • input 输入的文本

由于我设置了两个捕获分组, 所有这里作为捕获内容的引用有 $1, $2 两个参数

const str = "1234 2345 3456"
const reg = /(\d)\d{2}(\d)/g

str.replace(reg, function (match, $1, $2, index, input) {
    console.log([match, $1, $2, index, input]);
})
/*
    ['1234', '1', '4', 0, '1234 2345 3456']
    ['2345', '2', '5', 5, '1234 2345 3456']
    ['3456', '3', '6', 10, '1234 2345 3456']
*/

怎么书写?

假设我们要匹配一个固定电话.

055188888888 
0551-88888888 
(0551)88888888

1. 了解各个部分的模式规则

这三个字符串的共同点在于, 由4位区号和8位号码组成, 而我们知道区号以0开头, 号码不以0开头. 所以

  • 区号的的匹配规则为: 0\d{2, 3}
  • 号码的匹配规则为: [1-9]\d{6, 7}

因此, 匹配这三个字符串的正则为

  1. /^0\d{2, 3}[1-9]\d{6, 7}$/

  2. /^0\d{2, 3}-[1-9]\d{6, 7}$/

  3. /^\(0\d{2, 3}\)[1-9]\d{6, 7}$/

2. 找到所有需要匹配的字符串的可能性后, 明确他们之间的关系.

因此可以得出这样的正则

/^0\d{2, 3}[1-9]\d{6, 7}$|^0\d{2, 3}-[1-9]\d{6, 7}$|^\(0\d{2, 3}\)[1-9]\d{6, 7}$/

这里只是粗暴地将上述三个正则用或|粗暴的叠加在一起

3. 提取公共部分, 就像平时敲代码, 抽象一些功能

将号码部分向外抽离

/^(0\d{2, 3}|0\d{2, 3}-|\(0\d{2, 3}\))[1-9]\d{6, 7}$/

继续优化, 将区号的-?进行判断

/^(0\d{2, 3}-?|\(0\d{2, 3}\))[1-9]\d{6, 7}$/


🤭到这里基本上业务开发中我们需要了解的内容就都差不多了, 但是其实还有很多细节, 我可能没有谈到. 比如正则表达的回溯原理, 这并不是一个很难懂的概念, 只是我觉得敲了又删, 还是觉得作者讲得好. 所以还希望您能好好读一下原文, 更深入地理解正则!

感谢😘


如果觉得文章内容对你有帮助: