正则表达式

50 阅读11分钟

自从GPT技术成为热门话题以来,生成正则表达式变得前所未有的便捷。过去,我们常常需要通过繁琐的搜索来寻找合适的正则表达式,而现在,只需向GPT提出问题,它便能迅速提供自动生成的解决方案。如下面的示例所示,通过简洁的对话,我们就能轻松获得相对准确的正则表达式。

image.png

然而,正如大家所熟知的,GPT有时也会一本正经地提供不准确的信息。因此,掌握一些基础的正则表达式语法知识是非常有价值的。接下来,我将为您介绍一些常用的正则表达式语法。

什么是正则?

正则表达式,简称Regular Expression,是一种用于描述特定文本模式的字符串。它允许我们以一种高度凝练和精确的方式来定义文本内容的结构和规则。作为一种强大的文本处理工具,其应用范围广泛,包括但不限于:

  • 数据验证:确保输入的数据符合特定的格式要求。
  • 文本搜索:快速定位并提取符合特定模式的文本片段。
  • 文本编辑:对文本进行高效的分割、删除或替换操作。

元字符

在正则表达式的世界里,元字符是那些被赋予特殊意义的字符,它们是构建正则表达式的基础。通过这些元字符,我们可以定义复杂的文本匹配规则。

特殊单字符

特殊单字符是指在正则表达式中具有特定含义的单个字符,它们用于匹配文本中的特定类型字符。

元字符描述
.匹配任意单个字符(除换行符外)
\d匹配任意数字(0-9)
\D匹配任意非数字字符
\w匹配任意字母数字或下划线
\W匹配任意非字母数字非下划线字符
\s匹配任意空白字符
\S匹配任意非空白字符

空白符

空白符元字符用于匹配文本中的空白字符,如空格、制表符、换行符等。

元字符描述
\r匹配回车符
\n匹配换行符
\t匹配制表符(Tab)
\f匹配换页符
\v匹配垂直制表符
\s匹配任意空白字符,包括回车符、换行符等

除了回车、换行空白符外,最常见的还有空格,在正则中,空格就是用普通的“空格”来表示,没有特殊的元字符。

量词

正如之前所述,特殊单字符和空白符元字符通常用于匹配文本中的单一字符,例如 \d 仅用于匹配单个数字。然而,当我们的需求变得更加复杂,比如需要匹配某个字符至少出现一次,或者至多出现三次时,量词元字符就显得尤为重要了。通过使用量词,我们可以扩展正则表达式的匹配能力,使其能够应对更多样化的文本模式。

元字符描述
*匹配0次或多次(等价于{0,})
+匹配1次或多次(等价于{1,})
?匹配0次或1次(等价于{0,1})
{m}匹配恰好m次
{m,}匹配至少m次
{m, n}匹配m到n次之间

范围

范围元字符用于匹配一组字符中的任意一个,或者通过逻辑“或”来匹配多个选项中的任意一个。

元字符描述
|逻辑“或”,匹配两项中的任意一项
[...]匹配中括号内的任意单个字符
[a-z]匹配任意小写字母(a-z)
[^...]匹配不在中括号内的任意单个字符

断言(Assertion)

断言是正则表达式中用于定位特定匹配位置的强大工具,它们并不消耗任何字符,只是对位置进行验证。这使得我们可以解决一些复杂的文本匹配问题,例如,我们要查找 tom 这个单词,但其他单词,如 tomorrow 中也包含 tom。为了解决类似这样的问题,正则中提供了一些结构,只用于匹配位置,不是文本内容本身,这种结构就是断言。

单词边界

在正则表达式中,\b 用于标识单词的边界。这里的 "b" 代表 "Boundary",它标志着一个单词字符与一个非单词字符的交界。需要注意的是匹配的单词边界不包括在最终的匹配文本中。

tom\btomtom\b\btom\b
tom
tomorrow××
atom××
atomic×××

\b 相反的还有一个 \B,表示匹配非单词边界。它表示上一个字符和下一个字符属于同一类型的位置:要么两者都必须是单词,要么两者都必须是非单词。

开始/结束

  • ^ 断言匹配字符串的开始。
  • $ 断言匹配字符串的结束。

image.png

