正则表达式引擎分成两类,一类称为 DFA (确定性有限状态机)引擎,另一类称为NFA(不确定有限状态机)引擎,有时它们也被称为文本导向(Text-Directed)引擎和正则导向(Regex-Directed)引擎。
java和javascript 的正则都是NFA
先介绍一下不确定有限状态机(NFA)和 确定性有限状态机(DFA)
有限状态机(Finite Automation),有限状态机=有限控制器+字符输入带
状态机是一个有一组不同状态的集合的系统,有初始状态、终止状态(接收状态)、后继状态。有限状态机在读入一个字符时,其状态改变为另一个状态,则改变后的状态被称为后继状态。
如果有限状态机每次转换后的状态不是唯一的则称之为不确定有限状态机(NFA),如果是唯一的则称之为确定性有限状态机(DFA)
1.DFA
是一个以线性时间顺序执行的确定性有限自动机,因为它们不需要"回溯"(下面有具体介绍),也正是因为这样,它们也从不会连续两次地匹配相同字符。文本导向引擎会尽可能地匹配最长的字符串。但是由于确定性有限自动机只能包含唯一的有限状态,它就不可能利用逆向引用来匹配一个样式;DFA也不能显示地构建一个扩展(expansion),从而也就无法捕获子表达式。
总的来说,DFA的匹配效率要高一些。DFA是由字符串作驱动来匹配的,在每个字符串中的每个字符只被扫描一次。这种方式就是尝试此状态时可能的每种输入同时进行匹配。
例如要匹配abab|abbb,其DFA的状态是
S1~S10对应着NFA的S1~S10
文本导向的引擎的特性如下:
- 查找迅速
- 查找时间与字符串长度相关
- 比非确定性有限自动机消耗更多内存
- 编译正则表达式时,需要更多的时间
2.NFA
正则导向的引擎是一个非确定性有限自动机,这种算法将会测试所有可能的匹配情况,并且每次只接受第一个匹配的内容。因为非确定性有限自动机(NFA)能成功地为正则表达式构建一个特定的扩展,从而也就能捕获子表达式(捕获组)的匹配内容和逆向引用所指向的内容。并且,NFA可以进行回溯操作,这样的话它也就能精确地多次访问同样的状态,即使这个状态已经处于一个不同的路径,所以,在一个非常糟糕的情况下,它运行起来是很慢的。
在使用正则导向引擎时有一点是非常值得注意的,它总是返回最左侧的匹配内容,即使是后面的内容可能是更好的匹配内容;但这并不代表它永远地抛弃剩下的匹配内容,在适当的情况下,回溯操作可能会把其他匹配内容给筛选出来。当我们把构造好的正则表达式应用到一个字符串上时,引擎就会从第一个字符开始。它将按照表达式中的顺序,把所有可能的内容都尝试一次,但每次的结果只是正则导向引擎执行该次匹配操作所能返回的最左侧的匹配内容。
例如要匹配abab|abbb,其NFA的状态是
正则导向的引擎的特性如下:
- 查找较慢
- 查找时间与正则表达式相关
- 比DFA占用的内存少(具体占用内存多少与构造的正则表达式相关)
- 编译速度比DFA要快(具体编译时间与构造的正则表达式相关)
- 使用回溯算法来尝试所有可能的匹配内容
- 不同的回溯风格直接影响性能
- 能够捕获子表达式
- 支持逆向引用
正则缺点:正则表达式在使用之前都是需要编译的,语法越复杂编译起来也就越慢,而且执行起来也不见得会比我们普通编写的程序代码跑得快——效率低!
代码示例:过滤所有以"<"开头以">"结尾的标签
非正则方式:
public static String filterByNoRegex(String tag) {
StringBuilder ret = new StringBuilder(tag.length());
int tmp = 0;
while (tmp < tag.length()) {
int s = tag.indexOf("<", tmp);
if (s == tmp) {
int e = tag.indexOf(">", s);
if (e < 0) {
ret.append(tag.substring(s));
break;
}
tmp = e + 1;
} else if (s > tmp) {
ret.append(tag.substring(tmp, s));
tmp = s;
//## added by cjx : filter non-tag '<'
if (s < tag.length() - 1) {
char nextChar = tag.charAt(s + 1);
if (nextChar == '<' || nextChar == ' '){ //不是tag的开始
++tmp;
ret.append(tag.charAt(s++));
}
} //end if
} else {
ret.append(tag.substring(tmp));
break;
}
}
return ret.toString();
}
正则方法:
private final static String regxpForHtml = "<([^>]*)>"; // 过滤所有以<开头以>结尾的标签
public static String filterByRegex(String str) {
//return tag.replaceAll("\\<.*?\\>","");
Pattern pattern = Pattern.compile(regxpForHtml);
Matcher matcher = pattern.matcher(str);
StringBuffer sb = new StringBuffer();
boolean result1 = matcher.find();
while (result1) {
matcher.appendReplacement(sb, "");
result1 = matcher.find();
}
matcher.appendTail(sb);
return sb.toString();
}
结果:同样的字符串例如String tag = "1234567123<<tr 8901>0234<567>-< 890>AAAA<";,用非正则方式匹配要比正则方式匹配快3倍左右
注释:回溯
回溯就像是在走岔路口,当遇到岔路的时候就先在每个路口做一个标记。如果走了死路,就可以照原路返回,直到遇见之前所做过的标记,标记着还未尝试过的道路。如果那条路也走不能,可以继续返回,找到下一个标记,如此重复,直到找到出路,或者直到完成所有没有尝试过的路。
可以参考文章:http://www.jb51.net/article/22993.htm