前言

之前还写过一个版本的词法分析,但是对类的设计不太满意以及扩展性比较差。所以,又重新设计再写了一份,解决了之前遗留的一些问题。

上个版本链接

项目地址

github(还没有上传)

前导知识

  1. 什么是正则表达式?
  2. 什么是状态转换图?
  3. 非确定的有穷状态自动机(NFA)和确定的有穷状态自动机(DFA)的区别
  4. 如何将正则表达式构造成状态转换图?(Thompson算法)
1. 什么是正则表达式?

百度百科

2. 什么是状态转换图?

正则引擎主要有两大类,一种是NFANFANFA,一种是DFADFADFA;
NFANFANFA与DFADFADFA就是由状态转换图组成的(比较复杂而已),我们需要通过自动化的方式将一组正则表达式集合构造出状态转换图(transition diagram)。

组成:
状态转换图有一组被称为“状态(state)”的结点或圆圈。

状态转换图的(edge)从图的一个状态指向另一个状态,每条边的标号包含了一个或多个符号;假设我们处于某个状态SSS,并且下一个输入符号是aaa,我们就会寻找一条从SSS离开且标号为aaa的边,到达另一个状态FFF。通常称为转换函数,即有 F=f(S,a)F = f(S, a)F=f(S,a)

一个状态(有且只能有一个)没有前驱状态,称为开始状态或初始状态。
某些状态(多个)没有后继状态,称为接受状态或最终状态。通常使用两个圆圈表示。

3. 非确定的有穷状态自动机(NFA)和确定的有穷状态自动机(DFA)的区别

对于NFA的某个状态SSS, 存在输入符号III, SSS能通过III到达一组后继状态P{p∣p∈P}P\{p | p ∈ P\}P{p∣p∈P}。
对于DFA的某个状态SSS, 存在输入符号III, SSS能通过III只能到达一个后继状态QQQ。

4. 如何将正则表达式构造成状态转换图?

语言
字母表(alphabet)是一个有限的符号集合。
符号的典型例子包括字母、数位和标点符号。
集合{0, 1}是二进制字母表,ASCII是一个字母表的重要例子。

语言是某个给定字母表上一个任意的可数的串的集合,像∅和仅包含空串的集合{εεε}都是语言。

如果xxx和yyy是串,那么xxx和yyy的连接(concatenation)(记作xyxyxy)是把yyy附加到xxx后面形成的串。例如x=dogx=dogx=dog且y=housey=housey=house,那么xy=doghousexy=doghousexy=doghouse。空串ε是连接运算的单位元,也就是,对于任何串sss都有sε=εs=ssε=εs=ssε=εs=s。

如果把两个串的连接看成是这两个串的“乘积”,我们可以定义串的“指数”运算,即:s0s^0s0为εεε,并且对于i>0i>0i>0,sis^isi为si−1ss^{i-1}ssi−1s。
因为εs=sεs=sεs=s,由此可知s1=ss^1=ss1=s,s2=sss^2=sss2=ss,s3=ssss^3=ssss3=sss以此类推。

语言的运算

Kleene闭包是将LLL连接000次或多次得到的串集;正闭包是将LLL连接111次或多次得到的串集;注意,L0L^0L0是将LLL连接000次得到的集合,被定义为{ε}\{ε\}{ε}。
LLL和MMM的并 → L∪M={s∣s属于L或者s属于M}L∪M = \{s | s 属于L或者s属于M\}L∪M={s∣s属于L或者s属于M}
LLL和MMM的连接 → LM={st∣s属于L且t属于M}LM = \{st | s 属于L且t属于M\}LM={st∣s属于L且t属于M}
LLL的Kleene闭包 → L∗=∪i=0∞LiL^* =∪_{i=0}^∞L^iL∗=∪i=0∞​Li
LLL的正闭包 → L+=∪i=1∞LiL^+ = ∪_{i=1}^∞L^iL+=∪i=1∞​Li

补充: LLL的零一闭包 → L?是将L连接0次或1次得到的串集L^?是将L连接0次或1次得到的串集L?是将L连接0次或1次得到的串集

