Как сохранить HTML из DOMDocument без HTML-оболочки?

116

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

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

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

<p> Если хотите </p>

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

Какие-либо предложения?

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;
}
Скотт Б.
источник

Ответы:

217

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

Следовательно, если мы загрузим 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)

Алессандро Вендрусколо
источник
10
Это работает как шарм. Должен быть принятый ответ. Я просто добавил один флаг, и все мои головные боли прошли ;-)
Just Plain High
8
Это не работает с PHP 5.4 и Libxml 2.9. loadHTML не принимает никаких опций :(
Acyra
11
Обратите внимание, что это не совсем идеально. См. Stackoverflow.com/questions/29493678/…
Джош Левинсон
4
Извините, но это вообще не кажется хорошим решением (по крайней мере, на практике). Это действительно не должно быть общепринятым ответом. Помимо упомянутых проблем, есть также неприятная проблема с кодировкой,DOMDocument которая также влияет на код в этом ответе. Afaik DOMDocumentвсегда интерпретирует входные данные как latin-1, если во входных данных не указана другая кодировка . Другими словами: <meta charset="…">кажется, что тег нужен для входных данных, отличных от latin-1. В противном случае вывод будет прерван, например, для многобайтовых символов UTF-8.
mermshaus 05
1
LIBXML_HTML_NOIMPLIED также портит HTML-код, удаляя табуляторы, отступы и разрывы строк
Золтан Суле,
72

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

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

# remove <html><body></body></html> 
$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
Alex
источник
это более чистый ответ для меня.
KnF
39
Следует отметить, что это работает, если <body> имеет только один дочерний узел.
Ян Милин
Сработало отлично. Спасибо! Намного чище и быстрее, чем другой ответ preg.
Ligemer
Спасибо тебе за это! Я просто добавил еще один фрагмент внизу для обработки пустых узлов.
redaxmedia 01
2
Код для удаления <!DOCTYPE работает. Вторая строка разрывается, если <body>имеется более одной дочерней заметки.
Free Radical
21

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

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

http://php.net/domdocument.savexml

Иона
источник
Так лучше, но я все еще получаю <html><body> <p> обертывание содержимого.
Scott B
2
Следует отметить, что saveXML () сохранит XHTML, а не HTML.
alexantd 02
@ Скотт: это действительно странно. Он показывает, что вы пытаетесь сделать, прямо в разделе примеров. Вы уверены, что в вашем DOM нет этого HTML? Какой именно HTML находится в вашем DOMDocument? Возможно, нам нужен доступ к дочернему узлу.
Иона
@Jonah, это не странно. Когда вы делаете это, loadHTMLlibxml использует модуль парсера HTML, который вставляет недостающий скелет HTML. Следовательно, $dom->documentElementбудет корневым элементом HTML. Я исправил ваш пример кода. Теперь он должен сделать то, о чем просит Скотт.
Гордон
19

Проблема с верхним ответом в том, что 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. Во-вторых, мы не удаляем doctype с помощью, 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 скоро получит новый слой краски, и мы сможем более непосредственно устранить эти нежелательные теги.

Супер Кот
источник
Великий ответ, один небольшой комментарий, почему не $html = $dom -> saveHTML();вместо $dom -> saveHTML();повторно?
Стивен
15

Изящный трюк - использовать loadXMLа затем saveHTML. htmlИ bodyтеги вставляются на loadстадии, а не на saveсцене.

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

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

lonesomeday
источник
4
Однако это не удастся для недопустимого HTML.
Гордон
1
@Gordon Именно поэтому я поставил отказ от ответственности внизу!
lonesomeday
1
Когда я пробую это и вызываю echo $ dom-> saveHTML (), он просто возвращает пустую строку. Как будто loadXML ($ content) пуст. Когда я делаю то же самое с $ dom-> loadHTML ($ content), затем echo $ dom-> saveXML () я получаю содержимое, как и ожидалось.
Scott B
Использование loadXML при желании загрузить HTMl - это большой палец. Тем более, что LoadXML не умеет обрабатывать HTML.
botenvouwer
15

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

$html = 'what you want';
$doc = new DomDocument();
$fragment = $doc->createDocumentFragment();
$fragment->appendXML($html);
$doc->appendChild($fragment);
echo $doc->saveHTML();
JCP
источник
3
Самый чистый ответ для pre php5.4.
Ник Джонсон
Это работает для меня, как старше, так и новее, чем версия Libxml 2.7.7. Почему это должно быть только для pre php5.4?
RobbertT
У этого должно быть больше голосов. Отличный вариант для версий libxml, не поддерживающих LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD. Спасибо!
Марти Маллиган
13

Сейчас 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 );

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

