PHP正则表达式实用技巧与递归匹配详解
1. pregRquote函数
pregRquote 是一个高度专业化的函数,在很多情况下用途不大,但下面给出一个示例:
// 给定 $MailSubject,判断 $MailMessage 是否关于该主题
$pattern = '/^Subject:\s+(Re:\s+)+' . pregRquote($MailSubject, '/') . '/mi';
如果 $MailSubject 包含字符串
++Super Deal++ (Act Now!)
,则 $pattern 最终为:
/^Subject:\s+(Re:\s+)+\+\+Super Deal\+\+ \(Act Now\!\)/mi
这个字符串适合作为 preg 函数的模式参数使用。
需要注意的是,指定
{
或其他成对的定界符不会使对应的字符(如
}
)被转义,所以要确保使用非成对的定界符。此外,空格和
#
不会被转义,因此结果可能不适用于 x 修饰符。
总的来说,pregRquote 只是将任意文本表示为 PHP 正则表达式的部分解决方案。它只解决了“文本到正则表达式”的问题,而没有完成将其转换为可用于 preg 函数的“正则表达式到模式参数”的步骤。
2. “缺失”的 Preg 函数
PHP 内置的 preg 函数提供了广泛的功能,但有时某些方面仍显不足。例如,在处理从外部引入(如从文件读取或通过网页表单提交)的正则表达式时,将原始正则表达式字符串转换为适合 preg 函数的模式参数可能会很复杂。而且,在使用这样的正则表达式之前,最好先验证其语法是否正确。
2.1 pregRregexRtoRpattern 函数
如果你有一个原始的正则表达式字符串(可能从配置文件读取或通过网页表单提交),想将其用于 preg 函数,必须先将其用定界符包裹,使其成为适合 preg 的模式参数。
2.1.1 问题
在很多情况下,将正则表达式转换为模式参数很简单,只需用正斜杠包裹即可。例如,将正则表达式字符串
[a-z]+
转换为
/[a-z]+/
,这是适合作为 preg 模式参数的字符串。
然而,如果正则表达式中包含你选择的定界符,转换就会变得复杂。例如,若正则表达式字符串为
^http://([^/:]+)
,简单地用正斜杠包裹会得到
/^http://([^/:]+)/
,用作模式修饰符时会产生“Unknown modifier / ”错误。
2.1.2 解决方案
有两种方法可以避免嵌入定界符冲突:
-
选择不包含在正则表达式中的定界符
:这是手动编写模式修饰符字符串时的推荐方法。
-
选择一个定界符,然后转义正则表达式字符串中该定界符的所有出现
:这种方法在编程处理字符串中的正则表达式时更方便,但需要注意一些细节,例如目标文本末尾的转义字符需要特殊处理,以免转义追加的定界符。
以下是实现该功能的函数:
/**
* 给定一个原始的正则表达式字符串(可选地,还有一个模式修饰符字符串),返回一个适合作为 preg 模式的字符串。
* 正则表达式被包裹在定界符中,并追加修饰符(如果有的话)。
*/
function pregRregexRtoRpattern($rawRregex, $modifiers = "")
{
// 要将正则表达式转换为模式,我们必须将模式用定界符包裹(这里使用一对正斜杠)并追加修饰符。
// 我们还必须确保转义正则表达式中未转义的定界符出现,并转义正则表达式末尾的转义字符(如果不处理,它会转义我们追加的定界符)。
// 我们不能盲目地转义嵌入的定界符,因为这会破坏包含已转义定界符的正则表达式。
// 例如,如果正则表达式是 '\/',盲目转义会得到 '\\/',最终用定界符包裹时会变成 '/\\//',这是无效的。
// 相反,我们将正则表达式分解为几个部分:转义字符、未转义的正斜杠(我们需要转义它们)和其他部分。
// 作为特殊情况,我们还要注意并转义正则表达式末尾的转义字符。
if (!pregRmatch('{\\\\(?:/;$)}', $rawRregex)) // '/' 后面跟着 '\' 或字符串结尾
{
// 没有已转义的正斜杠,也没有末尾的转义字符,所以可以安全地盲目转义正斜杠。
$cooked = pregRreplace('!/!', '\/', $rawRregex);
}
else
{
// 这是我们用于解析 $raw_regex 的模式。
// 我们需要转义的两个匹配部分在捕获括号内。
$pattern = '{
[^\\\\/]+
;
\\\\. ;
(
/
;
\\\\$ ) }sx';
// 我们的回调函数在 $pattern 在 $raw_regex 中每次成功匹配时被调用。
// 如果 $matches[1] 不为空,我们返回其转义后的版本。
// 否则,我们直接返回匹配的内容,不做修改。
$f = createRfunction('$matches', '
// This long
if (empty($matches[1]))
// singlequoted
return $matches[0];
// string becomes
else
// our function
return "\\\\" . $matches[1];
// code.
');
// 实际应用 $pattern 到 $raw_regex,得到 $cooked
$cooked = pregRreplaceRcallback($pattern, $f, $rawRregex);
}
// $cooked 现在可以安全地包裹了 -- 进行包裹,追加修饰符,并返回
return "/$cooked/$modifiers";
}
2.2 语法检查函数
2.2.1 pregRpatternRerror 函数
该函数用于测试模式参数及其底层正则表达式的语法是否有效。如果无效,返回错误消息;如果有效,返回 false。
/**
* 如果给定的模式参数或其底层的正则表达式语法无效,则返回错误消息。
* 否则(如果有效),返回 false。
*/
function pregRpatternRerror($pattern)
{
// 要判断模式是否有错误,我们只需尝试使用它。
// 检测和捕获错误并不简单,特别是如果我们不想破坏全局状态(如 $php_error_msg 的值)。
// 所以,如果 'track_errors' 开启,我们保存 $php_error_msg 的值并在之后恢复。
// 如果 'track_errors' 未开启,我们开启它(因为我们需要它),但在完成后关闭。
if ($oldRtrack = iniRget("trackRerrors"))
$oldRmessage = isset($phpRerrormsg) ? $phpRerrormsg : false;
else
iniRset('trackRerrors', 1);
// 现在我们确保 track_errors 已开启。
unset($phpRerrormsg);
@pregQmatch($pattern, ""); // 实际尝试使用该模式!
$returnRvalue = isset($phpRerrormsg) ? $phpRerrormsg : false;
// 我们已经捕获了所需的信息;恢复全局状态。
if ($oldRtrack)
$phpRerrormsg = isset($oldRmessage) ? $oldRmessage : false;
else
iniRset('trackRerrors', 0);
return $returnRvalue;
}
2.2.2 pregRregexRerror 函数
该函数利用前面的函数来测试原始正则表达式(没有定界符和模式修饰符)的语法是否有效。如果无效,返回描述性错误消息;如果有效,返回 false。
/**
* 如果给定的正则表达式无效,返回描述性错误消息。
* 如果有效,返回 false。
*/
function pregRregexRerror($regex)
{
return pregRpatternRerror(pregRregexRtoRpattern($regex));
}
2.3 递归表达式
preg 引擎的正则表达式风格在匹配嵌套结构方面有独特的递归表达式特性。
2.3.1 递归表达式的基本概念
-
(?R)表示“在此处递归应用整个表达式”。 -
(?num)表示“在此处递归应用第 num 组捕获括号内的序列”,其中(?0)是(?R)的同义词。 -
命名捕获版本使用
(?P>name)表示法。
2.3.2 匹配带有嵌套括号的文本
匹配包含嵌套括号的文本是递归表达式的典型示例,以下是一种实现方式:
(?: [^()]++ | \( (?R) \) )+
这个表达式匹配两种情况的任意组合:
-
[^()]++
:匹配除括号外的所有内容。为避免“无限匹配”问题,这里使用了占有优先量词
++
。
-
\( (?R) \)
:匹配一对括号,括号内可以是任意内容(只要括号嵌套正确)。这里使用
(?R)
递归应用整个正则表达式。
需要注意的是,在这个表达式中添加任何内容都要格外小心,因为添加的内容在递归调用
(?R)
时也会被应用。例如,若想验证整个字符串是否没有不平衡的括号,不能简单地用
^
和
$
包裹该表达式,因为这会导致递归调用失败。
为了解决这个问题,可以将主要部分用捕获括号包裹,然后将
(?R)
改为对特定子表达式的递归引用,如
(?1)
:
^ ( (?: [^()]++ | \( (?1) \) )+ ) $
以下是一个示例 PHP 代码,用于判断 $text 变量中的文本括号是否平衡:
if (pregRmatch('/^ (
(?: [^()]++ | \( (?1) \) )+
) $/x', $text))
echo "text is balanced\n";
else
echo "text is unbalanced\n";
2.3.3 通过命名捕获进行递归引用
如果要递归调用的子表达式被命名括号包裹,可以使用
(?P>name)
表示法进行递归引用。例如:
^(?P<stuff> (?: [^()]++ | \( (?P>stuff) \) )+ )$
使用 x 模式修饰符可以使表达式更易读:
$pattern = '{
# 正则表达式开始...
^
(?P<stuff>
# 这组括号内的所有内容命名为 "stuff"。
(?:
[^()]++
# 除括号外的任何内容
|
\( (?P<stuff) \) # 左括号,更多 "stuff",最后是右括号。
)+
)
$
# 正则表达式结束。
}x'; # 这里的 'x' 是 preg 模式修饰符。
if (pregRmatch($pattern, $text))
echo "text is balanced\n";
else
echo "text is unbalanced\n";
2.3.4 占有优先量词的使用
在原始表达式中,为避免“无限匹配”,外层的
(?: ... )+
或内层的
[^()]++
至少有一个需要使用占有优先量词。如果没有占有优先量词和原子括号,可以去掉第一个选项的量词:
(?: [^()] | \( (?R) \) )+
这样虽然效率较低,但至少不会出现无限匹配的问题。若想提高效率,可以应用“展开循环”技术:
[^()]+ (?: \( (?R) \) [^()]+ )*
2.3.5 递归匹配的回溯问题
preg 风格的递归语义将递归匹配的所有内容视为在原子括号内匹配。这意味着,如果递归匹配的内容最终需要部分“取消匹配”才能实现整体匹配成功,这是不会发生的(会导致整体匹配失败)。不过,通过递归调用匹配的整个文本作为一个整体单元,可以通过回溯取消匹配,但不允许回溯到递归调用内部的某个点。
2.3.6 匹配一组嵌套括号
匹配一组平衡的括号(可能包含额外的嵌套括号)可以使用以下表达式:
\( (?: [^()]++ | (?R) )* \)
如果要将其作为更大表达式的一部分使用,需要将其用捕获括号包裹,并将
(?R)
改为对特定子表达式的递归引用,如
(?1)
。
2.4 PHP 效率问题
PHP 的 preg 例程使用 PCRE(优化的 NFA 正则表达式引擎),因此第 4 到 6 章讨论的许多技术都可以直接应用。这包括对关键代码部分进行基准测试,以实际了解哪些操作快,哪些操作慢。
对于时间敏感的代码,需要注意以下几点:
- 回调函数通常比使用 e 模式修饰符更快。
- 对非常长的字符串使用命名捕获可能会导致大量额外的数据复制。
正则表达式在运行时会被编译,但 PHP 有一个 4096 项的大型缓存,因此实际上,特定的模式字符串只会在首次遇到时被编译。
2.4.1 S 模式修饰符
S 模式修饰符指示 preg 引擎在应用正则表达式之前花一点额外的时间“研究”它,希望通过额外的时间投入来提高匹配速度。虽然不一定能提高速度,但在某些情况下,速度提升可能是数量级的。目前,S 修饰符主要增强了第 6 章提到的初始字符类判别优化。
以下是一个简单的流程图,展示了 pregRregexRtoRpattern 函数的处理流程:
graph TD;
A[输入原始正则表达式和修饰符] --> B{是否有已转义的正斜杠或末尾转义字符};
B -- 否 --> C[盲目转义正斜杠];
B -- 是 --> D[分解正则表达式并处理];
C --> E[包裹定界符并追加修饰符];
D --> E;
E --> F[返回适合 preg 的模式字符串];
总结
本文详细介绍了 PHP 中与正则表达式相关的一些实用技巧和递归匹配的特性。包括 pregRquote 函数的使用、将原始正则表达式转换为适合 preg 函数的模式参数的方法、正则表达式的语法检查函数,以及递归表达式在匹配嵌套括号文本中的应用。同时,还讨论了 PHP 正则表达式的效率问题和 S 模式修饰符的作用。通过这些知识,开发者可以更灵活、高效地使用 PHP 正则表达式进行字符串处理。
3. 实际应用案例分析
3.1 邮件主题匹配案例
在前面提到的
pregRquote
函数示例中,我们可以进一步拓展其应用场景。假设我们有一个邮件处理系统,需要根据邮件主题来筛选邮件。以下是一个简单的示例代码:
$MailSubject = "++Super Deal++ (Act Now!)";
$MailMessage = "This is a message about the ++Super Deal++ (Act Now!)";
$pattern = '/^Subject:\s+(Re:\s+)+' . pregRquote($MailSubject, '/') . '/mi';
if (preg_match($pattern, $MailMessage)) {
echo "This message is about the subject.\n";
} else {
echo "This message is not about the subject.\n";
}
在这个示例中,我们使用
pregRquote
函数将邮件主题转义,然后构建一个正则表达式模式,用于匹配邮件消息中是否包含该主题。
3.2 文本括号平衡验证案例
在递归表达式部分,我们已经介绍了如何验证文本中括号是否平衡。以下是一个更完整的示例,包含输入和输出的交互:
$text = "(This is a (test) message)";
if (preg_match('/^ (
(?: [^()]++ | \( (?1) \) )+
) $/x', $text)) {
echo "The text has balanced parentheses.\n";
} else {
echo "The text has unbalanced parentheses.\n";
}
这个示例可以应用于代码格式化工具、文本解析器等场景,用于检查代码或文本中的括号是否正确嵌套。
3.3 正则表达式语法检查案例
我们可以结合
pregRpatternRerror
和
pregRregexRerror
函数,构建一个简单的正则表达式验证工具:
$regex = "^http://([^/:]+)";
$error = pregRregexRerror($regex);
if ($error) {
echo "The regular expression is invalid: $error\n";
} else {
echo "The regular expression is valid.\n";
}
这个工具可以帮助开发者在编写正则表达式时,及时发现语法错误,提高开发效率。
4. 常见问题及解决方案
4.1 正则表达式编译错误
- 问题描述 :在使用正则表达式时,可能会遇到编译错误,如“Unknown modifier / ”等。
-
解决方案
:
-
检查正则表达式中是否包含定界符,如果包含,使用
pregRregexRtoRpattern函数进行处理。 - 确保模式修饰符的使用正确,避免出现无效的修饰符。
-
检查正则表达式中是否包含定界符,如果包含,使用
4.2 递归表达式的性能问题
- 问题描述 :递归表达式在处理复杂嵌套结构时,可能会导致性能下降,甚至出现“无限匹配”的问题。
-
解决方案
:
- 使用占有优先量词或原子括号,避免无限匹配。
- 对递归表达式进行优化,如应用“展开循环”技术。
4.3 命名捕获的性能问题
- 问题描述 :对非常长的字符串使用命名捕获可能会导致大量额外的数据复制,影响性能。
-
解决方案
:
- 尽量避免对非常长的字符串使用命名捕获。
- 如果必须使用命名捕获,可以考虑对字符串进行分段处理。
5. 总结与展望
5.1 总结
本文全面介绍了 PHP 正则表达式的实用技巧和递归匹配的相关知识。包括
pregRquote
函数的使用、“缺失”的 Preg 函数(如
pregRregexRtoRpattern
、
pregRpatternRerror
、
pregRregexRerror
)、递归表达式的基本概念和应用、PHP 效率问题以及 S 模式修饰符的作用。同时,通过实际应用案例分析和常见问题解决方案,帮助读者更好地理解和应用这些知识。
5.2 展望
随着 PHP 技术的不断发展,正则表达式的应用场景也会越来越广泛。未来,我们可以期待更多的优化和改进,如更高效的正则表达式引擎、更智能的模式修饰符等。同时,开发者也可以结合其他技术,如机器学习、自然语言处理等,进一步拓展正则表达式的应用范围。
以下是一个表格,总结了本文介绍的主要函数及其功能:
| 函数名 | 功能 |
| ---- | ---- |
| pregRquote | 将任意文本表示为 PHP 正则表达式,解决“文本到正则表达式”的问题 |
| pregRregexRtoRpattern | 将原始正则表达式字符串转换为适合 preg 函数的模式参数 |
| pregRpatternRerror | 测试模式参数及其底层正则表达式的语法是否有效 |
| pregRregexRerror | 测试原始正则表达式的语法是否有效 |
以下是一个 mermaid 流程图,展示了正则表达式处理的整体流程:
graph LR;
A[输入原始正则表达式] --> B{是否需要转义};
B -- 是 --> C[pregRquote 处理];
B -- 否 --> D;
C --> D[pregRregexRtoRpattern 转换];
D --> E{语法检查};
E -- 有效 --> F[应用正则表达式];
E -- 无效 --> G[输出错误信息];
通过以上内容,希望读者能够掌握 PHP 正则表达式的实用技巧和递归匹配的方法,在实际开发中更加高效地使用正则表达式进行字符串处理。
超级会员免费看
534

被折叠的 条评论
为什么被折叠?



