从0到1打造正则表达式执行引擎(一) 正则表达式转NFA (1)

简介: 重复匹配(正则表达式中的 ? + *)正则表达式里有4种表示重复的方式,分别是:

今天是五一假期第一天,这里先给大家拜个晚 咳咳!!祝大家五一快乐,我这里给大家奉上一篇硬核教程。首先声明,这篇文章不是教你如何写正则表达式,而是教你写一个能执行正则表达式的 执行引擎。 网上教你写正则表达式的文章、教程很多,但教你写引擎的并不多。很多人认为我就是用用而已,没必要理解那么深,但知道原理是在修炼内功,正则表达式底层原理并不单单是用在这,而是出现在计算机领域的各个角落。理解原理可以让你以后写字符串匹配时正则表达式能够信手拈来,理解原理也是触类旁通的基础。废话不多说,直接开始正式内容。

本文是我个人做的动手实践性项目,所以未完整支持所有语法,而且因为是用NFA实现的所以性能比生产级的执行引擎差好多。目前源码已开放至https://github.com/xindoo/regex,后续会继续更新,欢迎Star、Fork 提PR。


目前支持的正则语义如下:


基本语法: . ? * + () |

字符集合: []

特殊类型符号: \d \D \s \S \w \W

前置知识

声明:本文不是入门级的文章,所以如果你想看懂后文的内容,需要具备以下的基本知识。


基本的编程知识,虽然这里我是用java写的,但并不要求懂java,懂其他语法也行,基本流程都是类似,就是语法细节不同。

了解正则表达式,知道简单的正则表达式如何写。

基本的数据结构知识,知道有向图的概念,知道什么是递归和回溯。

有限状态机

有限状态机(Finite-state machine),也被称为有限状态自动机(finite-state automation),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型(From 维基百科 状态机) 。 听起来晦涩难懂,我用大白话描述一遍,状态机其实就是用图把状态和状态之间的关系描述出来,状态机中的一个状态可以在某些给定条件下变成另外一种状态。举个很简单的例子你就懂了。


比如我今年18岁,我现在就是处于18岁的状态,如果时间过了一年,我就变成19岁的状态了,再过一年就20了。当然我20岁时时光倒流2年我又可以回到18岁的状态。这里我们就可以把我的年龄状态和时间流逝之间的关系用一个自动机表示出来,如下。

9096c5b780511d336a05f90c29a565bd_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

每个圈代表一个节点表示一种状态,每条有向边表示一个状态到另一个状态的转移条件。上图中状态是我的年龄,边表示时间正向或者逆向流逝。


有了状态机之后,我们就可以用状态机来描述特定的模式,比如上图就是年龄随时间增长的模式。如果有人说我今年18岁,1年后就20岁了。照着上面的状态机我们来算下,1年后你才19岁,你这不是瞎说吗! 没错,状态机可以来判定某些内容是否符合你状态机描述的模式了。哟,一不小心就快扯到正则表达式上了。

我们这里再引入两种特殊的状态:起始态和接受态(终止态),见名知意,不用我过多介绍了吧,起始态和终止态的符号如下。

我们拿状态机来做个简单的字符串匹配。比如我们有个字符串“zsx”,要判断其他字符串是否和"zxs"是一致的,我们可以为"zxs"先建立一个自动机,如下。

1eecb229897e624293ed7d9977bd0ce6_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

对于任意一个其他的字符串,我们从起始态0开始,如果下一个字符能匹配到0后边的边上就往后走,匹配不上就停止,一直重复,如果走到终止态3说明这个字符串和”zxs“一样。任意字符串都可以转化成上述的状态机,其实到这里你就知道如何实现一个只支持字符串匹配的正则表达式引擎了,如果想支持更多的正则语义,我们要做的更多。


状态机下的正则表达式

我们再来引入一条特殊的边,学名叫ϵ \epsilonϵ闭包(emm!看到这些符号我就回想起上学时被数学支配的恐惧),其实就是一条不需要任何条件就能转移状态的边。

2ae3986b4997439a2fb95cf8f7e1fd4e_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

没错,就只这条红边本边了,它在正则表达式状态机中起着非常重要的连接作用,可以不依赖其他条件直接跳转状态,也就是说在上图中你可以直接从1到2。

有了 ϵ \epsilonϵ闭包的加持,我们就可以开始学如何画正则表达式文法对应的状态机了。


串联匹配

首先来看下纯字符匹配的自动机,其实上面已经给过一个"zxs"的例子了,这里再贴一下,其实就是简单地用字符串在一起而已,如果还有其他字符,就继续往后串。

1eecb229897e624293ed7d9977bd0ce6_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

