【问题标题】:Regex / DOMDocument - match and replace text not in a linkRegex / DOMDocument - 匹配和替换不在链接中的文本
【发布时间】:2011-05-01 23:35:49
【问题描述】:

我需要以不区分大小写的方式查找和替换所有文本匹配项,除非文本位于锚标记内 - 例如:

<p>Match this text and replace it</p>
<p>Don't <a href="/">match this text</a></p>
<p>We still need to match this text and replace it</p>

搜索“匹配此文本”只会替换第一个实例和最后一个实例。

[编辑] 根据 Gordon 的评论,在这种情况下可能更喜欢使用 DOMDocument。我完全不熟悉 DOMDocument 扩展,并且非常感谢此功能的一些基本示例。

【问题讨论】:

  • 在这里使用 DOM as shown 并适应
  • 锚内嵌套标签的首选行为是什么,例如&lt;p&gt;This is &lt;a href="#"&gt;a link &lt;span&gt;with &lt;strong&gt;don't match this text&lt;/strong&gt; content&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;

标签: php regex xpath preg-replace domdocument


【解决方案1】:

这是一个 UTF-8 安全解决方案,它不仅适用于格式正确的文档,还适用于文档片段。

需要 mb_convert_encoding,因为 loadHtml() 似乎存在 UTF-8 编码的错误(请参阅herehere)。

mb_substr 正在从输出中修剪正文标签,这样您就可以在没有任何额外标记的情况下取回原始内容。

<?php
$html = '<p>Match this text and replace it</p>
<p>Don\'t <a href="/">match this text</a></p>
<p>We still need to match this text and replace itŐŰ</p>
<p>This is <a href="#">a link <span>with <strong>don\'t match this text</strong> content</span></a></p>';

$dom = new DOMDocument();
// loadXml needs properly formatted documents, so it's better to use loadHtml, but it needs a hack to properly handle UTF-8 encoding
$dom->loadHtml(mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"));

$xpath = new DOMXPath($dom);

foreach($xpath->query('//text()[not(ancestor::a)]') as $node)
{
    $replaced = str_ireplace('match this text', 'MATCH', $node->wholeText);
    $newNode  = $dom->createDocumentFragment();
    $newNode->appendXML($replaced);
    $node->parentNode->replaceChild($newNode, $node);
}

// get only the body tag with its contents, then trim the body tag itself to get only the original content
echo mb_substr($dom->saveXML($xpath->query('//body')->item(0)), 6, -7, "UTF-8");

参考:
1. find and replace keywords by hyperlinks in an html fragment, via php dom
2. Regex / DOMDocument - match and replace text not in a link
3. php problem with russian language
4. Why Does DOM Change Encoding?

我阅读了该主题的几十个答案,如果我忘记了某人,我很抱歉(请评论它,在这种情况下我也会添加你的)。

感谢 Gordon,感谢您对 my other answer 的评论。

【讨论】:

  • +1 用于尝试 DOM :) 不过,这不考虑 &lt;a&gt; 元素的文本节点内的内联元素。 //text()[not(ancestor::a)] 的 XPath 将仅返回 &lt;a&gt; 树之外的 DOMText 节点。实际上,我认为迄今为止的答案都没有考虑到这一点。
  • @Gordon 能否请您提供此案例的文本字符串?
  • @styu &lt;p&gt;This is &lt;a href="#"&gt;a link &lt;span&gt;with &lt;strong&gt;inline&lt;/strong&gt; content&lt;/span&gt;&lt;/a&gt;&lt;/p&gt; - 当您遍历 //text 的结果时,您将获得文档中的所有文本节点。你只挑出那些有直接父 &lt;a&gt; 元素的人,而不是那些上面有&lt;a&gt; 元素的人。
  • @Gordon 我已根据您的建议编辑了我的答案。
  • @styu 我最终通过添加 $replaced = str_replace('&','&',$replaced); 解决了这个问题- 这有效地用 xml 实体替换了 & 符号
【解决方案2】:

试试这个:

$dom = new DOMDocument;
$dom->loadHTML($html_content);

function preg_replace_dom($regex, $replacement, DOMNode $dom, array $excludeParents = array()) {
  if (!empty($dom->childNodes)) {
    foreach ($dom->childNodes as $node) {
      if ($node instanceof DOMText && 
          !in_array($node->parentNode->nodeName, $excludeParents)) 
      {
        $node->nodeValue = preg_replace($regex, $replacement, $node->nodeValue);
      } 
      else
      {
        preg_replace_dom($regex, $replacement, $node, $excludeParents);
      }
    }
  }
}

preg_replace_dom('/match this text/i', 'IT WORKS', $dom->documentElement, array('a'));

