【问题标题】:Preg_replace, please little support?preg_replace,请大家多多支持?
【发布时间】:2024-01-23 22:48:02
【问题描述】:

所以我有这个 preg_replace 函数(来自其他人编写的脚本),它为所有链接添加了 target="_blank" 属性。但是,当我有一个已经具有 target="_blank" 属性的链接时,它会添加另一个链接。这会导致链接中出现双 target="_blank" 属性。有没有办法在下面的 preg_replace 函数中解决这个问题?

$text = preg_replace('%(<a[^>]+)(href="https?://)((?:(?!(' . $host . '))[^"])+|(?:(?=(' . $host . '/' . $base_url . '/))[^"]+))"%i', '$1$2$3"target="_blank"', $text);

非常感谢!

【问题讨论】:

    标签: php html-parsing preg-replace


    【解决方案1】:

    Regex 不是这种 html 操作的好方法,而且非常方便。一种首选方法是使用 DOMDocument,它是一种使用 libxml 从 HTML 文档构建节点树(DOMNode 实例)的工具。 DOMNode 类有几个有用的方法和属性来做你想做的事情,比如hasAttributesetAttribute

    $dom = new DOMDocument;
    $dom->loadHTMLFile('yourhtmlfile.html'); 
    // or $dom->loadHTML($htmlContent); //if the html is already in a variable
    
    // get all the link nodes
    $linkNodeList = $dom->getElementsByTagName('a');
    
    foreach($linkNodeList as $linkNode) {
        if (!$linkNode->hasAttribute('target'))
            $linkNode->setAttribute('target', '_blank');
    }
    
    $result = $dom->saveHTML();
    

    注意:如果要在 href 属性中定位特定域和基本 url,可以将 if 语句更改为:

    if ( $linkNode->hasAttribute('target')
      && strpos($host . '/' . $baseurl, $linkNode->getAttribute('href')) !== false)
    

    或者另一种方式是使用 XPath 查询来立即定位您想要的链接:

    $dom = new DOMDocument;
    $dom->loadHTMLFile('yourhtmlfile.html'); 
    
    $xp = new DOMXPath($dom);
    
    $query = '//a[contains(@href, "' . $host . '/' . $baseurl . '") and not(@target)]';
    
    $linkNodeList = $xp->query($query);
    
    foreach ($linkNodeList as $linkNode) {
        $linkNode->setAttribute('target', '_blank');
    }
    
    $result = $dom->saveHTML(); 
    

    注意:如果您使用的是部分 html 文档,DOMDocument 会自动添加一个 DTD 并创建 html 和 body 标记。为了防止这种情况,有几种解决方法:

    使用 PHP >= 5.4 时,您需要在加载文档时添加两个选项:

    $dom->loadHTMLFile('yourhtmlfile.html', LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
    

    (有时由于未知原因,未定义常量 LIBXML_HTML_NODEFDTDLIBXML_HTML_NOIMPLIED。在这种情况下,您可以将它们替换为它们的值 48192,或者在之前定义它们,或者使用直接81964 | 8192的结果)

    使用 PHP >= 5.1 的方法是使用 saveXML 一个一个地保存每个 body childNodes 并将字符串连接起来:

    $result = '';
    $bodyChildNodes = $dom->getElementsByTagName('body')->item(0)->childNodes;
    foreach ($bodyChildNodes as $childNode) {
        $result .= $dom->saveXML($childNode);
    }
    

    对于较低的 PHP 版本,使用字符串方法:

    $result = preg_replace('~\A.*?<body>|</body></html>\z~s', '', $result);
    

    $result = explode('<body>', $result, 2);
    $result = substr($result[1], 0, -14); // 14 is the string length of "</body></html>"
    

    【讨论】:

    • 谢谢,让我大开眼界:) 我在我的代码中尝试过,它可以工作。我使用 $text 变量来加载。工作正常,但它也输出 (我不需要)代码标签。有没有办法解决这个问题?
    • @RobbertT:我已经添加了关于这个问题的详细信息。
    • 太棒了,谢谢!还有一个问题:我也将这种技术用于 rel="nofollow" 属性。一切正常,但我有这个变量 $follow_list 包含域白名单(不应该具有 nofollow 属性的域)。旧代码是:$text = preg_replace('%(&lt;a[^&gt;]+)(href="https?://)((?:(?!(' . $host . $follow_list . '))[^"])+|(?:(?=(' . $host . '/' . $base_url . '/))[^"]+))"%i', '$1$2$3" rel="nofollow" ', $text); 我如何在 if 语句中实现这个“检查”if (!$linkNode-&gt;hasAttribute('rel', 'nofollow') 任何想法?
    • @RobbertT:请参阅 PHP 手册以更好地了解不同方法的工作原理(hasAttributegetAttributesetAttribute)。如果您需要检查多个url主机或路径,我建议您获取href属性并使用parse_url提取主机和路径。然后,您可以轻松地将它们与您的列表进行比较。但是,如果您的白名单不是很大,您可以使用 XPath 查询。 (注意:也可以在 XPath 查询中使用自己的 php 函数,详情请参阅手册)
    • @RobbertT:如果我是你,我会将$follow_list 构建为一个数组,而我将使用in_array。关于strpos,永远不要写strpos(...) == false,因为strpos可以返回偏移量0,它被转换为false(所以即使找到字符串,你的条件也会成功,实际上0 == false在PHP中是一个真实的条件),总是使用strpos(...) === false 来检查类型。