正则表达式

160 阅读5分钟

1. 认识正则和JS创建

  • 正则表达式是一种字符串匹配利器,可以搜索、获取、替代字符串

  • 在JavaScript中,正则表达式使用RegExp类来创建,也有对应的字面量的方式:

    • 正则表达式主要由两部分组成:模式(patterns)和修饰符(flags)
      • const re1 = new RegExp("abc", "ig")
      • const re2 = /abc/ig

2. 正则常见的方法

  • 正则方法
    • exec
    • test
  • 字符串方法
    • match
    • matchAll
    • search
    • replace
    • replaceAll
    • split image.png
  • 初体验

    const message = "fdabc123 faBC323 dfABC222 A2324aaBc"
    
    // 将所有的abc(忽略大小写)换成cba
    const newMessage = message.replaceAll(/abc/ig, "cba")
    console.log(newMessage) // fdcba123 fcba323 dfcba222 A2324acba
    
    // 需求: 将字符串中数字全部删除
    const newMessage2 = message.replaceAll(/\d+/ig, "")
    console.log(newMessage2) // fdabc faBC dfABC AaaBc
    
  • 使用

    // 创建正则
    const re1 = /abc/ig
    const message = "fdabc123 faBC323 dfABC222 A2324aaBc"
    
    // 1.使用正则对象上的实例方法
    // 1.1.test方法: 检测某一个字符串是否符合正则的规则
    if (re1.test(message)) {
      console.log("message符合规则")
    } else {
      console.log("message不符合规则")
    }
    
    // 1.2.exec方法: 使用正则执行一个字符串
    const res1 = re1.exec(message)
    console.log(res1)
    
    // 2.使用字符串的方法, 传入一个正则表达式
    // 2.1. match方法:匹配查找
    const result2 = message.match(re1)
    console.log(result2) // ['abc', 'aBC', 'ABC', 'aBc']
    
    // 2.2. matchAll方法
    // 注意: matchAll传入的正则修饰符必须加g,返回的结果时一个迭代器
    const result3 = message.matchAll(re1)
    // console.log(result3.next())
    // console.log(result3.next())
    // console.log(result3.next())
    // console.log(result3.next())
    for (const item of result3) {
      console.log(item)
    }
    
    // 2.3. split方法
    // const result4 = message.split(re1)
    // console.log(result4)
    
    // 2.4. search方法: 返回索引,查不到返回-1
    const result5 = message.search(re1)
    console.log(result5) // 2
    

3. 正则规则

3.1 常见修饰符

image.png

3.2 字符类:是一个特殊的符号,匹配特定集中的任何符号

image.png

3.3 反向类(Inverse classes)

  • \D 非数字:除 \d 以外的任何字符,例如字母
  • \S 非空格符号:除 \s 以外的任何字符,例如字母
  • \W 非单字字符:除 \w(即[a-zA-Z0-9]) 以外的任何字符,例如非拉丁字母或空格

3.4 锚点

  • 符号 ^ 和符号 $ 在正则表达式中具有特殊的意义,它们被称为锚点

    • 符号 ^ 匹配文本开头
    • 符号 $ 匹配文本末尾

3.5 词边界(Word boundary)

  • 词边界 \b 是一种检查,就像 ^ 和 $ 一样,它会检查字符串中的位置是否是词边界
  • 词边界测试 \b 检查位置的一侧是否匹配 \w,而另一侧则不匹配 “\w”

3.6 转义字符

  • 把特殊字符作为常规字符来使用,需要对其进行转义:

    • 在它前面加个反斜杠
  • 常见的需要转义的字符:[] \ ^ $ . | ? * + ( )

    • 斜杠符号 / 并不是一个特殊符号,在字面量正则表达式中也需要转义
const fileNames = ["abc.html", "Home.jsx", "index.html", "index.js", "util.js", "format.js"]
// 获取所有的js的文件名(webpack)
// x?: x是0个或者1个
const jsfileRe = /\.jsx?$/
// 1.for循环做法
// const newFileNames = []
for (const filename of fileNames) {
  if (jsfileRe.test(filename)) {
    newFileNames.push(filename)
  }
}
console.log(newFileNames)

// 2.filter高阶函数
const newFileNames = fileNames.filter(filename => jsfileRe.test(filename))
console.log(newFileNames)

3.7 集合和范围

  • 选择多个匹配字符的其中之一
    • 在方括号 […] 中的几个字符或者字符类意味着“搜索给定的字符中的任意一个
  • 集合
    • [eao] 意味着查找在 3 个字符 ‘a’、‘e’ 或者 ‘o’ 中的任意一个
  • 范围
    • 方括号也可以包含字符范围
    • [a-z] 会匹配从 a 到 z 范围内的字母,[0-5] 表示从 0 到 5 的数字
    • [0-9A-F] 表示两个范围:它搜索一个字符,满足数字 0 到 9 或字母 A 到 F
    • \d —— 和 [0-9] 相同
    • \w —— 和 [a-zA-Z0-9_] 相同
  • 除了普通的范围匹配,还有类似 [^…] 的“排除”范围匹配
    • 比如:[^0-9]等同于\D
// 手机号的规则: 1[3456789]033334444
const phoneStarts = ["132", "130", "110", "120", "133", "155"]
const phoneStartRe = /^1[3456789]\d/
const filterPhone = phoneStarts.filter(phone => phoneStartRe.test(phone))
console.log(filterPhone)

const phoneNum = "133888855555"
const phoneRe = /^1[3-9]\d{9}$/
console.log(phoneRe.test(phoneNum))

