【问题标题】:Regular Expression causing Stack Overflow正则表达式导致堆栈溢出
【发布时间】:2013-07-05 04:34:29
【问题描述】:

关于我之前的问题:ECMAScript Regex for a multilined string,我已经实现了以下加载过程:

void Load( const std::string& szFileName )
{
     static const std::regex regexObject( "=== ([^=]+) ===\\n((?:.|\\n)*)\\n=== END \\1 ===", std::regex_constants::ECMAScript | std::regex_constants::optimize );
     static const std::regex regexData( "<([^>]+)>:([^<]*)\\n", std::regex_constants::ECMAScript | std::regex_constants::optimize );

     std::ifstream inFile( szFileName );
     inFile.exceptions( std::ifstream::badbit );

     std::string szFileData( (std::istreambuf_iterator<char>(inFile)), (std::istreambuf_iterator<char>()) );

     inFile.close();

     std::vector<std::future<void>> vecFutures;

     for( std::sregex_iterator itObject( szFileData.cbegin(), szFileData.cend(), regexObject ), end; itObject != end; ++itObject )
     {
          if( (*itObject)[1] == "OBJECT1" )
          {
               vecFutures.emplace_back( std::async( []( std::string szDataString ) {
                    for( std::sregex_iterator itData( szDataString.cbegin(), szDataString.cend(), regexData ) { // Do Stuff }
               }, (*itObject)[2].str() ) );
          }
          else if( (*itObject)[1] == "OBJECT2" )
          {
               vecFutures.emplace_back( std::async( []( std::string szDataString ) {
                    for( std::sregex_iterator itData( szDataString.cbegin(), szDataString.cend(), regexData ) { // Do Stuff }
               }, (*itObject)[2].str() ) );
          }
     }

     for( auto& future : vecFutures )
     {
          future.get();
     }
}

但是,使用此文件加载它会导致堆栈溢出(参数:0x00000001、0x00332FE4):

=== OBJECT2 ===
<Name>:Test Manufacturer
<Supplier>:Test Supplier
<Address>:Test Multiline
Contact
Address
<Email>:test@test.co.uk
<Telephone Number>:0123456789
=== END OBJECT2 ===
=== OBJECT1 ===
<Number>:1
<Name>:Test
<Location>:Here
<Manufacturer>:
<Model Number>:12345
<Serial Number>:54321
<Owner>:Me
<IP Address>:0.0.0.0
=== END OBJECT1 ===

我一直无法找到堆栈溢出的来源,但看起来外部 std::sregex_iterator 循环负责。

提前致谢!

【问题讨论】:

标签: c++ regex c++11 stack-overflow standard-library


【解决方案1】:

试试这个模式:

static const std::regex regexObject( "=== (\\S+) ===\\n((?:[^\\n]+|\\n(?!=== END \\1 ===))*)\\n=== END \\1 ===", std::regex_constants::ECMAScript | std::regex_constants::optimize );

【讨论】:

  • 当应用于上述文件时,这似乎导致了一个非终止循环。
  • @Shaktal:这个版本呢?
  • 不,不幸的是它仍然导致无限运行时循环
【解决方案2】:

您的表达似乎引起了很多回溯。我会将您的表达方式更改为:

第一:^===\s+(.*?)\s+===[\r\n]+^(.*?)[\r\n]+^===\s+END\s+\1\s+===

二:^&lt;([^&gt;]+)&gt;:([^&lt;]*)

这两个表达式都适用于以下选项:Multiline 和 DotMatchesAll 选项。通过包含行锚^ 的开始,它将回溯限制为最多一行或一组。

【讨论】:

  • 这 2 个正则表达式导致数据不匹配(即外循环立即终止)。
  • 我已经用活生生的例子更新了答案,展示了表达式是如何工作的。我怀疑问题不在于正则表达式,而在于您的代码中。
  • 即使是一个简单的例子也无法匹配;我会提供一个活生生的例子,但不幸的是 ideone 使用了 GCC,它目前不提供有效的 &lt;regex&gt; 实现。
【解决方案3】:

神圣的灾难性回溯。罪魁祸首是(?:.|\\n)*。每当您看到这样的构造时,您就知道您是在自找麻烦。

为什么?因为您告诉引擎匹配任何字符(换行符除外)或换行符,尽可能多地匹配,或者不匹配。让我带你了解一下。

引擎将按预期启动并匹配=== OBJECT2 ===-part 没有任何重大问题,将消耗一个换行符,然后地狱将开始。引擎消耗一切,一直到=== END OBJECT1 ===,并从那里回溯到合适的匹配。回溯基本上意味着返回一步并再次应用正则表达式以查看它是否有效。基本上用你的字符串尝试所有可能的排列。在您的情况下,这将导致几次 十万 次尝试。这可能就是您遇到问题的原因。

我不知道您的代码是否更好或其中是否有任何错误,但(?:.|\\n)* 与使用 *s* 单行修饰符编写 .* 相同(点匹配换行符)或[\S\s]*。如果您用我推荐的两个结构之一替换该结构,您希望不会再看到堆栈溢出错误。

编辑: 也请查看其他解决方案,除了解释为什么它如此糟糕之外,我真的没有时间深入并为您的问题提供可靠的解决方案。

【讨论】:

  • +1 用于回溯解释。但是您的意思是用[\S\s]* 替换(?:.|\\n)* 吗? (或者.*,如果 VC++ 有“点匹配换行符”——据我所知,标准 C++ 没有)。 [\S\s]* 据我所知,将有相同数量的回溯,只是没有那么灾难性,因为正则表达式对每个回溯执行的步骤更少。但我很可能是错的。 :-)
  • @JimmiTh:我提供的解决方案仍然会有很多回溯,但与原始帖子的数量相差甚远。这将是完全可管理的并且可以。回溯将有所不同,因为引擎只需将匹配减少[\S\s] 并且不考虑交替。在这种情况下,惰性匹配会使引擎回溯更少。
  • 我不确定我是否接受这个解释。 (?:.|\\n)* 效率稍低,但它是如何导致catastrophic backtracking 的呢?假设. 不匹配换行符,只有一种方法可以匹配字符串。回溯将是线性的(匹配成功或失败) - 它必须返回 .s 并尝试匹配 \n,但立即失败,使其比 (?s:.) 慢一点。
  • @Kobi:因为表达式看起来像(.)*,所以量词在组上,而不是点上。因此,您基本上是在告诉引擎一次消耗一个字符,而不是尽可能多地消耗。这意味着引擎将从&lt; 开始&lt;Name&gt;:Test Manufacturer 并一次吃一个字符直到结束(直到=== END OBJECT1 ===,然后从那里开始回溯。然后回溯到最近的\n,重新开始,回溯等。
  • @Lindrian - 我希望 .*(?:.)* 在这里的行为完全相同。 .* 也一次回溯一个字符。例如,当您匹配/.*Z/s 时,.* 匹配直到字符串结尾,然后回溯每个字符直到可以匹配Z。你描述的听起来像Possessive Quantifiers
【解决方案4】:

这是另一个尝试:

=== ([^=]+) ===\n((?:(?!===)[^\n]+\n)+)=== END \1 ===

在你的 C++ 中,它显然会写成:

=== ([^=]+) ===\\n((?:(?!===)[^\\n]+\\n)+)=== END \\1 ===

它是为最小化回溯而设计的(至少在匹配时),虽然我现在有点老脸先生,所以可能错过了很多改进它的方法。

它做了两个假设,用于避免大量回溯(这可能导致堆栈溢出,正如其他人所说):

  1. 除了开始/结束标记行之外,行首永远不会有 ===
  2. C++ 支持这些正则表达式功能 - 特别是使用负前瞻 (?!)。应该考虑到它是 ECMAScript 方言。

解释:

=== ([^=]+) ===\n

匹配并捕获对象开始标记。 [^=] 是在这里避免相对少量回溯的一种方法,与您的相同 - 我们没有使用 [^ ],因为我不知道 OBJECT id 中是否可能有空格。

((?:

开始捕获数据组。在其中,一个非捕获组,因为我们要单独匹配每一行。

   (?!===)

负前瞻 - 我们不希望 === 在我们捕获的行的开头。

   [^\n]+\n

单独匹配一行。

)+)

在开始和结束标记之间至少匹配一行,然后将所有行捕获到一个组中。

=== END \1 ===

匹配结束标记。

比较(使用 RegexBuddy):

原版:

  • 第一场比赛:1277 步
  • 匹配失败:1 步(这是由于对象之间的换行所致)
  • 第二场比赛:396 步

每个添加的对象都会导致前一个对象的步数增加。例如,再添加一个对象(对象 2 的副本,重命名为 3)将导致:2203 步、1322 步、425 步。

这个版本:

  • 第一场比赛:67 步
  • 匹配失败:1 步(再次由于对象之间的换行符)
  • 第二场比赛:72 步
  • 匹配失败:1 步
  • 第三场比赛:67 步

【讨论】:

  • 这是一个很好的方法:)。我建议将[^\n]+ 更改为.+(或.*,具体取决于您的需要)。
  • @Lindrian:是的,[^x]+x 往往是我在疲倦模式下的默认设置,确保我避免任何意外的贪婪。
猜你喜欢
  • 1970-01-01
  • 2013-08-09
  • 2013-03-19
  • 2013-03-17
  • 2016-12-17
  • 1970-01-01
  • 1970-01-01
  • 2015-05-21
  • 2014-02-14
相关资源
最近更新 更多