环视

环视是一种高级断言,它要求匹配的部分前后必须满足(或不满足)某种规则,但本身不消耗任何字符。

断言类型描述例子
肯定逆序环视 (?<=y)x当 "x" 前面有 "y" 时才匹配 "x"(?<=\d)th 匹配数字后的 "th",如 "9th"
否定逆序环视 (?<!y)x当 "x" 前面没有 "y" 时才匹配 "x"(?<!\d)th 匹配非数字后的 "th",如 "health"
肯定顺序环视 x(?=y)当 "x" 后跟 "y" 时才匹配 "x"six(?=\d) 匹配数字前的 "six",如 "six6"
否定顺序环视 x(?!y)当 "x" 后面没有 "y" 时才匹配 "x"hi(?!\d) 匹配非数字前的 "hi",如 "high"

虽然内容比较多,但还是有一定的规则:左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思。

转义

如前面所说,许多字符具有特殊的含义,用于定义复杂的匹配模式。然而,在某些情况下,我们可能需要按照这些字符的字面意义进行匹配,而非它们的特殊功能。这时,转义就显得尤为重要。要将特殊字符转换为字面意义上的字符,我们可以通过在其前面添加一个反斜线(/)来实现转义。这样,正则表达式引擎就会将这些字符视为普通字符,而不是其预设的特殊含义。

匹配模式

匹配模式是正则表达式中用于调整量词匹配行为的关键概念,它决定了量词如何与文本中的字符进行匹配。

贪婪匹配(Greedy)

贪婪匹配是指量词尽可能多地匹配字符,以满足模式的匹配条件。在正则表达式中,量词默认采用贪婪模式,这意味着它们会尽可能地匹配更多的字符。

非贪婪匹配(Lazy)

非贪婪匹配与贪婪匹配相反,它尽可能少地匹配字符,以满足模式的匹配条件。要启用非贪婪匹配,只需在量词后面加上一个英文问号(?)。这样,正则表达式就会尽可能少地匹配字符。

独占模式(Possessive)

独占模式类似于贪婪匹配,它会尽可能多地匹配字符。不同之处在于,一旦独占模式开始匹配,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。要启用独占模式,需要在量词后面加上一个加号(+)。这样,正则表达式就会尽可能长地匹配字符串,且匹配过程中不会进行回溯。

独占模式在性能上通常更优,因为它可以减少匹配过程中的时间和CPU资源消耗。然而,在某些情况下,独占模式可能不适用,例如:

image.png

image.png

在独占模式下,正则表达式的匹配过程可以分解为以下几个步骤:

  1. 开始匹配:正则表达式从字符串的起始部分 "xy" 开始进行匹配。
  2. 匹配 "y" :随后,正则表达式尝试匹配字符 "y"。由于独占量词 {1,3}+ 的应用,它会贪婪地匹配尽可能多的 "y",直到没有更多的 "y" 可以匹配。
  3. 独占模式的效果:在独占模式的作用下,正则表达式在匹配 "y" 时会锁定这些匹配的字符,尽可能地扩展匹配范围,即使这可能导致后续的模式 "yz" 无法找到合适的 "y" 来完成匹配。在本例中,正则表达式会匹配两个 "y",形成 "xyyz"。
  4. 匹配 "yz" :由于独占模式已经消耗了所有可用的 "y",导致末尾的 "yz" 部分无法找到足够的 "y" 来完成匹配,因此整个正则表达式无法完全匹配字符串 "xyyz"。

需要注意:JavaScript 不支持独占模式,上述正则可以在 Java 中进行尝试。

捕获组与反向引用

在正则表达式中,捕获组和反向引用是两个强大的工具,它们允许我们捕获和重用匹配的文本。

编号捕获组

在正则表达式中,括号 () 用于创建捕获组,将子表达式的一部分捕获并保存起来,以便后续使用。通过计算左括号的位置,可以确定捕获组的编号。捕获组的内容可以通过匹配对象的数组索引来访问。

image.png

const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31

不保存捕获组

但在某些情况下,我们可能只需要将子表达式视为一个整体,而不需要保存捕获的内容。这时,可以使用非捕获组。使用方式也很简单,只需要在括号内添加 ?: 即可。