正则定义:为了方便表示,我们希望给某些正则表达式命名,并在之后的正则表达式像使用符号一样使用这些名字。如果∑∑∑是基本符号的集合,那么一个正则定义(regular definition)是具有如下形式的定义序列。
d1→r1d2→r2...dn→rnd_1 → r_1 \\ d_2 → r_2 \\ ...\\ d_n → r_n d1​→r1​d2​→r2​...dn​→rn​
其中,每个did_idi​都是新符号,都不在∑∑∑中,并且都不相同。
每个rir_iri​是字母表∑∪{d1,d2,d3...dn}∑∪\{d_1, d_2, d_3 ... d_n\}∑∪{d1​,d2​,d3​...dn​}上的正则表达式。
限制每个rir_iri​只含有∑∑∑中的符号和在它之前定义的各个djd_jdj​。

例如,Java语言的标识符是由字母,数字,下划线组成,其中第一个符号不能是数字,所以,我们可以很容易的构造出标识符的正则定义(注意顺序):
letter_→A∣B∣...Z∣a∣b∣...∣z∣_digit→0∣1∣...∣9id→letter_(letter_∣digit)∗letter\_ → A |B|...Z|a|b|...|z|\_ \\ digit → 0 |1|...|9 \\ id → letter\_ (letter\_|digit)* letter_→A∣B∣...Z∣a∣b∣...∣z∣_digit→0∣1∣...∣9id→letter_(letter_∣digit)∗

假设需要匹配一个字符aaa,那么我们能很轻松地构造出它的转换转换图N(R)N(R)N(R),只需要将输入符号(标号)设置为aaa。正则定义为: R→aR → aR→a

构造连接运算的状态转换图时,需要将每个正则表达式的状态转换图都构造出来,如N(R1)N(R_1)N(R1​)和N(R2)N(R_2)N(R2​),然后使用连接运算符的单位元连接;

以N(R1)N(R_1)N(R1​)的开始状态为新的开始状态,再以N(R1)N(R_1)N(R1​)的接受状态构造出一条ε边到N(R2)N(R_2)N(R2​)的开始状态,以N(R2)N(R_2)N(R2​)的接受状态为新的接受状态。最后得到N(R)N(R)N(R).

在例子中只能先匹配输入符号aaa到达111号状态,然后再匹配输入符号bbb到达333号状态[单位元无意义, 不匹配字符] 。假设正则定义是:
R1→aR2→bR→R1R2R_1 → a \\ R_2 → b \\ R → R_1R_2R1​→aR2​→bR→R1​R2​




构造或运算时,同样需要将每个正则表达式的状态转换图构造出来,如N(R1)N(R_1)N(R1​)和N(R2)N(R_2)N(R2​),然后使用单位元连接。

新建一个开始状态SSS,S分别有一个ε边连接到N(R1)N(R_1)N(R1​)和N(R2)N(R_2)N(R2​)的开始状态,且N(R1)N(R_1)N(R1​)和N(R2)N(R_2)N(R2​)的接受状态都有一条单位元边连接到新建的接受状态FFF。

(在例子中,开始状态要么通过R1R_1R1​,要么通过R2R_2R2​到达接受状态[单位元无意义, 不匹配字符] ),假设正则定义是:
R1→aR2→bR→R1∣R2R_1 → a \\ R_2 → b \\ R → R_1|R_2R1​→aR2​→bR→R1​∣R2​



构造Kleene闭包,Kleene是将LLL连接000次或多次得到的串集:

构造0次的状态转换图,只需要从开始状态构造一条ε边到接受状态(即不需要任何输入字符);构造多次的状态转换图,只需要从N(R)N(R)N(R)的接受状态构造出条到N(R)N(R)N(R)的开始状态(假设串是aaaaaa,当分析第一个aaa时,000号状态可以通过输入符号aaa到达222号状态;此时,222号状态可以直接到达333号接受状态,也可以回到111号状态再接受输入字符aaa,以此循环)。
R→a∗R →a^*R→a∗

同样,也能构造正闭包零一闭包
R→a+R →a^+R→a+
R→a?R →a^?R→a?

