失控的正则表达式:灾难性的回溯失控的正则表达式:灾难性的回溯失控的正则表达式:灾难性的回溯失控的正则表达式:灾难性的回溯
  • 文章
  • 正则表达式
    • 工具
  • 登录
找到的结果: {phrase} (显示: {results_count} 共: {results_count_total})
显示: {results_count} 共: {results_count_total}

加载更多搜索结果...

搜索范围
模糊匹配
搜索标题
搜索内容
发表 admin at 2024年3月5日
类别
  • 正则表达式
标签
失控的正则表达式:灾难性的回溯
  • 简
  • 繁
  • En
关于正则表达式 » 正则表达式范例 » 失控的正则表达式:灾难性的回溯

范例
正则表达式范例
数字范围
浮点数
电子邮件地址
IP 地址
有效的日期
数字日期转为文本
信用卡号码
比对完整行
删除重复行
编程
两个相近的字词
陷阱
灾难性的回溯
过多的重复
拒绝服务
让所有内容变成可选
重复的捕获组
混合 Unicode 和 8 比特
本网站更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式范例
正则表达式参考
替换字符串参考

失控的正则表达式:灾难性的回溯

考虑正则表达式 (x+x+)+y。在您惊声尖叫并说这个人为范例应该写成 xx+y 或 x{2,}y 以完全比对相同内容,而不需要那些可怕的嵌套量词:假设每个「x」都代表更复杂的内容,某些字符串会同时被「x」比对到。请参阅下方 HTML 文件区段以取得实际范例。

让我们看看将这个正则表达式套用至 xxxxxxxxxxy 时会发生什么事。第一个 x+ 将比对到所有 10 个 x 字符。第二个 x+ 失败。第一个 x+ 接着回溯至 9 个比对,而第二个则截取剩余的 x。群组现在已比对一次。群组重复,但在第一个 x+ 失败。由于一次重复就已足够,因此群组比对成功。y 比对到 y,并找到整体比对。正则表达式声明功能正常,代码运送至客户,而他的电脑爆炸了。几乎如此。

当主旨字符串中缺少 y 时,上述正则表达式会变得丑陋。当 y 失败时,正则表达式引擎会回溯。群组有一个可以回溯的迭代。第二个 x+ 仅匹配一个 x,因此它无法回溯。但第一个 x+ 可以放弃一个 x。第二个 x+ 会立即匹配 xx。群组再次有一个迭代,失败下一个,而 y 失败。再次回溯,第二个 x+ 现在有一个回溯位置,将其缩小为匹配 x。群组尝试第二次迭代。第一个 x+ 匹配,但第二个卡在字符串结尾。再次回溯,群组第一次迭代中的第一个 x+ 将其缩小为 7 个字符。第二个 x+ 匹配 xxx。失败 y,第二个 x+ 缩小为 xx,然后 x。现在,群组可以匹配第二次迭代,每个 x+ 有 x。但这个 (7,1),(1,1) 组合也失败了。因此,它会转到 (6,4),然后 (6,2)(1,1),然后 (6,1),(2,1),然后 (6,1),(1,2),然后我想你开始掌握要领了。

如果你在某些 Regex 的调试器 中对 10x 字符串尝试这个正则表达式,它可能需要 2558 个步骤才能找出最后一个 y 遗失。对于 11x 字符串,它需要 5118 个步骤。对于 12,它需要 10238 个步骤。显然,我们这里有 O(2^n) 的指数复杂度。在 19x 时,调试器会退出,因为它不会显示超过一百万个步骤。

某些 Regex 的调试器很宽容,因为它们会侦测到它正在绕圈圈,并中止匹配尝试。其他正则表达式引擎(例如 .NET)会一直运行下去,而其他引擎会因堆栈溢出而崩溃(例如 Perl,在 5.10 版之前)。在 Windows 上,堆栈溢出特别令人讨厌,因为它们往往会让你的应用程序消失,而没有任何痕迹或解释。如果你运行允许用户提供自己的正则表达式的网络服务,请务必小心。正则表达式经验不足的人在提出指数复杂的正则表达式方面具有惊人的技巧。

所有格量词和原子组救援

在上述范例中,最明智的做法显然是将其改写为 xx+y,如此一来便能完全消除嵌套量词。嵌套量词是指在本身重复或交替的群组内部重复或交替的代币。这些几乎总是会导致灾难性的回溯。唯一不会造成回溯的情况是群组内部每个选项的开头都不是可选的,且与所有其他选项的开头互斥,以及与其后面的代币互斥(在群组内部其选项内)。例如 (a+b+|c+d+)+y 是安全的。如果任何项目失败,正则表达式引擎会回溯整个正则表达式,但会线性运行。原因在于所有代币都互斥。没有任何代币可以比对任何其他代币比对的字符。因此,在每个回溯位置的比对尝试都会失败,导致正则表达式引擎线性回溯。如果您在 aaaabbbbccccdddd 上测试这个正则表达式,Regex 引擎只需要 13 个步骤就能找出结果,而不是数百万个步骤。

然而,要改写正则表达式让所有项目都互斥并非总是可行或容易。因此,我们需要一种方法来告诉正则表达式引擎不要回溯。当我们取得所有 x 时,不需要回溯。在 x+ 比对的任何项目中都不可能会有 y。使用 所有格量词,我们的正则表达式会变成 (x+x+)++y。这会在仅仅 7 个步骤中让 21x 字符串失败。其中 6 个步骤用于比对所有 x,1 个步骤用于找出 y 失败。几乎没有进行回溯。使用 原子组,正则表达式会变成 (?>(x+x+)+)y,结果完全相同。

实际范例:比对 CSV 记录

以下是我曾经处理过的一个技术支持案例的实际范例。客户尝试在一个逗号分隔文本文件中找出第 12 个项目以 P 开头的行。他使用看似无害的正则表达式 ^(.*?,){11}P。

