文章来自微信公众号:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相关原创技术文章。 如果喜欢,请关注公众号:前端工坊
版权归公众号所有,转载请注明出处。
作者:京东商城-成都研究院-JSHOP研发部 卢兴元
正则表达式-从模糊到清晰
1. 什么是正则
简单点,正则是一些用来匹配和处理文本的字符串(或者叫工具),往往用于查找特定的信息(搜索),或者查找并编辑特定的信息(替换)。它是一种内置在其他语言里的一种“迷你”语言,比如内置在Javscript、Java等语言中。
2. 要认可的事实
正则答案不唯一。几乎所有的问题,往往都会有不止一种解决方案。有的比较简单,有的比较快速,有点兼容性更好,有的功能更全。我们需要依据自己的需求,确认一种最适合自己的方案。
3. 正则引擎概述
正则引擎可以分为2类。一种称之为NFA(非确定型有穷自动机),另一种称之为DFA(确定型又穷自动机)。嗯,概念不好理解,我们举个栗子:
正则:to(Jack|Rose|Jerry)
匹配文本:xxx···toJerry
1)NFA(表达式主导)匹配过程
正则表达式从正则的第一个 t 开始,每次由正则引擎查看表达式的一部分,同时检查当前文本是否匹配表达式的当前部分。如果是,则继续表达式的下一部分,如果继续,直到表达式的所有部分都能匹配到。此时发现当检查到当前文本中的字符 t 时,所以正则表达式的第一项匹配成功,接着会检查紧跟其后的字符是否能由 o 来匹配,然后发现可以,则接着检查后面的元素,此时后面的元素是 (Jack|Rose|Jerry) ,引擎会尝试着3种可能进行分别测试,直到匹配成功。
2)DFA(文本主导)匹配过程
引擎在扫码当前文本的时候,会记录当前有效的所有匹配可能。当引擎移动到文本的 t 时,它会在当前处理的匹配可能中添加一个潜在的可能:
接下来扫描的每个字符,都会更新当前的可能匹配序列。例如扫码到匹配文本的 J 时,有效的可能匹配变成了2个,Rose被淘汰出局。
扫描到匹配文本的 e 时,Jack也被淘汰出局,此时就只剩一个可能的匹配了。当完成后续的rry的匹配时,整个匹配完成。
3)两句话点评NFA与DFA
1、DFA匹配速度快但特性少(比如不支持捕获组、反向引用),NFA匹配稍慢但能力强大;
2、DFA就好比搭载电动发动机的汽车,加速度很快,但续航短,不能出远门,而NFA可以认为是汽油发动机的汽车,加速度没那么快,但是适应性广,哪里都能去,但由于适应性广,所以调教很重要。
4)需要注意的
Java、Javascript、PHP、Python这些都是NFA引擎。
4. 过基础(老手请跳过)
5. 要点讲解
1)贪婪与懒惰
贪婪模式:
尽可能匹配更多的字符。举个栗子:
正则:<p>.*</p>
结果:
从匹配过程我们也可以发现对于 .* 这个表达式会尝试尽可能多的匹配字符,直到匹配到尽头,才尝试匹配正则结尾的 </p> 。
懒惰模式:
与贪婪模式相反,尽可能匹配更少的字符。举个栗子:
正则:<p>.*?</p>
结果:
从匹配过程我们也可以发现,会优先匹配正则结尾的 </p> ,在没有满足此结尾的情况下,才尽可能的去少匹配 .*? 这个表达式。
2)子表达式与反向引用
子表达式:
考虑这种场景,有些短语虽然由多个单词构成,但其实是一个整体,需要把它当做一个独立元素来使用,这种时候就需要使用子表达式。子表达式必须用()圆括号括起来。用途就是,可以精确的设定需要重复匹配的文本及重复次数。
反向引用:
它允许我们在正则中引用之前子表达式匹配到的结果。这有什么用?还是举个栗子:
需求:匹配Html代码片段中的h1~h6标签
正则:<h[1-6]>.*?</h[1-6]>(没有使用反向引用)
结果:
正则:<h([1-6])>.*?</h1>(使用了反向引用)
结果:
3)回溯
NFA引擎匹配能力强大,但是调教不好,有可能引发性能问题,它有另一个叫法,叫做回溯失控。那么问题来了,什么是回溯?
举个栗子:
我们醒来的时候,突然发现被困在山洞里,这时候需要寻找出路,然而前方是一个岔路口。这个时候也并没有任何依据可以告诉我们哪一条是出路,只有挨个尝试,于是我们可以在岔路口做个标记,以便万一选择的这条路走不通,可以原路返回,直到遇见做了标记的岔路口,以便继续尝试另一条路是不是出路。我们可以把每次尝试失败然后往回走,找到之前做标记的地方的这个过程,称之为回溯。
很多情况下,依据你写的正则表达式,正则引擎或多或少都需要进行这种2个或者多个选项的选择。
4)断言(环视)
先不做专业术语解释,先来看这么一个应用场景
需求:匹配网页里所有PC商品详情页地址所包含的sku信息
PC商品详情页地址格式://item.jd.com/xxxxxx.html
方法一:先正则匹配,再截断后面固定的.html
正则:/d+\.html/g
方法二:先正则匹配,再截断前后固定的字符
正则:/item\.jd\.com/d+\.html/g
方法三:使用正向断言和反向断言,保证准确性,同时只返回sku数字
正则:/(?<=item\.jd\.com/)d+(?=\.html)/g
断言分类:正向肯定断言、正向否定断言、反向肯定断言、反向否定断言
需要注意的:Javascript不支持反向断言,Java也是有限制的支持反向断言
总而言之,言而总之,当我们匹配目标关键字的时候,同时期望对目标关键字的前后进行限制,并且又不期望这些限制会出现在匹配结果中。这时候,就可以使用断言。
6. 正则优化
1)怎样才算是一个好正则
准确性:只匹配期望的文本,排除掉不期望的文本
需求:匹配jshop手机活动页url的域名部分
jshop手机活动页URL格式://xxxx.jd.xxx/m/act/xxxxxx.html
正则:///(.*)(?=/m)/g
正则:///(1*)/g
点评:如果不需要匹配/,那就应该在正则表达式中作出这样的规定
匹配效率:很快返回匹配结果,如果不能匹配,尽可能短的时间报告匹配失败
前面有提到过,NFA引擎功能强大,但是写不好很容易引发效率问题。其中太多的多选分支很容易成为效率杀手,因为任何多选分支只要匹配失败,都会导致回溯。所以提高正则匹配效率的方法之一就是减少多选分支。
举个栗子:
需求:匹配用户输入的一个字符串是否是一个4位IP里的一位,直白的说就是匹配0~255
分析:可能有1位,也可能有2位,也可能有3位。3位的时候需要分开判断,当第一位是0或者1的时候,后面两位可以是任意数字。当第一位是2的时候,第二位只能是0-5。并且当第二位是0-4的时候,第三位可以是任意数字,但第二位是5的时候,第三位只能是0-5。
翻译过来正则:/d|dd|[01]dd|2[0-4]d|25[0-5]/
合并同类项后:/[01]?dd?|2[0-4]d|25[0-5]/
点评:可以通过合并同类项来减少多选分支。同时第一个多选分支使用的是 dd? 而不是 d?d ,这样如果根本不存在数字,NFA引擎会更快地报告失败
易读性
……
2)使用工具
分析正则表达式
比如这个网站 https://jex.im/regulex
可以实现对复杂整个表达式的一个
测试正则表达式性能
比如这个猫头鹰工具 RegexBuddy
可以用来测试正则表达式的匹配过程以及性能,包括各种语言下的正则特性支持情况。
3)优化手段
优化方针:减少回溯
1、减少或者合并多选分支
2、避免量词的嵌套
3、占有优先量词。可以减少回溯,遗憾的是js不支持,但java支持。
举个栗子:考虑到 /a+b/ 和 /a++b/ 两个正则,测试的字符串 aaaa
/a+b/ 的匹配过程
/a++b/ 的匹配过程
4、使用正确的边界匹配器(^、$、b、B等),限定搜索字符串位置
5、尽量不使用通配符".";字符使用具体的元字符、字符类(d、w、s等)(推荐)
6、使用正确的量词(+、*、?、{n,m}),如果能够限定长度,匹配最佳
7、使用非捕获型括号。如果不需要引用括号内的文本,请使用非捕获型括号(?:……),好处就是节省捕获时间,同时减少回溯使用的状态数量。
- / ↩