注意,当某几个状态转换图合成一个时,那几个状态转换图的开始状态和接受状态将变成普通状态,然后根据不同的Thompson转换方法,重新设置不同的开始状态和接受状态。

支持文法(Lex的正则表达式)

相对于上面的正则定义,下面的表示方法更简洁。

表达式 匹配 例子
c 单个非运算字符c a, b, z
\c 字符c的字面值 \\
“s” 串s的字面值 “.abc”
. 除换行符以外的所有字符 .o
[s] 字符串s中的任意一个字符 [abc], [a-z], [0-9]
[^s] 不在字符串s中的任意一个字符 [^abc]
r1r2 r1后加上r2 ac
(r) 与r相同 (ab)
r{m, n} 最少m个,最多n个r重复出现 r{1, 5}
r1 | r2 r1或r2 a | b
r* 和r匹配的零个或多个串连接的串 a*, [abc]*
r+ 和r匹配的一个或多个串连接的串 a+, [0-9]+
r? 零个或一个r a?, (ab)?

正文

正文部分都是遇到的一些比较有意思的问题,不会讲全部代码,只讲遇到过哪些问题,如果有更好的办法,欢迎讨论~

一、 如何设计‘状态转换图’类?

1. 这部分比较容易设计,State(状态)通过Transition Function(状态)到达下一个State(状态);
2. 每个State都有一个id属性和一个StateType属性(是开始状态,还是普通状态,还是接受状态)。
3. Transition Function有一个match方法,检查匹配符号III是否是该转换所需要(能匹配)的值。

!! 为了某些情况的方便(试着去掉了TransitionProcedure,有点紧耦合),将match这个过程,分离出一个类----Transition Procedure。

二、转换图的简化

这里的简化是或运算 的简化。
假设正则定义是:
R→[a−z]R→ [a-z]R→[a−z]
那么,状态转换图(后文称NFA ---- Thompson算法是将正则表达式构造成NFA, 然后NFA通过子集构造算法再转换为状态更少更精简的DFA)为:

足足21个状态!! NFA的效率可是跟状态数量的多寡有关。所以有一种更精简(更让脑壳疼)的方法,“连词符 ‘-’”(Conjunction Symbol)。例如R→[a−z]R→ [a-z]R→[a−z]中‘-’就是连词符。
在子集构造算法,只要一个输入符号而不是连词符,使用连词符稍微有点别扭…如果不用,状态数量又太多了!!

补充:每个TransitionProcedure有一个左边界与右边界,当是连词符时,左边界是连词符左边的字符值,右边界是连词符右边的字符值;当输入字符是一个字符时,TransitionProcedure的左边界等于右边界。

三、元字符如何匹配?

什么是元字符?
类似‘.’在Lex正则表达式中是指通配符,""是指串s的字面值… 这样类似的,在正则表达式中有特殊意义的字符称为元字符。
我如何设计?
例如正则定义RRR:
R→abc.\.R → abc.\backslash.R→abc.\.

因为类的设计问题,这样一段正则表达式会转换为CharacterRegular(后文实例讲解提到该类),而每个Regular(是CharacterRegular的基类)会根据属性expression生成状态图(diagram)。而expression则是该正则表达式的文本值,最终,该值是:
expression="abc.."expression = "abc.."expression="abc.."
当调用generateDiagram方法时(生成状态图),分析expression读到第二个‘.’时,到底是通配符呢?还是普通字符呢?
所以,在Character中,增加一个Set<Integer>,用于存放元字符位于字符串中的下标,当读到第二个‘.’字符时,并且Set<Integer>中有该下标值,则代表命中元字符,构造出特殊的TransitionProcedure(通配符的TransitionProcedure左边界是0, 右边界是0xFFFF, 0xFFFF是假设的unicode字符上限,能匹配绝大部分了)。

(其实好像可以边读取边生成?)

四、[^a-z0-9RE] 如何构造?

如果是[a-z0-9RE] 就很容易构造啦!
构造出a-z,0-9,R,E的转换图,再用或运算连接就好了。

思考:如何构造带有取反性质的或运算??(先别往下看,先想想)


