58、正则表达式优化与PHP应用实例

正则表达式优化与PHP应用实例

1. S模式修饰符的使用场景

在处理正则表达式时,除非你打算将一个正则表达式应用于大量文本,否则可能一开始就没有太多时间可以节省。只有在将相同的正则表达式应用于大块文本或许多小块文本时,才需要关注S模式修饰符。

2. 无S模式修饰符的标准优化

以简单表达式 <(\w+)> 为例,由于该正则表达式的特性,我们知道每个匹配必须以 < 字符开头。正则表达式引擎可以(在preg套件的情况下也确实如此)利用这一点,先在目标字符串中预搜索 < ,然后仅在这些位置应用完整的正则表达式(因为匹配必须以 < 开头,从其他字符开始应用是没有意义的)。

这种简单的预搜索比完整的正则表达式应用要快得多,这就是优化所在。特别是,相关字符在目标文本中出现的频率越低,优化效果就越好。而且,正则表达式引擎检测第一个字符失败所需的工作量越大,优化的好处就越大。例如,对于 <i><</i><<b><</b> ,这种优化比 <(\w+)> 更有帮助,因为在第一种情况下,正则表达式引擎否则必须尝试四种不同的替代方案才能进行下一次尝试,这避免了大量的工作。

3. 使用S模式修饰符增强优化

preg引擎足够智能,可以对大多数只有一个必须作为任何匹配开头的字符的表达式应用这种优化,如前面的例子所示。然而,S模式修饰符告诉引擎预先分析表达式,以便对可能匹配有多个起始字符的表达式启用这种优化。

以下是一些需要S模式修饰符才能以这种方式进行优化的示例表达式:
| 正则表达式 | 可能的起始字符 |
| — | — |
| <(\w+) < &(\w+); | < & |
| [Rr]e: | R r |
| (Jan;Feb;...;Dec)\b | A D F J M N O S |
| (Re:\s+)? SPAM | R S |
| \s+,\s+ | \x09 \x0A \x0C \x0D \x20 , |
| [&<">] | & < " > |
| \r?\n\r?\n | \r \n |

4. S模式修饰符不起作用的情况

以下类型的表达式不会从S模式修饰符中受益:
- 具有前导锚点(例如 ^ \b ),或在全局级别替代中具有锚点的表达式。这是当前实现的一个限制,理论上在未来的版本中,对于 \b 可能会被消除。
- 可以匹配空的表达式,例如 \s+
- 可以从任何字符(或大多数任何字符)开始匹配的表达式,例如 (?: [^()]++ < \( (?R) \) ) ,此表达式可以从除 ) 之外的任何字符开始,因此预检查不太可能消除许多起始位置。
- 只有一个可能起始字符的表达式,因为它们已经被优化了。

5. 建议使用方式

preg引擎执行S模式修饰符调用的额外分析并不需要很长时间,因此如果你要将正则表达式应用于相对较大的文本块,使用它不会有坏处。如果你认为它可能有应用的机会,潜在的好处是值得的。

6. 扩展示例
6.1 PHP解析CSV

以下是第6章中CSV(逗号分隔值)示例的PHP版本。正则表达式已更新为使用占有量词,以使其呈现更简洁。

首先,使用的正则表达式如下:

$csvRregex = '{
    \G(?:^;,)
    (?:
        # 要么是双引号字段...
        " # 字段开始引号
        (
            [^"]++
            (?: "" [^"]++ )++
        )
        " # 字段结束引号
        ; # ... 或者 ...
        # ... 一些非引号/非逗号文本 ...
        ( [^",]++ )
    )
}x';

然后,使用它来解析一行CSV文本:

// 应用正则表达式,将各种数据填充到 $all_matches 中
preg_match_all($csvRregex, $line, $all_matches);
// $Result 将保存从 $all_matches 中收集的字段数组
$Result = array ();
// 遍历每个成功的匹配...
for ($i = 0; $i < count($all_matches[0]); $i++) {
    // 如果第二组捕获括号有捕获内容,直接使用它
    if (strlen($all_matches[2][$i]) > 0) {
        array_push($Result, $all_matches[2][$i]);
    } else {
        // 这是一个带引号的值,在使用之前处理嵌入的双引号
        array_push($Result, preg_replace('/""/', '"', $all_matches[1][$i]));
    }
}
// 数组 $Result 现在已填充并可供使用
6.2 检查标记数据的正确嵌套

这是一个有点复杂的任务,涵盖了许多有趣的点:检查XML(或XHTML,或任何类似的标记数据)是否包含无匹配或不匹配的标签。采用的方法是查找正确匹配的标签、非标签文本和自闭合标签(例如 <br/> ,在XML术语中是“空元素标签”),并希望找到整个字符串。

完整的正则表达式如下:

'^ ((?:<(\w++)[^>]++(?<!/)>(?1)</\2><[^<>]++<\w[^>]++/>),+) $'

匹配此正则表达式的字符串没有不匹配的标签(稍后会有一些注意事项)。

这个表达式看起来可能很复杂,但将其分解为各个部分就可以处理。表达式的外部 ^ (...) $ 包裹了正则表达式的主体,以确保在返回成功之前匹配整个主题字符串。主体也被另一组捕获括号包裹,这允许稍后递归引用“主体”。

正则表达式的主体是三个替代方案(为了视觉清晰,在正则表达式中每个都有下划线),包裹在 (?: ...)++ 中,以允许它们的任何组合进行匹配。这三个替代方案分别尝试匹配:匹配的标签、非标签文本和自闭合标签。

由于每个替代方案可以匹配的内容是唯一的(即,一个替代方案匹配的地方,其他两个都不能匹配),因此后续回溯永远不会允许另一个替代方案匹配相同的文本。可以利用这一知识,通过在“允许任何组合匹配”的括号上使用占有 + 来提高效率。这告诉引擎甚至不必尝试回溯,从而在找不到匹配时加快结果的得出。

出于同样的原因,这三个替代方案可以按任何顺序排列,因此将最有可能经常匹配的替代方案放在前面。

下面分别看一下这些替代方案:
- 第二个替代方案:非标签文本 [^<>]++ ,这个替代方案匹配非标签文本跨度。这里使用占有量词可能有些过度,考虑到包裹的 (?: ...)++) 也是占有的,但为了安全起见,当知道不会有坏处时,还是喜欢使用占有量词。(占有量词通常用于提高效率,但也可能改变匹配的语义。这种改变可能有用,但要确保理解其影响。)
- 第三个替代方案:自闭合标签 <\w [^>]++/> ,匹配自闭合标签,如 <br/> <img .../> (自闭合标签的特征是在右括号之前有 / )。和之前一样,这里使用占有量词可能有些过度,但肯定没有坏处。
- 第一个替代方案:一组匹配的标签 <(\w++) [^>]++(?<!/)> (?1) </\2> 。这个子表达式的第一部分(有下划线标记)匹配一个开始标签,其中 (\w++) 捕获标签名,这是整个正则表达式的第二组捕获括号。 (?<!/) 是负向后瞻,确保刚刚没有匹配到斜杠。将其放在匹配开始标签部分的 > 之前,以确保不会匹配到自闭合标签,如 <hr/> (自闭合标签由第三个替代方案处理)。

在匹配开始标签后, (?1) 递归应用第一组捕获括号内的子表达式,即前面提到的“主体”,实际上是一段没有不平衡标签的文本。一旦匹配完成,应该找到与开始标签对应的结束标签(其名称在第二组捕获括号中捕获)。 </\2 > 的前导 </ 确保它是一个结束标签; \2> 中的反向引用确保它是正确的结束标签。

如果检查的是HTML或其他标签名不区分大小写的数据,确保在正则表达式前加上 (?i) ,或使用 i 模式修饰符。

7. 占有量词的使用

在第一个替代方案 <(\w++) [^>]++(?<!/)> 中使用占有 \w++ 。如果使用的是表达能力较弱的正则表达式风格,没有占有或原子分组,会在匹配标签名的 (\w+) 后面加上 \b ,写成 <(\w+)\b[^>]+(?<!/)>

\b 很重要,它可以防止 (\w+) 匹配,例如 <link> ... </li> 序列中的第一个 li 。否则, nk 将在捕获括号外匹配,导致后续的 \2 反向引用的标签名被截断。

通常不会出现这种情况,因为 \w+ 是贪婪的,想要匹配整个标签名。然而,如果将此正则表达式应用于嵌套不当的文本,应该失败时,为了寻找匹配而进行的回溯可能会迫使 \w+ 匹配小于完整标签名的内容,就像在 <link> ... </li> 示例中一样。 \b 可以防止这种情况发生。

幸运的是,PHP强大的preg引擎支持占有量词,在 (\w++) 中使用占有量词与在后面添加 \b 具有相同的“不允许回溯拆分标签名”的效果,但更高效。

8. 实际应用中的XML和HTML处理
8.1 实际XML处理

实际的XML格式比简单的标签匹配更复杂。还需要考虑XML注释、CDATA部分和处理指令等。

添加对XML注释的支持很简单,只需添加第四个替代方案 <!-- .+?--> ,并确保使用 (?s) s 模式修饰符,以便其点可以匹配换行符。

类似地,CDATA部分(格式为 <![CDATA[ ... ]]> )可以用新的 <![CDATA[ .+?]]> 替代方案处理,XML处理指令(如 <?xml version="1.0"?> )可以通过添加 <\? .+?\?> 作为替代方案来处理。

实体声明的格式为 <!ENTITY ...> ,可以用 <!ENTITY\b .+?> 处理。XML中有许多类似的结构,在大多数情况下,可以通过将 <!ENTITY\b .+?> 更改为 <![A-Z] .+?> 来将它们作为一组处理。

以下是将所有这些组合在一起的PHP代码片段:

$xmlRregex = '{
    ^(
        (?: <(\w++) [^>]++ (?<!/)>
            (?1)
            </\2> # 匹配的标签对
            ; [^<>]++ 
            # 非标签内容
            ; <\w[^>]++/> 
            # 自闭合标签
            ; <!--.+?--> 
            # 注释
            ; <![CDATA[.+?]]> 
            # CDATA块
            ; <\?.+?\?> 
            # 处理指令
            ; <![A-Z].+?> 
            # 实体声明等
        )++
    )$
}sx';
if (preg_match($xmlRregex, $xmlRstring)) {
    echo "块结构似乎有效\n";
} else {
    echo "块结构似乎无效\n";
}
8.2 实际HTML处理

现实世界中的HTML通常存在各种问题,使得这样的检查不切实际,例如无匹配和不匹配的标签,以及无效的原始 < > 字符。然而,即使是正确平衡的HTML也有一些特殊情况需要考虑:注释和 <script> 标签。

HTML注释的处理方式与XML注释相同:使用 <!-- .+?--> s 模式修饰符。

<script> 部分很重要,因为其中可能包含原始 < > 字符,所以希望允许从开始的 <script ...> 到结束的 </script> 匹配任何内容。可以用 <script\b[^>]+> .,? </script> 处理。有趣的是,不包含禁止的原始 < > 字符的脚本序列会被第一个替代方案捕获,因为它们符合“匹配的标签对”模式。如果 <script> 确实包含这样的原始字符,第一个替代方案失败,由这个替代方案来匹配该序列。

以下是HTML版本的PHP代码片段:

$htmlRregex = '{
    ^(
        (?: <(\w++) [^>]++ (?<!/)> (?1) </\2> # 匹配的标签对
            ; [^<>]++ 
            # 非标签内容
            ; <\w[^>]++/> 
            # 自闭合标签
            ; <!--.+?--> 
            # 注释
            ; <script\b[^>]+>.+?</script> 
            # 脚本块
        )++
    )$
}isx';
if (preg_match($htmlRregex, $htmlRstring)) {
    echo "块结构似乎有效\n";
} else {
    echo "块结构似乎无效\n";
}

通过以上的正则表达式优化和实际应用示例,可以在处理大量文本时提高效率,同时准确处理各种复杂的文本匹配需求。在实际使用中,根据具体情况灵活运用这些技巧和方法,能够更好地完成文本处理任务。

正则表达式优化与PHP应用实例(续)

9. 常用正则表达式元字符与模式修饰符总结

正则表达式中有许多常用的元字符和模式修饰符,下面为大家详细总结:

元字符/模式修饰符 含义
\( ... \) 捕获括号,用于捕获匹配的内容
(?: ... ) 非捕获括号,不捕获匹配的内容
(?!) 负向前瞻断言
(?# ... ) 注释
(?1) 递归引用第一个捕获组的内容
(?i) 大小写不敏感模式
(?m) 多行模式
(?s) 点号匹配所有字符模式
(?x) 注释和自由间距模式
+ 匹配前面的元素一次或多次
? 匹配前面的元素零次或一次,或使量词变为非贪婪模式
* 匹配前面的元素零次或多次
{min,max} 匹配前面的元素至少 min 次,最多 max
^ 行起始锚点
$ 行结束锚点
\b 单词边界
\B 非单词边界
\d 匹配数字
\D 匹配非数字
\s 匹配空白字符
\S 匹配非空白字符
\w 匹配单词字符(字母、数字、下划线)
\W 匹配非单词字符
10. 不同编程语言中的正则表达式特性对比

不同编程语言对正则表达式的支持有所不同,下面以.NET、Java、Perl、PHP为例进行对比:

特性 .NET Java Perl PHP
正则表达式对象模型
命名捕获 支持 支持 支持 支持
递归引用 支持 支持 支持 支持
模式修饰符 丰富 丰富 丰富 丰富
性能优化机制 JIT等 多种优化 多种优化 多种优化
11. 正则表达式性能优化策略

正则表达式的性能优化是一个重要的话题,以下是一些常见的优化策略:
1. 合理使用锚点 :使用 ^ $ 等锚点可以减少不必要的匹配尝试,提高匹配效率。例如, ^Subject: example 可以确保从行首开始匹配,避免在文本中间不必要的查找。
2. 避免回溯过多 :回溯是正则表达式匹配过程中的一个重要机制,但过多的回溯会导致性能下降。可以通过使用占有量词、原子分组等方式减少回溯。例如, \w++ \w+ 更能避免不必要的回溯。
3. 优化交替顺序 :交替的顺序会影响匹配效率,将最有可能匹配的分支放在前面。例如,在 (pattern1|pattern2|pattern3) 中,将最常匹配的 pattern1 放在首位。
4. 利用预搜索优化 :对于有明确起始字符的正则表达式,可以利用预搜索来减少匹配的起始位置,如前面提到的 <(\w+)> 利用 < 进行预搜索。
5. 使用合适的模式修饰符 :根据具体需求使用合适的模式修饰符,如 i 模式修饰符用于大小写不敏感匹配, s 模式修饰符用于让点号匹配所有字符。

12. 正则表达式在实际项目中的应用流程

在实际项目中使用正则表达式,一般可以遵循以下流程:

graph TD;
    A[明确需求] --> B[设计正则表达式];
    B --> C[测试正则表达式];
    C --> D{是否通过测试};
    D -- 是 --> E[集成到项目中];
    D -- 否 --> B;
    E --> F[监控与优化];
  1. 明确需求 :确定要匹配的文本特征和规则,例如要从文本中提取邮箱地址、电话号码等。
  2. 设计正则表达式 :根据需求设计合适的正则表达式,可以参考前面总结的元字符和模式修饰符。
  3. 测试正则表达式 :使用测试数据对正则表达式进行测试,确保其能准确匹配所需的文本。
  4. 集成到项目中 :将通过测试的正则表达式集成到项目代码中,实现相应的功能。
  5. 监控与优化 :在项目运行过程中,监控正则表达式的性能,根据实际情况进行优化。
13. 常见错误及解决方法

在使用正则表达式时,可能会遇到一些常见的错误,下面为大家列举并给出解决方法:
- 匹配结果不符合预期
- 原因 :正则表达式设计错误,如量词使用不当、锚点位置错误等。
- 解决方法 :仔细检查正则表达式,使用测试数据逐步调试,确保每个部分都能正确匹配。
- 性能问题
- 原因 :回溯过多、正则表达式过于复杂等。
- 解决方法 :参考前面提到的性能优化策略,如使用占有量词、优化交替顺序等。
- 兼容性问题
- 原因 :不同编程语言对正则表达式的支持存在差异。
- 解决方法 :了解目标编程语言的正则表达式特性,根据其特点进行调整。

14. 拓展学习建议

正则表达式是一个强大的工具,要想熟练掌握并灵活运用,需要不断学习和实践。以下是一些拓展学习建议:
- 阅读优秀的正则表达式教程 :可以选择一些经典的正则表达式书籍或在线教程,深入学习正则表达式的原理和高级应用。
- 参与开源项目 :在开源项目中寻找使用正则表达式的代码,学习他人的优秀实践和经验。
- 参加技术社区 :加入正则表达式相关的技术社区,与其他开发者交流心得和遇到的问题,获取更多的学习资源和灵感。
- 自己动手实践 :通过实际项目和练习题,不断巩固所学的知识,提高运用能力。

总之,正则表达式在文本处理中有着广泛的应用,通过深入学习和实践,我们可以更好地利用它来解决各种复杂的文本匹配和处理问题。希望大家在今后的工作和学习中,能够灵活运用正则表达式,提高工作效率和代码质量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值