【问题标题】:Catastrophic backtracking issue with regular expression正则表达式的灾难性回溯问题
【发布时间】:2016-03-24 09:00:05
【问题描述】:

我是使用正则表达式的新手,目前面临一个问题。

我正在尝试构建一个与以下格式的字符串匹配的正则表达式:

OptionalStaticText{OptionalStaticText %(Placholder) OptionalStaticText {OptionalSubSection} OptionalStaticText} OptionalStaticText

每个SectionSubsection 都用{...} 表示。每个Placeholder%(...) 表示。每个SectionSubsection 可以具有OptionalStaticText%(Placholder)OptionalSubSection 的任意排列。

为此,我创建了一个正则表达式,如下所示(也可以看到here)。

/^(?:(?:(?:[\s\w])*(?:({(?:(?:[\s\w])*[%\(\w\)]+(?:[\s\w])*)+(?:{(?:(?:[\s\w])*[%\(\w\)]+(?:[\s\w])*)+})*})+)(?:[\s\w])*)+)$/g

此表达式完美匹配有效字符串(例如:abc {st1 %(ph1) st11} int {st2 %(ph2) st22}{st3 %(ph3) st33 {st31 %(ph4) st332}} cd,可以在给定的链接中进行测试。

但是,只要输入字符串无效(例如:abc {st1 %(ph1) st11} int {st2 %(ph2) st22}{st3 %(ph3) st33 {st31 %(ph4) st332}} c-d- 不是[\s\w] 字符组中的有效字符),就会导致超时。

这种无效的字符串会通过Catastrophic backtracking导致超时,也可以在上面的链接中测试。

我一定犯了一些新手错误,但不确定是什么。为了避免这种情况,我应该做出改变吗?

谢谢。

【问题讨论】:

  • 我认为单独的正则表达式并不是完成这项工作的正确工具。即使你能做到,生成的正则表达式对大多数人来说都是完全不可读的,因此你将遇到严重的维护问题。您可以尝试编写解析器。
  • 开始删除无用组。

标签: javascript regex


【解决方案1】:

如果您遇到超时问题,可能是因为[%\(\w\)]+
这是您要查找的表单中包含的一类字符。

改用表单本身。

^(?:(?:[\s\w]*(?:({(?:[\s\w]*%\(\w*\)[\s\w]*)+(?:{(?:[\s\w]*%\(\w*\)[\s\w]*)+})*})+)[\s\w]*)+)$

Formatted and tested:

 ^ 
 (?:
      (?:
           [\s\w]* 
           (?:
                (                             # (1 start)
                     {
                     (?:
                          [\s\w]* 
                          % \( \w* \) 
                          [\s\w]* 
                     )+
                     (?:
                          {
                          (?:
                               [\s\w]* 
                               % \( \w* \) 
                               [\s\w]* 
                          )+
                          }
                     )*
                     }
                )+                            # (1 end)
           )
           [\s\w]* 
      )+
 )
 $

【讨论】:

  • 这个正则表达式匹配到字符串的末尾,但在几个测试用例上不起作用,请参阅(此链接)[regex101.com/r/wT7jV3/1]。它还有一个不需要的括号级别(第一个)
  • @GsusRecovery - 是的,它确实有一个不需要的嵌套集群组。重要的是,我不会尝试对人们进行风格教育。我只是拿了他的正则表达式并修复了他的回溯问题。你怎么知道我不知道 他的 测试用例,但这真的是问题吗?并向我展示单个嵌套集群组比没有它时慢一微秒的证据。
  • 您可以在regex101 上自行测试(如果我正确格式化了上面的链接):没有外部组,正则表达式引擎会跳过 71 个步骤,但这不是重点。我已经从 OP 在问题中的字符串描述中提取了测试用例:OptionalStaticText{OptionalStaticText %(Placholder) OptionalStaticText {OptionalSubSection} OptionalStaticText} OptionalStaticText
  • skips 71 steps 我正在尝试将每个 step 的微秒数相加得到总数,但我就是做不到。引擎存在于编译世界中,而不是步进世界。就在线测试人员而言,他们是无用的垃圾……充满了噪音。
  • 我已经指出了一些不符合您的答案的问题(主要是在这种情况下,外部括号只是您可以忽略的一种瘾)只是为了诱使您修复它们,如果您能做到的话请。我真的很好奇是否存在纯正则表达式解决方案(主要在 javascript 中)。
【解决方案2】:

尝试使用所有这些嵌套重复运算符(*+)从开头 ^ 到结尾 $ 完全匹配行会导致灾难性的回溯。

删除结束锚$ 并简单地检查输入字符串的长度与匹配的长度。

我已经重写了正则表达式以在可选部分也被删除的情况下工作:

^(?:[\w \t]*(?:{(?:[\w \t]*|%\(\w+\)|{(?:[\w \t]*|%\(\w+\))+})+})?)+

Online Demo

传奇

^                              # Start of the line
(?:                            # OPEN NGM1 - Non matching group 1
  [\w \t]*                     # regex word char or space or tab (zero or more)
  (?:                          # OPEN NMG2
    {                          # A literal '{'
    (?:                        # OPEN NMG3 with alternation between:
      [\w \t]*|                # 1. regex word or space or tab (zero or more)
      %\(\w+\)|                # 2. A literal '%(' follower by regex word and literal ')'
      {(?:[\w \t]*|%\(\w+\))+} # 3. 
    )+                         # CLOSE NMG3 - Repeat one or more time
    }                          # A literal '}'
  )?                           # CLOSE NMG2 - Repeat zero or one time
)+                             # CLOSE NMG1 - Repeat one or more time

正则表达式架构

Js 演示

var re = /^(?:[\w \t]*(?:{(?:[\w \t]*|%\(\w+\)|{(?:[\w \t]*|%\(\w+\))+})+})?)+/;

var tests = ['OptionalStaticText{OptionalStaticText %(Placeholder) OptionalStaticText {OptionalSubSection} OptionalStaticText} OptionalStaticText', '{%(Placeholder) OptionalStaticText {OptionalSubSection}}', 'OptionalStaticText{%(Placeholder)} OptionalStaticText', 'abc {st1 %(ph1) st11} int {st2 %(ph2) st22}{st3 %(ph3) st33 {st31 %(ph4) st332}} cd', 'abc {st1 %(ph1) st11} int {st2 %(ph2) st22}{st3 %(!ph3!) st33 {st31 %([ph4]) st332}} cd', 'abc {st1 %(ph1) st11} int {st2 %(ph2) st22}{st3 %(ph3) st33 {st31 %(ph4) st332}} c-d',  'abc {st1 %(ph1) st11} int {st2 %(ph2) st22}{st3 %(ph3) st33 {st31 %(ph4) st332}} cd'];
var m;

while(t = tests.pop()) {
    document.getElementById("r").innerHTML += '"' + t + "'<br/>";
    document.getElementById("r").innerHTML += 'Valid string? ' + ( (t.match(re)[0].length == t.length) ? '<font color="green">YES</font>' : '<font color="red">NO</font>') + '<br/><br/>';
}
    
&lt;div id="r"/&gt;

【讨论】:

  • 仅供参考。 Trying to match the line exactly from the start ^ to end $ with all these nested repetition operators (* or +) cause the catastrophic backtracking - 这本身不会导致回溯问题。像这样^(?:a(?:b(?:(?:c(?:d+)+)*)*)+)$ 不会。
【解决方案3】:

您可以编写解析器来解析此类结构化字符串,解析器本身将允许您检查字符串的有效性。例如(不完整):

var sample = "OptionalStaticText{OptionalStaticText %(Placholder) OptionalStaticText {OptionalSubSection} OptionalStaticText} OptionalStaticText";

function parse(str){

  return parseSection(str);

  function parseSection(str) {
    var section = new Section();
    var pointer = 0;

    while(!endOfSection()){

      if (placeHolderAhead()){
        section.push(parsePlaceHolder());
      } else if (sectionAhead()){
        section.push(parseInnerSection());
      } else {
        section.push(parseText());
      }
    }

    return section;

    function eat(token){
      if(str.substr(pointer, token.length) === token) {
        pointer += token.length;
        section.textLength += token.length;
      } else {
        throw ("Error: expected " + chr + " but found " + str.charAt(pointer));
      }
    }

    function parseInnerSection(){
      eat("{");
      var innerSection = parseSection(str.substr(pointer));
      pointer += innerSection.textLength;
      eat("}");
      return innerSection;
    }

    function endOfSection(){
      return (pointer >= str.length)
            || (str.charAt(pointer) === "}");
    }

    function placeHolderAhead(){
      return str.substr(pointer, 2) === "%(";
    }

    function sectionAhead(){
      return str.charAt(pointer) === "{";
    }

    function parsePlaceHolder(){
      var phText = "";
      eat("%(");
      while(str.charAt(pointer) !== ")") {
        phText += str.charAt(pointer);
        pointer++;
      }
      eat(")");
      return new PlaceHolder(phText);
    }

    function parseText(){
      var text = "";

      while(!endOfSection() && !placeHolderAhead() && !sectionAhead()){
        text += str.charAt(pointer);
        pointer++;
      }
      return text;
    }
  }
}

function Section(){
  this.isSection = true;
  this.contents = [];
  this.textLength = 0;

  this.push = function(elem){
    this.contents.push(elem);
    if(typeof elem === "string"){
      this.textLength += elem.length;
    } else if(elem.isSection || elem.isPlaceHolder) {
      this.textLength += elem.textLength;
    }
  }

  this.toString = function(indent){
    indent = indent || 0;
    var result = "";
    this.contents.forEach(function(elem){
      if(elem.isSection){
        result += elem.toString(indent+1);
      } else {
        result += Array((indent*8)+1).join(" ") + elem + "\n";
      }
    });
    return result;
  }
}

function PlaceHolder(text){
  this.isPlaceHolder = true;
  this.text = text;
  this.textLength = text.length;

  this.toString = function(){
    return "PlaceHolder: \"" + this.text + "\"";
  }
}


console.log(parse(sample).toString());

/* Prints:
OptionalStaticText
        OptionalStaticText 
        PlaceHolder: "Placholder"
         OptionalStaticText 
                OptionalSubSection
         OptionalStaticText
 OptionalStaticText
*/

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-24
    • 1970-01-01
    • 1970-01-01
    • 2016-10-07
    • 1970-01-01
    • 2017-07-20
    相关资源
    最近更新 更多