两个表达式如何传在一起,也很简单,假如我们已经有两个表达式A B对应的状态机,我们只需要将其用ϵ \epsilonϵ串一起就行了。

f1e2ae3aa35225347621936b19645332_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png


并联匹配 (正则表达式中的 |)

正则表达式中的**|** 标识二选一都可以,比如A|B A能匹配 B也能匹配,那么A|B就可以表示为下面这样的状态图。

d1f7a7debf896df1b03c27c76a1178f2_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

从0状态走A或B都可以到1状态,完美的诠释了A|B语义。


重复匹配(正则表达式中的 ? + *)

正则表达式里有4种表示重复的方式,分别是:


?重复0-1次

重复1次以上

重复0次以上

{n,m} 重复n到m次

我来分别画下这4种方式如何在状态机里表示。

重复0-1次 ?

1eda72b43bf08e31a79cc86903f55a5b_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

0状态可以通过E也可以依赖ϵ \epsilonϵ直接跳过E到达1状态,实现E的0次匹配。


重复1次以上

a3fc1b6b536c40bc1db1bb911a0da15e_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

0到1后可以再通过ϵ \epsilonϵ跳回来,就可以实现E的1次以上匹配了。


重复0次以上

38ca5a5b992b6dc16500fe6a234bbcfb_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

仔细看其实就是**? +**的结合。


匹配指定次数

ed029523e6ae5a67843d3c8204b54c85_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

这种建图方式简单粗暴,但问题就是如果n和m很大的话,最后生成的状态图也会很大。其实可以把指定次数的匹配做成一条特殊的边,可以极大减小图的大小。


特殊符号(正则表达式中的 . \d \s……)

正则表达式中还支持很多某类的字符,比如**.表示任意非换行符,\d标识数字,[]**可以指定字符集…… ,其实这些都和图的形态无关,只是某调特殊的边而已,自己实现的时候可以选择具体的实现方式,比如后面代码中我用了策略模式来实现不同的匹配策略,简化了正则引擎的代码。


子表达式(正则表达式 () )

子表达可以Tompson算法,其实就是用递归去生成**()**中的子图,然后把子图拼接到当前图上面。(什么Tompson说的那么高大上,不就是递归吗!)


练习题

来练习画下 a(a|b)* 的状态图,这里我也给出我画的,你可以参考下。

860d684cc36cfe9a652a6898594e2ecf_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbmRvbw==,size_16,color_FFFFFF,t_70#pic_center.png

目录
相关文章
|
6月前
|
自然语言处理 算法 编译器
编译原理复习一:正则表达式-NFA NFA-DFA DFA最小化(附题目与答案 超详细)
编译原理复习一:正则表达式-NFA NFA-DFA DFA最小化(附题目与答案 超详细)
324 0
|
算法 测试技术
从0到1打造正则表达式执行引擎(二) NFA转DFA
然后对DFA的节点0执行步骤1,找到NFA中所有a可达的NFA节点(1#2#4#6#8#9)构成NFA中的节点1,如下图。
122 0
|
设计模式 算法
从0到1打造正则表达式执行引擎(一) 正则表达式转NFA (2)
看完上文之后相信你一直知道如果将一个正则表达式转化为状态机的方法了,这里我们要将理论转化为代码。首先我们要将图转化为代码标识,我用State表示一个节点,其中用了Map<MatchStrategy, List> next表示其后继节点,next中有个key-value就是一条边,MatchStrategy用来描述边的信息。
72 0
|
存储 Java
一种高性能单模正则表达式引擎
背景正则表达式匹配引擎的实现方式分为NFA(非确定有穷自动机,Non-deterministic finite automaton)和DFA(确定有穷自动机,Deterministic finite automaton)两类。由于NFA的状态数量与正则串长度成正比,因此大量开源库采用了NFA的执行方式,比如JDK regex、joni。也有一部分正则库在NFA翻译为DFA的理论基础上,采用了处理速
一种高性能单模正则表达式引擎
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA | 示例 ) ★★(一)
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA | 示例 ) ★★(一)
163 0
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA | 示例 ) ★★(一)
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA | 示例 ) ★★(二)
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA | 示例 ) ★★(二)
148 0
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA | 示例 ) ★★(二)
|
资源调度
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA ) ★★
【计算理论】计算理论总结 ( 正则表达式转为非确定性有限自动机 NFA ) ★★
348 0
|
5月前
|
数据库 Python
Python网络数据抓取(8):正则表达式
Python网络数据抓取(8):正则表达式
54 2
|
5月前
|
自然语言处理 JavaScript 前端开发
Python高级语法与正则表达式(二)
正则表达式描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。