Как сохранить HTML DOMDocument без HTML-оболочки? - PullRequest
101 голосов
/ 03 февраля 2011

Я функция ниже, я изо всех сил пытаюсь вывести DOMDocument без добавления обертки тегов XML, HTML, body и p перед выводом содержимого.Предлагаемое исправление:

$postarray['post_content'] = $d->saveXML($d->getElementsByTagName('p')->item(0));

Работает только тогда, когда в контенте нет элементов уровня блока.Однако, когда это происходит, как в примере ниже с элементом h1, результирующий вывод saveXML усекается до ...

Если вам нравится

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

Есть предложения?

function rseo_decorate_keyword($postarray) {
    global $post;
    $keyword = "Jasmine Tea"
    $content = "If you like <h1>jasmine tea</h1> you will really like it with Jasmine Tea flavors. This is the last ocurrence of the phrase jasmine tea within the content. If there are other instances of the keyword jasmine tea within the text what happens to jasmine tea."
    $d = new DOMDocument();
    @$d->loadHTML($content);
    $x = new DOMXpath($d);
    $count = $x->evaluate("count(//text()[contains(translate(., 'ABCDEFGHJIKLMNOPQRSTUVWXYZ', 'abcdefghjiklmnopqrstuvwxyz'), '$keyword') and (ancestor::b or ancestor::strong)])");
    if ($count > 0) return $postarray;
    $nodes = $x->query("//text()[contains(translate(., 'ABCDEFGHJIKLMNOPQRSTUVWXYZ', 'abcdefghjiklmnopqrstuvwxyz'), '$keyword') and not(ancestor::h1) and not(ancestor::h2) and not(ancestor::h3) and not(ancestor::h4) and not(ancestor::h5) and not(ancestor::h6) and not(ancestor::b) and not(ancestor::strong)]");
    if ($nodes && $nodes->length) {
        $node = $nodes->item(0);
        // Split just before the keyword
        $keynode = $node->splitText(strpos($node->textContent, $keyword));
        // Split after the keyword
        $node->nextSibling->splitText(strlen($keyword));
        // Replace keyword with <b>keyword</b>
        $replacement = $d->createElement('strong', $keynode->textContent);
        $keynode->parentNode->replaceChild($replacement, $keynode);
    }
$postarray['post_content'] = $d->saveXML($d->getElementsByTagName('p')->item(0));
//  $postarray['post_content'] = $d->saveXML($d->getElementsByTagName('body')->item(1));
//  $postarray['post_content'] = $d->saveXML($d->getElementsByTagName('body')->childNodes);
return $postarray;
}

Ответы [ 26 ]

187 голосов
/ 19 марта 2014

Все эти ответы теперь неверны , потому что с PHP 5.4 и Libxml 2.6 loadHTML теперь есть параметр $option, который инструктирует Libxml о том, как он должен анализироватьcontent.

Поэтому, если мы загружаем HTML с этими параметрами