乍看之下,这个正则表达式似乎可以顺利完成任务。懒惰点和逗号比对单一逗号分隔字段,而 {11} 会略过前 11 个字段。最后,P 会检查第 12 个字段是否确实以 P 开头。事实上,当第 12 个字段确实以 P 开头时,就会发生这种情况。

当第 12 个字段不以 P 开头时,问题就会浮现。假设字符串为 1,2,3,4,5,6,7,8,9,10,11,12,13。在那个时候,正则表达式引擎会回溯。它会回溯到 ^(.*?,){11} 已消耗 1,2,3,4,5,6,7,8,9,10,11 的位置,放弃逗号的最后一个比对。下一个代币再次为点。点比对逗号。点比对逗号!然而,逗号不比对第 12 个字段的 1,因此点会持续到 .*?, 的第 11 次反复已消耗 11,12,。你已经可以看到问题的根源:正则表达式的一部分(点)比对字段的内容也会比对分隔符(逗号)。由于重复两次({11} 内的星号),这会导致大量的回溯。

正则表达式引擎现在会检查第 13 个字段是否以 P 开头。它没有。由于第 13 个字段后没有逗号,正则表达式引擎无法再比对 .*?, 的第 11 次反复。但它不会就此放弃。它会回溯到第 10 次反复,将第 10 次反复的比对扩充为 10,11,。由于仍然没有 P,因此第 10 次反复扩充为 10,11,12,。再次到达字符串的结尾,同样的故事从第 9 次反复开始,随后将其扩充为 9,10,、9,10,11,、9,10,11,12,。但在每次扩充之间,都有更多可能性需要尝试。当第 9 次反复消耗 9,10, 时,第 10 次反复可以比对 11, 也可以比对 11,12,。引擎持续失败,回溯到第 8 次反复,再次尝试第 9、10 和 11 次反复的所有可能组合。

你应该了解了:正则表达式引擎将尝试第 12 个字段不以 P 开头的每一行的组合数量非常庞大。如果你对一个大型 CSV 文件运行这个正则表达式,而大多数列的第 12 个字段开头都没有 P,这将会花费很长的时间。

防止灾难性的回溯

解决方案很简单。在嵌套重复操作符时,务必确保只有一个方式可以匹配相同的匹配。如果重复内部循环 4 次和外部循环 7 次会产生与重复内部循环 6 次和外部循环 2 次相同的整体匹配,则可以确定正则表达式引擎会尝试所有这些组合。

在我们的范例中,解决方案是更精确地说明我们要匹配的内容。我们要匹配 11 个以逗号分隔的字段。字段不得包含逗号。因此,正则表达式变为:^([^,\r\n]*,){11}P。如果找不到 P,引擎仍会回溯。但它只会回溯 11 次,而且每次[^,\r\n]都无法扩展到逗号以外,迫使正则表达式引擎立即回到 11 次反复运算中的前一次,而不尝试进一步的选项。

使用原子组的替代解决方案

在上述范例中,我们可以通过更精确地指定我们要的内容,轻松地将回溯量减少到非常低的程度。但这种方式并非总是可行。在这种情况下,您应该使用原子组来防止正则表达式引擎进行回溯。

使用 原子组,上述正则表达式会变成 ^(?>(.*?,){11})P。一旦正则表达式引擎离开群组,(?>) 之间的所有内容都会被正则表达式引擎视为单一记号。由于整个群组只是一个记号,因此一旦正则表达式引擎找到群组的比对,就不会进行回溯。如果需要回溯,引擎必须回溯到群组之前的正则表达式记号(在我们的范例中为插入符号)。如果群组之前没有记号,正则表达式必须在字符串中的下一个位置重试整个正则表达式。

让我们看看 ^(?>(.*?,){11})P 如何套用于 1,2,3,4,5,6,7,8,9,10,11,12,13。插入符号会与字符串开头相符,而引擎会进入原子组。星号是延迟的,因此点号最初会被跳过。但逗号与 1 不相符,因此引擎会回溯至点号。没错:在此允许回溯。星号并非独占,且并未立即被原子组包围。也就是说,正则表达式引擎并未跨越原子组的右括号。点号与 1 相符,逗号也相符。 {11} 会导致进一步重复,直到原子组与 1,2,3,4,5,6,7,8,9,10,11, 相符。

现在,引擎会离开原子组。由于群组是原子的,因此所有回溯信息都会被舍弃,而群组现在被视为单一记号。引擎现在会尝试将 P 与第 12 个字段中的 1 相符。这会失败。

到目前为止,所有事情都与原始的麻烦正则表达式一样。现在出现了差异。 P 无法相符,因此引擎会回溯。但原子组让它忘记所有回溯位置。在字符串开头的相符尝试会立即失败。

这就是原子组和独占量词的用途:通过不允许回溯来提升效率。针对我们手边的问题,最有效的正则表达式会是 ^(?>((?>[^,\r\n]*),){11})P,因为星号的独占贪婪重复比延迟延迟点号还要快。如果可以使用独占量词,你可以通过撰写 ^(?>([^,\r\n]*+,){11})P 来减少杂讯。

如果你在 Regex 的调试器中测试最终解决方案,你会看到它需要相同的 24 个步骤来与 11 个字段相符。然后它会运行 1 个步骤来退出原子组并舍弃所有回溯信息。发现 P 无法相符仍然需要 1 个步骤。但由于原子组,回溯所有信息只需要 1 个步骤。

快速相符完整的 HTML 文件

