【问题标题】:PHP PREG_REPLACE Returning wrong result depending on order checkedPHP PREG_REPLACE 根据检查的顺序返回错误的结果
【发布时间】:2011-11-23 04:34:24
【问题描述】:

我偶然发现了一个与 PHP 的 preg_replace 函数和一些正则表达式模式有关的非常奇怪的错误。我想要做的是替换用括号分隔的自定义标签并将它们转换为 HTML。正则表达式必须考虑将保留在输出 HTML 中的自定义“填充”标签,以便在页面加载时可以即时替换它(例如替换为站点名称)。

每个正则表达式模式都会自行工作,但由于某种原因,如果先检查其他模式之一,其中一些会提前退出函数。当我偶然发现这一点时,我使用 preg_match 和一个 foreach 循环在继续之前检查模式,如果找到就会返回结果 - 所以假设它对每个模式来说都是新鲜的。

这也没有用。

检查代码:

function replaceLTags($originalString){
    $patterns = array(
                '#^\[l\]([^\s]+)\[/l\]$#i' => '<a href="$1">$1</a>',
                '#^\[l=([^\s]+)]([^\[]+)\[/l\]$#i'=> '<a href="$1">$2</a>',
                '#^\[l=([^\s]+) title=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" title="$2">$3</a>',
                '#^\[l=([^\s]+) rel=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" rel="$2">$3</a>',
                '#^\[l=([^\s]+) onClick=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" onClick="$2">$3</a>',
                '#^\[l=([^\s]+) style=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" style="$2">$3</a>',
                '#^\[l=([^\s]+) onClick=([^\[]+) style=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" onClick="$2" style="$3">$4</a>',
                '#^\[l=([^\s]+) class=([^\[]+) style=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" class="$2" style="$3">$4</a>',
                '#^\[l=([^\s]+) class=([^\[]+) rel=([^\[]+)] target=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" class="$2" rel="$3" target="$4">$5</a>'
            );

    foreach ($patterns as $pattern => $replace){
        if (preg_match($pattern, $originalString)){
            return preg_replace($pattern, $replace, $originalString);
        }
    }
}

$string = '[l=[site_url]/site-category/ class=hello rel=nofollow target=_blank]Hello there[/l]';

echo $alteredString = $format->replaceLTags($string);

上面的“字符串”会变成:

<a href="[site_url">/site-category/ class=hello rel=nofollow target=_blank]Hello there</a>

什么时候出现:

<a href="[site_url]/site-category/" class="hello" rel="nofollow" target="_blank">Hello there</a>

但如果将该模式在列表中进一步向上移动以便更快地检查,它的格式会正确。

我很难过,因为似乎每次检查字符串时都会以某种方式被覆盖,即使这没有意义。

【问题讨论】:

  • 你真的想要return吗? return preg_replace($pattern, $replace, $originalString); 应该是$originalString = preg_replace($pattern, $replace, $originalString); 这样循环才能继续处理吗?
  • 我想过这个,德鲁。但是只要没有找到匹配项,foreach 循环就应该继续。如果找到匹配项,您不希望循环继续,这只会浪费 CPU 资源。这就是为什么我告诉它一旦找到就返回。我想让它提取属性名称和值,这样我就可以拥有一个能够回溯标签中所有属性的正则表达式模式(无论类型如何:img、a、p、div、等),但到目前为止我还没有能够让它成功工作。上周我在这上面花了太多时间。

标签: php regex preg-replace


【解决方案1】:

在我看来,你做的工作比你需要做的要多得多。为什么不使用preg_replace_callback 在单独的步骤中处理属性,而不是为每个可能的属性列表使用单独的正则表达式/替换?例如:

function replaceLTags($originalString){
  return preg_replace_callback('#\[l=((?>[^\s\[\]]+|\[site_url\])+)([^\]]*)\](.*?)\[/l\]#',
                               replaceWithinTags, $originalString);
}

function replaceWithinTags($groups){
  return '<a href="' . $groups[1] . '"' . 
         preg_replace('#(\s+\w+)=(\S+)#', '$1="$2"', $groups[2]) .
         '>' . $groups[3] . '</a>';
}

查看完整演示 here(已更新;请参阅 cmets)。

以下是基于 cmets 中提供的新信息的代码更新版本:

function replaceLTags($originalString){
  return preg_replace_callback('#\[l=((?>[^\s\[\]]+|\[\w+\])+)([^\]]*)\](.*?)\[/l\]#',
                               replaceWithinTags, $originalString);
}

