回溯设计是对错误设计的一种妥协

在正则表达式设计中,必须考虑匹配的回溯:如果匹配失败,就要从可能的上一个分支重新进行匹配。

回溯设计,极大的降低了匹配的效率,让一些简单的匹配耗费大量的资源。

   pattern = / a.*?abc /

这就是典型的一个需要回溯支持的正则表达式,在每次匹配字符的时候,都要试着匹配 a. 如果匹配成功,则设置一个锚点,继续匹配,如果在匹配 abc 过程中失败,那么匹配过程(自动机)将重置到最近设置的锚点,前进一位后,继续相同的过程。

这种匹配效率很低。

那么如何设计正则表达式,消除这种回溯呢?

需要一个不匹配字符串单元:

	pattern = / a (!abc)* /

似乎,正则表达式没有这种设计,是的,绝大多数正则表达式没有这种匹配单元。只有字符级别的否定:

	pattern = / a [^abc]* /

正则表达式设计的引领者,Perl 语言的先驱们,在 Perl6 的设计中提出了 Longest Matching 概念。也就是最长匹配:

	pattern = / abc | abcd /

这个匹配中,通常会匹配先命中的规则,但 Perl6 会尝试所有的匹配,只返回那个匹配字符长度最长的部分。 这听起来很不错,但这需要牺牲正则表达式的性能。如果提前对规则进行排序(假设都是字符串),那么就不需要这种方式了。

回溯设计,是因为许多规则有重复,所以才不得不设计回溯引擎来让匹配可以中途停止,重新来过。

	funcName = / \a+ /
	variable = / \a+ /

在许多语言中,函数的名称和变量的名称,用的是一样的规则,但实际上,他们是不同的东西:函数名称通常要调用参数,而变量则直接返回值。 pattern = / return funcname / pattern = / variable(callargs) /

这两种情况都是错误的语法,但却无法在匹配中识别,但在 PHP 中:

	funcname = / [_\a]+ /
	variable = / \$[_\a]+ /

PHP 语言的解析器,在识别函数名称和变量上,更简单,当然匹配速度更快。

规则冲突设计的越多,在语法树的处理上就越复杂,在 Java 中:

	Name.Name

可能是一个 class 的名称,一个类型的名称,也可能是方法调用,甚至是结构的 field 调用,这需要看的人有相当的知识才能分辨。 但在 Perl 中,则不需要这些东西:

	Class name: Name::Name
	Object call: Name->Name
	get field: Name->{Name}

这是语言设计上对回溯的影响,在实际的工作中,我们需要设计一些正则来匹配规则,如果不考虑相似规则的回溯,效率会很低。

如何设计规则,尽量避免回溯呢?

	越早分辨越好

完全不回溯的规则匹配,是设计了完全不同的首字符:

	group = / ( ... ) /
	chars = / [ ... ] /
	expr  = / { ... } /
	string = / " ... " /
	char = / ' ... ' /

这些规则的第一个字符不同,所以在匹配中,不会出现回溯问题,也不用担心顺序问题。在有回溯的设计中,不同的顺序可能会出现不同的结果。

     pattern = / keyword keywordname /

这个匹配永远不会匹配到完整的 keywordname.

过多的回溯设计,会让解析器结果需要更大的缓存,在流数据处理上,效率影响很大。

正则表达式虽然很好用,但在处理复杂的数据结构上,依然有很多天然的缺陷,这时候,就要考虑使用另外的匹配工具:语法匹配。

下次在讲语法匹配。

转载于:https://my.oschina.net/u/563463/blog/2395849

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值