《正则表达式经典实例(第2版)》——2.15 避免失控重复

2.15 避免失控重复

问题描述
使用一个正则表达式来匹配一个完整的HTML文件,并检查其中的html、head、title和body标签是否进行了正确嵌套。当HTML文件中不拥有正确标签的时候,该正则表达式必须能够高效地宣布匹配失败。

解决方案

<html>(?>.*?<head>)(?>.*?<title>)(?>.*?</title>)
(?>.*?</head>)(?>.*?<body[^>]*>)(?>.*?</body>).*?</html>
正则选项:不区分大小写、点号匹配换行符
正则流派:.NET、Java、PCRE、Perl、Ruby

JavaScript和Python不支持固化分组。因此在这两种正则流派中无法消除不必要的回溯。当使用JavaScript或者Python编程时,可以通过对这些标签一一进行字面文本查找来解决这个问题,在找到一个标签之后,再在剩余的目标文本中查找下一个标签。

讨论
从下面这个最原始的解答入手,对这个问题的正确解答会更加容易理解:

<html>.*?<head>.*?<title>.*?</title>↵
.*?</head>.*?<body[^>]*>.*?</body>.*?</html>
正则选项:不区分大小写、点号匹配换行符
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby

如果你在一个规范正确的HTML文件上测试这个正则表达式,它会完全正常地运行。‹.*?›会略过所有的内容,因为我们打开了“点号匹配换行符”的选项。懒惰的星号量词会确保这个正则表达式一次只会前进一个字符,每次都会检查是否匹配到了下一个标签。实例2.4和实例2.13中已经讲解过这一切。

但是,如果这个正则表达式需要处理并不包含所有这些HTML标签的目标文本,你就遇到麻烦了。最坏的情形是当</html>缺失的时候。

设想,正则引擎已经匹配了所有前面的标签,现在正在忙着处理最后‹.?›的匹配。因为‹</html>›永远无法匹配,所以‹.?›会一直匹配到文件的结尾。现在它无法匹配更多字符,所以宣布匹配失败。

但是这还不是故事的结束。另外6个‹.?›都记住了一个回溯位置,因此会允许它们进一步扩展。当最后一个‹.?›匹配失败的时候,它前面的那个会进行扩展,逐步匹配到< /body >。该标签之前被正则表达式中的字面量‹</body>›匹配。这个‹.?›会一直扩展到文件的结尾,在它之前所有的‹.?›惰性点号量词也同样会这样做。只有当第一个到达了文件末尾的时候,正则引擎才会宣布匹配失败。

这个正则表达式拥有最坏情形的复杂度1FFO(n7),也就是目标文本长度的7次方。其中包含7个惰性点号量词,每一个都可能尝试匹配到文件的结尾。如果文件的大小增加一倍,正则表达式就可能需要增加128倍的处理步骤才能计算出它无法匹配。

我们把这种情形称作灾难性回溯(catastrophic backtracking)。由于出现了太多的回溯,正则表达式会无休止的匹配下去或者让你的应用程序崩溃。一些正则表达式实现会比较聪明,可能会及早退出这种失控的匹配尝试,但是即使在这种情况下,正则表达式也还是会严重影响程序的性能。

灾难回溯是一种被称作组合爆炸(combinatorial explosion)的现象的一种实例,其中几种正交条件交织起来,不得不尝试所有的组合。你也可以说正则表达式是不同重复操作符的笛卡尔乘积。
解决方案是使用固化分组来避免不必要的回溯。在‹< /body >›匹配成功之后,第6个‹.*?›就没有必要进行扩展。如果‹< /html >›匹配失败的话,那么扩展第6个惰性点号也不可能神奇地变出一个html终止标签。

要想使一个量词化的正则表达式记号在紧随其后的分界符匹配之后停止扩展,就需要把正则表达式的量词部分与分界符一起放到一个固化分组中:‹(?>.? body >)›。这样正则引擎就会在找到‹ body >›之后丢弃‹.? body >›所有的匹配位置。如果‹ html >›随后匹配失败的话,因为正则引擎已经忘记了‹.*? body >›的回溯位置,所以不会进行任何扩展。