Третий вариант:

 $mock = new DOMDocument;
 $body = $dom->getElementsByTagName('body')->item(0);
  foreach ($body->childNodes as $child){
     $mock->appendChild($mock->importNode($child, true));
  }
$html = trim($mock->saveHTML());
Vixxs
источник
3
Вам следует улучшить свой ответ, избегая более дорогостоящих mb_convert_encodingи вместо этого добавляя <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>и изменяя substrсоответственно. Кстати, твое здесь самое элегантное решение. Upvoted.
Hlsg
10

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

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

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

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

Затем удаляются все прямые дочерние элементы документа. Это включает в себя любой добавлено <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)
hakre
источник
у меня это не сработало с более сложным источником HTML. Он также удалил данную часть HTML.
Zoltán Süle
4

Ни одно из других решений на момент написания этой статьи (июнь 2012 г.) не смогло полностью удовлетворить мои потребности, поэтому я написал решение, которое обрабатывает следующие случаи:

  • Принимает текстовое содержимое без тегов, а также содержимое HTML.
  • Не добавлять тэги ( в том числе <doctype>, <xml>, <html>, <body>, и <p>теги)
  • Оставляет все завернутым в <p>покое.
  • Оставляет пустой текст.

Итак, вот решение, которое устраняет эти проблемы:

class DOMDocumentWorkaround
{
    /**
     * Convert a string which may have HTML components into a DOMDocument instance.
     *
     * @param string $html - The HTML text to turn into a string.
     * @return \DOMDocument - A DOMDocument created from the given html.
     */
    public static function getDomDocumentFromHtml($html)
    {
        $domDocument = new DOMDocument();

        // Wrap the HTML in <div> tags because loadXML expects everything to be within some kind of tag.
        // LIBXML_NOERROR and LIBXML_NOWARNING mean this will fail silently and return an empty DOMDocument if it fails.
        $domDocument->loadXML('<div>' . $html . '</div>', LIBXML_NOERROR | LIBXML_NOWARNING);

        return $domDocument;
    }

    /**
     * Convert a DOMDocument back into an HTML string, which is reasonably close to what we started with.
     *
     * @param \DOMDocument $domDocument
     * @return string - The resulting HTML string
     */
    public static function getHtmlFromDomDocument($domDocument)
    {
        // Convert the DOMDocument back to a string.
        $xml = $domDocument->saveXML();

        // Strip out the XML declaration, if one exists
        $xmlDeclaration = "<?xml version=\"1.0\"?>\n";
        if (substr($xml, 0, strlen($xmlDeclaration)) == $xmlDeclaration) {
            $xml = substr($xml, strlen($xmlDeclaration));
        }

        // If the original HTML was empty, loadXML collapses our <div></div> into <div/>. Remove it.
        if ($xml == "<div/>\n") {
            $xml = '';
        }
        else {
            // Remove the opening <div> tag we previously added, if it exists.
            $openDivTag = "<div>";
            if (substr($xml, 0, strlen($openDivTag)) == $openDivTag) {
                $xml = substr($xml, strlen($openDivTag));
            }

            // Remove the closing </div> tag we previously added, if it exists.
            $closeDivTag = "</div>\n";
            $closeChunk = substr($xml, -strlen($closeDivTag));
            if ($closeChunk == $closeDivTag) {
                $xml = substr($xml, 0, -strlen($closeDivTag));
            }
        }

        return $xml;
    }
}

Я также написал несколько тестов, которые будут жить в том же классе:

public static function testHtmlToDomConversions($content)
{
    // test that converting the $content to a DOMDocument and back does not change the HTML
    if ($content !== self::getHtmlFromDomDocument(self::getDomDocumentFromHtml($content))) {
        echo "Failed\n";
    }
    else {
        echo "Succeeded\n";
    }
}

