高性能的正则
啦啦啦 我是一个搬运工,搬错打轻点儿 _-
说说性能
因为DFA是文本主导的,所以没什么意思,我们主要来看看NFA。有时候一个细微的改变 就可能给表达式带来翻天覆地的变化。写正则的时候,需要在准确率和效率之间做权衡。
一些例子
“(\. | [^\])*”
这个例子 匹配字符串 允许出现转义的 “,转义的字符由前者匹配,非转义字符 前者匹配失败-回溯-匹配后者成功
我们先来看正常情况(就是转义字符占少数,正常情况不会一句话都是转移字符吧????)
我们下面来匹配1\”df45\” sdfdsfdsfdsf这个字符串
表达式 | 回溯次数 |
---|---|
“(\. | [^\”])*” | .1\”.d.f.4.5\”. .s.d.f.d.s.f.d.s.f.d.s.f. |
“([^\”] | \.)*” | 1.\”df45.\” sdfdsfdsfdsf. |
- 这种改动对POSIX NFA 没有影响,因为他本来就需要匹配所有情况,但是对NFA好处很大
- 匹配成功才有效果,匹配失败 替换顺序对NFA也没用(如果最后一次成功 貌似也没啥用HHH)
有人说了,”([^”] | (?<=\))*” 环视也可以解决,比如匹配 dfd\”fdfd\”
- ”/sfdsdf\” fdsf ” sdfsdf ” ==> “/sfdsdf\” fdsf “,虽然\” 这里表达的是转义的\ + “,但是环视会把\”匹配到,然后 fdsf 之后的 ” 当做结束的引号
“( \. | [^”] )*”这个行不行呢?里面无非就是转义字符 \. + 非转义字符 [^”]
- 如果是 “sfdsdfdsf sdfdsf \”fdfdf \” fddsfdsfdsf, “( \. | [^”] )*会匹配所有字符,然后释放字符给结尾的 ” 匹配,最后的转义 ” 会被认为是结尾的 “
- 这种还有救没了呢? 必须有 使用占有优先量词 或者是 固话分组 “( \. | [^”] )+” “(?> \. | [^”] )“,会抛弃回溯 不会释放字符给结尾的 ” 匹配
- 这个可交换分支位置 “( [^”] | \. )*”? 不可以!!! “sfdsdfdsf sdfdsf \”fdfdf \” fddsfdsfdsf ” 匹配到的是 “sfdsdfdsf sdfdsf \”
上面的表达式,不管是交换分支之前 还是之后 都有一个问题,就是 * 会把表达式控制权在括号内外不听的切换,所以 是不是要有一个种优化方式:减少进出次数? 怎么办呢—尽量在括号没匹配更多的字符 [^\”]+
表达式 | 回溯次数 |
---|---|
“(\. | [^\”])*” | .1\”.d.f.4.5\”. .s.d.f.d.s.f.d.s.f.d.s.f. |
“(\. | [^\”]+)*” | .1\”.df45\”. sdfdsfdsfdsf. |
“([^\”] | \.)*” | 1.\”df45.\” sdfdsfdsfdsf. |
“([^\”]+ | \.)*” | 1.\”df45.\” sdfdsfdsfdsf. |
可见 在上面这种里面,是大大减少了回溯次数的;下面的表达式中 也大大减少了进出口号的次数
总体来说减少了维护子表达式 匹配位置 回溯版本的开销性能会提升不少
填坑
生活就是真么苦逼,搞发明总是有代价了。上面的最后一种发明,对NFA是天使,对POSIX NFA来说 就是噩梦了
([^\”])*我们只看这段,括号内的是 * 号的约束对象,用来匹配有某些字符,追都就是匹配到所有字符。(NFA POSIX NFA都是这样的)。但是如果是 ([^\”]+)* ,就不一样了。+ 代表一次或者多次, * 代表任意多次,所有情况的总和是多少?不知道。。。不过它有一个名字:超线性匹配,就是指数级别的增长
这种增长对POSIX NFA,就是回溯了。因为它要匹配所有情况,然后去最长的作为结果
- 一次假的死机 (见邮箱项目)
- 如果任何时候都可以很快出结果 DFA
- 如果有时候是很快出结果(成功比较靠前 就快) 是NFA
- 如果可以成功 但是一直很慢 POSIX NFA
- 有一些优化 可以让POSIX NFA 出现类似 DFA的效果(250!!!!!!!!!)
说说回溯
我们用 ” . * ” 做实验,匹配 dfdfd fdfd ” dfdsfsdf ” fdfd fdfdf ” dfsfsdfsdf ” fdssdfsdff
结果就是: dfdfd fdfd 1”9 dfdsfsdf “78 fdfd fdfdf “56 dfsfsdfsdf “34 fdssdfsdff2
- 1到2–释放字符-到3 此时NFA结束
- POSIX NFA会继续类似 234一样 进行 456 678 89,然后先NFA的结果作为最终结果(当然 89不会成功,因为第一个 ” 不会释放),期间依靠的就是驱动装置 在匹配成功之后 驱动进入下一轮回溯
如果匹配失败呢
” . * ” & 这种,是不会匹配成功的,但是正则还是会进行运作,进行四轮尝试 分别在 四个 ” 号那里
优化
换成 ” [ ^ ” ] * “
- 一直失败 一直到第一个引号 匹配成功 ” dfdsfsdf “,POSIX 在这里会说一次 但是只是在这段字符之间回溯一次,然后进入下一轮匹配
- 一直失败 匹配成功第二个” dfsfsdfsdf “,不会匹配第 23引号之前的文本,因为第一轮消耗了第二个引号(环视不消耗字符)
- ’123 “1111” 22222 “3333” 4444’.match(/”[^”]*”/) 这个可以试试
避免多选结构
a | b | c 比 [ abc ] 好垃圾的多,因为 如果匹配abcbcaabc 每次前进一步 就会进行3次匹配
想点骚东西
正则表达式的执行,分为编译、传动开始、元素检查、出匹配结果 ,23循环进行,在每一步都可以进行一些优化
比如使用开头结尾优化啊(^开头没有匹配到 就是失败了)、消除不必要的括号(.* 替换 (?:.))、忽略优先量词优化、占有优先量词避免回溯、量词互换(\d\d\d 换成 \d{4} 后者某些时候快一点)、拆分正则为小表达式 分别匹配
在面向对象的处理中,避免重编译啊
通用的表达式匹配算法
。。。待续