我是将a-z,0-9,R,E每个都取反,然后求出它们的交集;
例如a-z 就可以转换为min—(a-1),(z+1)—max;(min和max是某个数值取值)
0-9就可以转换为min—(0-1),(9+1)—max;
R就相当于min—(R-1),(R+1)—max;

再想想:如何快速求出它们的交集?(可是试试)



橙色部分即是我们想要求解的区域,只需要以左端排序一遍,然后在遍历一遍取出橙色部分就好了!

五、([a-zA-Z] | [0-9]){6, 16} 如何构造?

只包含数字和字母正则定义是:
password→([a−zA−Z]∣[0−9]){6,16}password→([a-zA-Z] | [0-9])\{6, 16\} password→([a−zA−Z]∣[0−9]){6,16}
该密码匹配6~16位字符,如果是5个字符以下或超过16个字符,那么则不匹配该正则表达式。
如何设计状态转换图?
我的办法比较… 笨点,因为会导致状态数量很多,跟m,n{m,n}m,n中nnn的值相关。passwordpasswordpassword的状态数量多达200200200多个。
首先,我构造出([a−zA−Z]∣[0−9])([a-zA-Z] | [0-9])([a−zA−Z]∣[0−9])的NFA−N(B)NFA-N(B)NFA−N(B),即:
B→([a−zA−Z]∣[0−9])B→([a-zA-Z] | [0-9])B→([a−zA−Z]∣[0−9])
再以N(B)N(B)N(B)为基础,重复连接m个N(B)m个N(B)m个N(B), 再连接n−m个N(B)n-m个N(B)n−m个N(B),每个n−m的N(B)n-m的N(B)n−m的N(B)都会有一条从n−m的N(B)n-m的N(B)n−m的N(B)的接受状态到N(password)N(password)N(password)的接受状态。
例如这样一个匹配a{4,6}a\{4, 6\}a{4,6}的正则表达式,每组颜色都以不同颜色标明。在7号状态和9号状态有一条ε边到接受状态。

六、优化与不足

因为隔了好久才写了这篇blog…突然忘记优化点和不足了。
总体上仍然不是很满意,有的地方设计地奇奇怪怪的,不过还好.

七、实例讲解

产生式:
产生式的概念是在文法中称呼的,即:
Head→BodyHead → BodyHead→Body
跟正则定义相似,head部分叫做产生式头部或产生式左部,body部分称为产生式体或产生式右部。那么有正则定义的名称可以称为产生式头部,正则表达式部分可以称为产生式体。

在这部分以匹配一个整数的正则表达式举例,讲解代码如何运行的,该正则定义有:
digit→[0−9]number→[+−]?[1−9]{digit}∗digit → [0-9] \\ number→[+-]^?[1-9]\{digit\}^* digit→[0−9]number→[+−]?[1−9]{digit}∗
该正则表达式匹配整数,包括 +13,-10,1,10302…
首先匹配一个+\-,是一个01闭包,即可以有+符号,也可以没有; 然后再匹配一个字符,该字符取值范围是1-9;最后是一个0-9的Kleene闭包。

  1. 从文件中读取正则定义,分离正则定义的名称和正则表达式的主体部分。例子将被分为numbernumbernumber和[+−]?[1−9]{digit}∗[+-]^?[1-9]\{digit\}^*[+−]?[1−9]{digit}∗两部分。
  2. Thompson的analyze算法接收产生式体部分,遍历产生式体中每个字符。
  3. 对于产生式体(正则表达式)[+−]?[1−9]{digit}∗[+-]^?[1-9]\{digit\}^*[+−]?[1−9]{digit}∗,要做的就是将产生式体分解(我使用策略模式,根据不同的需求,分析不同的正则表达式),比如该产生式体可以分解为三个部分:
    [+−]?[1−9]{digit}∗[+-]^? \\ [1-9] \\ \{digit\}^* [+−]?[1−9]{digit}∗
  4. 其中,[+−]?[+-]^?[+−]?又可以分解为[+−][+-][+−](OrRegular,Regular这些是类),然后再以此构造出它的闭包(ClosureRegular);思考:当前例子中的[+−][+-][+−]的‘−’‘-’‘−’是会被当做连词符?还是会被当做普通字符‘−’‘-’‘−’?判断依据是‘−’‘-’‘−’的前后是否存在相同属性的字符,例如前后都是小写字母,前后都是数字…等等。
  5. [1-9]则直接生成OrRegular。并且‘−’‘-’‘−’符号是作为连词符使用。
  6. 当遇到{digit}∗\{digit\}^*{digit}∗时,同样,这是两个部分,一个是{digit}\{digit\}{digit},然后则是它的闭包(ClosureRegular)。但是,{digit}\{digit\}{digit}是什么?
  7. {digit}\{digit\}{digit}是指引用另外一个名称为digitdigitdigit的正则表达式,当遇到该符号时,会在map中(每次分析完一个正则表达式都会放入map,以正则表达式的名词为key)查找,如果存在,那么将当前位置的引用替换为表达式,有:
    number→[+−]?[1−9][0−9]∗number→[+-]^?[1-9][0-9]^*number→[+−]?[1−9][0−9]∗
  8. 当每个部分分析完后,再用单位元连接,使之成为一个整体(CombinationRegular)。


