Як зберегти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
Це працює як шарм. Повинна бути прийнята відповідь. Я щойно додав один прапор, і всі мої головні болі минули ;-)
Просто рівний

8
Це не працює з PHP 5.4 та Libxml 2.9. loadHTML не приймає жодних варіантів :(
Acyra

11
Зауважте, що це не зовсім ідеально. Дивіться stackoverflow.com/questions/29493678/…
Джош Левінсон,

4
Вибачте, але це зовсім не здається хорошим рішенням (принаймні, не на практиці). Це дійсно не повинно бути прийнятою відповіддю. Крім згаданих питань, є також неприємний питання кодування з , DOMDocumentщо також впливає на код в цій відповіді. Afaik, DOMDocumentзавжди інтерпретує вхідні дані як латинську-1, якщо вхід не вказує іншу діаграму . Іншими словами: <meta charset="…">тег, здається, потрібен для введення даних, що не є латинським-1. Інакше вихід буде порушений, наприклад, для багатобайтових символів UTF-8.
mermshaus

1
LIBXML_HTML_NOIMPLIED також псує HTML-код, видаляючи вкладки, відступи та розриви рядків
Zoltán Süle

72

Просто видаліть вузли безпосередньо після завантаження документа з loadHTML ():

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

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

це чистіша відповідь для мене.
KnF

39
Слід зазначити, що це працює, якщо <body> має лише один дочірній вузол.
Ян Мілін

Працювали чудово. Дякую! Набагато чистіше і швидше, ніж на іншу відповідь preg.
Лігемер

Дякую за це! Я просто додав ще один фрагмент внизу, щоб обробити порожні вузли.
redaxmedia

2
Код для видалення <!DOCTYPE працює. Другий рядок переривається, якщо <body>має більше ніж одну дочірню ноту.
Безкоштовний радикальний

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> обгортання вмісту.
Скотт Б


2
Слід зазначити, що saveXML () збереже XHTML, а не HTML.
Олександр

@Scott: це дійсно дивно. Це показує, що ви намагаєтесь зробити прямо там, у розділі прикладів. Ви впевнені, що цього 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. По-друге, ми не видаляємо доктрип, 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);

Ні доктіпу, ні тега 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();

Зверніть увагу, що це трохи хакітно, і вам слід скористатися відповіддю Йони, якщо ви зможете змусити його працювати.


4
Однак недійсний HTML не вдасться.
Гордон

1
@Gordon Саме тому я поставив відмову внизу!
самотній день

1
Коли я спробую це і відлунюю $ dom-> saveHTML (), він просто повертає порожню рядок. Ніби loadXML ($ вміст) порожній. Коли я те ж саме роблю з $ dom-> loadHTML ($ content), тоді echo $ dom-> saveXML () я отримую вміст, як очікувалося.
Скотт Б

Використання 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();

3
Найчистіша відповідь для попереднього php5.4.
Нік Джонсон

Це працює для мене як старшої, так і новішої, ніж версія Libxml 2.7.7. Чому це було б виключно для pre php5.4?
РобертТ

Це повинно мати більше голосів. Відмінний варіант для версій 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 );

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

Варіант 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());

3
Ви повинні вдосконалити свою відповідь, уникаючи більш дорогих mb_convert_encodingі замість цього додавши <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>та змінюючи substrвідповідно. До речі, ваше - це найелегантніше рішення. Отримано.
Hlsg

10

Я трохи запізнився в клубі, але не хотів не ділитися методом, про який я дізнався. Перш за все, у мене є правильні версії для 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)

він не працював для мене зі складнішим джерелом HTML. Він також видалив задану частину HTML.
Золтан Сюле

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.
хакре

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
}

Гаразд, сподіваємось, це нічого не опускає і допомагає комусь?


2
Не обробляє випадок, коли loadHTML завантажує рядок без розмітки
copndz

3

Використовуйте цю функцію

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

13
Можливо, є деякі читачі, які наткнулися на цю публікацію через цю публікацію , вирішили не використовувати регулярний вимір для розбору свого HTML і використовувати замість цього аналізатор DOM, і в кінцевому підсумку потенційно потрібен відповідь на регулярний вираз для досягнення повного рішення ... ironic
Роббі Аверилл

Я не розумію, чому noboy просто повертає вміст BODY. Чи не вважається, що цей тег завжди присутній, коли аналізатор додає весь заголовок / доктіп документа? Регекс вище був би навіть коротшим.
Серхіо

@boksiora "це робить роботу" - тоді чому ми в першу чергу використовуємо методи парсерів DOM?
Дякую