public static function testAll()
{
    self::testHtmlToDomConversions('<p>Here is some sample text</p>');
    self::testHtmlToDomConversions('<div>Lots of <div>nested <div>divs</div></div></div>');
    self::testHtmlToDomConversions('Normal Text');
    self::testHtmlToDomConversions(''); //empty
}

Вы можете сами проверить, работает ли это. DomDocumentWorkaround::testAll()возвращает это:

    Succeeded
    Succeeded
    Succeeded
    Succeeded
пахарь
источник
1
HTML = / = XML, вы должны использовать загрузчик HTML для HTML.
hakre
4

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

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

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

rclai
источник
2
Не обрабатывает случай, когда loadHTML загружает строку без разметки
copndz
3

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

$layout = preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $layout);
boksiora
источник
13
Могут быть некоторые читатели, которые наткнулись на этот пост через этот пост , решили не использовать регулярное выражение для синтаксического анализа своего HTML и вместо этого использовать парсер DOM и в конечном итоге потенциально нуждаются в ответе регулярного выражения для достижения полного решения ... иронично
Робби Аверилл
Я не понимаю, почему noboy просто возвращает содержимое BODY. Не предполагается ли, что этот тег всегда присутствует, когда парсер добавляет весь заголовок / тип документа документа? Регулярное выражение выше было бы даже короче.
sergio
@boksiora "он выполняет свою работу" - тогда почему мы вообще используем методы парсера DOM?
Спасибо
@naomik я не сказал не использовать парсер DOM, конечно, есть много разных способов добиться того же результата, это зависит от вас, в то время, когда я использовал эту функцию, у меня была проблема со встроенным php dom парсер, который неправильно разбирал html5.
боксиора 01
1
Мне пришлось использовать, preg_replaceпотому что использование основанных на DOMDocument методов удаления тегов html и body не сохраняло кодировку UTF-8 :(
wizonesolutions
3

Если решение flags, на которое ответил Алессандро Вендрусколо , не работает, вы можете попробовать следующее:

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

//do your stuff..

$finalHtml = '';
$bodyTag = $dom->documentElement->getElementsByTagName('body')->item(0);
foreach ($bodyTag->childNodes as $rootLevelTag) {
    $finalHtml .= $dom->saveHTML($rootLevelTag);
}
echo $finalHtml;

$bodyTagбудет содержать ваш полный обработанный HTML-код без всех этих HTML-оберток, за исключением <body>тега, который является корнем вашего контента. Затем вы можете использовать регулярное выражение или функцию обрезки, чтобы удалить его из конечной строки (после saveHTML) или, как в случае выше, перебрать все его дочерние элементы, сохраняя их содержимое во временную переменную $finalHtmlи возвращая его (что я считаю безопаснее).

Хосе Рикардо Жуниор
источник
3

Я наткнулся на эту тему, чтобы найти способ удалить оболочку HTML. С помощьюLIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD отлично работает, но у меня проблема с utf-8. После долгих усилий я нашел решение. Я публикую это ниже, потому что у кого-то такая же проблема.

Проблема возникла из-за <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

Эта проблема:

$dom = new DOMDocument();
$dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$dom->saveHTML();

Решение 1:

$dom->loadHTML(mb_convert_encoding($document, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
    $dom->saveHTML($dom->documentElement));

Решение 2:

$dom->loadHTML($document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
utf8_decode($dom->saveHTML($dom->documentElement));
Панайотис Курсарис
источник
1
Мне приятно, что вы поделились своими выводами, но Решение 2 уже присутствует с этими точными вопросами здесь, а Решение 1 находится в другом месте. Также для проблемы решения 1 ответ неясен. Я уважаю ваши добрые намерения, но имейте в виду, что это может создать много шума, а также помешать другим найти решения, которые они ищут, что, как я полагаю, является своего рода противоположностью тому, чего вы хотите достичь своим ответом. Stackoverflow работает лучше всего, если вы обрабатываете один вопрос за раз. Просто намек.
hakre
3

Я борюсь с этим на RHEL7 с PHP 5.6.25 и LibXML 2.9. (Я знаю старые вещи 2018 года, но это Red Hat для вас.)

Я обнаружил, что решение, предложенное Алессандро Вендрусколо, за которое было много голосов, нарушает HTML, переставляя теги. То есть:

<p>First.</p><p>Second.</p>'

будет выглядеть так:

<p>First.<p>Second.</p></p>'

Это касается обоих вариантов, которые он предлагает вам использовать: LIBXML_HTML_NOIMPLIEDи LIBXML_HTML_NODEFDTD.

Решение, предложенное Алексом, идет наполовину, но оно не работает, если у <body>него более одного дочернего узла.

Для меня работает следующее решение:

Во-первых, чтобы загрузить DOMDocument, я использую:

$doc = new DOMDocument()
$doc->loadHTML($content);

Чтобы сохранить документ после массажа DOMDocument, я использую:

// remove <!DOCTYPE 
$doc->removeChild($doc->doctype);  
$content = $doc->saveHTML();
// remove <html><body></body></html> 
$content = str_replace('<html><body>', '', $content);
$content = str_replace('</body></html>', '', $content);

Я первый согласен с тем, что это не очень элегантное решение, но оно работает.

Свободный радикал
источник
2

Добавление <meta>тега вызовет исправление поведенияDOMDocument . Хорошо то, что вам вообще не нужно добавлять этот тег. Если вы не хотите использовать кодировку по вашему выбору, просто передайте ее как аргумент конструктора.

http://php.net/manual/en/domdocument.construct.php

$doc = new DOMDocument('1.0', 'UTF-8');
$node = $doc->createElement('div', 'Hello World');
$doc->appendChild($node);
echo $doc->saveHTML();

Вывод

<div>Hello World</div>

Спасибо @Bart

botenvouwer
источник
2

У меня тоже было это требование, и мне понравилось решение, опубликованное Алексом выше. Однако есть пара проблем: если <body>элемент содержит более одного дочернего элемента, итоговый документ будет содержать только первый дочерний элемент <body>, а не все из них. Кроме того, мне нужна была зачистка, чтобы обрабатывать вещи условно - только когда у вас есть документ с заголовками HTML. Я уточнил это следующим образом. Вместо удаления <body>я преобразовал его в a <div>и удалил объявление XML и <html>.

function strip_html_headings($html_doc)
{
    if (is_null($html_doc))
    {
        // might be better to issue an exception, but we silently return
        return;
    }

    // remove <!DOCTYPE 
    if (!is_null($html_doc->firstChild) &&
        $html_doc->firstChild->nodeType == XML_DOCUMENT_TYPE_NODE)
    {
        $html_doc->removeChild($html_doc->firstChild);     
    }

    if (!is_null($html_doc->firstChild) &&
        strtolower($html_doc->firstChild->tagName) == 'html' &&
        !is_null($html_doc->firstChild->firstChild) &&
        strtolower($html_doc->firstChild->firstChild->tagName) == 'body')
    {
        // we have 'html/body' - replace both nodes with a single "div"        
        $div_node = $html_doc->createElement('div');

        // copy all the child nodes of 'body' to 'div'
        foreach ($html_doc->firstChild->firstChild->childNodes as $child)
        {
            // deep copies each child node, with attributes
            $child = $html_doc->importNode($child, true);
            // adds node to 'div''
            $div_node->appendChild($child);
        }

        // replace 'html/body' with 'div'
        $html_doc->removeChild($html_doc->firstChild);
        $html_doc->appendChild($div_node);
    }
}
blackcatweb
источник
2

Как и другие участники, я сначала упивался простотой и потрясающей силой ответа @Alessandro Vendruscolo. Возможность просто передать некоторые помеченные константы конструктору казалась слишком хорошей, чтобы быть правдой. Для меня это было. У меня есть правильные версии как LibXML, так и PHP, однако независимо от того, что он все равно добавит тег HTML в структуру узлов объекта Document.

Мое решение сработало лучше, чем использование ...

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

Флаги или ....

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

# remove <html><body></body></html>
$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);

Удаление узла, которое становится беспорядочным без структурированного порядка в DOM. Опять же, фрагменты кода не могут предопределить структуру DOM.

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

До сих пор объект DOMDocument оставил мне желание ... Как и многие другие программисты, кажется ... Я знаю, что видел много разочарований в этом вопросе, так как я НАКОНЕЦ ... (после примерно 30 часов попыток и неудач) Типовое тестирование) Я нашел способ получить все это. Я надеюсь, что это поможет кому-то...

Во-первых, я цинично отношусь ко ВСЕМ ... лол ...

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

Если вы используете фрагменты кода и нуждаетесь в чистом коде, который никак не влияет на анализатор, без использования дополнительных тегов, используйте simplePHPParser .

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

Вы можете найти его файлы для загрузки здесь , инструкции по запуску здесь и его API здесь . Я настоятельно рекомендую использовать этот класс с его простыми методами, которые могут делать .find(".className")то же самое, что и метод поиска JQuery, или даже знакомые методы, такие как getElementByTagName()или getElementById()...

Когда вы сохраняете дерево узлов в этом классе, оно вообще ничего не добавляет. Вы можете просто сказать, $doc->save();и он выводит все дерево в строку без каких-либо проблем.

Теперь я буду использовать этот синтаксический анализатор для всех проектов без ограничения полосы пропускания в будущем.

GoreDefex
источник
2

У меня PHP 5.3, и ответы здесь мне не помогли.

$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);заменил весь документ только первым дочерним элементом, у меня было много абзацев, и только первый сохранялся, но решение дало мне хорошую отправную точку, чтобы написать что-то без regexкомментариев, и я почти уверен, что это можно улучшить, но если у кого-то такая же проблема, как у меня, это может быть хорошей отправной точкой.

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

    // lets get all children inside the body tag
    foreach ($doc->firstChild->firstChild->childNodes as $k => $v) {
        if($k !== 0){ // don't store the first element since that one will be used to replace the html tag
            $doc->appendChild( clone($v) ); // appending element to the root so we can remove the first element and still have all the others
        }
    }
    // replace the body tag with the first children
    $doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
    return $doc;
}