【讨论】:

    【解决方案3】:

    这是使用 DOM 树的前序遍历的无堆栈非递归方法。

      libxml_use_internal_errors(TRUE);
      $dom=new DOMDocument('1.0','UTF-8');
    
      $dom->substituteEntities=FALSE;
      $dom->recover=TRUE;
      $dom->strictErrorChecking=FALSE;
    
      $dom->loadHTMLFile($file);
      $root=$dom->documentElement;
      $node=$root;
      $flag=FALSE;
      for (;;) {
          if (!$flag) {
              if ($node->nodeType==XML_TEXT_NODE &&
                  $node->parentNode->tagName!='a') {
                  $node->nodeValue=preg_replace(
                      '/match this text/is',
                      $replacement, $node->nodeValue
                  );
              }
              if ($node->firstChild) {
                  $node=$node->firstChild;
                  continue;
              }
         }
         if ($node->isSameNode($root)) break;
         if ($flag=$node->nextSibling)
              $node=$node->nextSibling;
         else
              $node=$node->parentNode;
     }
     echo $dom->saveHTML();
    

    libxml_use_internal_errors(TRUE);$dom=new DOMDocument; 之后的 3 行代码应该能够处理任何格式错误的 HTML。

    【讨论】:

      【解决方案4】:
      $a='<p>Match this text and replace it</p>
      <p>Don\'t <a href="/">match this text</a></p>
      <p>We still need to match this text and replace it</p>';
      
      echo preg_replace('~match this text(?![^<]*</a>)~i','replacement',$a);
      

      负前瞻确保仅当下一个标签不是关闭链接时才会发生替换。它适用于您的示例,但如果您碰巧在链接中使用其他标签,它将无法正常工作。

      【讨论】:

        【解决方案5】:

        您可以使用PHP Simple HTML DOM Parser。它类似于 DOMDocument,但在我看来它更易于使用。 这是与Netcoder's DomDocument solution并行的替代方案:

        function replaceWithSimpleHtmlDom($html_content, $search, $replace, $excludedParents = array()) {
            require_once('simple_html_dom.php');
            $html = str_get_html($html_content);
            foreach ($html->find('text') as $element) {
                if (!in_array($element->parent()->tag, $excludedParents))
                    $element->innertext = str_ireplace($search, $replace, $element->innertext);
            }
            return (string)$html;
        }
        

        我刚刚根据我的 DomDocument 解决方案分析了这段代码(女巫打印完全相同的输出),并且 DomDocument (不足为奇)更快(~4ms 对 ~77ms)。

        【讨论】:

        • 建议的第三方替代 SimpleHtmlDom 实际使用 DOM 而不是字符串解析:phpQueryZend_DomQueryPathFluentDom
        • @Gordon:我认为它们都是通过解析字符串(包括 DOMDocument)来构建 DOM。问题是这些是如何做到的(例如,他们是否将文档与不需要的实体弄乱了,或者他们只是在做他们的工作)。速度在这里不是一个真正的问题,因为您只想在文档被修改时处理它。无论如何,感谢您的建议,我会进一步调查。
        • @styu 所有这些都是基于 DOM 并且 DOM 使用 libxml。
        • @Gordon 也许 libxml 中存在一个错误,但如果它们都使用 DOM,那么它们都有相同的问题(它们只是同一个库的不同包装器)。 phpQuery 和 Zend_Dom 可以在没有 DocType 声明的情况下正常工作,但它们都不能处理 UTF-8 编码。他们正在将 ÁÍŰŐ 转换为 ÃŰŠ或 ÁÍŰŐ如果您知道 DOM 的正确解决方案,请描述它,我会很乐意使用它。
        • @styu DOM 可以在 UTF-8 上正常工作,除非您告诉它,否则不会转换任何内容。如果您在使用 DOM 时需要帮助,请随时提出问题,我可能会倾向于回答。 Some of my many previous answers on DOM usage might help you too, too,可能Best methods to parse HTML
        【解决方案6】:
        <?php
        $a = '<p>Match this text and replace it</p>
        <p>Don\'t <a href="/">match this text</a></p>
        <p>We still need to match this text and replace it</p>
        ';
        $res = preg_replace("#[^<a.*>]match this text#",'replacement',$a);
        echo $res;
        ?>
        

        这种方式有效。希望你真的要区分大小写,所以要匹配小写字母。

        【讨论】:

        • 很抱歉,但这在很多情况下都行不通。现在,您正在寻找“匹配此文本”,前面是除 &lt;.*&gt; 之外的任何字符...
        • 这段代码真的不能胜任这项工作。有十几个 senarios 无法完成它的工作。
        【解决方案7】:

        使用正则表达式解析 HTML 是一个巨大的挑战,它们很容易变得过于复杂并占用大量内存。我想说最好的方法是这样做:

        preg_replace('/match this text/i','replacement text');
        preg_replace('/(<a[^>]*>[^(<\/a)]*)replacement text(.*?<\/a)/is',"$1match this text$3");
        

        如果您的 replacement text 是其他情况下可能发生的事情,您可能需要添加一个带有一些唯一标识符的中间步骤。

        【讨论】:

        • 巨大的挑战是一个很好的表达方式:)
        • 有点轻描淡写,嗯? :) 对于某些事情,这几乎是不可能的。不过,这个小任务几乎可以管理。
        • 不错的尝试,“replace back”确实避免了此操作的几个潜在陷阱,但我认为您的解决方案在嵌套标签、跨越多行的标签和其他几种情况下仍然会失败。做到这一点的唯一方法是使用真正解析 DOM 的东西。
        • @Caleb - 同意。 (尽管我添加了 s 修饰符以使其适用于多行标签。)我认为将标签嵌套在 标签内并不常见。这取决于 OP 需要它的强大程度,具体取决于它的使用位置。
        猜你喜欢
        • 2023-01-12
        • 2015-06-24
        • 1970-01-01
        • 1970-01-01
        • 2013-10-24
        • 1970-01-01
        • 2020-09-01
        • 1970-01-01
        • 2020-02-24
        相关资源
        最近更新 更多