如果我们对正则表达式中所有其他的‹.*?›都做同样的操作,那么它们就都不会再继续扩展。虽然在正则表达式中还是存在7个惰性点号,但是它们永远也不会产生交叉。这样就会把正则表达式的复杂度降低到O(n),它与目标文本的长度之间是线性关系。而正则表达式的效率不可能比这更高了。

变体
如果你确实想知道灾难性回溯如何执行,那么可以在xxxxxxxxxx之上测试一下‹(x+x+)+y›。如果它很快就会失败,那么在目标文本中再添加一个x。重复这个过程,直到正则表达式要花费很长的时间来完成匹配或者你的应用程序因此崩溃。除非你使用的是Perl,否则并不需要添加太多的x字符就会出问题。

在本书中讨论的所有正则流派中,只有Perl有能力检测过分复杂的正则表达式,并且它会终止匹配尝试,而不会造成程序崩溃。

这个正则表达式的复杂度是O(2n)。当‹y›匹配失败的时候,正则引擎会尝试所有可能的排列组合,重复每个‹x+›以及包含它们的分组。例如,在尝试的过程后期,会出现一个这样的排列:‹x+›匹配xxx,第二个‹x+›匹配x,然后接着这个分组会被重复3次,其中每个‹x+›匹配x。如果存在10个x字符的话,就会存在1024种组合。如果把这个数量加到32,那么我们就会要处理超过40亿种组合,这肯定会让任何一种正则引擎耗尽内存,除非它包含一个安全开关,能够自己放弃,并且宣布你的正则表达式过于复杂。

该例中,这个没有多大意义的正则表达式可以很容易被重写为‹xx+y›,这样它就可以在线性时间内找到完全一样的匹配。在实践中,对于更加复杂的正则表达式可能就不会找到这样显而易见的解决方案了。

本质上来说,必须要小心某个正则表达式的两个或者更多个部分会匹配相同文本的情形。在这些情形中,可能会需要固化分组来确保正则引擎不会去尝试所有的方式以把目标文本根据正则表达式的这两个部分进行分割。在测试你的正则表达式时,应该总要使用包含部分可以匹配,但是又不能整体被正则表达式匹配的(较长)测试目标文本。

时间: 2024-10-28 10:08:40

《正则表达式经典实例(第2版)》——2.15 避免失控重复的相关文章

《正则表达式经典实例(第2版)》——导读

** 前言 **正则表达式在过去十多年间越来越普及.如今,所有常用的编程语言都会包含一个强大的正则表达式函数库,甚至在语言本身就内嵌了对于正则表达式的支持.许多开发人员都会利用这些正则表达式的功能,在应用程序中为用户提供使用正则表达式对其数据进行查找或者过滤的能力.正则表达式已经无处不在.随着正则表达式的广泛采用,出现了许多相关的著作.其中大多数都很好地讲解了正则表达式的语法,并且还会提供一些例子以及参考实现.然而,我们还没有看到有任何一本书能够针对处理计算机和不同因特网应用上的文本所遇到的各种

《正则表达式经典实例(第2版)》——1.3 正则表达式工具

1.3 正则表达式工具 除非已经拥有了相当长的使用正则表达式编程的经验,否则建议你先在一个工具中试验一下正则表达式,而不是直接在源代码中使用它们.本章和第2章中提供的正则表达式示例都是原始正则表达式,其中并不包含编程语言(即使是Unlx shell)所必需的额外的转义符号.因此可以直接把这些正则表达式输入到一个应用程序的查找框中. 第3章讲解如何把正则表达式整合到源代码中.把一个字面的(literal)正则表达式作为一个字符串引用,会让它更加难懂,因为字符串的转义规则会与正则表达式的转义规则混杂

《正则表达式经典实例(第2版)》——2.2 匹配不可打印字符

2.2 匹配不可打印字符 问题描述 匹配一个包含下列ASCII控制字符的字符串:振铃符(bell).转义符(escape).换页符(form feed).换行符(line feed).回车符(carriage return).水平制表符(horizontal tab)和垂直制表符(vertical tab).这些字符的十六进制ASCII编码分别是07.1B.0C.0A.0D.09.0B. 下面演示了转义序列的用法,以及如何以十六进制编码代表字符. 解决方案 \a\e\f\n\r\t\v 正则选项