Тогда мы могли бы использовать это так:

$doc = new DOMDocument();
$doc->encoding = 'UTF-8';
$doc->loadHTML('<p>Some html here</p><p>And more html</p><p>and some html</p>');
$doc = extractDOMContent($doc);

Обратите внимание, что appendChildпринимает, DOMNodeпоэтому нам не нужно создавать новые элементы, мы можем просто повторно использовать существующие, которые реализуют, DOMNodeнапример, DOMElementэто может быть важно для сохранения кода «в здравом уме» при манипулировании несколькими документами HTML / XML.

Неизменяемый кирпич
источник
Это не сработает для фрагментов, только для одного дочернего элемента, который вы хотите сделать первым дочерним элементом документа. Это довольно ограничено и эффективно не выполняет свою работу, LIBXML_HTML_NOIMPLIEDпоскольку делает это только частично. Удаление doctype эффективно LIBXML_HTML_NODEFDTD.
hakre
2

У меня с DOMDocumentклассом 3 проблемы .

1- Этот класс загружает html с кодировкой ISO, а символы utf-8 не отображаются на выходе.

2 Даже если мы дадим LIBXML_HTML_NOIMPLIEDфлаг методу loadHtml, пока наш вход HTML не содержит корневой тег, он не будет правильно синтаксического анализа.