另一个会发生灾难性回溯的常见情况是,当尝试比对「某个东西」后接「任何东西」后接「另一个东西」后接「任何东西」,其中使用惰性点号 .*?。越多的「任何东西」,回溯就越多。有时候,惰性点号只是惰性程序员的征兆。 ".*?" 不适合用来比对双引号字符串,因为你并不想允许引号之间出现任何东西。字符串不能有(未转义)嵌入式引号,所以 "[^"\r\n]*" 比较适合,而且在与较大的正则表达式结合时不会导致灾难性回溯。然而,有时候「任何东西」真的就是这样。问题在于「另一个东西」也符合「任何东西」的资格,让我们遇到真正的 x+x+ 情况。

假设你想要使用正则表达式来比对完整的 HTML 文件,并从文件中截取基本部分。如果你知道 HTML 文件的结构,撰写正则表达式 <html>.*?<head>.*?<title>.*?</title>.*?</head>.*?<body[^>]*>.*?</body>.*?</html> 非常容易。在打开「点号比对换行」或「单行」比对模式 的情况下,它会在有效的 HTML 文件上正常运作。

不幸的是,此正则表达式在缺少某些标签的 HTML 文件中无法顺利运作。最糟的情况是文件结尾缺少 </html> 标签。当 </html> 无法配对时,正则表达式引擎会回溯,放弃 </body>.*? 的配对。然后,它会进一步扩充 </body> 前面的惰性点,寻找 HTML 文件中的第二个关闭 </body> 标签。当这项动作失败时,引擎会放弃 <body[^>]*>.*?,并开始寻找第二个打开 <body[^>]*> 标签,一直到文件结尾。由于这项动作也会失败,因此引擎会继续寻找第二个关闭 head 标签、第二个关闭 title 标签等,一直到文件结尾。

如果您在 Regex 的调试器 中运行此正则表达式,输出结果看起来会像锯齿状。正则表达式会配对整个文件,后退一点,再次配对整个文件,再后退一点,再后退一点,再次配对所有内容等,直到 7 个 .*? 权杖中的每个权杖都到达文件结尾。结果是此正则表达式的最差情况复杂度为 N^7。如果您通过在结尾附加文本来将缺少 <html> 标签的 HTML 文件长度加倍,正则表达式将需要花费 128 倍 (2^7) 的时间才能找出 HTML 文件无效。这不像我们第一个范例的 2^N 复杂度那么惨烈,但对于较大的无效文件,性能会非常不可接受。

在这种情况下,我们知道正则表达式中的每个文本区块 (HTML 标签,用作分隔符号) 在有效的 HTML 文件中只会出现一次。这使得将每个惰性点 (分隔内容) 封装在原子组中变得非常容易。

<html>(?>.*?<head>)(?>.*?<title>)(?>.*?</title>)(?>.*?</head>)(?>.*?<body[^>]*>)
(?>.*?</body>).*?</html>
将在与原始正则表达式相同的步骤数中比对有效的 HTML 文件。优点是,它会在无效 HTML 文件中几乎与比对有效文件一样快地失败。当 </html> 无法比对时,正则表达式引擎会回溯,放弃比对最后一个惰性点。但是,之后就没有什么可以回溯的了。由于所有惰性点都在原子组中,因此正则表达式引擎已舍弃其回溯位置。群组的作用是「不要进一步扩充」的障碍。正则表达式引擎被迫立即声明失败。

您一定注意到每个原子组在惰性点之后还包含一个 HTML 标签。这一点至关重要。我们允许惰性点回溯,直到找到其匹配的 HTML 标签。例如,当 .*?</body> 正在处理 Last paragraph</p></body> 时,</ 正则表达式代币将匹配 </ 中的 </p>。但是,b 将无法匹配 p。在那个时候,正则表达式引擎将回溯并扩充惰性点以包含 </p>。由于正则表达式引擎尚未离开原子组,因此它可以在群组内自由回溯。一旦 </body> 匹配,正则表达式引擎离开原子组,它会舍弃惰性点的回溯位置。然后它将无法再扩充。

基本上,我们所做的是将一个重复的正则表达式代币(惰性点用于匹配 HTML 内容)与其后面的非重复正则表达式代币(文本 HTML 标签)绑定。由于任何内容,包括 HTML 标签,都可能出现在我们正则表达式中的 HTML 标签之间,因此我们无法使用否定字符类别来代替惰性点,以防止分隔 HTML 标签被匹配为 HTML 内容。但是,我们可以通过将每个惰性点与其后面的 HTML 标签组合成一个原子组来达成相同的结果。一旦 HTML 标签匹配,惰性点的匹配就会被锁定。这可确保惰性点永远不会匹配应该由正则表达式中的文本 HTML 标签匹配的 HTML 标签。

失控的正規表示式:災難性的回溯
  • 简
  • 繁
  • En
關於正規表示式 » 正規表示式範例 » 失控的正規表示式:災難性的回溯

範例
正規表示式範例
數字範圍
浮點數
電子郵件地址
IP 位址
有效的日期
數字日期轉為文字
信用卡號碼
比對完整行
刪除重複行
程式設計
兩個相近的字詞
陷阱
災難性的回溯
過多的重複
拒絕服務
讓所有內容變成可選
重複的擷取群組
混合 Unicode 和 8 位元
本網站更多內容
簡介
正規表示式快速入門
正規表示式教學
替換字串教學
應用程式和語言
正規表示式範例
正規表示式參考
替換字串參考

失控的正規表示式:災難性的回溯

考慮正規表示式 (x+x+)+y。在您驚聲尖叫並說這個人為範例應該寫成 xx+y 或 x{2,}y 以完全比對相同內容,而不需要那些可怕的巢狀量詞:假設每個「x」都代表更複雜的內容,某些字串會同時被「x」比對到。請參閱下方 HTML 檔案區段以取得實際範例。

