python正则表达式简单入门_用Python实现简单的正则表达式NFA

正则表达式(Regular Expression)即正则语言是现代计算机语言的重要基石,虽然原始,却非常强大。之所以写此文是因为去年看Udacity上面Peter Norvig的教程Design of Computer Programs时对编译方面的内容感到理解困难。该教程留了一份练习要求用上下文无关语法(Contex-Free Grammar)和递归下降法(Recursive-Descendent Parsing)来解析正则表达式,耗费许久仍未找到思路,一直耿耿于怀。后来结合一些编译方面的入门资料,意识到正则语法基于非确定性状态自动机(NFA),是比上下文无关语法更低层的语法,用后者描述前者有本末倒置之嫌(文末继续讨论该问题)。后来读了http://swtch.com/~rsc/regexp/regexp1.html这篇文章,文章不错,较系统地用C语言描述了构造NFA和DFA的方法,值得一读。本文用Python对该文中的部分内容进行转述,以求进一步的理解。

在Python中利用class来模拟C语言中的结构。状态State有两种,常规状态是只有一个出口的,只有一种状态SplitState是有两个出口的(对State扩展而得),其数据结构和链表并没有本质区别。状态机片段Fragment相当于是一个黑盒,包装了一系列的状态流程。

class State:

counter = 0

def __init__(self, entry, next1=None):

self._num = State.counter

self.entry = entry

self.next1 = next1

State.counter += 1

def __repr__(self): ## 可视化, 用于调试

return '(No.%d: %s)' % (self._num, self.entry)

_SPLIT = 'SPLIT'

_MERGE = 'MERGE'

class SplitState(State):

def __init__(self, next1, next2):

super(SplitState, self).__init__(_SPLIT, next1)

self.next2 = next2

class Fragment:

def __init__(self, begin, end=None):

self.begin = begin

self.end = end if end else begin

def __repr__(self): ## 可视化, 用于调试

return 'Frag[%s -> %s]' % (self.begin, self.end)

为对每个基本状态以及根据不同的正则关键词如 * 和 ? 对片段进行打包,需要准备一些工厂函数。右侧插图转自http://swtch.com/~rsc/regexp/regexp1.html,但本文中相比图中要增加一个汇合状态即_MERGE_STATE

_BEGIN = '^'

def make_begin():

return Fragment(State(_BEGIN))

_END = '$'

_END_STATE = State(_END)

def make_end():

return Fragment(_END_STATE)

def make_atom(char):

return Fragment(State(char))

def make_seq(frag1, frag2):

if frag2 is None: return frag1

else:

frag1.end.next1 = frag2.begin

return Fragment(frag1.begin, frag2.end)

def make_alt(frag1, frag2):

split = SplitState(frag1.begin, frag2.begin)

merge = frag1.end.next1 = frag2.end.next1 = State(_MERGE)

return Fragment(split, merge)

def make_opt(frag1):

split = SplitState(frag1.begin, None)

merge = frag1.end.next1 = split.next2 = State(_MERGE)

return Fragment(split, merge)

def make_star(frag1):

split = SplitState(frag1.end.next1, frag1.begin)

frag1.end.next1 = split

return Fragment(split)

构建自动机的函数利用更为简洁的尾递归表达(也可显式地调用栈),类似scheme的风格。为了更符合状态机读取“纸带”的模型,此处默认输入的字符串已被iter(...)函数转化为一个迭代器即参数@inp,之后用next(...)函数来右移字符指针。参数@frag用于保存已经构建好的状态机片段。参数@alpha即字母表也应指定,从而判断输入的合法性。

