在正则表达式设计中,必须考虑匹配的回溯:如果匹配失败,就要从可能的上一个分支重新进行匹配。
回溯设计,极大的降低了匹配的效率,让一些简单的匹配耗费大量的资源。
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.
过多的回溯设计,会让解析器结果需要更大的缓存,在流数据处理上,效率影响很大。
正则表达式虽然很好用,但在处理复杂的数据结构上,依然有很多天然的缺陷,这时候,就要考虑使用另外的匹配工具:语法匹配。
下次在讲语法匹配。