讓我們看看將這個正規表示式套用至 xxxxxxxxxxy 時會發生什麼事。第一個 x+ 將比對到所有 10 個 x 字元。第二個 x+ 失敗。第一個 x+ 接著回溯至 9 個比對,而第二個則擷取剩餘的 x。群組現在已比對一次。群組重複,但在第一個 x+ 失敗。由於一次重複就已足夠,因此群組比對成功。y 比對到 y,並找到整體比對。正規表示式宣告功能正常,程式碼運送至客戶,而他的電腦爆炸了。幾乎如此。

當主旨字串中缺少 y 時,上述正規表示式會變得醜陋。當 y 失敗時,正規表示式引擎會回溯。群組有一個可以回溯的迭代。第二個 x+ 僅匹配一個 x,因此它無法回溯。但第一個 x+ 可以放棄一個 x。第二個 x+ 會立即匹配 xx。群組再次有一個迭代,失敗下一個,而 y 失敗。再次回溯,第二個 x+ 現在有一個回溯位置,將其縮小為匹配 x。群組嘗試第二次迭代。第一個 x+ 匹配,但第二個卡在字串結尾。再次回溯,群組第一次迭代中的第一個 x+ 將其縮小為 7 個字元。第二個 x+ 匹配 xxx。失敗 y,第二個 x+ 縮小為 xx,然後 x。現在,群組可以匹配第二次迭代,每個 x+ 有 x。但這個 (7,1),(1,1) 組合也失敗了。因此,它會轉到 (6,4),然後 (6,2)(1,1),然後 (6,1),(2,1),然後 (6,1),(1,2),然後我想你開始掌握要領了。

如果你在某些 Regex 的偵錯程式 中對 10x 字串嘗試這個正規表示式,它可能需要 2558 個步驟才能找出最後一個 y 遺失。對於 11x 字串,它需要 5118 個步驟。對於 12,它需要 10238 個步驟。顯然,我們這裡有 O(2^n) 的指數複雜度。在 19x 時,偵錯程式會退出,因為它不會顯示超過一百萬個步驟。

某些 Regex 的偵錯程式很寬容,因為它们會偵測到它正在繞圈圈,並中止匹配嘗試。其他正規表示式引擎(例如 .NET)會一直執行下去,而其他引擎會因堆疊溢位而崩潰(例如 Perl,在 5.10 版之前)。在 Windows 上,堆疊溢位特別令人討厭,因為它們往往會讓你的應用程式消失,而沒有任何痕跡或解釋。如果你執行允許使用者提供自己的正規表示式的網路服務,請務必小心。正規表示式經驗不足的人在提出指數複雜的正規表示式方面具有驚人的技巧。

所有格量詞和原子群組救援

在上述範例中,最明智的做法顯然是將其改寫為 xx+y,如此一來便能完全消除巢狀量詞。巢狀量詞是指在本身重複或交替的群組內部重複或交替的代幣。這些幾乎總是會導致災難性的回溯。唯一不會造成回溯的情況是群組內部每個選項的開頭都不是可選的,且與所有其他選項的開頭互斥,以及與其後面的代幣互斥(在群組內部其選項內)。例如 (a+b+|c+d+)+y 是安全的。如果任何項目失敗,正規表示式引擎會回溯整個正規表示式,但會線性執行。原因在於所有代幣都互斥。沒有任何代幣可以比對任何其他代幣比對的字元。因此,在每個回溯位置的比對嘗試都會失敗,導致正規表示式引擎線性回溯。如果您在 aaaabbbbccccdddd 上測試這個正規表示式,Regex 引擎只需要 13 個步驟就能找出結果,而不是數百萬個步驟。

然而,要改寫正規表示式讓所有項目都互斥並非總是可行或容易。因此,我們需要一種方法來告訴正規表示式引擎不要回溯。當我們取得所有 x 時,不需要回溯。在 x+ 比對的任何項目中都不可能會有 y。使用 所有格量詞,我們的正規表示式會變成 (x+x+)++y。這會在僅僅 7 個步驟中讓 21x 字串失敗。其中 6 個步驟用於比對所有 x,1 個步驟用於找出 y 失敗。幾乎沒有進行回溯。使用 原子群組,正規表示式會變成 (?>(x+x+)+)y,結果完全相同。

實際範例:比對 CSV 記錄

以下是我曾經處理過的一個技術支援案例的實際範例。客戶嘗試在一個逗號分隔文字檔案中找出第 12 個項目以 P 開頭的行。他使用看似無害的正規表示式 ^(.*?,){11}P。

乍看之下,這個正規表示式似乎可以順利完成任務。懶惰點和逗號比對單一逗號分隔欄位,而 {11} 會略過前 11 個欄位。最後,P 會檢查第 12 個欄位是否確實以 P 開頭。事實上,當第 12 個欄位確實以 P 開頭時,就會發生這種情況。

當第 12 個欄位不以 P 開頭時,問題就會浮現。假設字串為 1,2,3,4,5,6,7,8,9,10,11,12,13。在那個時候,正規表示式引擎會回溯。它會回溯到 ^(.*?,){11} 已消耗 1,2,3,4,5,6,7,8,9,10,11 的位置,放棄逗號的最後一個比對。下一個代幣再次為點。點比對逗號。點比對逗號!然而,逗號不比對第 12 個欄位的 1,因此點會持續到 .*?, 的第 11 次反覆已消耗 11,12,。你已經可以看到問題的根源:正規表示式的一部分(點)比對欄位的內容也會比對分隔符(逗號)。由於重複兩次({11} 內的星號),這會導致大量的回溯。

正規表示式引擎現在會檢查第 13 個欄位是否以 P 開頭。它沒有。由於第 13 個欄位後沒有逗號,正規表示式引擎無法再比對 .*?, 的第 11 次反覆。但它不會就此放棄。它會回溯到第 10 次反覆,將第 10 次反覆的比對擴充為 10,11,。由於仍然沒有 P,因此第 10 次反覆擴充為 10,11,12,。再次到達字串的結尾,同樣的故事從第 9 次反覆開始,隨後將其擴充為 9,10,、9,10,11,、9,10,11,12,。但在每次擴充之間,都有更多可能性需要嘗試。當第 9 次反覆消耗 9,10, 時,第 10 次反覆可以比對 11, 也可以比對 11,12,。引擎持續失敗,回溯到第 8 次反覆,再次嘗試第 9、10 和 11 次反覆的所有可能組合。