$html->loadHTML($content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

при выполнении saveHTML(), не будет ни doctype, ни <html>, ни <body>.

LIBXML_HTML_NOIMPLIED отключает автоматическое добавление подразумеваемых элементов html / body LIBXML_HTML_NODEFDTD предотвращает добавление типа документа по умолчанию, если он не найден.

Полная документацияо параметрах Libxml здесь

(обратите внимание, что в документах loadHTML сказано, что Libxml 2.6 необходим, но LIBXML_HTML_NODEFDTD доступен только в Libxml 2.7.8, а LIBXML_HTML_NOIMPLIED доступен вLibxml 2.7.7)

66 голосов
/ 05 августа 2011

Просто удалите узлы непосредственно после загрузки документа с помощью loadHTML ():

# remove <!DOCTYPE 
$doc->removeChild($doc->doctype);           

# remove <html><body></body></html> 
$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
18 голосов
/ 03 февраля 2011

Вместо этого используйте saveXML() и передайте documentElement в качестве аргумента.

$innerHTML = '';
foreach ($document->getElementsByTagName('p')->item(0)->childNodes as $child) {
    $innerHTML .= $document->saveXML($child);
}
echo $innerHTML;

http://php.net/domdocument.savexml

14 голосов
/ 22 мая 2014

использовать DOMDocumentFragment

$html = 'what you want';
$doc = new DomDocument();
$fragment = $doc->createDocumentFragment();
$fragment->appendXML($html);
$doc->appendChild($fragment);
echo $doc->saveHTML();
13 голосов
/ 03 февраля 2011

Аккуратный трюк - использовать loadXML, а затем saveHTML. Теги html и body вставляются на этапе load, а не на этапе save.

$dom = new DOMDocument;
$dom->loadXML('<p>My DOMDocument contents are here</p>');
echo $dom->saveHTML();

Обратите внимание, что это немного глупо, и вы должны использовать ответ Джоны, если сможете заставить его работать.

12 голосов
/ 02 июля 2017

Проблема с верхним ответом заключается в том, что LIBXML_HTML_NOIMPLIED является нестабильным .

. Он может переупорядочивать элементы (в частности, перемещая закрывающий тег верхнего элемента в конец документа),добавить случайные p теги и, возможно, множество других вопросов [1] .Он может удалить теги html и body для вас, но за счет нестабильного поведения.На производстве это красный флаг.Короче говоря:

Не используйте LIBXML_HTML_NOIMPLIED. Вместо этого используйте substr.


Подумайте об этом.Длины <html><body> и </body></html> фиксированы и находятся на обоих концах документа - их размеры никогда не меняются и не меняют своих положений.Это позволяет нам использовать substr, чтобы вырезать их:

$dom = new domDocument; 
$dom->loadHTML($html, LIBXML_HTML_NODEFDTD);

echo substr($dom->saveHTML(), 12, -15); // the star of this operation

( ЭТО НЕ ФИНАЛЬНОЕ РЕШЕНИЕ ОДНАКО! Полный ответ см. Ниже , продолжайте читать для контекста)

Мы отсекаем 12 от начала документа, потому что <html><body> = 12 символов (<<>>+html+body = 4 + 4 + 4), и мы идем назад и срезаем 15 с конца, потому что \n</body></html>= 15 символов (\n+//+<<>>+body+html = 1 + 2 + 4 + 4 + 4)

Обратите внимание, что я все еще использую LIBXML_HTML_NODEFDTD, исключая !DOCTYPE из числа включенных.Во-первых, это упрощает удаление substr тегов HTML / BODY.Во-вторых, мы не удаляем тип документа с помощью substr, потому что мы не знаем, будет ли 'default doctype' всегда иметь фиксированную длину.Но, самое главное, LIBXML_HTML_NODEFDTD останавливает синтаксический анализатор DOM от применения к документу не-HTML5-документа, что по крайней мере не позволяет парсеру обрабатывать элементы, которые он не распознает как свободный текст.

Мы знаем, чтоДело в том, что теги HTML / BODY имеют фиксированную длину и позиции, и мы знаем, что такие константы, как LIBXML_HTML_NODEFDTD, никогда не удаляются без какого-либо уведомления об устаревании, поэтому приведенный выше метод должен хорошо перейти в будущее НО ...


... единственное предостережение в том, что реализация DOM может изменить способ размещения тегов HTML / BODY в документе - например, удалениесимвол новой строки в конце документа, добавление пробелов между тегами или добавление символов новой строки.

Это можно исправить, выполнив поиск позиций открывающих и закрывающих тегов для body и используя эти смещения какдля наших длин, чтобы урезать.Мы используем strpos и strrpos, чтобы найти смещения спереди и сзади соответственно:

$dom = new domDocument; 
$dom->loadHTML($html, LIBXML_HTML_NODEFDTD);

$trim_off_front = strpos($dom->saveHTML(),'<body>') + 6;
// PositionOf<body> + 6 = Cutoff offset after '<body>'
// 6 = Length of '<body>'

$trim_off_end = (strrpos($dom->saveHTML(),'</body>')) - strlen($dom->saveHTML());
// ^ PositionOf</body> - LengthOfDocument = Relative-negative cutoff offset before '</body>'

echo substr($dom->saveHTML(), $trim_off_front, $trim_off_end);

В закрытии, повторение окончательного, будущего ответа :

$dom = new domDocument; 
$dom->loadHTML($html, LIBXML_HTML_NODEFDTD);

$trim_off_front = strpos($dom->saveHTML(),'<body>') + 6;
$trim_off_end = (strrpos($dom->saveHTML(),'</body>')) - strlen($dom->saveHTML());

echo substr($dom->saveHTML(), $trim_off_front, $trim_off_end);

Нет doctype, нет html-тега, нет тега body.Мы можем только надеяться, что парсер DOM скоро получит новый слой краски, и мы сможем более прямо устранить эти нежелательные теги.

10 голосов
/ 14 августа 2017

Сейчас 2017 год, и на этот вопрос 2011 года мне не нравятся ответы. Много регулярных выражений, большие классы, loadXML и т. Д ...

Простое решение, которое решает известные проблемы:

$dom = new DOMDocument();
$dom->loadHTML( '<html><body>'.mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8').'</body></html>' , LIBXML_HTML_NODEFDTD);
$html = substr(trim($dom->saveHTML()),12,-14);

Легко, просто, твердо, быстро. Этот код будет работать в отношении тегов HTML и кодировки, например:

$html = '<p>äöü</p><p>ß</p>';

Если кто-нибудь обнаружит ошибку, пожалуйста, скажите, я сам буду ее использовать.

Редактировать , Другие допустимые параметры, которые работают без ошибок (очень похоже на уже предоставленные):

@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$saved_dom = trim($dom->saveHTML());
$start_dom = stripos($saved_dom,'<body>')+6;
$html = substr($saved_dom,$start_dom,strripos($saved_dom,'</body>') - $start_dom );

Вы можете добавить тело самостоятельно, чтобы предотвратить какие-либо странные вещи на фуруре.

Опция Thirt:

 $mock = new DOMDocument;
 $body = $dom->getElementsByTagName('body')->item(0);
  foreach ($body->childNodes as $child){
     $mock->appendChild($mock->importNode($child, true));
  }
$html = trim($mock->saveHTML());
10 голосов
/ 07 апреля 2015

Я немного опоздал в клуб, но не хотел не поделиться методом, о котором я узнал. Прежде всего, у меня есть правильные версии для loadHTML (), чтобы принять эти приятные опции, но LIBXML_HTML_NOIMPLIED не работал в моей системе. Также пользователи сообщают о проблемах с анализатором (например, здесь и здесь ).

Решение, которое я создал, на самом деле довольно простое.

HTML для загрузки помещается в элемент <div>, поэтому в нем есть контейнер, содержащий все загружаемые узлы.

Затем этот элемент контейнера удаляется из документа (но элемент DOME все еще существует).

Тогда все прямые потомки из документа удаляются. Это включает любые добавленные теги <html>, <head> и <body> (опция LIBXML_HTML_NOIMPLIED), а также декларация <!DOCTYPE html ... loose.dtd"> (LIBXML_HTML_NODEFDTD).

Затем все прямые дочерние элементы контейнера снова добавляются в документ, и он может быть выведен.

$str = '<p>Lorem ipsum dolor sit amet.</p><p>Nunc vel vehicula ante.</p>';

$doc = new DOMDocument();

$doc->loadHTML("<div>$str</div>");

$container = $doc->getElementsByTagName('div')->item(0);

$container = $container->parentNode->removeChild($container);

while ($doc->firstChild) {
    $doc->removeChild($doc->firstChild);
}

while ($container->firstChild ) {
    $doc->appendChild($container->firstChild);
}

$htmlFragment = $doc->saveHTML();

XPath работает как обычно, просто позаботьтесь о том, чтобы сейчас было несколько элементов документа, а не один корневой узел:

$xpath = new DOMXPath($doc);
foreach ($xpath->query('/p') as $element)
{   #                   ^- note the single slash "/"
    # ... each of the two <p> element

  • PHP 5.4.36-1 + deb.sury.org ~ точное значение + 2 (cli) (построено: 21 декабря 2014 г. 20:28:53)
4 голосов
/ 27 июля 2012

Хорошо, я нашел более элегантное решение, но оно просто утомительно:

$d = new DOMDocument();
@$d->loadHTML($yourcontent);
...
// do your manipulation, processing, etc of it blah blah blah
...
// then to save, do this
$x = new DOMXPath($d);
$everything = $x->query("body/*"); // retrieves all elements inside body tag
if ($everything->length > 0) { // check if it retrieved anything in there
      $output = '';
      foreach ($everything as $thing) {
           $output .= $d->saveXML($thing);
      }
      echo $output; // voila, no more annoying html wrappers or body tag
}

Хорошо, надеюсь, это ничего не пропустит и кому-нибудь поможет?

4 голосов
/ 24 июля 2012

Используйте эту функцию

$layout = preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $layout);
...