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)