编写一个正则表达式解析器

编写一个正则表达式解析器
    正则表达式是进行文本处理的强有力工具,比如,UNIX环境下的Sed命令,Perl脚本语言都支持基于正则表达式的文本匹配。很多人都知道怎么使用正则表达式,但正则表达式的工作原理,相信很少有人知道。前段时间看了一些有关编写正则表达式解析器的文章,总结了一下,算是读书笔记吧。首先,我们将简要介绍正则表达式,接着我们将以一个简单的正则表达式作为例子,讲解如何构建正则表达式的解析器。
    1. 正则表达式简介
    一个正则表达式就是由普通字符(例如字符 a 到 z)以及特殊字符(称为元字符)组成的文字模式。该模式描述在查找文字主体时待匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。
    比如:正则表达式(a|b)*c表示匹配字符a或者字符b重复0或多次,接着一个字符c的字符串。其中*表示重复0或者多次,|表示或者。与模式匹配的字符串可以是ac, bc, abc, aabc, abbc, c等。
    这里只是举了一个正则表达式的例子,有关正则表达式的介绍见 正则表达式
 
    2. 构建一个正则表达式的解析器
    为了便于讲解,我们只讨论正则表达式中三个基本操作符:
    a*  匹配字符a重复0次或者多次  (star)
    a|b 匹配字符a或者b            (union)
    ab  匹配字符ab    (concatenation)
    其他一些操作符可以有这3个操作符构建而成:
    A+ = AA* (At least one A)
    [0-9] = (0|1|2|3|4|5|6|7|8|9)
    [A-Z] = (A|B|...|Z), etc.

    2.1  NFA和DFA
    NFA (nondeterministic finite-state automata)是不确定性有限状态自动机的简写,NFA的定义为:
    一个不确定性有限状态自动机由以下部分所组成:
    一个有限的输入字符集I
    一个有限的状态集S
    状态转换函数f: S x I -> P(S),P(S)为s的幂集
    一个结束状态集Q,Q是S的子集
    一个初始状态s0 (属于S)
    表示为A(I, S, f, Q, s0)
    例如,下图就是一个不确定性有限状态自动机:
                                          NFA例子
    图中,有一种特殊的转换Epsilon转换,表示输入一个空字符串,由一个状态转换为另一个状态。例如,状态s3到s4,s3到s5之间的转换就是Eplison转换。Epsilon转换是NFA的一种特有的转换。另外,状态s0输入字符a,可以转换为s1或者s3,是不确定的,这也就是NFA成为不确定性有限状态自动机的原因。图中有两个圈的状态(s2, s6, s7)表示终结状态,即找到匹配正则表达式的字符串。
    与NFA相对应,DFA (deterministic finite-state automata)表示确定性有限状态自动机。与NFA不同,DFA不存在Epsilon转换,并且每一个状态转换函数的值只对应一个状态,即一个状态输入一个字符,只能有一个状态相对应。
    显然,DFA更加适合我们进行字符串匹配,因为输入一个字符,总能从一个状态确定地转换为另一个状态,直到终结状态。NFA一个输入可能对应多个状态,因此需要进行回溯,先尝试一条路径,发现走不通,再回退到原来的状态尝试另外一条路径,显然匹配算法不如DFA简单。
    给定一个NFA,总有一个DFA与之对应,即一个NFA可以转换成一个等价的DFA,我们将在2.3里介绍转换的算法(子集构造算法)。
 
    2.2 将正则表达式转换为一个NFA (Thompson算法)
    首先,我们将正则表达式转换为NFA,采用的算法是Thompson算法。对Epsilon和字符a,我们构造如下之等价的NFA:
                                 与正则表达式基本元素等价的NFA
    对基本的操作符RS, R|S, R*,我们构造如下与之等价的NFA:
                           与正则表达式基本操作符等价的NFA
    有了这些基本的NFA,我们可以根据这些NFA构造更为复杂的NFA。构造的方法可以采用类似计算表达式的方法。例如正则表达式(a|b)*cd,计算过程为:
    PUSH a
    PUSH b
    UNION (POP b, POP a, 构造与a|b等价的NFA, PUSH a|b)
    STAR  (POP a|b, 构造与(a|b)*等价的NFA, PUSH (a|b)*)
    PUSH c
    CONCAT (POP c, POP (a|b)*, 构造与(a|b)*c等价的NFA, PUSH (a|b)*c)
    PUSH d
    CONCAT (POP d, POP (a|b)*c, 构造与(a|b)*cd等价的NFA, PUSH (a|b)*cd)
    POP R  (POP result)
    在上面的例子中,我们需要比较运算符的优先级,比如"("的优先级比UNION高,UNION的优先级比")"高,CONCAT的优先级比UNION高。
 
    2.3 将NFA转换为DFA (子集构造算法)
    上一节,我们将正则表达式转换成了等价的NFA,本节我们介绍如何将一个NFA转换为DFA,我们采用的算法是子集构造算法,详细描述见红龙书《编译器-原理,技术以及工具》(Ullman等)。
    这里我们通过一个例子来介绍子集构造算法。给定如下图所示的NFA,
                            NFA
    与之等价的DFA为:
                  DFA
    通过NFA的介绍,我们知道NFA存在两种转换,Epsilon转换和输入字符转换。对NFA中的初始状态s1,我们构造s1的Epsilon闭包(s1通过Epsilon转换所能到达的状态集),为{s1,s2,s4},对应DFA中的初始状态。从{s1,s2,s4}我们可以构造NFA对输入字符a的转换状态集{s3,s4},{s3,s4}的Epsilon闭包为{s3,s4}(产生新的状态集),对应DFA中初始状态{s1,s2,s4}对输入字符a的转换状态。同理,{s1,s2,s4}对输入字符b的转换状态集为{s5},{s5}的Epsilon闭包也为{s5}(产生新的状态集),对应DFA中初始状态{s1,s2,s4}对输入字符b的转换状态。从{s3,s4},{s5},我们又可以构造对每个输入字符的状态转换集,直到不再产生新的状态集。就得到与NFA等价的DFA。DFA中的状态集中只要包含一个NFA中的终结状态,该状态集就是DFA中的终结状态。
    得到DFA,我们还可以进一步地优化DFA,找出DFA中只有入边没有出边的状态,假如节点不是终结状态,则该状态可以删除。
 
    2.4 通过DFA进行正则表达式的字符串匹配
    通过DFA,我们可以从初始状态节点开始,进行遍历,每输入一个字符,转换到另一个状态,如此进行下去,如果转换以后的状态为终结状态,则找到了一个匹配字符串的表达式。
   
    至此,我们介绍了如何编写一个简单的正则表达式解析器的全过程,虽然没有给出具体的实现,但实现的方式已经十分明确。实际中使用的正则表达式比这里介绍的要复杂很多,但解析的方法基本和上面的方法一样。
 
 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值