要了解正则表达式的原理,须要先了解一些计算机语言文法的基础知识。java
一个文法能够用一个四元来定义,G = {Vt,Vn,S,P}正则表达式
其中Vt是一个非空有限的符号集合,它的每一个元素成为终结符号。Vn也是一个非空有限的符号集合,它的每一个元素称为非终结符号,而且Vt∩Vn=Φ。S∈Vn,称为文法G的开始符号。P是一个非空有限集合,它的元素称为产生式。所谓产生式,其形式为α→β,α称为产生式的左部,β称为产生式的右部,符号“→”表示“定义为”,而且α、β∈(Vt∪Vn)*,α≠ε,即α、β是由终结符和非终结符组成的符号串。开始符S必须至少在某一产生式的左部出现一次。算法
文法可推导的语言标记为L(G)。编程
著名语言学家Chomsky(乔姆斯基)根据对产生式所施加的限制的不一样,把文法分红四种类型,即0型、1型、2型和3型。闭包
正则表达式就是最后一种,正则文法,的一种表达形式,以整个字母表做为终结符集合Vt。编程语言
假设有一个文法的产生式是{S->Sa; S->b;},那么对应的正则表达式为ba*
。翻译
所以正则表达式,正则文法,有限状态自动机这个三个概念虽然指不一样的东西,可是具有内在的等价性。设计
正则表达式是正则文法,限制多于上下文无关文法,而咱们使用的编程语言语法都是上下文无关文法,所以试图经过正则表达式去处理代码(好比语言翻译、代码生成)的努力很可能归于徒劳。不过,把代码当作纯文本,而后在处理过程当中使用正则表达式,仍然能大大提升效率。3d
正则表达式包含不少的元字符来表达规则,不过本文不是要介绍如何使用正则表达式,关于正则表达式规则最好的参考书是《精通正则表达式》。code
实际上,正则表达式核心的运算符只有如下几种:
名称 | 示例 | 备注 |
---|---|---|
或运算 | r|s | 匹配的语言是L(r)和L(s)的并集 |
链接运算 | rs | 匹配的语言是L(r)和L(s)链接 |
Kleene运算 | r* | 匹配的语言是L(r)和L(s)链接 |
括号 | (r) | 匹配的语言与L(r)一致 |
kleene运算符优先级最高,且是左结合的,链接第二,或运算优先级最低。
运算定律:
示例 | 备注 |
---|---|
r|s = s|r | | 运算知足交换律 |
r|s|t = r|(s|t) | | 知足结合律 |
r(st) | 链接能够结合 |
r(s|t) = rs|rt | 链接对|能够分配 |
ℇr = rℇ = r | ℇ是链接的单位元 |
r* = (r|ℇ)\* | 闭包中必定包含ℇ |
r** = r* | *具备幂等性 |
扩展运算符使得正则表达式更具表达力,下面仅举几个例子:
扩展运算符 | 等价形式 |
---|---|
+ | r+ = rr* = r*r |
? | r? = r | ℇ |
字符类 | [a1a2…an] = a1|a2|…|an;若是是连续的字符类,能够写成[a1-an] |
高级特性:
正则表达式具有不少高级特性,好比捕获、环视、固化分组等等,这些特性是为了提升正则表达式的实用价值被设计出来的,不属于正则文法的范畴。
前面说过,正则文法对应于有限状态自动机,又分肯定型有限状态自动机(DFA)和非肯定型有限状态自动机(NFA),这两种状态机的能力是同样的,都能识别正则语言。正则表达式的识别引擎,都是基于DFA或NFA构造的。关于状态机的基础理论,这里就不描述了,只要稍微有点印象,就不妨碍继续阅读。
NFA
一个字母能够标记离开状态的多条边,而且ℇ 也能够标记一条边;这说明NFA的匹配过程面临不少的岔路,须要作出选择,一旦某条岔路失败,就须要回朔。
下图是正则表达式(a|b)*abb对应的NFA,它至关直观,基本能够从正则表达式直接转换而来。
DFA
对于每一个状态以及字母表中的每一个字母,只能有一条以该字母为标记的,离开该状态的边;这说明DFA的匹配过程是肯定的,每一个字母是须要匹配一次。
与上面NFA等价的DFA以下图,至关地不直观:
因为NFA和DFA的能力是同样的,每一个NFA必然能够转化成一个等价的DFA。既然DFA对每一个输入能够到达的状态时是肯定的,那么输入串s在NFA中可能达到的状态集合对应为等价DFA中某个状态。从这个思路出发,能够构造出DFA。
最终获得的DFA以下,(0,3)包含了NFA的终结状态3,所以也是DFA的中介状态,对状态从新命名能够获得上面一样的DFA。
DFA和NFA的效率差别
很容易理解,构造DFA的代价远大于NFA,假设NFA的状态数为K,那么等价DFA的状态数目理论上可达2的k次方,不过实际上几乎不会出现这么极端的状况,能够确定的是构造DFA会消耗更多的时间和内存。
可是DFA一旦构造好了以后,执行效率就很是理想了,若是一个串的长度是n,那么匹配算法的执行复杂度是O(n);而NFA在匹配过程当中,存在大量的分支和回朔,假设NFA的状态数为s,由于每输入一个字符可能达到的状态数作多为s,那么匹配算法的复杂度及时输入串的长度乘以状态数O(ns)。
正则表达式的NFA&DFA构造、转化、简化有一整套理论及方法,远比上面的例子复杂,本文仅经过一个简单的例子来讲明原理。
NFA和DFA这两种匹配算法,除了效率上的差异外,从更高的视点看,造成了两种风格的引擎,进而对正则表达式的匹配的其余方面能力形成差别。NFA被称之为"表达式主导"引擎,而DFA被称之为“文本主导”引擎。
从表达式的第一个部分开始,每次检查一部分,同时检查当前文本是否匹配表达式的当前部分,若是是,则继续表达式的下一部分,如此继续,直到表达式的全部部分都能匹配,即整个表达式匹配成功。
咱们来看表达式to(nite|knight|night)
匹配文本...tonight...
的过程: 表达式的第一个部分是t,它会不断重复扫描,直到在字符串中找到t,以后就检查随后的o,若是能匹配就继续检查下面的元素。这个例子中,下面的元素是(nite|knight|night)
,意思是nite或者knight或者night,引擎会依次尝试这三种可能。
整个过程,控制权在表达式的元素之间转换,所以被称之为“表达式主导”。“表达式主导”的特色是每一个子表达式都是独立的,不存在内在联系。 子表达式与整个正则表达式的控制结构(多选、量词)的层级关系控制了整个匹配过程。
DFA在读入一个文本的时候,会记录当前有效的全部匹配的表达式位置(这些位置集合对应于DFA的一个状态)。
以上面的匹配过程为例:
这种方式被称之“文本主导”是由于被扫描的字符串,控制了引擎的执行过程。
NFA表达式主导的特性,使得经过修改正则表达式来影响引擎,所以下面三个表达式尽管可以匹配一样的文本,可是引擎的执行过程各不相同:
可是对于DFA来讲,没有任何区别。
对于包含或选项的表达式,NFA在成功匹配一个选项以后可能报告匹配成功,此时并不知道后面的选项是否也会成功,是否包含一个更长的匹配。
假设使用one(self)?(selfsufficient)?
来匹配oneselfsufficient
,NFA首先匹配one,而后匹配self,此时发现selfsufficient
没法匹配剩余子串,可是这个子表达式不是必须的,所以能够当即返回成功,此时匹配的串为oneself
。
实际上NFA引擎的匹配结果与具体实现有关,而DFA必然会成功匹配oneselfsufficient
。
NFA可以支持“捕获group”,“环视”,“占有优先量词”,“固话分组”等高级功能,这些功能都基于“子表达式独立进行匹配”这一特色。 而DFA没法记录匹配历史与子表达式之间的关系,于是也没法实现这些功能。
可见NFA引擎具有更大的实用价值,于是,咱们在编程语言里面使用的正则表达式库都是基于NFA的。java的Pattern就是基于NFA的,Pattern.compile()方法显然就是在构造NFA状态图。