该函数运行的过程中借助了一个中间参数@temp,初始值为None,用于存储仍待完成构造的自动机片段,参数@frag用于存储已购造完成的自动机片段。优先处理的应是读得终止符$即终点情形。每当所读字符c为修饰符即 ? 、 + 或 * 时即调用相应工厂函数对@temp进行打包,@frag保持不变,进行下一步递归。其余情形皆需要将当前@temp和@frag用make_seq函数打包,并传入下一步递归。另外,当所读字符为 | 或 ( 时,要重新开辟一个自动机,再将开辟所得的自动机和当前自动机片段进行打包。

def build_nfa(pat, alpha='abcdefghijklmnopqrstuvwxyz'):

def build(pat, temp, frag):

c = next(pat)

if c == '$':

return make_seq(frag, temp)

elif c in alpha:

return build(pat, make_atom(c), make_seq(frag, temp))

elif c == '?':

return build(pat, make_opt(temp), frag)

elif c == '*':

return build(pat, make_star(temp), frag)

elif c == '+':

return build(pat, make_plus(temp), frag)

elif c == '|':

return make_alt(make_seq(frag, temp),

build(pat, None, make_begin()))

elif c == '(':

return build(pat, build(pat, None, make_begin()), make_seq(frag, temp))

elif c == ')':

return make_seq(frag, temp)

else:

raise ValueError("Symbol '%s' not recognizable." % c)

return make_seq(build(pat, None, make_begin()), make_end())

至此可以通过该函数生成完整的NFA状态机。合法的输入字符串应形如a(b|cd)*ef$模式,开头字符上折号^此处予以忽略,结尾字符美元号$不能忽略。

在此基础上,验证一个输入是否符合给定正则表达式便可通过下一函数完成。算法的原型可参见龙书Algorithm 3.22,基本思路是用一个当前集合curr_set来维持匹配至今的合法状态,每接纳一个新的输入就对curr_set中的所有状态进行转移,其中一部分因无法转移而被过滤掉。此处有两个要点:一. ipsilon闭包转移问题,即所有过渡状态(即BEGIN,SPLIT和MERGE)都需要连续转移至非过渡状态为止;二. 某些状态作为多个现有状态基于某一输入的共同转移目标,可能会被重复添加进curr_set,此处采用哈希集来避免重复添加,省略了判断是否重复添加的语句。

关于循环的终止,此函数所进行的是贪婪匹配,即只要curr_set不为空,就继续读取输入进行下一步匹配,无论之前是否已成功匹配多少次。所有匹配成功的结尾位置会被数组matches记录下来(例如匹配成功字符inp[k],即该匹配状态有终点状态作为下一步,则k+1被记录下来)。最终结果将记录下所有成功匹配的位置。

def check(pat, inp, alpha='abcdefghijklmnopqrstuvwxyz'):

nfa = build_nfa(iter(pat), alpha=alpha)

curr_set = { nfa.begin }

matches = []

i = 0

while i < len(inp):

matched_set = set()

while curr_set: # 若当前状态集不为空,则尝试转移其中每一状态

s = curr_set.pop()

if s.entry == _BEGIN or \

s.entry == _MERGE:

curr_set.add(s.next1)

elif s.entry == _SPLIT:

curr_set.add(s.next1)

curr_set.add(s.next2)

elif s.entry == _END: # 若当前状态集包含终点状态,即从0到i-1位置皆匹配成功,记录i

matches.append(i)

elif s.entry == inp[i]: # 单个字符匹配成功,

matched_set.add(s)

else: continue # 既无过渡也无匹配状态被抛弃

if matched_set:

curr_set = { x.next1 for x in matched_set }

i += 1

else:

return matches

return matches

输入测试:

>>> check( '()$', '\0' ) # 空串测试

[0]

>>> check( '(a|b|)$', 'a\0' ) # 空分支测试

[0, 1]

>>> check( 'a(b|cd)*e?$', 'acdbbbbbcd\0' ) # 顺序重复匹配

[1, 3, 4, 5, 6, 7, 8, 10]

>>> check( 'a(b|cd)*e?$', 'ae\0' )

[1, 2]

>>> check( '(a|b)*(cd|ef)*$', 'cdef\0')

[0, 2, 4]

>>> check( '(a|b)*(cd|ef)*$', 'cdefcc\0') # 仅成功匹配至第4个字符,之后无匹配

[0, 2, 4]

回到上下文无关语法的问题,我曾尝试完成Peter Norvig布置的练习,按照上下文无关语法构建的正则语法产生式如下:

seq ->  seq exp | exp

exp -> lit | star | opt | alt | ( seq )

star -> lit  * | ( seq ) *

opt -> lit ? | ( seq ) ?

alt -> seq | seq

lit -> a | b | c | ... | y | z

个人感觉在构建语法时会遭遇不少问题。按上述语法,alt -> seq | seq、seq -> exp、exp -> alt这三个式形成的回路,故消除左递归的方法不适用。到现在我仍未想出有效的无回路的正则语法。另外,正则语法的关键词如*,+,?等是后置的,完全不符合递归下降法的初衷,递归下降法更适用于关键词开头的、已经近似于树状结构的语法。也有可能通过词法分析(Tokenizing)对输入字串进行前处理,但这使问题变得复杂了。具体答案还在搜寻中-_-,若有朋友思考过类似问题,还望多多指教。

你可能感兴趣的:(python正则表达式简单入门)