《正则表达式经典实例(第2版)》——第 1 章 正则表达式简介 1.1正则表达式的定义

第 1 章 正则表达式简介 在你打开这本书的时候,很可能已经热切地期望,要在代码中插入本书中找到的那些包含诸多括号和问号的古怪字符串了.如果你已经准备好要"即查即用",我们非常欢迎,第4-9章中会列出并讲解了各种实用的正则表达式. 但是如果阅读本书的前几章,你将为未来节省大量的时间.例如,本章会向读者介绍许多工具-其中一些工具是本书作者之一的Jan所开发的,这些工具可以帮你事先测试和调试正则表达式,而不用等到把它们塞到代码中之后再处理,那时候查找错误就非常困难了.而且开始这几章还会展示

《正则表达式经典实例(第2版)》——2.14 消除不必要的回溯

2.14 消除不必要的回溯 问题描述上一个实例解释了贪心和懒惰量词之间的区别,以及它们是如何进行回溯的.在有些情形下,这种回溯是不必要的. ‹bd+b›使用了一个贪心量词,而‹bd+?b›使用的是懒惰量词.它们都会匹配相同的内容:一个整数.如果给它们同样的目标文本,它们都会找到完全一样的匹配.在这里所做的任何回溯都是不必要的.试着改写这个正则表达式,明确地消除所有回溯,使正则表达式更加高效. 解决方案 \b\d++\b 正则选项:无 正则流派:Java.PCRE.Perl 5.10.Ruby 1

《正则表达式经典实例(第2版)》——2.9 分组和捕获匹配中的子串

2.9 分组和捕获匹配中的子串 问题描述 改进匹配Mary.Jane或Sue的正则表达式,使之只能匹配完整单词.使用分组来实现这个功能,整个正则表达式只需要一对单词分界符,而不是给每个选择分支都使用一对分界符. 创建一个正则表达式,使之匹配yyyy-mm-dd格式的任意日期,并且分别捕获年.月和日.目标是在处理匹配的代码中可以更容易处理这些分别捕获的值.你可以假设目标文本中的所有日期都是合法的.正则表达式不必要考虑去掉像9999-99-99这样的非法数据,因为它们根本不可能出现在目标文本中. 解

《正则表达式经典实例(第2版)》——2.3 匹配多个字符之一

2.3 匹配多个字符之一 问题描述 创建一个正则表达式来匹配calendar的所有常见的错误拼写形式,使你能够在一份文档中找到这个单词而无需依赖作者的拼写能力.在每个元音位置都允许使用a或者e.创建另外一个正则表达式来匹配一个单个的十六进制字符.再创建一个正则表达式来匹配不属于十六进制字符的单个字符. 本节中的这个问题用于解释一个重要的.经常使用的正则结构-字符组(character class). 解决方案 错误拼写的calendar c[ae]l[ae]nd[ae]r 正则选项:无 正则流派

《正则表达式经典实例(第2版)》——2.12 把正则表达式的一部分重复多次

2.12 把正则表达式的一部分重复多次 问题描述 创建正则表达式来匹配下列种类的数字. 一个googol(一个100位的十进制整数). 一个32位的十六进制整数. 一个32位的十六进制整数,带有一个可选的h后缀. 一个浮点数,包含可选的整数部分.必需的小数部分和可选的指数部分.每个部分都允许任意多个数字. 解决方案 Googol \b\d{100}\b 正则选项:无 正则流派:.NET.Java.JavaScript.PCRE.Perl.Python.Ruby 十六进制整数 ```javascr

《正则表达式经典实例(第2版)》——2.19 在替代文本中添加字面文本

2.19 在替代文本中添加字面文本 问题描述查找并把任何正则表达式匹配从字面上替换为这8个字符:$%*$11. 解决方案 $%\*$$1\1 替代文本流派:.NET.JavaScript \$%\\*\$1\\1 替代文本流派:Java $%\*\$1\\1 替代文本流派:PHP \$%\*\$1\\1 替代文本流派:Perl $%\*$1\\1 替代文本流派:Python.Ruby 讨论在替代文本中转义字符的时机和方式这个实例显示了各种替代文本流派中使用的不同转义规则.在替代文本中,你可能会需