function replaceWithinTags($groups){
  return '<a href="' . $groups[1] . '"' . 
         preg_replace(
             '#(\s+[^\s=]+)\s*=\s*([^\s=]+(?>\s+[^\s=]+)*(?!\s*=))#',
             '$1="$2"', $groups[2]) .
         '>' . $groups[3] . '</a>';
}

demo

在第一个正则表达式中,我将[site_url] 更改为\[\w+\],因此它可以匹配任何自定义填充标签。

这是第二个正则表达式的细分:

(\s+[^\s=]+)   # the attribute name and its leading whitespace
\s*=\s*
(
  [^\s=]+   # the first word of the attribute value
  (?>\s+[^\s=]+)*  # the second and subsequent words, if any
  (?!\s*=)  # prevents the group above from consuming tag names
)

最棘手的部分是匹配多词属性值。 (?&gt;\s+[^\s=]+)* 将始终使用下一个标签名称(如果有的话),但前瞻迫使它回溯。通常它一次只回退一个字符,但原子组有效地强制它回溯整个单词或根本不回溯。

【讨论】:

  • 艾伦太棒了!但是它缺少两件事:在 groups 数组中的第二项之后的“ . '>' ”,因此结束 > 不会被遗漏在第一个链接标记之外,并且能够将多个单词作为值URL 以外的属性。上周,当我试图制作一个像你的通用功能时,这就是阻碍我的原因(你的更接近了)。如果我有积分,我会为你 +1。
  • 我假设属性值不能包含空格,因为在您的原始字符串中没有任何引号。你是说其中一些可以被引用吗?如果是这样,是否可以像在 HTML 中那样用双引号引用一些用单引号引用一些?属性值是否可以包含空格而没有被引用,如class=hello world?我修复了丢失的尖括号错误,但我将推迟处理其他问题等待反馈。
  • 在方括号标签中,属性值不被引用,因为它们是在运行 htmlentities 之前传递的,因此对于特殊字符的任何引号都将被删除。
  • 关于自定义填充标签的其他问题,只有 [site_url] 会显示在模式的开头。其他标签将是 [site_name]、[category]、[manufacturer]、[collection]、[style] 和 [color],但这些标签很可能不会出现,除非出现在产品页面上,而且通常只出现在 div 或 p 标签内.我认为它们不会在 title 属性的值中找到,因为填充标签主要是为了帮助 madlib 内容,并且为了用于链接,人们需要知道一个 URL 才能使用它,然后他们可以只需输入样式或颜色即可。
  • 哦,我稍微修改了您的代码,以便能够在任何情况下处理 [l] 标签,并且能够在其中没有属性的情况下返回链接。这是修改后的版本codepad.org/iT7unQ1P。最后两个示例字符串的属性值仍然被截断,因为有多个单词。
【解决方案2】:

你搞砸了正则表达式。如果您在每次迭代中将字符串打印为:

foreach ($patterns as $pattern => $replace){
    echo "String: $originalString\n";
    if (preg_match($pattern, $originalString)){
        return preg_replace($pattern, $replace, $originalString);
    }
}

你会看到字符串没有被修改。从我的运行中,我注意到第二个正则表达式匹配。我在preg_match 调用中放置了第三个参数并打印了匹配项。这是我得到的:

Array (
    [0] => [l=[site_url]/site-category/ class=hello rel=nofollow target=_blank]Hello there[/l]
    [1] => [site_url
    [2] => /site-category/ class=hello rel=nofollow target=_blank]Hello there )

