正则表达式是如何运作的?

news2024/9/28 13:18:59

在日常的开发工作当中,我们必不可免的会碰到需要使用正则的情况。

正则在很多时候通过不同的组合方式最后都可以达到既定的目标结果。比如我们有一个需要匹配的字符串:

hello

,我们可以通过 /

.</p>/ 以及 /

.?</p>/ 来匹配,两种方式就像就像中文语法中的陈述句以及倒装句,不同的语序往往不影响我们的理解,但是他们的运作方式却完全不一样。

为了让大家有一个更加直观的感受,这里将

hello

分三次存到一份百万字符文档的最前面,中间以及最后面,然后分别使用上面的 2 个正则表达式进行匹配,并测算匹配到对应字符所需要的时间,结果如下(实验结果通过 https://regexr.com/ 得出):

最前面 中间 最后面
/

.</p>/ 1.1ms 0.7ms 0.2ms
/

.?</p>/ 0.1ms 0.2ms 0.3ms
由此我们可以明显地看出不同的撰写方式,匹配的效率也有很大的不同。

撰写正则是一个不断思考与抉择的过程。就像一道数学题中往往包含一种或几种最优解的可能,想要趋近这个结果,就需要我们理清题意,掌握正则的表达方式以及加强对正则运作方式的理解。

那么,我们应该如何趋近最优解,养成良好的撰写习惯,锻炼撰写健壮的正则表达式的能力呢?

知己知彼,百战不殆,要写好正则表达式就需要做到知其然还要知其所以然。因此,本文将尝试从正则原理的角度出发,介绍正则匹配的规则,梳理正则与有限自动机的关系、及有限自动机之间的转化逻辑,并解释回溯等相关概念。希望能够帮助大家更好地理解及使用正则。

正则表达式与有限自动机(FA)
正则表达式是建立在 有限自动机 ( Finite Automaton ) 的理论基础上的,是自动机理论的应用。当我们写完相关的表达式之后,正则引擎会按照我们所写的表达式构建相应的自动机,若该自动机接受输入的文本并抵达最终状态,则表示输入的文本可以被我们所写的正则表达式匹配到。

有限自动机的图示
自动机的图形化包含以下元素:

箭头:表示路径,可以在上面标注字母,表示状态 1 经过该字母可以转换到状态 2 (也可以是标注 ε,即空串,表示无需经过转换就可以从状态 1 过渡到状态 2),
单圆圈:表示非结束的中间状态
双圆圈:表示结束状态
由自身指向自身的箭头:用于表示 Kleene 闭包(也就是正则表达式中的 ),指可以走任意次数的路径。
以 ab
c 这条正则表达式为例,其有限自动机的图示如下,表示状态 1 经过 a 可以过渡到状态 2 ,在状态 2 可以经过 ε 再循环经过 b 也可以不经过 b 而直接通过通过 ε 再经由 c 过渡到最终的结束状态。

不确定有限自动机(NFA)及确定有限自动机(DFA)
有限自动机又可以分为 NFA:不确定有限自动机(Nondeterministic Finite Automaton )及 DFA:确定有限自动机(Deterministic Finite Automaton ),NFA 的不确定性表现在其状态可以通过 ε 转换 以及对同一个字符可以有不同的路径。而 DFA 可以看作是一种特殊的 NFA,其最大的特点就是确定性,即输入一个字符,一定会转换到确定的状态,而不会有其他的可能。

以 ab|ac 这条正则表达式为例子,我们可以得到以下 NFA 及 DFA 两种自动机。

可以看到自动机 1 中我们有两条路径都是 a,经由 a 可以到达下一个状态,而自动机 2 中只有一条路径,对于图一来说,经由相同的 a 路径却可以导致不同的结果,这即是 NFA,具有不确定性。而对图二来说,其路径都是通往明确的结果,具有确定唯一性,所以是 DFA。

正则替换成 NFA
从上面的图中我们可以看出 DFA 的匹配路径比 NFA 要少且匹配的速度要更快,但是目前大多数的语言中的正则引擎使用的却是 NFA,为什么不直接使用 DFA 而要使用 NFA?为了解答这个问题,我们需要知道如何通过正则转化成 NFA,而 NFA 又可以怎样转成 DFA。

正则表达式转 NFA 主要基于以下 3 条规则(R 表示正则表达式)

连接 R = AB

选择 R = A|B

重复 R = A*

其他的运算基本都可以通过以上三种运算转换得到,比如 A+ 可以用 AA* 来表示

Thompson 算法
为了更好地理解上面的 3 条转换规则,这里介绍比较实用且容易理解由 C 语言 & Unix 之父之一的 Ken Thompson 提出的 Thompson 算法。其思想主要就是通过简单的表达式组合成复杂的表达式。

Thompson算法 中两种最基本的单元(或者说两种最基本的 NFA):

表示经过字符 a 过渡的下一个状态以及不需要任何字符输入的 ε 转换 过渡到下一个状态。

对于连接运算 R = AB,我们将 AB 拆解成 NFA(A) 与 NFA(B) 两个单元,然后进行组合:

对于选择运算 R = A|B,我们同样将 A 与 B 拆解成 NFA(A) 与 NFA(B) 两个单元,然后进行组合:

对于重复运算 R = A*,其表示可能不需要经过转换,可能经过 1 次或者多次,所以拆解成单一 NFA 后,我们可以这样表示:

由此,我们就可以根据上面的 3 条转换规则,将正则表达式进行拆分重组,最后得出相应的 NFA。

NFA 转换成 DFA 及其简化
DFA 实际上是一个特殊的 NFA,转 DFA,就是要将 NFA 中将所有等价的状态合并,消除不确定性。

这里以《编译原理》一书中的一道例题来完整地讲解一下如何从正则转 NFA 再转成相应的 DFA,并对其进行简化。

eg: (a|b)(aa|bb)(a|b)
这里我们依据正则转 NFA 的三条规则以及 Thompson 算法 的理念,将上述的表达式进行拆分与组合:

首先我们将该表达式以括号为分隔,视为 3 个正则表达式子通过连接符连接,以此拆分成 3 个表达式并将其组合
然后根据每个表达式括号内的内容继续拆分成更细的单元,碰到运算符号,则按照规则进行转换,以此类推直到 NFA 变成变成由最小单元组合而成。

子集构造算法
正如前面所说,NFA 转 DFA 本质是将等价的状态合并,消除不确定性。要找出等价的状态,我们需要先找出各个状态的集合。

上面的表达式只有 a 跟 b 两个字符,所以我们要得出各个状态经过 a 以及经过 b 的所有集合,然后再将等价的集合合并。这里先画出所有集合构成的转换表单,结合图示将更有助于我们的的理解。

I Ia Ib
{i,1,2} {1,2,3} {1,2,4}
{1,2,3} {1,2,3,5,6,f} {1,2,4}
{1,2,4} {1,2,3} {1,2,4,5,6,f}
{1,2,3,5,6,f} {1,2,3,5,6,f} {1,2,4,6,f}
{1,2,4,5,6,f} {1,2,3,6,f} {1,2,4,5,6,f}
{1,2,4,6,f} {1,2,3,6,f} {1,2,4,5,6,f}
{1,2,3,6,f} {1,2,3,5,6,f} {1,2,4,6,f}
图示第一列主要是放置所有不重复的集合,第二列表示经过 a 的集合,第三列表示经过 b 的集合

子集构造法 寻找集合的规则为碰到一个字符,如果这个字符后面有可以通过 空串(ε转换)到达下一个状态,则下一个状态包含在该集合中,一直往后直到碰到一个明确的字符

从上面构造的NFA的初始状态 i 开始,其经过 2 个 ε转换 转换可以到达 2 状态,此后必须经过 a 或者 b,由此我们可以得到第一个状态集合 {i,1,2}

从第一个集合开始,分析集合内分别经过 a 和 b 可以达到什么状态,可以看到初始集合中 i 只经过空串,不经过 a、b, 状态 1 经过 a 可以到达它自身,也可以再经过 ε 到达状态 2,2 经过 a 只能到达状态 3

据此我们得到初始集合经过 a 的集合为 {1,2,3},同样的,初始集合经过 b 只在状态 2 与经过 a 不同,所以我们可以得到经过 b 的集合为 {1,2,4}
因为 {1,2,3},{1,2,4} 都没有出现过,所以这里我们将其记到第一列的第二与第三行,并分析它们经过 a 与 b 的集合。
以此类推直到获得上述所有集合构成的转换表单(完整文字版推导过程附于文末附录)
可以看到上面的转换表的第一列中一共有 7 个集合,这里我们给第一列的集合进行排序(为方便与 NFA 对比,这里将序号 0 改为 i),并对右边经过 a 跟经过 b 的所有集合根据左边的序号进行标记,可以得到相应转换矩阵:

I Ia Ib
{i,1,2} i {1,2,3} 1 {1,2,4} 2
{1,2,3} 1 {1,2,3,5,6,f} 3 {1,2,4} 2
{1,2,4} 2 {1,2,3} 1 {1,2,4,5,6,f} 4
{1,2,3,5,6,f} 3 {1,2,3,5,6,f} 3 {1,2,4,6,f} 5
{1,2,4,5,6,f} 4 {1,2,3,6,f} 6 {1,2,4,5,6,f} 4
{1,2,4,6,f} 5 {1,2,3,6,f} 6 {1,2,4,5,6,f} 4
{1,2,3,6,f} 6 {1,2,3,5,6,f} 3 {1,2,4,6,f} 5
转换矩阵
S a b
i 1 2
1 3 2
2 1 4
3 3 5
4 6 4
5 6 4
6 3 5
依据转换矩阵,我们把第一列的数据作为每一个单独状态,并以 i 为初始状态,由矩阵可得,其经过 a 可以到达状态 1,经过 b 可以到达状态 2,同时我们将包含 f 的集合作为终态(即序号 3,4,5,6),以此类推,我们可以得到如下的 NFA:

因为该 NFA 不包含空串,也没有由一个状态分出两条相同的分支路径,所以该 NFA 就是上述表达式的 DFA。但该 DFA 看起来还比较复杂,所以我们还需要对其进一步简化。

Hopcroft 算法化简
Hopcroft 算法 是1986年图灵奖获得者 John Hopcroft 所提出的,其本质思想跟子集构造法相似,都是对等价状态的合并。

Hopcroft 算法 首先将未化简的 DFA 划分成 终态集 和 非终态集(因为这两种状态一定不等价),之后不断进行划分,直到不再发生变化。每轮划分对所有子集进行。对一个子集的划分中,若每个输入符号都能把状态转换到等价的状态,则两个状态等价。

这里依据 Hopcroft 算法 将上述 DFA 以终态以及非终态进行划分,可以得到 {i,1,2} 以及 {3,4,5,6} 两个集合。然后分别分析两个集合是否能够进一步划分。

集合 {i,1,2} 经过 a 可以得到状态 1 和 3,3 不在集合 {i,1,2} 中,依据矩阵图,我们可以看到 i 和 2 经过 a 都到状态 1,1 经过 a 可以达到状态 3,于是我们将 {i,1,2} 划分成 {i,2} 和 {1} 两个集合

因为 {1} 已经是最小集合,所以无法继续划分,所以分析 {i,2} 集合经过 b 的情况,{i,2} 经过 b 可以达到状态 2 和 4,4 同样不在集合中,所以需要对 {i,2} 进行划分,依据矩阵表,我们可以划分成 {i},{2},至此,非终态已经无法往下拆分,所以分析结束,我们得到的拆分集合为 {i},{1},{2}

终态集合 {3,4,5,6} 经过 a 可以达到状态 3 和 6,3 和 6 都在集合内部,所以无需往下拆分,经过 b 可以达到状态 4 和 5,4 和 5 同样都在集合内,无需拆分。所以我们可以将 {3,4,5,6} 当作是一个整体。

将集合 {3,4,5,6} 当作一个整体,记成状态 3,重新梳理上面的矩阵,并将所有指向 3,4,5,6 的路径都指向新的状态 3,我们可以得到新的也是最简单的 DFA:

从上面的转换过程可以看到,实际上,NFA 转 DFA 是一个繁琐的过程,如果正则采用 DFA 引擎,势必会消耗部分性能在 NFA 的转换上,而这个转换的效益很多时候远不比直接用使用 NFA 高效,同时 DFA 相对来说没有 NFA 直观,可操作空间也要比 NFA 少,所以大多数语言的采用 NFA 作为正则的引擎。

回溯
正则采用的是 NFA 引擎,那么我们就必须面对它的不确定性,体现在正则上就是 回溯 的发生。

遇到一个字符串,NFA 拿着正则表达式去比对文本,拿到一个字符,就把它与字符串做比较,如果匹配就记住并继续往下拿下一个字符,如果后面拿到的字符与字符串不匹配,则将之前记下的字符一个个往回退直到上一次出现岔路的地方。

假设现在有一个正则表达式 ab|ac,需要匹配字符串 ac。则 NFA 会先拿到正则的字符 a,然后去比较字符串 ac,发现匹配中了 a,则会去拿下一个字符 b,然后再匹配字符串,发现不匹配,则回溯,吐出字符 b,回到字符 a,取字符 c 去匹配字符串,发现匹配,完成比对。

文字或许比较枯燥,这里用下面得图示来表示上述的过程,蓝色圆圈表示NFA拿到正则的字符后去匹配字符串看是否可以过渡到下一个状态,同时字符的颜色变化表示是否可以被匹配中:

回溯

贪婪模式与惰性模式对比
回到一开始讲的通过 /

.</p>/ 以及 /

.?</p>/ 来匹配

hello

的问题,因为量词的特殊性以及 回溯 的存在,所以2种方式的匹配效率也不一样。
  • 表示重复零次或多次,一般情况下会以 贪婪模式 尽可能地匹配多次,因此在上面的匹配过程中,它会在匹配到

    之后一口气把之后的字符也吞并掉,然后通过回溯匹配 < 字符直到匹配到完整的 </p>,而当我们给 * 加上 ? 之后,它就变成非贪婪的 惰性模式,当匹配到

    之后它就会通过回溯逐步去匹配 </p> 直到匹配中完整的字符串。

目标字符串:

hello

在两个表达式下的匹配过程:

表达式:/

.</p>/ 表达式:/

.?</p>/
< 匹配 < < 匹配 <

表达式:ab
aaa 匹配 a

aaa 匹配 b
aa 回溯
aa 匹配 b
a 回溯
a 匹配 b
回溯
这个正则表达式回溯到最后也没有匹配到对应的字符串,而通常我们所写的正则并不单单像例子中只回溯这一小部分,设想一下,一个需要多处回溯的正则表达式子,去匹配成百上千甚至上万个字符,那对机器来说是多么可怕的一件事情。https://www.ixigua.com/7202959346417074688

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/366625.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据分析与SAS学习笔记8

过程步&#xff1a;一个典型的SAS完整程序&#xff1a; 代码说明&#xff1a; 1&#xff09;reg&#xff1a;回归分析&#xff1b; 2&#xff09;model&#xff1a;因变量和自变量。 proc开头部分叫过程步。 常用过程&#xff1a; SORT过程&#xff1a; PRINT过程与FORTMAT…

Linux环境下绕过长度限制写入webshell

前提当命令执行漏洞对长度有限制时&#xff0c;我们可以通过一些Linux命令生成文件进行执行。如果应用程序是使用shell解析器来执行命令或文件&#xff0c;那么不需要该文件具有执行属性(x)如果应用程序没有shell解析器&#xff0c;那么写入文件内容时&#xff0c;可以第一条语…

终极方案,清理 docker 占用磁盘过大问题, 亲测有效!

背景 在笔者的工作测试环境中&#xff0c;使用过程中突然出现根磁盘快吃满了&#xff08;docker也是使用的根池盘的/var/lib/docker&#xff09;&#xff0c; wtf &#xff1f; 服务用不了&#xff1f; 当然网上找到了一些常规的清楚docker 日志文件 但是通过df -hT 查看到over…

2023年Java面试题精选(蚂蚁金服/滴滴/美团/拼多多腾讯)

作为一名优秀的程序员&#xff0c;技术面试都是不可避免的一个环节&#xff0c;一般技术面试官都会通过自己的方式去考察程序员的技术功底与基础理论知识。 如果你参加过一些大厂面试&#xff0c;肯定会遇到一些这样的问题&#xff1a; 1、看你项目都用的框架&#xff0c;熟悉…

Linux 中使用 docker-compose 部署 MongoDB 6 以上版本副本集及配置 SSL / TLS 协议

一、准备环境 MongoDB 副本集部署至少 3 个节点&#xff08;奇数节点&#xff09;&#xff0c;为了保障数据安全性&#xff0c;可考虑将 MongoDB 节点分布在不同的主机上&#xff0c;本示例使用一台主机部署 3 个 MongoDB示例。 1、创建 MongoDB 集群数据相关目录 # 创建 Mo…

大数据|大数据基础(概念向)

目录 &#x1f4da;大数据概念 &#x1f407;常见数据存储单位 &#x1f407;大数据的特点&#xff08;5V&#xff09; &#x1f407;大数据 VS 数据库 &#x1f31f;数据库 &#x1f31f;大数据 &#x1f4da;大数据业务分析基本步骤 &#x1f407;收集数据 &#x1f4…

RockerMQ简介和单节点部署

目录一、RockerMQ简介二、Linux中单节点部署1、准备工作2、下载和解压3、修改初始内存4、启动5、查看进程6、发送接收消息测试7、关闭三、控制台的安装与启动(可视化页面)1、修改配置&#xff08;1&#xff09;修改端口号&#xff08;2&#xff09;指定RocketMQ的name server地…

企业知识管理常见的误区及解决方案

在企业信息化的背景下&#xff0c;越来越多的首席信息官&#xff08;CIO&#xff09;承担着促进组织知识管理实施的责任。然而&#xff0c;从实践的角度来看&#xff0c;虽然我国大多数知识管理实施项目都取得了一定的成果&#xff0c;但与预期有很大的不同&#xff0c;甚至许多…

这18个被全网吹爆了的AI绘画工具,分享给你!

伴随着ChatGPT的横空出世&#xff0c;一场史无前例的科技革命正在拉开序幕。 AI 拥有强大的信息储备和数据处理能力&#xff0c;无论是速度、质量&#xff0c;还是思维模式&#xff0c;都让人只呼不得了&#xff01;写代码、造论文丝毫不在话下&#xff0c;甚至还能和你探讨茶…

当 Amazon Lambda 遇上 Apache APISIX 可以擦出什么火花?

本文首先介绍了什么是 Serverless&#xff0c;以及为什么需要 Serverless&#xff1b;其次&#xff0c;讲述了一个好的网关在 Serverless 架构下的重要性&#xff0c;而 APISIX 就是这样的一个网关&#xff1b;最后&#xff0c;本文重点介绍了 APISIX 中的 Serverless 类型的插…

您应该知道的几个安卓照片恢复应用程序

如果您不小心删除了存储在 Android 手机上的一些重要照片&#xff0c;该怎么办&#xff1f;如果您之前已创建备份&#xff0c;则只需将备份文件中的照片恢复到您的手机即可。但数据丢失往往是突然发生的&#xff0c;可能是由于误操作、恢复出厂设置或物理损坏等原因造成的。如果…

高性能低功耗4口高速USB2.0 HUB NS1.1S 兼容FE1.1

NS1.1S是一款高性能、低功耗4口高速 USB2.0 HUB 控制器&#xff0c;上行端口兼容高速 480MHz和全速12MHz两种模式&#xff0c;4个下行端口兼容高速480MHz、全速12MHz、低速1.5MHz三种模式。 NS1.1S采用状态机单事务处理架构&#xff0c;而非单片机架构&#xff0c;多个事务缓冲…

Java无法通过形参设置为null改变实参

文章目录问题描述问题例子问题分析问题描述 在实际业务开发过程中&#xff0c;我们会把实参传递给形参&#xff0c;在方法体内对引用对象进行构建或者修改&#xff0c;从而改变实参&#xff0c;因为对形参对象属性修改时&#xff0c;实参对象也会随着改变&#xff0c;详情请看&…

《Java核心技术》笔记——第六章

文章目录CH6.接口、lambda表达式与内部类1.接口基本2.常用接口3.lambda表达式4.内部类5.服务加载器与代理前章&#xff1a; 第三章~第五章的学习笔记CH6.接口、lambda表达式与内部类 1.接口基本 接口基本——interface声明&#xff0c;方法无需指明public&#xff08;默认都是…

基于node vue的电商系统 mongodb express框架

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 基于node vue的电商系统 mongodb express框架前言技术栈基本功能普通用户管理员一、运行截图&#xff1f;二、使用步骤1.前端main.js2.后端admin路由前言 技术栈 本项目采用…

行业分析| OA系统中的实时通讯

前言 当前实时通讯市场中有QQ、MSN、网络电话以及视频会议等&#xff0c;同时也有各个互联网巨头推出的基于自身平台的实时通讯工具&#xff0c;比如&#xff0c;百度hi&#xff0c;淘宝旺旺等&#xff0c;相对而言其与纯粹的实时通讯工具不同&#xff0c;基于自身平台的实时通…

点击化学 PEG 试剂1858242-47-3,Propargyl丙炔基-PEG1-乙酸活性酯

Propargyl-PEG1-Acetic acid-NHS ester&#xff0c;丙炔基-聚乙二醇-乙酸琥珀酰亚胺酯&#xff0c;丙炔基-PEG1-乙酸活性酯&#xff0c;丙炔基-PEG1-乙酸-NHS 酯产品规格&#xff1a;1.CAS号&#xff1a;1858242-47-32.分子式&#xff1a;C9H9NO53.分子量&#xff1a;211.174.包…

ChatGPT商业前景如何?人工智能未来会如何发展?

ChatGPT不仅在互联网和多个行业引发人们的关注&#xff0c;在投资界还掀起了机构对人工智能领域的投资热潮。人工智能聊天程序ChatGPT在去年11月亮相之后&#xff0c;在推出仅两个月后&#xff0c;今年1月份的月活用户已达到了1亿&#xff0c;成为史上增长最快的消费者应用程序…

【selenium 自动化测试】如何搭建自动化测试环境,搭建环境过程应该注意的问题

最近也有很多人私下问我&#xff0c;selenium学习难吗&#xff0c;基础入门的学习内容很多是3以前的版本资料&#xff0c;对于有基础的人来说&#xff0c;3到4的差别虽然有&#xff0c;但是不足以影响自己&#xff0c;但是对于没有学过的人来说&#xff0c;通过资料再到自己写的…

UA-DETRAC数据集转YOLO格式

一&#xff1a; 数据集下载 链接&#xff1a;&#xff08;后续添加&#xff09; 二&#xff1a; 处理标注文件 先处理标注文件&#xff0c;UA-DETRAC提供的标注文件格式是VOC格式&#xff0c;需要先转为XML格式&#xff0c;然后再将每个XML文件转为YOLO文件。 下面提供两个代…