你應該了解了:正規表示式引擎將嘗試第 12 個欄位不以 P 開頭的每一行的組合數量非常龐大。如果你對一個大型 CSV 檔案執行這個正規表示式,而大多數列的第 12 個欄位開頭都沒有 P,這將會花費很長的時間。

防止災難性的回溯

解決方案很簡單。在巢狀重複運算子時,務必確保只有一個方式可以匹配相同的匹配。如果重複內部迴圈 4 次和外部迴圈 7 次會產生與重複內部迴圈 6 次和外部迴圈 2 次相同的整體匹配,則可以確定正規表示式引擎會嘗試所有這些組合。

在我們的範例中,解決方案是更精確地說明我們要匹配的內容。我們要匹配 11 個以逗號分隔的欄位。欄位不得包含逗號。因此,正規表示式變為:^([^,\r\n]*,){11}P。如果找不到 P,引擎仍會回溯。但它只會回溯 11 次,而且每次[^,\r\n]都無法擴展到逗號以外,迫使正規表示式引擎立即回到 11 次反覆運算中的前一次,而不嘗試進一步的選項。

使用原子群組的替代解決方案

在上述範例中,我們可以透過更精確地指定我們要的內容,輕鬆地將回溯量減少到非常低的程度。但這種方式並非總是可行。在這種情況下,您應該使用原子群組來防止正規表示式引擎進行回溯。

使用 原子群組,上述正規表示式會變成 ^(?>(.*?,){11})P。一旦正規表示式引擎離開群組,(?>) 之間的所有內容都會被正規表示式引擎視為單一記號。由於整個群組只是一個記號,因此一旦正規表示式引擎找到群組的比對,就不會進行回溯。如果需要回溯,引擎必須回溯到群組之前的正規表示式記號(在我們的範例中為插入符號)。如果群組之前沒有記號,正規表示式必須在字串中的下一個位置重試整個正規表示式。

讓我們看看 ^(?>(.*?,){11})P 如何套用於 1,2,3,4,5,6,7,8,9,10,11,12,13。插入符號會與字串開頭相符,而引擎會進入原子群組。星號是延遲的,因此點號最初會被跳過。但逗號與 1 不相符,因此引擎會回溯至點號。沒錯:在此允許回溯。星號並非獨佔,且並未立即被原子群組包圍。也就是說,正規表示式引擎並未跨越原子群組的右括號。點號與 1 相符,逗號也相符。 {11} 會導致進一步重複,直到原子群組與 1,2,3,4,5,6,7,8,9,10,11, 相符。

現在,引擎會離開原子群組。由於群組是原子的,因此所有回溯資訊都會被捨棄,而群組現在被視為單一記號。引擎現在會嘗試將 P 與第 12 個欄位中的 1 相符。這會失敗。

到目前為止,所有事情都與原始的麻煩正規表示式一樣。現在出現了差異。 P 無法相符,因此引擎會回溯。但原子群組讓它忘記所有回溯位置。在字串開頭的相符嘗試會立即失敗。

這就是原子群組和獨佔量詞的用途:透過不允許回溯來提升效率。針對我們手邊的問題,最有效的正規表示式會是 ^(?>((?>[^,\r\n]*),){11})P,因為星號的獨佔貪婪重複比延遲延遲點號還要快。如果可以使用獨佔量詞,你可以透過撰寫 ^(?>([^,\r\n]*+,){11})P 來減少雜訊。

如果你在 Regex 的偵錯程式中測試最終解決方案,你會看到它需要相同的 24 個步驟來與 11 個欄位相符。然後它會執行 1 個步驟來退出原子群組並捨棄所有回溯資訊。發現 P 無法相符仍然需要 1 個步驟。但由於原子群組,回溯所有資訊只需要 1 個步驟。

快速相符完整的 HTML 檔案

另一個會發生災難性回溯的常見情況是,當嘗試比對「某個東西」後接「任何東西」後接「另一個東西」後接「任何東西」,其中使用惰性點號 .*?。越多的「任何東西」,回溯就越多。有時候,惰性點號只是惰性程式設計師的徵兆。 ".*?" 不適合用來比對雙引號字串,因為你並不想允許引號之間出現任何東西。字串不能有(未跳脫)嵌入式引號,所以 "[^"\r\n]*" 比較適合,而且在與較大的正規表示式結合時不會導致災難性回溯。然而,有時候「任何東西」真的就是這樣。問題在於「另一個東西」也符合「任何東西」的資格,讓我們遇到真正的 x+x+ 情況。

假設你想要使用正規表示式來比對完整的 HTML 檔案,並從檔案中擷取基本部分。如果你知道 HTML 檔案的結構,撰寫正規表示式 <html>.*?<head>.*?<title>.*?</title>.*?</head>.*?<body[^>]*>.*?</body>.*?</html> 非常容易。在開啟「點號比對換行」或「單行」比對模式 的情況下,它會在有效的 HTML 檔案上正常運作。

不幸的是,此正規表示式在缺少某些標籤的 HTML 檔案中無法順利運作。最糟的情況是檔案結尾缺少 </html> 標籤。當 </html> 無法配對時,正規表示式引擎會回溯,放棄 </body>.*? 的配對。然後,它會進一步擴充 </body> 前面的惰性點,尋找 HTML 檔案中的第二個關閉 </body> 標籤。當這項動作失敗時,引擎會放棄 <body[^>]*>.*?,並開始尋找第二個開啟 <body[^>]*> 標籤,一直到檔案結尾。由於這項動作也會失敗,因此引擎會繼續尋找第二個關閉 head 標籤、第二個關閉 title 標籤等,一直到檔案結尾。

