当前位置: 首页 > 科技观察

说说正则表达式的原理

时间:2023-03-22 17:30:27 科技观察

正则表达式可能大多数人都会用到,但是你在使用的时候有没有想过正则表达式背后的原理,或者当我告诉你正则表达式可能有性能的时候你是不是特别对导致在线挂断的问题感到惊讶吗?我们来看一个7月初的正则表达式引发的线上事故的例子。https://blog.cloudflare.com/d...简单来说就是一个正则表达式存在性能问题,造成灾难性的回溯,导致cpu满载。性能问题的规律性首先看问题的规律性。性能问题的关键部分是.*(?:.*=.*)。这里我们忽略非捕获组,将性能问题的规律性看成.*.*=.*。他们之中,表示匹配除换行符以外的任意字符(这里很多人会弄错,容易出bug),而.*表示贪心匹配任意字符任意次数。什么是回溯?当使用贪心匹配或惰性匹配或匹配进入匹配路径选择时,遇到一条失败的匹配路径而试图走另一条匹配路径,称为回溯。可以理解为走迷宫,走到一条路的尽头,无路可走时又回到最后一个岔路口选择另一条路。回溯现象//性能问题正则性//将以下代码粘贴到浏览器控制台并运行tryconstregexp=`[A-Z]+\\d+(.*):(.*)+[A-Z]+\\d+`;conststr=`A1:B$1,C$1:D$1,E$1:F$1,G$1:H$1`constreg=newRegExp(regexp);start=Date.now();constres=reg.test(str);end=Date.now();console.log('正则正则执行耗时:'+(end-start))下面我们看看回溯是怎么回事。假设我们有一个正则(.*)+\d,此时输入的字符串是abcd。注意此时只输入长度为4的字符串。下面分析一下回溯匹配过程:上面展示了一个回溯匹配过程,大致描述了前三轮匹配。注意,这里的(.*)+可以暂时看成执行了多次.*。(.*){1,}***匹配,因为.*可以匹配任意次数任意个字符,所以这里可以选择匹配empty,a,ab,abc,abcd,因为贪心性*,所以.*直接匹配到了abcd的4个字符,+因为后面没有其他字符,所以只看.*吃完abcd就匹配不上了。这里把+的值记录为1,然后\d没有匹配到,所以匹配失败。进行第一次回溯。第二次匹配,因为回溯,回到之前的匹配路径选择时,上次.*匹配了abcd,路径被挡住了,所以这次只能尝试匹配abc,此时有一个d最后,那么可以理解为.****第一次匹配到abc,然后因为(.*)+,.*可以匹配到第二次,这里.*可以匹配到d,这里+的值记录为2,然后\d没有匹配到,所以匹配失败,进行第二次回溯。第三次匹配,因为回溯了,所以回到前面匹配路径选择的时候,最后一个.*匹配abc,第二个.*匹配d,路径被挡住了,所以这里第二个.*不匹配.这时,末尾有一个d。\d和d匹配不上,进行第三次回溯。如何减少或避免回溯优化正则表达式:始终注意回溯对性能的影响。使用DFA正则引擎的正则表达式什么是DFA正则引擎传统的正则引擎分为NFA(非确定性有限状态自动机)和DFA(确定性有限状态自动机)。DFA对于任何给定的状态和输入的字符,DFA只会转移到某个状态。并且DFA不允许在没有输入字符的情况下进行状态转换。例如,在状态0下,输入字符A时,只有一个终点,只能到达状态1。NFA对于任何状态和输入字符,NFA可以转移的状态是一个非空集。例如,在状态0,输入字符A时,可以有多个端点,即可以到达状态1,也可以到达状态0。DFA和NFA的正则引擎的区别说了这么多,那么DFA和NFA的正则引擎有什么区别呢?或者DFA和NFA如何实现正则引擎?DFA正则表达式中的DFA引擎实际上是将正则表达式转化为一个图的邻接表,然后以跳表的形式判断一个字符串是否与正则表达式匹配。//大概是模拟functionmachine(input){if(typeofinput!=='string'){console.log('inputiswrong');return;}//这里比如:/abc/转成DFA后//我们定义了4个状态,即0,1,2,3,初始状态为0constreg={0:{a:1,},1:{b:3,},2:{isEnd:true,},3:{c:2,},};letstatus=0;for(leti=0;i