正则表达式:简单优美且功能强大,常见规则与字符匹配详解
正则表达式具备卓越的技术创新特征,其设计简洁而优雅,功能全面且应用广泛。在众多实际应用中,正则表达式宛如神奇的良药,能显著提升开发速度和程序品质。

1. 正则常见规则1.1 字符匹配
字符说明
转义符
\d
[0-9]。表示是一位数字。
\D
[^0-9]。表示除数字外的任意字符。
\w
[0-9a-zA-Z_]。表示数字、大小写字母和下划线。
\W
[^0-9a-zA-Z_]。非单词字符。
\s
空白字符包括空格、水平制表符、换行符、回车符、换页符以及格式化字符。
垂直制表符、换行符、回车符、换页符。
\S
[^\t\v\n\r\f]。非空白符。
在文本中,[^\n\r\u2028\u2029]这一符号被用来代表几乎所有的字符。
换行符、回车符、行分隔符和段分隔符除外。
\uxxxx
查找以十六进制数 xxxx 规定的 Unicode 字符。
\f
匹配一个换页符 (U+000C)。
\n
匹配一个换行符 (U+000A)。
\r
匹配一个回车符 (U+000D)。
\t
匹配一个水平制表符 (U+0009)。
\v
匹配一个垂直制表符 (U+000B)。
\0
识别 U+0000 字符时,请勿添加其他小数点后的数字,因为该字符本身即为空字符(\0)。
八进制转义序列。
[\b]
匹配一个退格(U+0008)。(不要和\b 混淆了。)
[abc]
any of a, b, or c
[^abc]
not a, b, or c
[a-g]
character between a & g
1.2 位置匹配
字符说明
\b
单词边界指的是特定的位置,具体而言,它位于\w和\W之间,亦或是在\w与符号^之间。
也包括\w 和 之间的位置。
\B
表示与\b相反的含义,指的是非单词的分界点。比如,在字符串的各个位置上,移除\b之后的结果。
剩下的都是\B 的。
^abc$
字符串开始、结束的位置
1.3 组
字符说明
(abc)
capture group,捕获组
\n
对第n个分组进行引用,指代第n个捕获组所对应的内容。
其中 n 是正整数
(?:abc)
non-capturing group,非捕获组
1.4 先行断言
字符说明
a(?=b)
正向先行断言,即a只有在b之前出现时才能进行匹配。
a(?!b)
负向前瞻,即先行否定断言,指的是只有当“a”不在“b”之前出现时,才能进行匹配。
1.5 后行断言
字符说明
(?1.6 量词和分支
字符说明
a*
0 or more
a+
1 or more
0 or 1
a{5}
exactly five
a{2,}
two or more
a{1,3}
between one & three
a{2,}?
力求匹配数量最少,惰性匹配策略,即力求实现尽可能低的匹配频率。
以下都是惰性匹配:
{m,n}?
{m,}?
1.7 分支字符说明
ab|cd
若进行匹配操作,可针对字符子串'ab'或'cd'进行匹配。
1.8 修饰符
字符说明
执行对大小写不敏感的匹配。
执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
执行多行匹配。
启用"Unicode"工作模式,以确保能够准确处理超过\uFFFF的Unicode字符。换言之,这种模式能够正确解析使用四个字节进行UTF-16编码的字符。
允许 . 匹配换行符。
y修饰符的功能与g修饰符相近,同样实现全局搜索,后续的匹配均从上一次成功匹配的下一个字符位置启动。二者区别在于,g修饰符仅需在剩余文本中找到匹配即可,而y修饰符则要求匹配必须从剩余文本的第一个字符位置开始,这正是“粘连”一词所要表达的含义。
2. 运算符优先级运算符描述
转义符
(), (?:), (?=), []
圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}
限定符
^, $, \任何元字符、任何字符
定位点和序列(即:位置和顺序)
替换,"或"操作
字符在运算中的优先级高于替换运算符,因此"m|food"可以匹配到"m"或"food"。若想匹配"mood"或"food",需通过括号构建子表达式,形成"(m|f)ood"这一模式。
3. 正则回溯3.1 什么是回溯算法
以下是来自摘自维基百科的部分解释:
回溯算法是一种广泛应用的计算机程序设计方法,旨在寻找特定计算问题的全部或部分解法,尤其擅长处理那些涉及约束条件满足的问题。该方法通过逐步构建可能的解,一旦发现某个候选解无法满足条件,便立即舍弃,从而高效地找到有效的解决方案。
回溯算法通常通过最基础的递归技术来执行,经过多次重复执行上述步骤,可能会遭遇以下两种情形:
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
3.2 什么是正则回溯
正则表达式引擎主要涵盖两大类别:一类是确定性有限自动机,简称DFA;另一类是非确定性有限自动机,简称NFA。NFA 的运行速度比 DFA 慢,而且其实现过程相对复杂。尽管如此,NFA 拥有远超 DFA 的强大功能,例如能够支持反向引用等特性。以 JavaScript、Java、PHP、Python 和 C# 等编程语言为例,它们所使用的正则表达式引擎大多基于 NFA。在 NFA 正则引擎的实现中,回溯技术被广泛应用。
3.2.1 没有回溯的正则
以一个网络中常见的例子来说,如正则表达式“/ab{1,3}c/g”用于匹配文本“abbc”,我们将在RegexBuddy中对其匹配步骤进行详细分析;而关于RegexBuddy的使用方法,将在后续章节中进行专门介绍。
如上图所示,让我们一步一步分解匹配过程:
正则引擎先匹配 a。
正则引擎尽可能多地(贪婪)匹配 b。
正则引擎匹配 c,完成匹配。
在这之中,匹配过程都很顺利,并没发生意外(回溯)。
3.2.2 有正则回溯的正则
对上方的正则表达式进行微调,将/ab{1,3}c/g更改为/ab{1,3}bc/g,随后使用RegexBuddy工具来检查和查看修改后的分析结果。
我们再一步一步分解匹配过程:
正则引擎先匹配 a。
正则引擎尽可能多地(贪婪)匹配 b{1,3}中的 b。
正则引擎去匹配 b,发现没 b 了,糟糕!赶紧回溯!
返回 b{1,3}这一步,不能这么贪婪,少匹配个 b。
正则引擎去匹配 b。
正则引擎去匹配 c,完成匹配。
以上,就是一个简单的回溯过程。
3.3 正则回溯的几种常见形式
通过观察上述正则表达式回溯的示例,我们可以发现,正则回溯实际上是一个不断尝试和纠正的过程,这正是回溯算法的核心所在。这种回溯机制虽然会增多匹配的步骤,从而不可避免地对文本匹配的效率造成影响。因此,为了提高正则表达式的匹配效率,深入理解回溯发生的具体情形(方式)显得尤为关键。
3.3.1 贪婪量词
在非确定有限自动机(NFA)正则表达式引擎中,量词的默认行为是贪婪的。一旦正则表达式中涉及到了下表所列的量词,引擎会首先尽可能多地匹配符合量词要求的文本。然而,当无法继续匹配时,引擎会启动回溯机制,通过不断尝试和错误,直到找到失败或成功的解决方案。
量词说明
a*
0 or more
a+
1 or more
0 or 1
a{5}
exactly five
a{2,}
two or more
a{1,3}
between one & three
若多个贪婪量词相邻且存在矛盾,遵循的是“先来后到”的规则,具体情形可参照以下示例:
let string = "12345";
let regex = /(\d{1,3})(\d{1,3})/;
console.log( string.match(regex) );
禁止对编号为12345的条目进行修改,该编号由123和45两部分组成,索引位置为0,输入内容为"12345"。
3.3.2 惰性量词
贪婪因素常是引发回溯的关键,然而,若我们尝试采用拖延策略来处理文本匹配,是否就能成功规避回溯问题呢?很遗憾,这样的做法并不能达到预期的效果。
让我们重新审视最初的案例,使用正则表达式 ab{1,3}c/g 来寻找匹配的字符串 abbc。随后,我们将对正则表达式进行微调,改为 /ab{1,3}?c/g,以便采用懒惰匹配模式来查找文本。具体操作步骤,如 RegexBuddy 所示图解中展示的那样。
正则引擎先匹配 a。
正则引擎尽可能少地(懒惰)匹配 b{1,3}中的 b。
正则引擎试图寻找字符 c,但不幸的是,遇到了障碍物 b,导致无法成功匹配 c。情况紧急,必须立即进行回溯操作!
返回 b{1,3}这一步,不能这么懒惰,多匹配个 b。
正则引擎尝试匹配字符c,却遭遇了不幸!b字符挡住了去路,导致无法成功匹配c。情急之下,必须迅速进行回溯操作!
返回 b{1,3}这一步,不能这么懒惰,再多匹配个 b。
正则引擎再去匹配 c,匹配成功,棒棒哒!
原本是运作顺畅、不会出现回溯问题的正则表达式,却因采用了惰性量词进行宽松匹配,结果却意外地引发了回溯现象。因此,我们得明白,惰性量词并非随意可用,使用时还需结合具体情境来决定。
3.3.3 分组
分支的匹配遵循这样的规则:需依次对各个分支进行检验,一旦前一个分支符合条件,后续的分支便不再考虑。
以一个简单的例子来说明,若想用正则表达式来查找文本“abcd”中符合模式“/abcde|abc/g”,可以参考使用RegexBuddy来查看具体的操作步骤。
正则引擎匹配 a。
正则引擎匹配 b。
正则引擎匹配 c。
正则引擎匹配 d。
正则引擎匹配 e,糟糕!下一个并不是 e,赶紧回溯!
上一个分支走不通,切换分支,第二个分支正则引擎匹配 a。
第二个分支正则引擎匹配 b。
第二个分支正则引擎匹配 c,匹配成功!
因此,我们可以观察到,在分组匹配的流程中,这实际上是一个不断尝试和纠正的过程,其间很可能会出现需要重新追溯的情况。
4. 正则的分析与调试
RegexBuddy是一款功能卓越的正则表达式学习、分析和调试工具。它兼容C++、Java、JavaScript、Python等多种流行编程语言。借助RegexBuddy,用户可以直观地观察正则表达式逐步构建的全过程。通过观察测试文本,我们可以直观地看到正则表达式逐步进行匹配的全过程,这一过程对于深入理解正则表达式的回溯机制以及对其进行后续优化,具有非常重要的意义。
4.1 安装分析调试工具
您可以在 RegexBuddy 的官方网站上下载并获取该软件。
下载完后,一步步点击安装即可。
4.2 工具界面介绍
下图便是 RegexBuddy 界面的各个面板及相关功能。
4.3 创建正则
为了便于操作,您可以在布局配置界面中将页面布局调整为并排显示模式。
在正则表达式输入框中输入您的正则表达式regex1,观察Create界面,您会发现该界面展示了正则表达式的构建过程,亦即匹配规则。随后,在Test界面区域输入您的测试文本,符合regex1匹配规则的内容将被突出显示,具体效果如图所示。
4.4 使用 RegexBuddy 的 Debug 功能
选择测试文本后,只需点击 debug 按钮,便能够激活 RegexBuddy 的调试模式。我个人认为,这正是 RegexBuddy 功能最为卓越之处。在该模式下,用户可以直观地观察到输入的正则表达式如何与测试文本进行匹配,每一步的执行情况,以及回溯发生的具体位置。同时,它还能明确指出哪些部分需要优化,所有这些信息都一目了然。
利用 RegexBuddy 的库功能。
RegexBuddy的正则表达式库中集成了众多常用模式,在编码日常任务时,所需的大量正则表达式大多可在此库中搜寻得到。
4.6 更多工具推荐5. 正则性能优化
正则表达式是一种极其实用的工具,若运用得法,仿佛拥有神力,可以大大减少编写代码的量。然而,若使用不当,则会在代码中留下诸多隐患。因此,本章的核心内容便是归纳总结如何编写一个高效的正则表达式。
5.1 避免量词嵌套
举个简单的例子对比:
我们采用了正则表达式“a*b”来对字符串“aaaaa”进行匹配,具体执行步骤可参考RegexBuddy的演示图示:
我们将对上述正则表达式进行修改,使其变为/(a*)*b/,以便能够匹配字符串aaaaa,随后我们将观察RegexBuddy的执行过程及其结果。
以上两个正则的基本执行步骤可以简单认为是:
贪婪匹配
回溯
直至发现匹配失败
令人感到意外的是,第一个正则表达式从开始匹配到最终失败,总共只经过了14个步骤。相比之下,第二个正则表达式则需要128步才能完成同样的过程。这充分说明,嵌套量词的存在会显著延长正则表达式的执行时间。由于这个过程涉及了两层回溯,因此执行步骤的增加使得算法的复杂度从O(n)上升到了O(n^2)。
所以,面对量词嵌套,我们需作出适当的转化消除这些嵌套:
(a*)* <=> (a+)* <=> (a*)+ <=> a*
(a+)+ <=> a+
5.2 使用非捕获组
NFA 正则引擎中的括号主要有两个作用:
主流功能,提升括号中内容的运算优先级
反向引用
括号反向引用的功能相当强大,然而其强大之处也带来了性能上的损耗。因此,在无需使用括号进行反向引用的情况下,我们应尽可能采用非捕获组,即:
// 捕获组与非捕获组
() => (?:)
5.3 分支优化
分支现象是导致正则表达式回溯的一大因素,因此,对于正则表达式中的分支结构,我们同样需要进行相应的优化处理。
5.3.1 减少分支数量
首先,我们必须削减分支的数目。例如,许多规则在处理http与https协议时,倾向于这样编写:
/^http|https/
其实上面完全可以优化成:
/^https?/
这样就能减少没必要的分支回溯
5.3.2 缩小分支内的内容
缩小分支内的信息同样至关重要,比如当我们需要对应 this 与 that 时,我们或许会这样表述:
/this|that/
但上面其实完全可以优化成
/th(?:is|at)/
或许有人觉得这两者并无明显差异,但唯有通过实际操作,我们才能验证真理。现在,就让我们用这两个正则表达式来对“that”进行匹配试验。
我们观察到,第一个正则表达式的执行过程较第一个正则表达式多了两步,这主要是因为第一个正则表达式的回溯路径较第二个正则表达式的回溯路径更长,从而使得执行步骤增多。
5.4 锚点优化
使用锚点时应尽可能采用,因为多数正则表达式引擎在构建过程中会进行额外的分析,以评估是否具备实现匹配所必需的字符或字符串。例如,诸如“^”和“$”等锚点符号能够为正则表达式引擎提供更多的优化信息。
由于正则表达式“hello(hi)?$”在执行匹配时,仅可能从目标字符串的倒数第七个字符位置开始,因此正则引擎能够直接跳转至该位置,从而跳过字符串中众多不必要的字符,显著提高了匹配的效率。
6. 结语
有一次,我编写了一个性能极差的正则表达式,结果导致代码在执行过程中因性能问题而崩溃。因此,我下定决心要彻底弄懂正则表达式。在阅读了大量文章和书籍,并进行了大量练习后,我终于领悟到了一些技巧,深刻感受到了正则表达式的优雅与强大。现在,我写下这篇文章,记录下我的学习心得和总结,希望能得到大家的批评和指正,共同提高。
7. 参考
【往期职位】