Regex / DOMDocument - сопоставлять и заменять текст, отсутствующий в ссылке - PullRequest
12 голосов
/ 28 октября 2010

Мне нужно найти и заменить все совпадения текста без учета регистра, если только текст не находится внутри тега привязки - например:

<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>

Поиск слова «сопоставить этот текст» заменит толькопервый и последний экземпляр.

[Правка] Согласно комментарию Гордона, в этом случае может быть предпочтительнее использовать DOMDocument.Я совсем не знаком с расширением DOMDocument и буду очень признателен за некоторые базовые примеры для этой функциональности.

Ответы [ 7 ]

17 голосов
/ 18 ноября 2010

Вот безопасное решение UTF-8, которое работает не только с правильно отформатированными документами, но и с фрагментами документов.

Требуется mb_convert_encoding, потому что loadHtml (), похоже, имеет ошибку с UTF-8 кодирования (см. здесь и здесь ).

mb_substr обрезает тег body из вывода, таким образом вы возвращаете исходное содержимое без дополнительной разметки.

<?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.найти и заменить ключевые слова гиперссылками во фрагменте html через php dom
2.Regex / DOMDocument - сопоставить и заменить текст, отсутствующий в ссылке
3.проблема php с русским языком
4.Почему DOM меняет кодировку?

Я прочитал десятки ответов в теме, поэтому мне жаль, если я кого-то забыл (пожалуйста, прокомментируйте это, и я добавлю ваши также в этом случае).

Спасибо за Гордона и все еще за комментирование моего другого ответа .

5 голосов
/ 11 ноября 2010

Попробуйте это:

$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 голосов
/ 12 ноября 2010

Это нерекурсивный подход без использования стека, использующий обход по предварительному порядку дерева 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); и 3 строки кода после $dom=new DOMDocument; должны обрабатывать любой искаженный HTML.

2 голосов
/ 16 ноября 2010
$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);

Отрицательный прогноз гарантирует, что замена произойдет, только если следующий тег не является закрывающей ссылкой. Он отлично работает с вашим примером, но не будет работать, если вы будете использовать другие теги в своих ссылках.

1 голос
/ 16 ноября 2010

Вы можете использовать PHP Простой HTML DOM Parser . Он похож на DOMDocument, но, на мой взгляд, его проще использовать. Вот альтернатива параллельно с Решение DomDocument Netcoder :

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 (что неудивительно) намного быстрее (~ 4 мс против ~ 77 мс).

0 голосов
/ 11 ноября 2010

Синтаксический анализ HTML с помощью регулярных выражений является огромной проблемой, и они могут очень легко оказаться слишком сложными и потребовать много памяти. Я бы сказал, что лучший способ сделать это:

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

Если ваш replacement text - это то, что может произойти в противном случае, вы можете добавить промежуточный шаг с некоторым уникальным идентификатором.

0 голосов
/ 11 ноября 2010
<?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;
?>

Этот способ работает. Надеюсь, вы хотите действительно чувствительный к регистру, так что сопоставьте с маленькой буквы.

...