image.png

命名捕获组

命名捕获组允许我们通过名称而不是编号来引用捕获的内容,这使得代码更易读和维护。通过使用 (?<name>...) 语法来创建命名捕获组。需要提醒一下,命名的捕获组也同样会创建编号。

image.png

反向引用

反向引用允许我们在正则表达式中引用之前捕获的文本。通过 \数字 来引用编号捕获组,或 \k<名称> 来引用命名捕获组。(如果是替换的话,需要用 $number

const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/;
// "12/31/1999"
'1999-12-31'.replace(RE_DATE, '$2/$3/$1');

const RE_DATE_NAMED = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
// "12/31/1999"
'1999-12-31'.replace(RE_DATE_NAMED, '$<month>/$<day>/$<year>');

image.png

模式修饰符

模式修饰符为正则表达式提供了额外的灵活性,允许我们根据特定需求调整匹配行为。在JavaScript中,模式修饰符被直接附加在正则表达式的末尾。例如,使用/pattern/modifiers的形式。

全局匹配模式(Global Search)

通过添加g修饰符,我们可以激活全局匹配模式,这使得正则表达式搜索整个字符串以找到所有可能的匹配,而非找到第一个匹配后停止。

image.png

image.png

不开启全局匹配时,找到第一个满足匹配的 a 就停止了,所以只找到1个符合条件的。而开启后,会一直匹配到文本结束,所以发现了3处符合匹配规则的文本

黏性搜索模式(Sticky Mode)

y修饰符,也称为黏性搜索模式,确保匹配必须从上一次成功匹配的下一个位置开始。

const str = 'a0bc1';
const rexWithout = /\d/;
const rexWithG = /\d/g;
const rexWithY = /\d/y;

rexWithout.lastIndex = 2;
console.log(rexWithout.exec(str)); // 从指定位置开始匹配

rexWithG.lastIndex = 2;
console.log(rexWithG.exec(str)); // 全局匹配,忽略lastIndex

rexWithY.lastIndex = 2;
console.log(rexWithY.exec(str)); // 黏性匹配,仅在指定位置匹配

image.png

不区分大小写模式(Case Insensitive)

为了在匹配时忽略大小写差异,可以使用i修饰符。这使得正则表达式能够匹配所有大小写变体的文本。

image.png

image.png

点号通配模式(Dot All Mode)

默认情况下,点号.不匹配换行符。

image.png

当我们需要匹配真正的任意字符时,需要使用[\s\S][\d\D][\w\W]

image.png

但是这么写不够简洁自然,所以正则提供了一种模式修饰符,可以让 . 匹配包括换行的任意字符。

这个模式就是点通配模式,使用 s 修饰符来激活,s 表示 Single Line,设计者的意图希望 . 将所有的内容当做一行,也就是文本中不存在换行符。

image.png

多行匹配模式(Multiline)

在默认情况下,^$ 仅定位于整个字符串的开始和结束。但通过启用多行匹配模式,这两个锚点的行为会发生改变,它们将分别匹配每一行文本的开始和结束位置,而不是仅限于整个字符串的边界。

image.png

多行模式通过在正则表达式中添加 m 修饰符来启用。这里的 m 代表 Multiline,它指示正则表达式引擎将输入的文本视为由多行组成的结构,并对每一行分别应用 ^$ 的匹配规则。

image.png

正则原理

正则表达式之所以能够高效处理复杂文本,是因为它背后依赖了有穷状态自动机(Finite State Automaton, FSA)的原理。

有穷状态自动机

有穷状态自动机是一种接受或拒绝字符串的自动机,它具有以下特点:

  • 有穷状态:自动机拥有有限个状态,每个状态代表匹配过程中的一个阶段。
  • 状态转移:根据输入字符和当前状态,自动机可以转移到另一个状态,或者保持当前状态。

正则表达式的引擎

正则表达式引擎主要分为两种类型:非确定性有限状态自动机(NFA)和确定性有限状态自动机(DFA)。

NFA(非确定性有限状态自动机)

NFA是一种在某些输入下可能有多个状态转移选择的自动机。它的特点包括:

  1. 多态性:对同一输入字符,NFA可能存在多个对应的状态。
  2. 无输入转移:NFA可以在没有输入的情况下,在不同状态之间进行转移。

DFA(确定性有限状态自动机)

DFA是一种对每个输入字符和每个状态,只有一个确定的状态转移的自动机。它的特点包括:

  1. 确定性:对每个输入字符和状态,DFA只有唯一的转移路径。
  2. 效率:DFA通常比NFA更快,因为它不需要回溯。

引擎的工作方式

通过下面的正则匹配的流程,来一起看下 DFA 和 NFA 引擎工作方式上的区别:

/fi(nish|x|sh|y)/.test("I like fishing.")

DFA的工作方式

DFA引擎的工作方式是自顶向下的,它首先查看文本,然后根据正则表达式进行匹配。以文本为主导,逐步匹配正则表达式的每一部分。

text: I like fishing.
regex: fi(nish|sy|sh|y)

DFA 会从文本的 I 开始依次查找 f,定位到 f,这个字符后面是 i,接着看正则部分是否有 i,如果正则后面是个 i。继续往后查看文本,后面是 s,DFA 借着看正则表达式部分,此时 nish、y 分支被淘汰,sy、sh 分支符合要求。然后 DFA 继续检查字符串 h,只有 sh 分支符合要求,sy 分支淘汰,最终,匹配成功。

NFA的工作方式

NFA引擎的工作方式是自底向上的,它首先查看正则表达式,然后根据文本进行匹配。以正则表达式为主导,尝试所有可能的匹配路径。

NFA 引擎的工作方式是:先看正则,再看文本,以正则为主导。

regex: fi(nish|sy|sh|y)
text: I like fishing.

正则中的第一个字符是 f,NFA 引擎在字符串中查找 f,接着匹配其后是否为 i,再根据正则查看文本后面是不是 n,发现不是,此时 nish 分支淘汰,再看 sy 分支,s 能对的上,但是 y 对不上;接着看 sh 分支,此时会从文本 fishing 中的 s 开始(NFA 引擎会记住这里),当匹配上 sh 后,整个文本匹配完毕,不会再看后面的 y 分支。

所以,对于 NFA 引擎来说,会反复测试字符串,这样字符串中同一部分,有可能被反复测试很多次。

DFA与NFA的比较

通过上述讨论,我们可以看到DFA和NFA两种正则表达式引擎在处理文本匹配时所采用的方法存在显著差异。通常情况下,DFA引擎的执行速度更快,原因在于它的匹配过程是线性的,即对字符串的扫描只需进行一次,不会对已检查的字符进行二次测试,从而避免了回溯。这种特性使得DFA引擎能够有效地找到最长的匹配字符串。然而,DFA引擎在处理正则表达式时存在一定的局限性,它不支持捕获子组和反向引用等高级功能。

回溯的机制

回溯是NFA引擎特有的一种机制,它主要在正则表达式包含量词或多选分支结构时发挥作用。这种机制允许NFA引擎在遇到匹配失败时,回退到之前的状态,并尝试其他可能的匹配路径。

示例分析

以正则表达式/a+ab/匹配文本"aab"为例,NFA引擎首先会贪婪地匹配"a+",将两个"a"全部匹配,然后发现还需要匹配一个"a",但由于文本中仅剩下一个"b",此时NFA引擎会进行回溯,释放一个"a",然后继续尝试匹配。

另一个例子是使用正则表达式/.*ab/匹配较长的字符串。在这个例子中,".*"会尽可能多地匹配所有字符,直到遇到"ab"。如果直接匹配失败,NFA引擎将开始回溯,逐步减少".*"匹配的字符数量,直到找到合适的匹配位置。

结语

在使用正则处理问题时,可以将问题分解成多个小问题,每个小问题见招拆招:某个位置上可能有多个字符的话,就用字符组。某个位置上有一个字符串的话,就用多选结构。出现的次数不确定的话,就用两次。对位置的出现有要求的话,就用锚点锁定位置等等等等....

但一定要谨记,不要总想着用正则来解决问题,不要手里拿着锤子,看啥都是钉子。用普通字符村能解决的问题,就不要用正则。想要高效的完成文本处理工作,正则只是其中一种,并不是唯一的一种。