如果您在 Regex 的偵錯程式 中執行此正規表示式,輸出結果看起來會像鋸齒狀。正規表示式會配對整個檔案,後退一點,再次配對整個檔案,再後退一點,再後退一點,再次配對所有內容等,直到 7 個 .*? 權杖中的每個權杖都到達檔案結尾。結果是此正規表示式的最差情況複雜度為 N^7。如果您透過在結尾附加文字來將缺少 <html> 標籤的 HTML 檔案長度加倍,正規表示式將需要花費 128 倍 (2^7) 的時間才能找出 HTML 檔案無效。這不像我們第一個範例的 2^N 複雜度那麼慘烈,但對於較大的無效檔案,效能會非常不可接受。

在這種情況下,我們知道正規表示式中的每個文字區塊 (HTML 標籤,用作分隔符號) 在有效的 HTML 檔案中只會出現一次。這使得將每個惰性點 (分隔內容) 封裝在原子群組中變得非常容易。

<html>(?>.*?<head>)(?>.*?<title>)(?>.*?</title>)(?>.*?</head>)(?>.*?<body[^>]*>)
(?>.*?</body>).*?</html>
將在與原始正規表示式相同的步驟數中比對有效的 HTML 檔案。優點是,它會在無效 HTML 檔案中幾乎與比對有效檔案一樣快地失敗。當 </html> 無法比對時,正規表示式引擎會回溯,放棄比對最後一個惰性點。但是,之後就沒有什麼可以回溯的了。由於所有惰性點都在原子群組中,因此正規表示式引擎已捨棄其回溯位置。群組的作用是「不要進一步擴充」的障礙。正規表示式引擎被迫立即宣告失敗。

您一定注意到每個原子群組在惰性點之後還包含一個 HTML 標籤。這一點至關重要。我們允許惰性點回溯,直到找到其匹配的 HTML 標籤。例如,當 .*?</body> 正在處理 Last paragraph</p></body> 時,</ 正規表示式代幣將匹配 </ 中的 </p>。但是,b 將無法匹配 p。在那個時候,正規表示式引擎將回溯並擴充惰性點以包含 </p>。由於正規表示式引擎尚未離開原子群組,因此它可以在群組內自由回溯。一旦 </body> 匹配,正規表示式引擎離開原子群組,它會捨棄惰性點的回溯位置。然後它將無法再擴充。

基本上,我們所做的是將一個重複的正規表示式代幣(惰性點用於匹配 HTML 內容)與其後面的非重複正規表示式代幣(文字 HTML 標籤)繫結。由於任何內容,包括 HTML 標籤,都可能出現在我們正規表示式中的 HTML 標籤之間,因此我們無法使用否定字元類別來代替惰性點,以防止分隔 HTML 標籤被匹配為 HTML 內容。但是,我們可以透過將每個惰性點與其後面的 HTML 標籤組合成一個原子群組來達成相同的結果。一旦 HTML 標籤匹配,惰性點的匹配就會被鎖定。這可確保惰性點永遠不會匹配應該由正規表示式中的文字 HTML 標籤匹配的 HTML 標籤。

Runaway Regular Expressions: Catastrophic Backtracking
  • 简
  • 繁
  • En
About Regular Expressions » Sample Regular Expressions » Runaway Regular Expressions: Catastrophic Backtracking

Examples
Regular Expressions Examples
Numeric Ranges
Floating Point Numbers
Email Addresses
IP Addresses
Valid Dates
Numeric Dates to Text
Credit Card Numbers
Matching Complete Lines
Deleting Duplicate Lines
Programming
Two Near Words
Pitfalls
Catastrophic Backtracking
Too Many Repetitions
Denial of Service
Making Everything Optional
Repeated Capturing Group
Mixing Unicode & 8-bit
More on This Site
Introduction
Regular Expressions Quick Start
Regular Expressions Tutorial
Replacement Strings Tutorial
Applications and Languages
Regular Expressions Examples
Regular Expressions Reference
Replacement Strings Reference

Runaway Regular Expressions: Catastrophic Backtracking

Consider the regular expression (x+x+)+y. Before you scream in horror and say this contrived example should be written as xx+y or x{2,}y to match exactly the same without those terribly nested quantifiers: just assume that each “x” represents something more complex, with certain strings being matched by both “x”. See the section on HTML files below for a real example.

Let’s see what happens when you apply this regex to xxxxxxxxxxy. The first x+ will match all 10 x characters. The second x+ fails. The first x+ then backtracks to 9 matches, and the second one picks up the remaining x. The group has now matched once. The group repeats, but fails at the first x+. Since one repetition was sufficient, the group matches. y matches y and an overall match is found. The regex is declared functional, the code is shipped to the customer, and his computer explodes. Almost.

The above regex turns ugly when the y is missing from the subject string. When y fails, the regex engine backtracks. The group has one iteration it can backtrack into. The second x+ matched only one x, so it can’t backtrack. But the first x+ can give up one x. The second x+ promptly matches xx. The group again has one iteration, fails the next one, and the y fails. Backtracking again, the second x+ now has one backtracking position, reducing itself to match x. The group tries a second iteration. The first x+ matches but the second is stuck at the end of the string. Backtracking again, the first x+ in the group’s first iteration reduces itself to 7 characters. The second x+ matches xxx. Failing y, the second x+ is reduced to xx and then x. Now, the group can match a second iteration, with one x for each x+. But this (7,1),(1,1) combination fails too. So it goes to (6,4) and then (6,2)(1,1) and then (6,1),(2,1) and then (6,1),(1,2) and then I think you start to get the drift.