3- Этот класс считает теги HTML5 недопустимыми.

Поэтому я переопределил этот класс для решения этих проблем и изменил некоторые методы.

class DOMEditor extends DOMDocument
{
    /**
     * Temporary wrapper tag , It should be an unusual tag to avoid problems
     */
    protected $tempRoot = 'temproot';

    public function __construct($version = '1.0', $encoding = 'UTF-8')
    {
        //turn off html5 errors
        libxml_use_internal_errors(true);
        parent::__construct($version, $encoding);
    }

    public function loadHTML($source, $options = LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD)
    {
        // this is a bitwise check if LIBXML_HTML_NOIMPLIED is set
        if ($options & LIBXML_HTML_NOIMPLIED) {
            // it loads the content with a temporary wrapper tag and utf-8 encoding
            parent::loadHTML("<{$this->tempRoot}>" . mb_convert_encoding($source, 'HTML', 'UTF-8') . "</{$this->tempRoot}>", $options);
        } else {
            // it loads the content with utf-8 encoding and default options
            parent::loadHTML(mb_convert_encoding($source, 'HTML', 'UTF-8'), $options);
        }
    }

    private function unwrapTempRoot($output)
    {
        if ($this->firstChild->nodeName === $this->tempRoot) {
            return substr($output, strlen($this->tempRoot) + 2, -strlen($this->tempRoot) - 4);
        }
        return $output;
    }

