Regex/DOMDocument - сопоставление и замена текста не в ссылке
Мне нужно найти и заменить все текстовые совпадения нечувствительным к регистру образом, если текст не находится в теге привязки - например:
<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 и очень ценю некоторые базовые примеры для этой функциональности.
Ответы
Ответ 1
Вот безопасное решение UTF-8, которое работает не только с правильно отформатированными документами, но и с фрагментами документа.
Требуется mb_convert_encoding, так как loadHtml(), похоже, имеет ошибку с кодировкой UTF-8 (см. здесь и здесь).
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 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 меняет кодировку?
Я читал десятки ответов в теме, поэтому мне жаль, если я забыл кого-нибудь (просьба прокомментировать это, и я добавлю ваше и в этом случае).
Спасибо за Гордона и по-прежнему за комментарий моего другого ответа.
Ответ 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;
должны иметь возможность обрабатывать любой неверный 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);
Отрицательный lookahead гарантирует замену, только если следующий тег не является закрывающей ссылкой. Он отлично работает с вашим примером, хотя он не будет работать, если вы используете другие теги внутри своих ссылок.
Ответ 5
Вы можете использовать PHP Simple HTML DOM Parser. Он похож на DOMDocument, но, на мой взгляд, его проще использовать.
Ниже приведена альтернатива параллельно с решением Netcoder DomDocument:
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 мс).
Ответ 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;
?>
Этот способ работает. Надеюсь, вы хотите, чтобы на самом деле был чувствителен к регистру, поэтому нужно совместить с маленькой буквой.
Ответ 7
Разбор HTML с регулярными выражениями представляет собой огромную проблему, и они могут очень легко стать слишком сложными и занимать массу памяти. Я бы сказал, что лучший способ сделать это:
preg_replace('/match this text/i','replacement text');
preg_replace('/(<a[^>]*>[^(<\/a)]*)replacement text(.*?<\/a)/is',"$1match this text$3");
Если ваш replacement text
- это то, что может произойти иначе, вы можете захотеть добавить промежуточный шаг с каким-то уникальным идентификатором.