If you try this regex on a 10x string in a particular Regex debug tool, it may need to take 2558 steps to figure out the final y is missing. For an 11x string, it needs 5118 steps. For 12, it takes 10238 steps. Clearly we have an exponential complexity of O(2^n) here. At 19x the debugger bows out because it won't show more than one million steps.

Some regex engines are forgiving in that it detects it’s going in circles and aborts the match attempt. Other regex engines (like .NET) will keep going forever, while others will crash with a stack overflow (like Perl, before version 5.10). Stack overflows are particularly nasty on Windows, since they tend to make your application vanish without a trace or explanation. Be very careful if you run a web service that allows users to supply their own regular expressions. People with little regex experience have surprising skill at coming up with exponentially complex regular expressions.

Possessive Quantifiers and Atomic Grouping to The Rescue

In the above example, the sane thing to do is obviously to rewrite it as xx+y which eliminates the nested quantifiers entirely. Nested quantifiers are repeated or alternated tokens inside a group that is itself repeated or alternated. These almost always lead to catastrophic backtracking. About the only situation where they don’t is when the start of each alternative inside the group is not optional, and mutually exclusive with the start of all the other alternatives, and mutually exclusive with the token that follows it (inside its alternative inside the group). E.g. (a+b+|c+d+)+y is safe. If anything fails, the regex engine will backtrack through the whole regex, but it will do so linearly. The reason is that all the tokens are mutually exclusive. None of them can match any characters matched by any of the others. So the match attempt at each backtracking position will fail, causing the regex engine to backtrack linearly. If you test this on aaaabbbbccccdddd, regex engine needs only 13 steps rather than millions of steps to figure it out.

However, it’s not always possible or easy to rewrite your regex to make everything mutually exclusive. So we need a way to tell the regex engine not to backtrack. When we’ve grabbed all the x’s, there’s no need to backtrack. There couldn’t possibly be a y in anything matched by either x+. Using a possessive quantifier, our regex becomes (x+x+)++y. This fails the 21x string in merely 7 steps. That’s 6 steps to match all the x’s, and 1 step to figure out that y fails. Almost no backtracking is done. Using an atomic group, the regex becomes (?>(x+x+)+)y with the exact same results.

A Real Example: Matching CSV Records

Here’s a real example from a technical support case I once handled. The customer was trying to find lines in a comma-delimited text file where the 12th item on a line started with a P. He was using the innocently-looking regexp ^(.*?,){11}P.

At first sight, this regex looks like it should do the job just fine. The lazy dot and comma match a single comma-delimited field, and the {11} skips the first 11 fields. Finally, the P checks if the 12th field indeed starts with P. In fact, this is exactly what will happen when the 12th field indeed starts with a P.

The problem rears its ugly head when the 12th field does not start with a P. Let’s say the string is 1,2,3,4,5,6,7,8,9,10,11,12,13. At that point, the regex engine will backtrack. It will backtrack to the point where ^(.*?,){11} had consumed 1,2,3,4,5,6,7,8,9,10,11, giving up the last match of the comma. The next token is again the dot. The dot matches a comma. The dot matches the comma! However, the comma does not match the 1 in the 12th field, so the dot continues until the 11th iteration of .*?, has consumed 11,12,. You can already see the root of the problem: the part of the regex (the dot) matching the contents of the field also matches the delimiter (the comma). Because of the double repetition (star inside {11}), this leads to a catastrophic amount of backtracking.

The regex engine now checks whether the 13th field starts with a P. It does not. Since there is no comma after the 13th field, the regex engine can no longer match the 11th iteration of .*?,. But it does not give up there. It backtracks to the 10th iteration, expanding the match of the 10th iteration to 10,11,. Since there is still no P, the 10th iteration is expanded to 10,11,12,. Reaching the end of the string again, the same story starts with the 9th iteration, subsequently expanding it to 9,10,, 9,10,11,, 9,10,11,12,. But between each expansion, there are more possibilities to be tried. When the 9th iteration consumes 9,10,, the 10th could match just 11, as well as 11,12,. Continuously failing, the engine backtracks to the 8th iteration, again trying all possible combinations for the 9th, 10th, and 11th iterations.

You get the idea: the possible number of combinations that the regex engine will try for each line where the 12th field does not start with a P is huge. All this would take a long time if you ran this regex on a large CSV file where most rows don’t have a P at the start of the 12th field.

Preventing Catastrophic Backtracking

The solution is simple. When nesting repetition operators, make absolutely sure that there is only one way to match the same match. If repeating the inner loop 4 times and the outer loop 7 times results in the same overall match as repeating the inner loop 6 times and the outer loop 2 times, you can be sure that the regex engine will try all those combinations.

In our example, the solution is to be more exact about what we want to match. We want to match 11 comma-delimited fields. The fields must not contain comma’s. So the regex becomes: ^([^,\r\n]*,){11}P. If the P cannot be found, the engine will still backtrack. But it will backtrack only 11 times, and each time the [^,\r\n] is not able to expand beyond the comma, forcing the regex engine to the previous one of the 11 iterations immediately, without trying further options.

Alternative Solution Using Atomic Grouping

In the above example, we could easily reduce the amount of backtracking to a very low level by better specifying what we wanted. But that is not always possible in such a straightforward manner. In that case, you should use atomic grouping to prevent the regex engine from backtracking.

Using atomic grouping, the above regex becomes ^(?>(.*?,){11})P. Everything between (?>) is treated as one single token by the regex engine, once the regex engine leaves the group. Because the entire group is one token, no backtracking can take place once the regex engine has found a match for the group. If backtracking is required, the engine has to backtrack to the regex token before the group (the caret in our example). If there is no token before the group, the regex must retry the entire regex at the next position in the string.