【讨论】:

  • 感谢您指出这一点!我明白你在说什么。我忘了为每个模式转义第一个标签上的尾随关闭 ]。我正在研究如何让第二个模式关闭 [.希望这会解决它。
【解决方案3】:

造成您眼前问题的原因有两个:

首先,适用的正则表达式(数组中的最后一个)中有一个错字。它在" target=" 之前有一个无关的文字右方括号。换句话说,这是:

'#^\[l=([^\s]+) class=([^\[]+) rel=([^\[]+)] target=([^\[]+)]([^\[]+)\[/l\]$#i'

应阅读:

'#^\[l=([^\s]+) class=([^\[]+) rel=([^\[]+) target=([^\[]+)]([^\[]+)\[/l\]$#i'

其次,数组中有两个正则表达式都匹配相同的字符串,不幸的是两者中更具体的一个(上面的正则表达式是我们想要的那个),排在第二位。另一个匹配的更通用的正则表达式是数组中的第二个:

'#^\[l=([^\s]+)]([^\[]+)\[/l\]$#i'

将更通用的正则表达式放在最后并删除无关的方括号可以解决问题。这是应用上述两项更改后修复的原始代码:

function replaceLTags($originalString){
    $patterns = array(
                '#^\[l\]([^\s]+)\[/l\]$#i' => '<a href="$1">$1</a>',
                '#^\[l=([^\s]+) title=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" title="$2">$3</a>',
                '#^\[l=([^\s]+) rel=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" rel="$2">$3</a>',
                '#^\[l=([^\s]+) onClick=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" onClick="$2">$3</a>',
                '#^\[l=([^\s]+) style=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" style="$2">$3</a>',
                '#^\[l=([^\s]+) onClick=([^\[]+) style=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" onClick="$2" style="$3">$4</a>',
                '#^\[l=([^\s]+) class=([^\[]+) style=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" class="$2" style="$3">$4</a>',
                '#^\[l=([^\s]+) class=([^\[]+) rel=([^\[]+) target=([^\[]+)]([^\[]+)\[/l\]$#i' => '<a href="$1" class="$2" rel="$3" target="$4">$5</a>',
                '#^\[l=([^\s]+)]([^\[]+)\[/l\]$#i'=> '<a href="$1">$2</a>'
            );

    foreach ($patterns as $pattern => $replace){
        if (preg_match($pattern, $originalString)){
            return preg_replace($pattern, $replace, $originalString);
        }
    }
}

$string = '[l=[site_url]/site-category/ class=hello rel=nofollow target=_blank]Hello there[/l]';

echo $alteredString = $format->replaceLTags($string);

请注意,这只修复了您问题中描述的直接特定错误,并没有解决您尝试完成的一些更基本的问题。我已经提出了一个更好的解决方案来回答您的后续问题:How do I make this REGEX ignore = in a tag's attribute?

但正如其他人所提到的,将两种不同的标记语言混合在一起并使用正则表达式进行处理是自找麻烦。

【讨论】:

    【解决方案4】:

    这里有一些通用代码,您可以使用它们来减少表达式,您可以随时从最终字符串中删除任何不允许的标签。

    <?php
    
    function replaceLTags($originalString) {
        if (preg_match('#^\[l\]([^\s]+)\[/l\]$#i', $originalString)) {
            // match a link with no description or tags
            return preg_replace('#^\[l\]([^\s]+)\[/l\]$#i', '<a href="$1">$1</a>', $originalString);
        } else if (preg_match('#^\[l=([^\s]+)\s*([^\]]*)\](.*?)\[/l\]#i', $originalString, $matches)) {
            // match a link with title and/or tags
            $attribs = $matches[2];
            $attrStr = '';
            if (preg_match_all('#([^=]+)=([^\s\]]+)#i', $attribs, $attribMatches) > 0) {
                $attrStr = ' ';
                for ($i = 0; $i < sizeof($attribMatches[0]); ++$i) {
                    $attrStr .= $attribMatches[1][$i] . '="' . $attribMatches[2][$i] . '" ';
                }
                $attrStr = rtrim($attrStr);
            }
    
            return '<a href="' . $matches[1] . '"' . $attrStr . '>' . $matches[3] . '</a>';
        } else {
            return $originalString;
        }
    }
    
    $strings = array(
        '[l]http://www.stackoverflow.com[/l]',
        '[l=[site_url]/site-category/ class=hello rel=nofollow target=_blank]Hello there[/l]',
        '[l=[site_url]/page.php?q=123]Link[/l]',
        '[l=http://www.stackoverflow.com/careers/ target=_blank class=default]Stack overflow[/l]'
    );
    
    foreach($strings as $string) {
        $altered = replaceLTags($string);
        echo "{$altered}<br />\n";
    }
    

    【讨论】:

    • 真的很好,德鲁。我会使用它,但我需要能够在属性值中包含多个单词(例如标题、样式或带有空格的 javascript)。除此之外,真的很酷。如果我有积分,我会为你 +1。
    • 我明白了,在值周围没有引号的情况下,使用这样的通用值确实有点困难。您可以匹配 (class|rel|title|target|...)=(...) 之类的内容,但编写该表达式的第二部分变得困难。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-05-17
    • 1970-01-01
    • 1970-01-01
    • 2019-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多