9. 将组合好的CombinationRegular调用generateDiagram方法,生成NFA再传入NondeterministicFiniteAutomaton类,此类会分析转换状态图,然后构造NFA五元组。


有限状态自动机是一个五元组
M=(Q,Σ,δ,q0,F)M=(Q, Σ, δ, q0, F)M=(Q,Σ,δ,q0,F)
QQQ 状态的非空有穷集合,∀q∈Q∀q∈Q∀q∈Q,qqq称为MMM的一个状态
ΣΣΣ 输入字母表
δδδ 状态转移函数,有时又叫作状态转换函数,δ:Q×Σ→Q,δ(q,a)=pδ:Q×Σ→Q,δ(q,a)=pδ:Q×Σ→Q,δ(q,a)=p
q0q0q0 MMM的开始状态,也可叫作初始状态或启动状态,q0∈Qq0∈Qq0∈Q
FFF MMM的终止状态集合,FFF被QQQ包含,任给q∈Fq∈Fq∈F,qqq称为MMM的终止状态

10. 最后调用NondeterministicFiniteAutomaton类中的match(String)函数即可。(还有更多的细节详见github代码)

标签

正则表达式,正则引擎,RE,有穷状态自动机,NFA
正则表达式转不确定的有限状态自动机
词法分析 JAVA实现

