Unicode/多字节修饰符和mb_egg_place的结果不同

2022-03-22 00:00:00 regex php preg-replace

此正则表达式似乎有很大问题:

(((?!a).)*)*a{

我知道正则表达式很糟糕。这不是这里的问题。

使用此字符串测试时:

AAAAAAAAAAAAAA{AA

字母Aa几乎可以替换为任何内容,并导致相同的问题。

此正则表达式和测试字符串对被压缩。完整示例可以找到here。

这是我用来测试的代码:

<?php
$regex = '(((?!a).)*)*a\{';
$test_string = 'AAAAAAAAAAAAAA{AA';
echo "1:".mb_ereg_replace('/'.$regex.'/','WORKED',$test_string)."
";
echo "2:".preg_replace('/'.$regex.'/u','WORKED',$test_string)."
";
echo "3:".preg_replace('/'.$regex.'/','WORKED',$test_string)."
";

可以在此处查看结果:

http://3v4l.org/Yh6FU

理想结果是返回相同的测试字符串,因为正则表达式不匹配。

preg_replaceu修饰符一起使用时,根据该注释应与mb_ereg_replace具有相同的结果:

php multi byte strings regex

mb_ereg_replace完全正常工作。它返回测试字符串,因为正则表达式不匹配。

但是,preg_replace对于4.3.4-4.4.5以外的PHP版本,4.4.9-5.1.6似乎不起作用。

  • 对于某些PHP版本,结果为错误:

    进程已退出,代码为139。

  • 对于其他一些PHP版本,结果为NULL
  • 睡觉mb_ereg_replace尚未制作

此外,仅从字符串或正则表达式中删除单个字母似乎会完全改变PHP的哪个版本有哪些结果。

从这条评论判断:

php multi byte strings regex

应避免

ereg*,这是有意义的,因为它速度较慢,并且比preg*支持的少。这使得使用mb_ereg_replace变得不可取。但是,没有mb_preg_replace选项,因此这似乎是唯一有效的选项。

所以,我的问题是:
对于给定的字符串和正则表达式对,是否有任何替代mb_ereg_replace可以正常工作?


解决方案

您知道(...)(?:...)的区别吗?

<2-14]>.这定义了一个标记组。圆括号内的表达式找到的字符串在内部存储在变量中,以供反向引用。

(?:...).这将定义非标记组。圆括号内的表达式找到的字符串不在内部存储。这样的非标记组通常用于在字符串上多次应用表达式。

现在让我们看一下您的表达式(((?!a).)*)*a{,在文本编辑器UltraEdit中使用Perl正则表达式时,哪个表达式会导致错误消息The complexity of matching expression has exceeded available resources

(?!a)..应找到下一个字符不是字母‘a’的字符。好吧。但是您想要查找包含0个或更多字符直到字母‘a’的字符串。您的解决方案是:((?!a).)*)

这不是一个好的解决方案,因为引擎现在要在每个字符上向前查找字母‘a’,如果下一个字符不是‘a’,则匹配该字符,将其存储为字符串以供反向引用,然后继续下一个字符。实际上,我甚至不知道在标记组上使用乘数时内部会发生什么,就像这里所做的那样。切勿在标记组上使用乘数。所以最好是(?:(?!a).)*

接下来,将表达式扩展为(((?!a).)*)*。是否再多一个带乘数的标记组?

看起来您希望标记不包含字母‘a’的整个字符串。但在这种情况下,最好使用:((?:(?!a).)*),因为这为内部表达式找到的字符串定义了1个且只有1个标记组。

因此,更好的表达式应该是((?:(?!a).)*)a{,因为现在只有一个标记组没有该标记组上的倍增。现在,引擎确切知道要将哪个字符串存储在变量中。

([^a]*?)a{要快得多,因为这个非贪婪的否定字符类定义还匹配a{中不包含字母‘a’的0个或更多个字符的字符串。如果没有必要,应避免向前看,因为这可避免回溯。


我不知道PHP函数mb_ereg_replacepreg_replace的源代码,需要用表达式一步一步地检查它们,以找出不同结果的确切原因。

但是,表达式(((?!a).)*)*a{肯定会导致繁重的recursion,因为它没有定义何时停止匹配数据以及临时存储什么内容。因此这两个函数(最有可能)从堆栈分配越来越多内存,也可能从堆分配越来越多的内存,直到出现堆栈溢出或"没有足够的可用内存"异常。

退出代码139是由未捕获的堆栈溢出引起的分段错误(内存边界冲突),或者使用malloc()从堆分配更多内存时返回NULL,并忽略返回值NULL。我认为malloc()返回NULL是退出代码139的原因。

所以最像错误的是两个函数分别进行异常处理。捕获内存异常或使用过多的出口对递归迭代进行计数,以便在内存异常真正发生之前阻止它,这可能是此表达式行为不同的原因。

如果不知道函数mb_ereg_replacepreg_replace的源代码,很难给出一个确切的答案,但在我看来这并不重要。

表达式(((?!a).)*)*a{总是导致大量递归,正如Sam在他的第一个注释中已经报告的那样。在替换仅包含17个字符的字符串期间,超过119000个步骤(=函数调用)是表达式有问题的强烈征兆。该表达式可用于让函数或整个应用程序(PHP解释器)运行到异常错误处理中,但不能用于真正的替换。因此,对于PHP函数的开发人员来说,该表达式非常适合于测试无限递归上的错误处理,但不适用于真正的替换操作。


引用的PHP沙箱中使用的完整正则表达式:

(?<!<br>)(?<!s)s*(((?:(?:(?!<br>|(|)).)*(?:((?:(?!<br>|(|)).)*))?)*?))s*({)

很难分析此表单中的此搜索字符串。

为了更好地理解此表达式中的条件和循环,让我们将搜索字符串视为带有缩进的代码片段。

(?<!<br>)(?<!s)s*
(
   (
   (?:
      (?:
         (?!<br>|(|)).
      )*
      (?:
         (
         (?:
            (?!<br>|(|)).
         )*
         )
      )?
   )*?
   )
)
s*
({)

我希望现在可以更容易地看到此搜索字符串中的递归。有两次相同的挡路,但不是按顺序,而是按嵌套顺序,这是一个经典递归。

此外,所有表达式(包括在最后的({)之前形成递归的、可以匹配任何字符的嵌套表达式)都带有乘数*或,表示可以存在,但不能存在。{的存在是整个搜索字符串的唯一真实条件。其他所有内容都是可选的,这并不好,因为此搜索字符串中存在递归。

如果完全不清楚从哪里开始和从哪里停止选择字符,则递归搜索表达式非常糟糕,因为这会导致无休止的递归,直到异常退出。

让我用[A-Za-z]+([a-z]+)

这样的简单表达式来解释这个问题 1个或多个大写或小写字母,后跟1个或多个小写字符(并且启用区分大小写的搜索)。很简单,不是吗?

但是第二字符类定义了字符集,该字符集是由第一类定义定义的字符集的子集。这不太好。

York这样的字符串上,括号中的表达式应该标记什么?

orkrk或仅k或甚至什么都不是,因为作为第一字符类找到的匹配字符串已无法匹配整个字词,因此不能再为第二字符类留下任何内容?

Perl正则表达式库通过将乘数*和+默认声明为贪婪,解决了这种常见问题,除非?在乘数之后使用,这会导致相反的匹配行为。这两条附加规则已经对此问题有帮助。

因此,此处使用的表达式仅标记k,用[A-Za-z]+?([a-z]+)标记字符串ork,用[A-Za-z]+?([a-z]+?)仅标记第一个o

还有一条规则:喜欢正面的结果,而不是负面的结果。此附加规则避免了第一个字符类已经选择了整个单词York

因此解决了字符集部分或完全重叠的主要问题。

但是,如果将这样的表达式放在递归中,并使用前视/后视和回溯使其变得更加复杂,并且回溯不仅由1个字符完成,甚至由多个字符完成,会发生什么情况?

是否仍明确定义从何处开始和停止为整个搜索字符串的每个表达式部分选择字符?

不,不是。

对于没有明确规则由搜索表达式的哪一部分选择搜索字符串的哪一部分的搜索字符串,每个结果或多或少都是有效的,包括意外的结果。

此外,由于缺少启动/停止条件,函数完全无法在字符串上应用表达式并退出异常,因此很容易发生这种情况。

对于使用搜索表达式的人来说,应用搜索字符串时异常退出肯定总是意外的结果。

搜索函数的不同版本可能会在表达式上返回不同的结果,从而使搜索函数运行到异常函数退出。搜索函数的开发者不断地改变搜索函数的程序代码,以更好地检测和处理导致无休止递归的搜索表达式,因为这仅仅是一个安全问题。正则表达式搜索从应用程序的堆栈或整个RAM中分配更多或更多内存,对于运行此应用程序的整个计算机的安全性、稳定性和可用性是非常有问题的。PHP主要用于不应停止工作的服务器上,因为递归内存分配会占用服务器越来越多的RAM,因为这最终会杀死整个服务器。

这就是根据使用的PHP版本获得不同结果的原因。

我花了很长时间查看了您的完整搜索表达式,并让它在示例字符串上运行了几次。但老实说,我找不到({)左边的表达式应该找到什么,应该忽略什么。

我理解表达式的某些部分,但为什么搜索字符串中会有递归?

s*上的负查找(?<!s)有什么作用?

s*匹配0个或更多空格,因此表达式"上一个字符不是空格"的目的对我来说是无法理解的。在我看来,消极的后视是毫无用处的,只会增加整个表达式的复杂性。而这仅仅是个开始。

我非常肯定,您真正想要的东西可以通过一个简单得多的表达式来实现,表达式中不会有递归导致异常函数根据搜索到的字符串退出,并且删除了所有或几乎所有的回溯步骤。

相关文章