@naomik Я не сказав не використовувати парсер DOM, але, звичайно, існує багато різних способів досягти того ж результату, це залежить від вас, у той час, коли я використовував цю функцію, у мене виникли проблеми із вбудованим домом php парсер, який неправильно розбирав html5.
boksiora

1
Мені довелося користуватися preg_replace оскільки використання методів видалення HTML і тегів на основі DOMDocument не зберігало кодування UTF-8 :(
wizonesolutions

3

Якщо рішення про прапорці, на яке відповів Алессандро Вендрусколо , не працює, ви можете спробувати це:

$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

Я борюся з цим на RHEL7 під управлінням PHP 5.6.25 та LibXML 2.9. (Старі речі в 2018 році, я знаю, але це Red 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


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);
    }
}

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 Object залишив мені бажання ... Як і у багатьох інших програмістів, здається ... Я знаю, що я бачив багато розчарувань у цьому питанні, так як я остаточно .... (після приблизно 30 годин спроб і помилок тестування типу) Я знайшов спосіб отримати це все. Я сподіваюся, що це комусь допоможе ...

По-перше, я цинічний ВСІМ ... хаха ...

Я б прожив усе життя, перш ніж погодитись з ким-небудь, що сторонній клас у будь-якому випадку потрібен у цій справі використання. Я дуже був і не є фанатом використання будь-якої сторонньої структури класу, проте я натрапив на великий аналізатор. (приблизно 30 разів в Google, перш ніж я поступився, тому не відчувайте себе самотнім, якщо ви цього уникали, оскільки це виглядало кульгом неофіційного в будь-якому випадку ...)

Якщо ви використовуєте фрагменти коду і вам потрібен код, чистий і не впливає на аналізатор будь-яким способом, без використання додаткових тегів, тоді використовуйте simplePHPParser .

Це дивовижно і дуже схоже на JQuery. Мене не часто вражає, але цей клас використовує багато хороших інструментів, і у мене ще не було помилок розбору. Я величезний шанувальник можливості робити те, що робить цей клас.

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

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

Зараз я буду використовувати цей аналізатор для всіх проектів, що не мають обмеженої смуги, в майбутньому.


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як це робить лише частково. Видалення вчення ефективно LIBXML_HTML_NODEFDTD.
хакре

2

Я наткнувся на цю тему, щоб знайти спосіб видалити обгортку 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 найкраще працює, якщо ви обробляєте одне питання за один раз. Просто натяк.
хакре

2

Я стикаюся з 3 проблемами з DOMDocumentкласом.

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();

Ваша точка 1. вирішується за допомогою mb_convert_encoding ($ string, 'HTML-ENTITIES', 'UTF-8'); перед тим, як використовувати loadHTML () та 2.nd, маючи тег 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

Відповідь Алекса правильна, але може спричинити помилку на порожніх вузлах:

Аргумент 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;

Додавання обрізки () також є хорошою ідеєю для видалення пробілів.


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 повинен зробити роботу за вас.
хакре

На жаль, це ( stackoverflow.com/questions/4879946/… ) не допоможе мені. Як я вже говорив: "Отже, додавання елемента <div> для мене не працює, тому що мені іноді подобається 3-4 незакритих div у html-фрагменті" Чомусь DOMDocument хочуть закрити всі "не закриті" елементи. У такому випадку я отримаю фрагмент в межах короткого коду або іншого маркера, видаляю фрагмент, і я хочу маніпулювати іншим фрагментом документа, коли закінчу з цим, я вставляю фрегмент назад.
Джо

Потрібно мати можливість залишити елемент div і оперувати елементом body після завантаження власного вмісту. Елемент тіла слід додавати неявно, коли ви завантажуєте фрагмент.
хакре

Моя проблема полягає в тому, що мій фрагмент містить незакритий тег. Він повинен залишатися незакритим, і DOMDocument закриє ці елементи. Fregment як: < div >< div > ... < /div >. Я все ще шукаю рішення.
Джо

Хм, я думаю, що у тегів div завжди є пара, що закриває. Можливо, Tidy впорається з цим, він може працювати і з фрагментами.
хакре

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

Отримано. Використовуйте цю функцію від API Drupal, добре працює на моєму сайті Drupal 7. Я думаю, що ті, хто не використовує Drupal, можуть просто скопіювати функцію на свій власний сайт - оскільки в цьому немає нічого конкретного для Drupal.
Безкоштовний радикал


-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?
Ділан Максей

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.