正则表达式引发的血案

1.问题及背景

一个需求,在一段字符串中识别是否有邮箱. 于是写了一个正则表达式

  private final static Pattern REGEX_EMAIL_PATTEN = Pattern.compile("([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+([a-zA-Z]*)?");
  
  
    public static String parseEmail(String str){
        if (StringUtil.isBlank(str)) {
            return null;
        }
        Matcher m = REGEX_EMAIL_PATTEN.matcher(str);
        if (m.find()) {
            int start = m.start();
            int end = m.end();
            return str.substring(start,end);
        }
        return null;
    }

然后做了几个简单的测试,嗯,都通过了.最终上线。 没多久运维通知服务cpu load很高,堆栈信息拉出来一看... 堆栈信息见本文末尾. 定位到了 parseEmail(String str) 这个方法.根据时间定位,拉取线上请求日志,发现方法入参是这样的:

http://mail.1etrip.com/OK/controller/journey/apv/toApprove.html?param=20855793060AD450E779B0CAC0CEB570B571740F86C17F660B12EDC2ED0FFDCA  2018-10-19

这样的

www.benke168.com/Admin/Wechatlogin/index/U/cbmeVnwkYeW1l6y8aWQ9NTMyL3Bhc3N3b3JkPWUxMGFkYzM5NDliYTU5YWJiZTU2ZTA1N2YyMGY4ODNlL3VzZ
XJuYW1lPTEzOTUyNDI1NTk1.html

本地运行一下,果然跑不出来结果. 搜索引擎查查看.

Java regex正则表达式类似死循环问题,详见:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6988218 这个问题其实是由于正则表达式很复杂时,java regex复杂度过高(复杂度成指数级),导致类似死循环的问题。

那么这个问题怎么解决...修改正则表达式,简单粗暴, 匹配 x@y.z这样的字符串就好了。x长度 1,y的长度1~30 ,z的长度1~10.


    private final static Pattern REGEX_EMAIL_PATTEN = Pattern.compile("([a-z0-9A-Z])@([a-z0-9A-Z]){1,30}\\.([a-z0-9A-Z]){1,10}");


修改后的测试

用9月20日当天1000w条历史数据做单元测试. 最终结果:耗时大于10毫秒的有26条数据.最大的有36毫秒。 耗时最大的数据是这样的:

1 172.25.50.202-DB: F:\\backup\\236\\DB\\2018/9/19 19:39:29本次备份完成文件: CheckOperationLastTime_backup_2018_09_03_092647_0992470.bak CheckOperationLastTime_backup_2018_09_03_130001_6685014.bak CheckOperationLastTime_backup_2018_09_19_130001_5866855.bak office_wx_backup_2018_09_03_092647_1178543.bak office_wx_backup_2018_09_03_130001_6841585.bak office_wx_backup_2018_09_19_130001_6023100.bak UMDataBase_backup_2018_09_03_092647_1178543.bak UMDataBase_backup_2018_09_03_130001_6997541.bak UMDataBase_backup_2018_09_19_130001_6023100.bak WXGZ_backup_2018_09_03_092647_1647407.bak WXGZ_backup_2018_09_03_130001_7466590.bak WXGZ_backup_2018_09_19_130001_6491867.bak WX_CAM_backup_2018_09_03_092647_1334902.bak WX_CAM_backup_2018_09_03_130001_6997541.bak WX_CAM_backup_2018_09_19_130001_6179345.bak WX_GDZC_backup_2018_09_03_092647_1334902.bak WX_GDZC_backup_2018_09_03_130001_7153797.bak WX_GDZC_backup_2018_09_19_130001_6179345.bak WX_GWGL_backup_2018_09_03_092647_1491175.bak WX_GWGL_backup_2018_09_03_130001_7153797.bak WX_GWGL_backup_2018_09_19_130001_6335584.bak WX_SWTJ_backup_2018_09_03_092647_1491175.bak WX_SWTJ_backup_2018_09_03_130001_7310320.bak WX_SWTJ_backup_2018_09_19_130001_6335584.bak WX_UMDataBase_backup_2018_09_03_092647_1647407.bak WX_UMDataBase_backup_2018_09_03_130001_7310320.bak WX_UMDataBase_backup_2018_09_19_130001_6491867.bak

emmmm...基本符合预期了.