Let’s see how ^(?>(.*?,){11})P is applied to 1,2,3,4,5,6,7,8,9,10,11,12,13. The caret matches at the start of the string and the engine enters the atomic group. The star is lazy, so the dot is initially skipped. But the comma does not match 1, so the engine backtracks to the dot. That’s right: backtracking is allowed here. The star is not possessive, and is not immediately enclosed by an atomic group. That is, the regex engine did not cross the closing parenthesis of the atomic group. The dot matches 1, and the comma matches too. {11} causes further repetition until the atomic group has matched 1,2,3,4,5,6,7,8,9,10,11,.

Now, the engine leaves the atomic group. Because the group is atomic, all backtracking information is discarded and the group is now considered a single token. The engine now tries to match P to the 1 in the 12th field. This fails.

So far, everything happened just like in the original, troublesome regular expression. Now comes the difference. P fails to match, so the engine backtracks. But the atomic group made it forget all backtracking positions. The match attempt at the start of the string fails immediately.

That is what atomic grouping and possessive quantifiers are for: efficiency by disallowing backtracking. The most efficient regex for our problem at hand would be ^(?>((?>[^,\r\n]*),){11})P, since possessive, greedy repetition of the star is faster than a backtracking lazy dot. If possessive quantifiers are available, you can reduce clutter by writing ^(?>([^,\r\n]*+,){11})P.

If you test the final solution in regex debugger, you’ll see that it needs the same 24 steps to match the 11 fields. Then it takes 1 step to exit the atomic group and throw away all the backtracking information. Discovering that P can’t be matched still takes one step. But because of the atomic group, backtracking it all takes only a single step.

Quickly Matching a Complete HTML File

Another common situation where catastrophic backtracking occurs is when trying to match “something” followed by “anything” followed by “another something” followed by “anything”, where the lazy dot .*? is used. The more “anything”, the more backtracking. Sometimes, the lazy dot is simply a symptom of a lazy programmer. ".*?" is not appropriate to match a double-quoted string, since you don’t really want to allow anything between the quotes. A string can’t have (unescaped) embedded quotes, so "[^"\r\n]*" is more appropriate, and won’t lead to catastrophic backtracking when combined in a larger regular expression. However, sometimes “anything” really is just that. The problem is that “another something” also qualifies as “anything”, giving us a genuine x+x+ situation.

Suppose you want to use a regular expression to match a complete HTML file, and extract the basic parts from the file. If you know the structure of HTML files, it’s very straightforward to write the regular expression <html>.*?<head>.*?<title>.*?</title>.*?</head>.*?<body[^>]*>.*?</body>.*?</html>. With the “dot matches newlines” or “single line” matching mode turned on, it will work just fine on valid HTML files.

Unfortunately, this regular expression won’t work nearly as well on an HTML file that misses some of the tags. The worst case is a missing </html> tag at the end of the file. When </html> fails to match, the regex engine backtracks, giving up the match for </body>.*?. It will then further expand the lazy dot before </body>, looking for a second closing </body> tag in the HTML file. When that fails, the engine gives up <body[^>]*>.*?, and starts looking for a second opening <body[^>]*> tag all the way to the end of the file. Since that also fails, the engine proceeds looking all the way to the end of the file for a second closing head tag, a second closing title tag, etc.

If you run this regex in regex debugger, the output will look like a sawtooth. The regex matches the whole file, backs up a little, matches the whole file again, backs up some more, backs up yet some more, matches everything again, etc. until each of the 7 .*? tokens has reached the end of the file. The result is that this regular has a worst case complexity of N^7. If you double the length of the HTML file with the missing <html> tag by appending text at the end, the regular expression will take 128 times (2^7) as long to figure out the HTML file isn’t valid. This isn’t quite as disastrous as the 2^N complexity of our first example, but will lead to very unacceptable performance on larger invalid files.

In this situation, we know that each of the literal text blocks in our regular expression (the HTML tags, which function as delimiters) will occur only once in a valid HTML file. That makes it very easy to package each of the lazy dots (the delimited content) in an atomic group.

<html>(?>.*?<head>)(?>.*?<title>)(?>.*?</title>)(?>.*?</head>)(?>.*?<body[^>]*>)
(?>.*?</body>).*?</html>
will match a valid HTML file in the same number of steps as the original regex. The gain is that it will fail on an invalid HTML file almost as fast as it matches a valid one. When </html> fails to match, the regex engine backtracks, giving up the match for the last lazy dot. But then, there’s nothing further to backtrack to. Since all of the lazy dots are in an atomic group, the regex engines has discarded their backtracking positions. The groups function as a “do not expand further” roadblock. The regex engine is forced to announce failure immediately.

You’ve no doubt noticed that each atomic group also contains an HTML tag after the lazy dot. This is critical. We do allow the lazy dot to backtrack until its matching HTML tag was found. E.g. when .*?</body> is processing Last paragraph</p></body>, the </ regex tokens will match </ in </p>. However, b will fail p. At that point, the regex engine will backtrack and expand the lazy dot to include </p>. Since the regex engine hasn’t left the atomic group yet, it is free to backtrack inside the group. Once </body> has matched, and the regex engine leaves the atomic group, it discards the lazy dot’s backtracking positions. Then it can no longer be expanded.

Essentially, what we’ve done is to bind a repeated regex token (the lazy dot to match HTML content) to the non-repeated regex token that follows it (the literal HTML tag). Since anything, including HTML tags, can appear between the HTML tags in our regular expression, we cannot use a negated character class instead of the lazy dot to prevent the delimiting HTML tags from being matched as HTML content. But we can and did achieve the same result by combining each lazy dot and the HTML tag following it into an atomic group. As soon as the HTML tag is matched, the lazy dot’s match is locked down. This ensures that the lazy dot will never match the HTML tag that should be matched by the literal HTML tag in the regular expression.

©2015-2025 艾丽卡 support@alaica.com