通过一个npm包学习正则表达式的用法

145 阅读2分钟

前言

在前端领域中,正则表达式是一种实用且强大的工具,可以帮助我们快速地匹配特定的内容,完成特定的需求,有着事半功倍的效果。比如,在用户信息模块中,我们会使用正则表达式去匹配手机号格式是否正确、身份证号是否正确,以及用户姓名是否输入规范等,在这些场景中使用正则表达式就显得很简单且高效。

最近刚好看到一个npm包,名字叫markdown-title,这个包在npm上的周下载量就达到了3k。其功能很简单也很实用,也就是从给点的Markdown文本中读取一级标题的内容并返回。然而,细看之后其代码实现其实很简单,只有短短的8行代码!而这正是借助正则表达式实现的。

借此机会,我们学习和回顾下正则表达式的用法。

markdown-title

npm包的代码内容如下所示。

我们可以看到,文件导出了一个 markdownTitle 函数,从输入的 markdown 内容中读出一级标题(以单个#开头)并返回,如果没有匹配到则返回的是undefined。

const markdownTitle = markdown => {
  let matches = markdown.match(/^#[^#][\s]*(.+?)#*?$/m)
  if (matches && matches.length) {
    return matches.pop().trim()
  }
  return
}
module.exports = markdownTitle

这个npm包使用起来也很简单:

const markdownTitle = require('markdown-title')
let markdown = `
# Title
`
let title = markdownTitle(markdown) //=> Title

// # Title
// #     I can title? #

我们输入markdown字符串后,最终会返回获取到的一级标题。上面两种标题都能正确识别到。

分析

markdown.match(/^#[^#][\s]*(.+?)#*?$/m)

我们可以看到,npm包的核心代码是通过正则表达式实现的,这句代码是什么意思呢?

首先,match() 是js字符串提供的方法,该方法用于在给定的js字符串中查找与正则表达式匹配的内容。如果能找到匹配,则返回一个数组,第0项是完全匹配的整个字符串,后续项是捕获组匹配的内容,还包含 index (匹配开始位置) 和 input (原始字符串) 属性。如果没找到匹配,则返回 null

其次,正则表达式部分,常见的语法包括:

  • ^# 表示 匹配行首的 # 符号
  • $ 表示到达行尾
  • m 是多行模式的标志

再次,其他部分是重点,包括:

  • [^#] - 确保下一个字符不是 #(避免匹配二级标题 ##)。在正则表达式中,[ ] 表示一个字符集合,而 ^ 在 [ ] 的开头时表示"否定"或"排除"。

  • [\s]* - 匹配0或多个空白字符。#[^#][\s]* 组合起来表示:匹配一个 # 号,后面跟着一个非 # 的字符,然后可以有任意数量的空白字符。例如以下几种情况都是匹配的:

    #标题(无空格)
    # 标题(一个空格)
    #  标题(多个空格)
    
  • (.+?) - 非贪婪匹配标题内容(捕获组)。其中,

    () 表示捕获组,会把匹配的内容保存起来。

    .+ 表示匹配任意单个字符一次或多次。

    ? 在这里表示"非贪婪"模式。正则表达式默认是贪婪模式,+ 会尽可能多地匹配字符。例如,a.*b 匹配 "aabab" 时会匹配整个字符串。

    +? 表示匹配尽可能少但至少一个字符,例如 a.*?b 匹配 "aabab" 时只会匹配 "aab"。

  • #*?$ - 匹配行尾可能存在的 # 号。组合起来 #*?$ 表示:在行尾可能有零个或多个井号(#),以最少必要为原则匹配。举例说明:

匹配 # 标题(无末尾#号)
匹配 # 标题#(一个末尾#号)
匹配 # 标题###(多个末尾#号)
但不匹配 # 标题# 其他内容(因为$要求必须是行尾)

最后,返回值是matches.pop().trim(),也就是返回匹配到的内容,并去掉首尾空格。

到这里,代码也就分析完了。我们不难看到,作者借助正则表达式去获取markdown文本的一级标题(获取第一个匹配到的一级标题,并排除了二级标题等),还兼容了# 标题###等情形。

后记

简而言之,正则表达式是一个强大的工具,也是前端乃至后端必备的一种技能。

多从别人的代码中去思考和学习,能学到更多东西。

开卷有益!