2.正则表达式算法分析
·常见正则表达式引擎

参考:正则表达式匹配原理

引擎区别点
DFA<br> Deterministic finite automaton 确定型有穷自动机DFA引擎它们不要求回溯(并因此它们永远不测试相同的字符两次),所以匹配速度快!DFA引擎还可以匹配最长的可能的字符串。不过DFA引擎只包含有限的状态,所以它不能匹配具有反向引用的模式,还不可以捕获子表达式。代表性有:awk,egrep,flex,lex,MySQL,Procmail
NFA<br>Non-deterministic finite automaton 非确定型有穷自动机,又分为传统NFA,Posix NFA传统的NFA引擎运行所谓的“贪婪的”匹配回溯算法(longest-leftmost),以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。传统的NFA回溯可以访问完全相同的状态多次,在最坏情况下,它的执行速度可能非常慢,但它支持子匹配。代表性有:GNU Emacs,Java,ergp,less,more,.NET语言,PCRE library,Perl,PHP,Python,Ruby,sed,vi等,一般高级语言都采用该模式。

DFA以字符串字符,逐个在正则表达式匹配查找,而NFA以正则表达式为主,在字符串中逐一查找。尽管速度慢,但是对操作者来说更简单,因此应用更广泛。

·解析引擎眼中的字符串组成

对于字符串“DEF”而言,包括D、E、F三个字符和 0、1、2、3 四个数字位置:0D1E2F3,对于正则表达式而言所有源字符串,都有字符和位置。正则表达式会从0号位置,逐个去匹配的。

·占有字符和零宽度

正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是零宽度的。占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。常见零宽字符有:^,(?=)等

工具推荐

regex101.zip 下载

注意事项:

慎用 "+,*",用 {1,30}这样的替代. +匹配1到无穷次. *匹配0到无穷次. {1,30}匹配1到30次 集合本次案例分析:

([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+([a-zA-Z]*)? 导致匹配长字符串是匹配复杂度指数级增加

附录:堆栈信息
[7] Busy(11.4%) thread(23901/0x5d5d) stack of java process(23087) under user(admin):
"catalina-exec-1" #381 daemon prio=5 os_prio=0 tid=0x00007facd4088800 nid=0x5d5d runnable [0x00007fad1079b000]
   java.lang.Thread.State: RUNNABLE
        at java.util.regex.Pattern$Curly.match(Pattern.java:4227)
        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
        at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
        at java.util.regex.Pattern$Ques.match(Pattern.java:4182)
        at java.util.regex.Pattern$Curly.match0(Pattern.java:4272)
        at java.util.regex.Pattern$Curly.match(Pattern.java:4234)
        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
        at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
        at java.util.regex.Pattern$Ques.match(Pattern.java:4182)
        at java.util.regex.Pattern$Curly.match0(Pattern.java:4272)
        at java.util.regex.Pattern$Curly.match(Pattern.java:4234)
        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
        at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
        at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
        at java.util.regex.Pattern$Ques.match(Pattern.java:4182)
        at java.util.regex.Pattern$Curly.match0(Pattern.java:4272)
        at java.util.regex.Pattern$Curly.match(Pattern.java:4234)
        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
....
  at java.util.regex.Pattern$Curly.match0(Pattern.java:4272)
        at java.util.regex.Pattern$Curly.match(Pattern.java:4234)
        at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
        at java.util.regex.Pattern$Loop.matchInit(Pattern.java:4801)
        at java.util.regex.Pattern$Prolog.match(Pattern.java:4741)
        at java.util.regex.Pattern$Start.match(Pattern.java:3461)
        at java.util.regex.Matcher.search(Matcher.java:1248)
        at java.util.regex.Matcher.find(Matcher.java:637)
        at com.weike.commons.util.RegexUtil.parseEmail(RegexUtil.java:260)
        at com.taovip.v2.action.SmsAction.lambda$needSmsRecheck$5(SmsAction.java:4670)
        at com.taovip.v2.action.SmsAction$$Lambda$10/40486007.test(Unknown Source)
        at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:174)
        at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
        at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
        at java.util.HashMap$ValueSpliterator.tryAdvance(HashMap.java:1633)
        at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
        at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498)
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
        at java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:230)
        at java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:196)
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:449)

转载于:https://my.oschina.net/u/2341924/blog/2996489

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值