    public function saveHTML(DOMNode $node = null)
    {
        $html = html_entity_decode(parent::saveHTML($node));
        if (is_null($node)) {
            $html = $this->unwrapTempRoot($html);
        }
        return $html;
    }

    public function saveXML(DOMNode $node = null, $options = null)
    {
        if (is_null($node)) {
            return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . PHP_EOL . $this->saveHTML();
        }
        return parent::saveXML($node);
    }

}

Теперь я использую DOMEditorвместо, DOMDocumentи до сих пор у меня это хорошо работало

        $editor = new DOMEditor();
        $editor->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        // works like a charm!
        echo $editor->saveHTML();
Mr.Hosseini
источник
Ваш пункт 1. решается с помощью mb_convert_encoding ($ string, 'HTML-ENTITIES', 'UTF-8'); перед использованием loadHTML () и 2-го, имея тег DIV в вашей вспомогательной функции, например, вокруг mb_convert_encoding (), который вы используете. Получилось у меня достаточно хорошо. В самом деле, если DIV нет, то в моем случае он автоматически добавляет параграф, что неудобно, поскольку обычно у них есть некоторый запас (бутстрап ..)
trainoasis
0

Я тоже столкнулся с этой проблемой.

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

Вот что я придумал, и он работает без проблем:

$domxpath = new \DOMXPath($domDocument);

/** @var \DOMNodeList $subset */
$subset = $domxpath->query('descendant-or-self::body/*');

$html = '';
foreach ($subset as $domElement) {
    /** @var $domElement \DOMElement */
    $html .= $domDocument->saveHTML($domElement);
}

По сути, он работает аналогично большинству представленных здесь решений, но вместо ручного труда он использует селектор xpath для выбора всех элементов в теле и объединяет их HTML-код.

Никола Петкански
источник
Как и все решения здесь, он работает не для всех случаев: если загруженная строка не начинается с разметки, <p> </p> был добавлен, тогда ваш код не работает, поскольку он добавит <p> </p> разметка в сохраненном содержимом
copndz
Честно говоря, я не тестировал это с сырым текстом, но теоретически должно работать. Для вашего конкретного случая вам может потребоваться изменить xpath на что-то вроде descendant-or-self::body/p/*.
Никола Петкански
0

мой сервер получил php 5.3 и не может обновиться, поэтому эти параметры

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD

не для меня.

Чтобы решить эту проблему, я говорю функции SaveXML напечатать элемент Body, а затем просто заменить «body» на «div».

вот мой код, надеюсь, он кому-то поможет:

<? 
$html = "your html here";
$tabContentDomDoc = new DOMDocument();
$tabContentDomDoc->loadHTML('<?xml encoding="UTF-8">'.$html);
$tabContentDomDoc->encoding = 'UTF-8';
$tabContentDomDocBody = $tabContentDomDoc->getElementsByTagName('body')->item(0);
if(is_object($tabContentDomDocBody)){
    echo (str_replace("body","div",$tabContentDomDoc->saveXML($tabContentDomDocBody)));
}
?>

utf-8 предназначен для поддержки иврита.

Томер Офер
источник
0

Ответ Alex правильный, но может вызвать следующую ошибку на пустых узлах:

Аргумент 1, переданный в DOMNode :: removeChild (), должен быть экземпляром DOMNode

А вот и мой маленький мод:

    $output = '';
    $doc = new DOMDocument();
    $doc->loadHTML($htmlString); //feed with html here

    if (isset($doc->firstChild)) {

        /* remove doctype */

        $doc->removeChild($doc->firstChild);

        /* remove html and body */

        if (isset($doc->firstChild->firstChild->firstChild)) {
            $doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
            $output = trim($doc->saveHTML());
        }
    }
    return $output;

Добавление trim () также является хорошей идеей для удаления пробелов.

redaxmedia
источник
0

Возможно, я опоздал. Но, возможно, у кого-то (вроде меня) все еще есть эта проблема.
Итак, у меня ничего из вышеперечисленного не помогло. Потому что $ dom-> loadHTML также закрывает открытые теги, а не только добавляет теги html и body.
Поэтому добавление элемента <div> у меня не работает, потому что у меня иногда бывает 3-4 незакрытых div в html-фрагменте.
Мое решение:

1.) Добавьте маркер для вырезания, затем загрузите фрагмент HTML.

$html_piece = "[MARK]".$html_piece."[/MARK]";
$dom->loadHTML($html_piece);