构建正则引擎的心得(类的设计)相关推荐

  1. 网络安全公司奇安信集团是如何基于 Flink 构建 CEP 引擎实时检测网络攻击【未来不可忽视的网络安全】

    摘要: 奇安信集团作为一家网络安全公司是如何基于 Flink 构建 CEP 引擎实时检测网络攻击?其中面临的挑战以及宝贵的实践经验有哪些?本文主要内容分为以下四个方面: 背景及现状 技术架构 产品及运 ...

  2. mahout+Eclipse,使用 Taste 构建推荐引擎实例 – 电影推荐引擎

    使用 Taste 构建推荐引擎实例 – 电影推荐引擎 根据上面的步骤,我们可以得到一个简单的推荐引擎 demo 环境,下面介绍如何使用 Taste 方便地构建自定义的推荐引擎. 抽取 Taste 工具 ...

  3. 一文讲透非标品的商品类目设计

    www.pmcaff.com 本文为作者 百转 于社区发布 只要做电商,商品类目设计就是绕不开的一环.好的类目设计能让供需双方更快.更好定义或找到所关心的商品,是整个电商体系的地基,是构建并链接各个模 ...

  4. 【华为云技术分享】解密如何使用昇腾AI计算解决方案构建业务引擎

    摘要:昇腾AI计算解决方案以极致算力,端边云融合.全栈创新,开放生态的硬核实力.用户可以使用标准的Matrix接口实现业务引擎,对外释放昇腾AI加速能力. 从卷积神经网络中的矩阵乘法(GEMM)说起 ...

  5. 浅谈ipad阅读类应用设计

    自古以来,人们从阅读中了解最新资讯,学习知识,陶冶情操.随着社会和科技的发展,新的阅读设备,阅读方式,丰富的多媒体展示,让阅读这一人类行为更加高效化和多样化.对于平板电脑这个较新的媒介,我们如何能进一 ...

  6. 构建一个你自己的类微信系统 -- 可扩展通信系统实践

    ##前言 正如你们所知的那样,微信是一个非常成功的在线服务系统,由几万台服务器组成的系统为几亿人提供着稳定的业务服务.可惜作为一个普通的工程师基本上不可能有整体设计这样一个系统的机会,即使加入xx 也 ...

  7. 20、ADS使用记录之E类功放设计(上)

    20.ADS使用记录之E类功放设计(上) 基于CGH40010F,主要是理想的仿真(上篇) 理论部分参考论文 基于GaN HEMT的高效率E类功率放大器的设计研究 0.源文件下载 1.设计指标 频率范 ...

  8. 软件工程导论实验报告二(类图设计)

    软件工程导论实验报告 实验二 类图设计 2.实验目的 (1)掌握绘制类图的基本步骤: (2)掌握识别类的方法. 3.实验内容 利用StarUML或其它UML绘图工具,绘制系统中的类图. (1)图书管理 ...

  9. 基于Java多线程的打怪升级类游戏设计与开发

    摘要:本文论述了通过JAVA多线程.基于JAVA Swing的GUI图形用户界面设计.IO输入输出流.JDBC技术,实现了游戏系统的UI设计.游戏数据的存储.关卡的设置.用户头像的上传与更改.游戏数据 ...

最新文章

  1. 生成对抗网络(GAN)的理论与应用完整入门介绍
  2. 实现做出html的上标以及下标
  3. P1865 A % B Problem (素数筛法,前缀和)
  4. 微课堂 | 典典养车COO:暴力运营美学,典典养车如何一年内拿到500万用户(今晚8点开始)...
  5. Windows环境下IOCP和SELECT模型性能比较
  6. Go语言 读写锁互斥锁原理剖析(1)
  7. spring5(6) ---Ioc和DI
  8. mysql数据库做关联查询_mysql 数据库join关联查询using(xxx)的作用
  9. redis 系列19 客户端
  10. dockerhub 拉取地址_使用docker,进行dockerhub仓库上传镜像,拉取镜像。
  11. html判断安装没安装qq,QQ提示安装路径无效您没有权限怎么办 QQ2015提示安装路径无效您没有权限的解决方法...
  12. javaWeb上传文件(jsp上传文件)
  13. 嵌入式Linux项目开发的几个步骤
  14. windows优化大师怎么用_用智慧和爱心经营——班主任经验交流录音稿
  15. Linux入门学习(十 三)—— 怎么给指定用户发送信息? 怎么发送广播消息?
  16. 计算机程序员带什么手表,程序员只爱格子衫?这些手表程序员也喜欢!
  17. 头条 上传图片大小_无锡抖音巨量运营培训南天值得选择——鹰手营子矿头条...
  18. 如何查看IP地址是否被占用
  19. ijk基于exo_github上十二款最著名的Android播放器开源项目
  20. 使用域名访问远程jupyter_如何设置远程访问的Jupyter Notebook服务器-01(之预备知识:什么是端口号?)...

热门文章

  1. 网络安全比赛A模块任务书
  2. 电脑剪切,CopyQ(电脑剪切板) V4.1.0 最新版
  3. PHP大学跳蚤市场的微信小程序设计与实现 毕业设计-附源码261620
  4. 西安Java培训 | java设计模式之工厂设计模式
  5. 鸿蒙5g时代短视频,5G时代,短视频先火!OPPO Reno3系列或搭载全新视频功能
  6. python段子_python 爬取 段子网 实例
  7. 收藏的博客 -- 深度学习AI框架与数学基础
  8. Loser应该知道的6个残酷人生事实
  9. Oracle数据库相关经典面试题
  10. 速成实用硬笔字——最常用高频汉字前400