3.8 量词

  • 数量 {n}

    • 确切的位数:{5}
    • 某个范围的位数:{3,5}
  • 缩写:

    • +:代表“一个或多个”,相当于 {1,}
    • ?:代表“零个或一个”,相当于 {0,1}。换句话说,它使得符号变得可选
    • *:代表着“零个或多个”,相当于 {0,}。也就是说,这个字符可以多次出现或不出现
// 案例: 字符串的html元素, 匹配出来里面所有的标签
const htmlElement = "<div><span>哈哈哈</span><h2>我是标题</h2></div>"
const tagRe = /<\/?[a-z][a-z0-9]*>/ig
const results = htmlElement.match(tagRe)
console.log(results) // ['<div>', '<span>', '</span>', '<h2>', '</h2>', '</div>']

3.9 贪婪( Greedy)和惰性( lazy)模式

  • 贪婪模式
    • 默认情况下的匹配规则是查找到匹配的内容后,会继续向后查找,一直找到最后一个匹配的内容
  • 惰性模式
    • 只要获取到对应的内容后,就不再继续向后匹配,可以在量词后面再加一个问号 ‘?’ 来启用它;
    • 匹配模式变为 *? 或 +?,甚至将 '?' 变为 ??
const message = "我最喜欢的两本书: 《黄金时代》和《沉默的大多数》、《一只特立独行的猪》"

// 默认.+采用贪婪模式
// const nameRe = /《.+》/ig

// const result1 = message.match(nameRe)
// console.log(result1)

// 使用惰性模式
const nameRe = /《.+?》/ig

const result1 = message.match(nameRe)
console.log(result1)

3.10 捕获组(capturing group)

  • 模式的一部分可以用括号括起来 (...),这称为“捕获组(capturing group)”。

  • 有两个作用:

    • 它允许将匹配的一部分作为结果数组中的单独项
    • 它将括号视为一个整体
  • str.match(regexp),如果 regexp 没有 g 标志,将查找第一个匹配并将它作为一个数组返回:

    • 在索引 0 处:完全匹配
    • 在索引 1 处:第一个括号的内容
    • 在索引 2 处:第二个括号的内容
  • 命名组

    • 在开始括号之后立即放置 ?<name> 来完成对括号命名
  • 非捕获组

    • 在开头添加 ?: 来排除组,匹配的组不出现在结果中
  • or是正则表达式中的一个术语,实际上是一个简单的“或”。

    • 在正则表达式中,它用竖线 | 表示
    • 会和捕获组一起来使用,在其中表示多个值
    • 需要括号才能正确应用量词,但我们不希望它们的内容出现在结果中,可以通过在开头添加 ?: 来排除组
const message = "我最喜欢的两本书: 《黄金时代》和《沉默的大多数》、《一只特立独行的猪》"

// 使用惰性模式
const nameRe = /(?:《)(?<book>.+?)(?:》)/ig
const iterator = message.matchAll(nameRe)
for (const item of iterator) {
  console.log(item)
}

// 2.将捕获组作为整体
const info = "dfabcabcfabcdfdabcabcabcljll;jk;j"
const abcRe = /(abc){2,}/ig
console.log(info.match(abcRe)) // ['abcabc', 'abcabcabc']

// 3.or
const info = "dfabcabcfabcdfdabcabcabcljcbacballnbanba;jk;j"
const abcRe = /(abc|cba|nba){2,}/ig
console.log(info.match(abcRe))

4. 练习案例

4.1 歌词解析

function parseLyric(lyricString) {
  // 1.根据\n切割字符串
  const lyricLineStrings = lyricString.split("\n")
  // console.log(lyricLineStrings)

  // 2.针对每一行歌词进行解析
  // [01:22.550]夏末秋凉里带一点温热有换季的颜色
  const timeRe = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/i
  const lyricInfos = []
  for (const lineString of lyricLineStrings) {
    // 1.获取时间
    const result = lineString.match(timeRe)
    if (!result) continue
    const minuteTime = result[1] * 60 * 1000
    const secondTime = result[2] * 1000
    const mSecondTime = result[3].length === 3? result[3]*1: result[3]*10
    const time = minuteTime + secondTime + mSecondTime
    
    // 2.获取内容
    const content = lineString.replace(timeRe, "").trim()

    // 3.将对象放到数组中
    lyricInfos.push({ time, content })
  }

  return lyricInfos
}

4.2 时间格式化

// timestamp: 1659252290626
// yyyy/MM/dd hh:mm:ss
// yyyy*MM*dd hh-mm-ss
// dayjs/moment
function formatTime(timestamp, fmtString) {
  // 1.将时间戳转成Date
  const date = new Date(timestamp)

  // // 2.获取到值
  // const year = date.getFullYear()
  // const month = date.getMonth() + 1
  // const day = date.getDate()
  // const hour = date.getHours()
  // const minute = date.getMinutes()
  // const second = date.getSeconds()

  // // 3.创建正则
  // const yearRe = /y+/
  // const monthRe = /M+/

  // 2.正则和值匹配起来
  const dateO = {
    "y+": date.getFullYear(),
    "M+": date.getMonth() + 1,
    "d+": date.getDate(),
    "h+": date.getHours(),
    "m+": date.getMinutes(),
    "s+": date.getSeconds()
  }

  // 3.for循环进行替换
  for (const key in dateO) {
    const keyRe = new RegExp(key)
    if (keyRe.test(fmtString)) {
      const value = (dateO[key] + "").padStart(2, "0")
      fmtString = fmtString.replace(keyRe, value)
    }
  }

  return fmtString
}