2.) делайте с документом все, что хотите
3.) сохраните html

$new_html_piece = $dom->saveHTML();

4.) прежде чем вернуть его, удалите теги <p> </ p> с маркера, как ни странно, он появляется только на [MARK], но не на [/ MARK] ...!?

$new_html_piece = preg_replace( "/<p[^>]*?>(\[MARK\]|\s)*?<\/p>/", "[MARK]" , $new_html_piece );

5.) удалить все до и после маркера

$pattern_contents = '{\[MARK\](.*?)\[\/MARK\]}is';
if (preg_match($pattern_contents, $new_html_piece, $matches)) {
    $new_html_piece = $matches[1];
}

6.) вернуть

return $new_html_piece;

Было бы намного проще, если бы LIBXML_HTML_NOIMPLIED работал у меня. Можно, но это не так. PHP 5.4.17, libxml версии 2.7.8.
Я нахожу действительно странным, я использую парсер HTML DOM, а затем, чтобы исправить эту "штуку", мне нужно использовать регулярное выражение ... Все дело в том, чтобы не использовать регулярное выражение;)

Джо
источник
То, что вы здесь делаете, выглядит опасно, stackoverflow.com/a/29499718/367456 должен сделать эту работу за вас.
hakre
К сожалению, это ( stackoverflow.com/questions/4879946/… ) для меня не сработает. Как я уже сказал: «Так что добавление элемента <div> у меня не работает, потому что у меня иногда бывает 3-4 незакрытых div в html-фрагменте». По какой-то причине DOMDocument хочет закрыть все «незакрытые» элементы. В некоторых случаях я получу фрагмент в шорткоде или другом маркере, удалю фрагмент и хочу манипулировать другим фрагментом документа, когда я закончу с этим, я вставлю фрагмент обратно.
Джо
Должна быть возможность оставить элемент div и работать с элементом body после загрузки собственного содержимого. Элемент body должен добавляться неявно при загрузке фрагмента.
hakre 01
Моя проблема в том, что мой фрагмент содержит незакрытый тег. Он должен оставаться незакрытым, и DOMDocument закроет эти элементы. Fregment как: < div >< div > ... < /div >. Я все еще ищу решения.
Джо
Хм, я думаю, у тегов div всегда есть закрывающая пара. Возможно, Tidy справится с этим, он может работать и с фрагментами.
hakre 02
0

Для всех, кто использует Drupal, есть встроенная функция для этого:

https://api.drupal.org/api/drupal/modules!filter!filter.module/function/filter_dom_serialize/7.x

Код для справки:

function filter_dom_serialize($dom_document) {
  $body_node = $dom_document->getElementsByTagName('body')->item(0);
  $body_content = '';

  if ($body_node !== NULL) {
    foreach ($body_node->getElementsByTagName('script') as $node) {
      filter_dom_serialize_escape_cdata_element($dom_document, $node);
    }

    foreach ($body_node->getElementsByTagName('style') as $node) {
      filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/');
    }

    foreach ($body_node->childNodes as $child_node) {
      $body_content .= $dom_document->saveXML($child_node);
    }
    return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content);
  }
  else {
    return $body_content;
  }
}
leon.nk
источник
Upvoted. Использование этой функции из Drupal API отлично работает на моем сайте Drupal 7. Я предполагаю, что те, кто не использует Drupal, могут просто скопировать функцию на свой сайт - поскольку в этом нет ничего специфичного для Drupal.
Free Radical
0

Вы можете использовать tidy с show-body-only:

$tidy = new tidy();
$htmlBody = $tidy->repairString($html, [
  'indent' =>  true,
  'output-xhtml' => true,
  'show-body-only' => true
], 'utf8');

Но помните: tidy удалите некоторые теги, такие как значки Font Awesome: проблемы с отступом HTML (5) с помощью PHP

Рафа Родригес
источник
-1
#remove doctype tag
$doc->removeChild($doc->doctype); 

#remove html & body tags
$html = $doc->getElementsByTagName('html')[0];
$body = $html->getElementsByTagName('body')[0];
foreach($body->childNodes as $child) {
    $doc->appendChild($child);
}
$doc->removeChild($html);
Дилан Макси
источник
Хотите поделиться, почему именно -1?
Дилан Макси,