Jak zachować HTML z DOMDocument bez opakowania HTML?

116

Jestem funkcją poniżej, staram się wyprowadzić DOMDocument bez dołączania opakowań XML, HTML, body i znaczników p przed wyjściem zawartości. Sugerowana poprawka:

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

Działa tylko wtedy, gdy treść nie zawiera elementów blokowych. Jeśli jednak tak się stanie, jak w poniższym przykładzie z elementem h1, wynikowy wynik funkcji saveXML jest obcinany do ...

<p> Jeśli chcesz </p>

Wskazano mi ten post jako możliwe obejście, ale nie mogę zrozumieć, jak zaimplementować go w tym rozwiązaniu (zobacz zakomentowane próby poniżej).

Jakieś sugestie?

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;
}
Scott B.
źródło

Odpowiedzi:

217

Wszystkie te odpowiedzi są teraz błędne , ponieważ od wersji PHP 5.4 i Libxml 2.6 loadHTMLma teraz $optionparametr, który instruuje Libxml, jak powinien analizować zawartość.

Dlatego jeśli załadujemy HTML z tymi opcjami

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

kiedy to zrobisz, saveHTML()nie będzie doctype, nie <html>, i nie <body>.

LIBXML_HTML_NOIMPLIEDwyłącza automatyczne dodawanie domniemanych elementów html / body LIBXML_HTML_NODEFDTDzapobiega dodaniu domyślnego typu doctype, gdy nie zostanie znaleziony.

Pełna dokumentacja dotycząca parametrów Libxml znajduje się tutaj

(Zauważ, że loadHTMLdokumenty mówią, że Libxml 2.6 jest potrzebny, ale LIBXML_HTML_NODEFDTDjest dostępny tylko w Libxml 2.7.8 i LIBXML_HTML_NOIMPLIEDjest dostępny w Libxml 2.7.7)

Alessandro Vendruscolo
źródło
10
To działa jak urok. Powinna być zaakceptowana odpowiedź. Właśnie dodałem jedną flagę i wszystkie bóle głowy ustąpiły ;-)
Just Plain High
8
To nie działa z PHP 5.4 i Libxml 2.9. loadHTML nie akceptuje żadnych opcji :(
Acyra
11
Zauważ, że nie jest to całkiem idealne. Zobacz stackoverflow.com/questions/29493678/…
Josh Levinson,
4
Przepraszamy, ale to wcale nie wydaje się być dobrym rozwiązaniem (przynajmniej nie w praktyce). To naprawdę nie powinna być akceptowana odpowiedź. Oprócz wspomnianych problemów występuje również nieprzyjemny problem z kodowaniem,DOMDocument który wpływa również na kod w tej odpowiedzi. Afaik, DOMDocumentzawsze interpretuje dane wejściowe jako latin-1, chyba że dane wejściowe określają inny zestaw znaków . Innymi słowy: <meta charset="…">znacznik wydaje się być potrzebny dla danych wejściowych, które nie są latin-1. W przeciwnym razie wyjście zostanie przerwane na np. Wielobajtowe znaki UTF-8.
syreny
1
LIBXML_HTML_NOIMPLIED również psuje kod HTML, usuwając tabulatory, wcięcia i podziały wierszy
Zoltán Süle
72

Po prostu usuń węzły bezpośrednio po załadowaniu dokumentu za pomocą loadHTML ():

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

# remove <html><body></body></html> 
$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
Alex
źródło
to jest dla mnie czystsza odpowiedź.
KnF
39
należy zauważyć, że działa to, jeśli <body> ma tylko jeden węzeł potomny.
Yann Milin
Działało świetnie. Dziękuję Ci! Znacznie czystsze i szybsze niż inne odpowiedzi preg.
Ligemer
Dziękuję Ci za to! Właśnie dodałem kolejny wycinek na dole, aby obsłużyć puste węzły.
redaxmedia
2
Kod do usunięcia <!DOCTYPE działa. Druga linia jest przerywana, jeśli <body>ma więcej niż jedną notatkę podrzędną.
Free Radical
21

Użyj saveXML()zamiast tego i przekaż documentElement jako argument do niego.

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

http://php.net/domdocument.savexml

Jonasza
źródło
Tak jest lepiej, ale nadal <html><body> <p> pakuję zawartość.
Scott B
2
Należy zauważyć, że metoda saveXML () zapisze XHTML, a nie HTML.
alexantd
@Scott: to naprawdę dziwne. Pokazuje, co próbujesz zrobić w sekcji przykładów. Czy na pewno nie masz tego kodu HTML w swoim DOM? Dokładnie jaki kod HTML znajduje się w Twoim DOMDocument? Możliwe, że musimy uzyskać dostęp do węzła podrzędnego.
Jonah
@Jonah to nie jest dziwne. Kiedy to robisz, loadHTMLlibxml używa modułu parsera HTML, który wstawi brakujący szkielet HTML. W konsekwencji $dom->documentElementbędzie głównym elementem HTML. Naprawiłem Twój przykładowy kod. Powinien teraz zrobić to, o co prosi Scott.
Gordon
19

Problem z pierwszą odpowiedzią polega na tym, że LIBXML_HTML_NOIMPLIEDjest niestabilna .

Może zmieniać kolejność elementów (w szczególności przenosząc znacznik zamykający górnego elementu na dół dokumentu), dodawać losowe pznaczniki i być może wiele innych problemów [1] . Może usunąć tagi htmli bodyza Ciebie, ale kosztem niestabilnego zachowania. W produkcji to czerwona flaga. W skrócie:

Nie używajLIBXML_HTML_NOIMPLIED . Zamiast tego użyjsubstr .


Pomyśl o tym. Długości <html><body>i </body></html>są ustalone na obu końcach dokumentu - ich rozmiary nigdy się nie zmieniają, podobnie jak ich położenie. To pozwala nam substrje odciąć:

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

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

( JEDNAK NIE JEST TO KOŃCOWE ROZWIĄZANIE! Pełna odpowiedź znajduje się poniżej , czytaj dalej, aby poznać kontekst)

Odcinamy 12początek dokumentu, ponieważ <html><body>= 12 znaków ( <<>>+html+body= 4 + 4 + 4), a cofamy się i odcinamy \n</body></html>15 znaków na końcu, ponieważ = 15 znaków ( \n+//+<<>>+body+html= 1 + 2 + 4 + 4 + 4)

Zauważ, że nadal używam LIBXML_HTML_NODEFDTDpomiń !DOCTYPEprzed dołączeniem. Po pierwsze, upraszcza to substrusuwanie tagów HTML / BODY. Po drugie, nie usuwamy doctype z, substrponieważ nie wiemy, czy „ default doctype” zawsze będzie miał stałą długość. Ale co najważniejsze, LIBXML_HTML_NODEFDTDpowstrzymuje parser DOM przed zastosowaniem do dokumentu typu dokumentu innego niż HTML5 - co przynajmniej zapobiega traktowaniu przez parser elementów, których nie rozpoznaje, jako luźnego tekstu.

Wiemy na pewno, że tagi HTML / BODY mają ustalone długości i pozycje, i wiemy, że stałe, takie jak, LIBXML_HTML_NODEFDTDnigdy nie są usuwane bez jakiegoś powiadomienia o wycofaniu, więc powyższa metoda powinna zostać zastosowana w przyszłości, ALE ...


... jedynym zastrzeżeniem jest to, że implementacja DOM może zmienić sposób umieszczania znaczników HTML / BODY w dokumencie - na przykład, usuwając znak nowej linii na końcu dokumentu, dodając spacje między tagami lub dodając znaki nowej linii.

Można temu zaradzić, wyszukując pozycje otwierających i zamykających znaczników bodyi używając tych przesunięć, tak jak w przypadku naszych długości do przycięcia. Używamy strposi, strrposaby znaleźć przesunięcia odpowiednio z przodu iz tyłu:

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

Na koniec powtórzenie ostatecznej, przyszłościowej odpowiedzi :

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

Bez doctype, bez tagu HTML, bez tagu body. Możemy mieć tylko nadzieję, że parser DOM wkrótce otrzyma nową warstwę farby i możemy bardziej bezpośrednio wyeliminować te niechciane tagi.

Super Cat
źródło
Świetna odpowiedź, jeden mały komentarz, dlaczego nie $html = $dom -> saveHTML();zamiast $dom -> saveHTML();kilkukrotnie?
Steven
15

Sprytną sztuczką jest użycie loadXMLi wtedy saveHTML. htmlI bodyznaczniki są umieszczone na loadetapie, a nie saveetap.

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

Uwaga, jest to trochę hakerskie i powinieneś użyć odpowiedzi Jonaha, jeśli możesz sprawić, że zadziała.

samotny dzień
źródło
4
To się jednak nie powiedzie w przypadku nieprawidłowego kodu HTML.
Gordon
1
@Gordon Dokładnie dlaczego umieściłem zastrzeżenie na dole!
lonesomeday
1
Kiedy próbuję tego i echo $ dom-> saveHTML (), zwraca po prostu pusty ciąg. Jakby loadXML ($ content) był pusty. Kiedy robię to samo z $ dom-> loadHTML ($ content), a następnie echo $ dom-> saveXML () otrzymuję zawartość zgodnie z oczekiwaniami.
Scott B
Używanie loadXML, gdy chcesz załadować HTMl to kciuk. Zwłaszcza, że ​​LoadXML nie wie, jak obsługiwać HTML.
botenvouwer
15

użyj DOMDocumentFragment

$html = 'what you want';
$doc = new DomDocument();
$fragment = $doc->createDocumentFragment();
$fragment->appendXML($html);
$doc->appendChild($fragment);
echo $doc->saveHTML();
jcp
źródło
3
Najczystsza odpowiedź dla pre php5.4.
Nick Johnson
To działa dla mnie, zarówno starsze, jak i nowsze niż wersja Libxml 2.7.7. Dlaczego miałoby to dotyczyć wyłącznie pre php5.4?
RobbertT
To powinno mieć więcej głosów. Świetna opcja dla wersji libxml, które nie obsługują LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD. Dzięki!
Marty Mulligan
13

Jest rok 2017, a na to pytanie 2011 nie podoba mi się żadna z odpowiedzi. Wiele wyrażeń regularnych, duże klasy, loadXML itp.

Proste rozwiązanie, które rozwiązuje znane problemy:

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

Łatwe, proste, solidne, szybkie. Ten kod będzie działał w odniesieniu do tagów HTML i kodowania, takich jak:

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

Jeśli ktoś znajdzie błąd, powiedz proszę, sam tego użyję.

Edytuj , Inne prawidłowe opcje, które działają bez błędów (bardzo podobne do już podanych):

@$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 );

Możesz sam dodać ciało, aby zapobiec pojawianiu się dziwnych rzeczy na futrze.

Trzecia opcja:

 $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
źródło
3
Powinieneś poprawić swoją odpowiedź, unikając droższego, mb_convert_encodinga zamiast tego odpowiednio dodając <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>i modyfikując substr. Przy okazji, twoje jest tutaj najbardziej eleganckim rozwiązaniem. Głosowano za.
Hlsg
10

Jestem trochę spóźniony w klubie, ale nie chciałem nie dzielić się metodą, o której się dowiedziałem. Przede wszystkim mam odpowiednie wersje dla loadHTML (), aby zaakceptować te fajne opcje, ale LIBXML_HTML_NOIMPLIEDnie działało w moim systemie. Również użytkownicy zgłaszają problemy z parserem (na przykład tutaj i tutaj ).

Rozwiązanie, które stworzyłem, jest właściwie dość proste.

HTML do załadowania jest umieszczany w <div>elemencie, więc ma kontener zawierający wszystkie węzły do ​​załadowania.

Następnie ten element kontenera jest usuwany z dokumentu (ale jego DOMElement nadal istnieje).

Następnie wszystkie bezpośrednie elementy podrzędne z dokumentu są usuwane. Obejmuje to każdy dodany <html>, <head>a <body>znaczniki (skutecznie LIBXML_HTML_NOIMPLIEDopcja), jak również <!DOCTYPE html ... loose.dtd">zgłoszenie (w praktyce LIBXML_HTML_NODEFDTD).

Następnie wszystkie bezpośrednie elementy podrzędne kontenera są ponownie dodawane do dokumentu i można je wyprowadzić.

$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 działa jak zwykle, po prostu uważaj, aby teraz było wiele elementów dokumentu, więc nie pojedynczy węzeł główny:

$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 ~ precyzyjne + 2 (cli) (zbudowano: 21 grudnia 2014 20:28:53)
hakre
źródło
nie działało to dla mnie z bardziej złożonym źródłem HTML. Usunął również daną część kodu HTML.
Zoltán Süle
4

Żadne z innych rozwiązań w momencie pisania tego tekstu (czerwiec 2012) nie było w stanie w pełni zaspokoić moich potrzeb, dlatego napisałem jedno, które zajmuje się następującymi przypadkami:

  • Akceptuje zwykły tekst bez tagów, a także zawartość HTML.
  • Nie dołącza żadnych tagów (w tym <doctype>, <xml>, <html>, <body>, i <p>znaczniki)
  • Pozostawia wszystko w <p>spokoju.
  • Pozostawia sam pusty tekst.

Oto rozwiązanie, które rozwiązuje te problemy:

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

Napisałem też kilka testów, które będą żyć w tej samej klasie:

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
}

Możesz sam sprawdzić, czy to działa. DomDocumentWorkaround::testAll()zwraca to:

    Succeeded
    Succeeded
    Succeeded
    Succeeded
oracz
źródło
1
HTML = / = XML, powinieneś użyć programu ładującego HTML do HTML.
hakre
4

Ok, znalazłem bardziej eleganckie rozwiązanie, ale jest po prostu żmudne:

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

Dobra, mam nadzieję, że to niczego nie pominie i komuś pomoże?

rclai
źródło
2
Nie obsługuje przypadku, gdy loadHTML ładuje ciąg bez znaczników
copndz
3

Użyj tej funkcji

$layout = preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $layout);
boksiora
źródło
13
Może być kilku czytelników, którzy natknęli się na ten post za pośrednictwem tego posta , zdecydowali się nie używać wyrażenia regularnego do analizowania kodu HTML i zamiast tego używać parsera DOM, i ostatecznie potencjalnie potrzebować odpowiedzi wyrażenia regularnego, aby uzyskać kompletne rozwiązanie ... ironiczne
Robbie Averill
Nie rozumiem, dlaczego nikt nie zwraca po prostu treści BODY. Czy ten tag nie powinien być zawsze obecny, gdy parser dodaje cały nagłówek dokumentu / typ dokumentu? Powyższe wyrażenie regularne byłoby nawet krótsze.
Sergio,
@boksiora "robi swoje" - w takim razie dlaczego w pierwszej kolejności używamy metod parsera DOM?
Dziękuję
@naomik Nie powiedziałem, żebym nie używał parsera DOM, oczywiście jest wiele różnych sposobów osiągnięcia tego samego wyniku, to zależy od Ciebie, kiedy korzystałem z tej funkcji miałem problem z wbudowanym php dom parser, który nie analizował poprawnie html5.
boksiora
1
Musiałem użyć, preg_replaceponieważ użycie metod opartych na DOMDocument do usuwania tagów html i body nie zachowywało kodowania UTF-8 :(
wizonesolutions
3

Jeśli rozwiązanie flagowe, na które odpowiedział Alessandro Vendruscolo , nie działa, możesz spróbować tego:

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

$bodyTagbędzie zawierać w pełni przetworzony kod HTML bez tych wszystkich opakowań HTML, z wyjątkiem <body>tagu, który jest głównym elementem treści. Następnie możesz użyć wyrażenia regularnego lub funkcji przycinającej, aby usunąć je z końcowego ciągu (po saveHTML) lub, jak w powyższym przypadku, iterować po wszystkich jego potomkach, zapisując ich zawartość w zmiennej tymczasowej $finalHtmli zwracając ją (uważam, że jest bezpieczniejsze).

José Ricardo Júnior
źródło
3

Natknąłem się na ten temat, aby znaleźć sposób na usunięcie opakowania HTML. Używanie LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTDdziała świetnie, ale mam problem z utf-8. Po wielu staraniach znalazłem rozwiązanie. Publikuję to poniżej, ponieważ każdy ma ten sam problem.

Problem spowodowany przez <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

Problem:

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

Rozwiązanie 1:

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

Rozwiązanie 2:

$dom->loadHTML($document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
utf8_decode($dom->saveHTML($dom->documentElement));
Panagiotis Koursaris
źródło
1
Miło mi, że dzielisz się swoimi spostrzeżeniami, ale rozwiązanie 2 jest już obecne z tymi dokładnymi pytaniami tutaj, a rozwiązanie 1 jest gdzie indziej. Również w przypadku problemu rozwiązania 1 udzielona odpowiedź jest niejasna. Szanuję Twoje dobre intencje, ale pamiętaj, że może to powodować dużo hałasu, a także utrudniać innym znalezienie rozwiązań, których szukają, co wydaje mi się, że jest przeciwieństwem tego, co chcesz osiągnąć dzięki swojej odpowiedzi. Stackoverflow działa najlepiej, jeśli odpowiadasz na jedno pytanie naraz. Tylko podpowiedź.
hakre
3

Walczę z tym na RHEL7 z PHP 5.6.25 i LibXML 2.9. (Wiem, że stare rzeczy w 2018 roku, ale to dla ciebie Red Hat).

Odkryłem, że rozwiązanie zasugerowane przez Alessandro Vendruscolo, cieszące się dużym uznaniem, łamie kod HTML poprzez zmianę kolejności tagów. To znaczy:

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

staje się:

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

Dotyczy to obu opcji, które sugeruje: LIBXML_HTML_NOIMPLIEDi LIBXML_HTML_NODEFDTD.

Rozwiązanie zaproponowane przez Alexa idzie w połowie, aby go rozwiązać, ale nie działa, jeśli <body>ma więcej niż jeden węzeł podrzędny.

Rozwiązanie, które działa dla mnie, to:

Najpierw, aby załadować DOMDocument, używam:

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

Aby zapisać dokument po masowaniu DOMDocument, używam:

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

Zgadzam się jako pierwszy, że to niezbyt eleganckie rozwiązanie - ale działa.

Free Radical
źródło
2

Dodanie <meta>tagu uruchomi działanie naprawcze programu DOMDocument. Zaletą jest to, że nie musisz w ogóle dodawać tego tagu. Jeśli nie chcesz użyć wybranego przez siebie kodowania, przekaż je jako argument konstruktora.

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

Wynik

<div>Hello World</div>

Dzięki @Bart

botenvouwer
źródło
2

Ja też miałem to wymaganie i podobało mi się rozwiązanie opublikowane przez Alexa powyżej. Jest jednak kilka problemów - jeśli <body>element zawiera więcej niż jeden element podrzędny, wynikowy dokument będzie zawierał tylko pierwszy element podrzędny <body>, a nie wszystkie. Poza tym potrzebowałem usuwania, aby obsługiwać rzeczy warunkowo - tylko wtedy, gdy masz dokument z nagłówkami HTML. Więc udoskonaliłem to w następujący sposób. Zamiast usuwać <body>, przekształciłem go na a <div>i usunąłem deklarację XML i <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
źródło
2

Podobnie jak inni członkowie, najpierw rozkoszowałem się prostotą i niesamowitą mocą odpowiedzi @Alessandro Vendruscolo. Możliwość prostego przekazania niektórych oznaczonych stałych do konstruktora wydawała się zbyt dobra, aby była prawdziwa. Dla mnie to było. Mam poprawne wersje zarówno LibXML, jak i PHP, jednak bez względu na to, co i tak dodałoby znacznik HTML do struktury węzłów obiektu Document.

Moje rozwiązanie działało znacznie lepiej niż korzystanie z ...

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

Flagi lub ....

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

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

Usuwanie węzłów, które staje się bałaganiarskie bez uporządkowanej kolejności w DOM. Ponownie fragmenty kodu nie mają możliwości wcześniejszego określenia struktury DOM.

Zacząłem tę podróż od szukania prostego sposobu na przechodzenie przez DOM, jak robi to JQuery, lub przynajmniej w jakiś sposób, który miał uporządkowany zestaw danych albo pojedynczo połączony, podwójnie połączony lub przechodzenie przez węzeł drzewa. Nie obchodziło mnie, jak długo mogę przeanalizować ciąg tak, jak robi to HTML, a także mam niesamowitą moc właściwości klasy encji węzła do wykorzystania po drodze.

Do tej pory obiekt DOMDocument sprawił, że chciałem ... Tak jak w przypadku wielu innych programistów wydaje się ... Wiem, że widziałem wiele frustracji w tym pytaniu, więc odkąd W KOŃCU ... (po około 30 godzinach prób i niepowodzeń testowanie typu) Znalazłem sposób, aby to wszystko uzyskać. Mam nadzieję, że to komuś pomoże ...

Po pierwsze, jestem cyniczny wobec WSZYSTKIEGO ... lol ...

Spędziłbym całe życie, zanim zgodziłbym się z kimkolwiek, że w tym przypadku i tak potrzebna jest klasa strony trzeciej. Bardzo byłem i NIE jestem fanem używania jakiejkolwiek struktury klas innej firmy, jednak natknąłem się na świetny parser. (około 30 razy w Google, zanim się poddałem, więc nie czuj się sam, jeśli tego unikasz, ponieważ w jakikolwiek sposób wyglądało to kiepsko lub nieoficjalnie ...)

Jeśli używasz fragmentów kodu i potrzebujesz, kod jest czysty i nie ma na niego żadnego wpływu parser, bez użycia dodatkowych tagów, użyj simplePHPParser .

Jest niesamowity i działa podobnie jak JQuery. Nie robiłem wrażenia, ale ta klasa korzysta z wielu dobrych narzędzi i jak dotąd nie miałem żadnych błędów parsowania. Jestem wielkim fanem robienia tego, co robi ta klasa.

Możesz znaleźć jego pliki do pobrania tutaj , instrukcje uruchamiania tutaj i jego API tutaj . Bardzo polecam używanie tej klasy z jej prostymi metodami, które mogą działać w .find(".className")ten sam sposób, w jaki byłaby używana metoda znajdowania JQuery, a nawet znane metody, takie jak getElementByTagName()lub getElementById()...

Kiedy zapisujesz drzewo węzłów w tej klasie, nic nie dodaje. Możesz po prostu powiedzieć $doc->save();i przekazuje całe drzewo do łańcucha bez żadnego zamieszania.

Będę teraz używać tego parsera we wszystkich projektach bez ograniczenia przepustowości w przyszłości.

GoreDefex
źródło
2

Mam PHP 5.3 i odpowiedzi tutaj nie działają.

$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);zastąpiłem cały dokument tylko pierwszym dzieckiem, miałem wiele akapitów i tylko pierwszy był zapisywany, ale rozwiązanie dało mi dobry punkt wyjścia do napisania czegoś bez regexzostawiłem kilka komentarzy i jestem pewien, że można to poprawić, ale jeśli ktoś ma ten sam problem co ja to może być dobry punkt wyjścia.

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

Wtedy moglibyśmy to wykorzystać w ten sposób:

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

Zwróć uwagę, że appendChildakceptuje a, DOMNodewięc nie musimy tworzyć nowych elementów, możemy po prostu ponownie wykorzystać istniejące, które implementują DOMNodetakie jak DOMElementten.

Niezmienna cegła
źródło
To nie zadziała dla fragmentów, tylko dla pojedynczego elementu podrzędnego, który chcesz uczynić pierwszym dzieckiem dokumentu. Jest to dość ograniczone i skutecznie nie LIBXML_HTML_NOIMPLIEDspełnia swojej roli, ponieważ robi to tylko częściowo. Usunięcie doctype jest skuteczne LIBXML_HTML_NODEFDTD.
hakre
2

Mam 3 problemy z DOMDocumentzajęciami.

1- Ta klasa ładuje HTML z kodowaniem ISO i znakami utf-8 nie wyświetlanymi na wyjściu.

2- Nawet jeśli damy LIBXML_HTML_NOIMPLIEDflagę metody loadHtml, dopóki nasz html wejściowy nie zawiera znacznik korzeniowy, to nie będzie parse poprawnie.

3- Ta klasa uważa tagi HTML5 za nieprawidłowe.

Więc nadpisałem tę klasę, aby rozwiązać te problemy i zmieniłem niektóre metody.

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

}

Teraz używam DOMEditorzamiast DOMDocumenti jak na razie działa dobrze

        $editor = new DOMEditor();
        $editor->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        // works like a charm!
        echo $editor->saveHTML();
Panie Hosseini
źródło
Twój punkt 1. rozwiązuje się za pomocą mb_convert_encoding ($ string, 'HTML-ENTITIES', 'UTF-8'); przed użyciem loadHTML () i 2.nd przez umieszczenie znacznika DIV w swojej funkcji pomocniczej, na przykład wokół mb_convert_encoding (), którego używasz. Wyszło mi wystarczająco dobrze. Rzeczywiście, jeśli nie ma DIV, to automatycznie dodaje akapit w moim przypadku, co jest niewygodne, ponieważ zwykle mają zastosowany margines (bootstrap ..)
trainoasis
0

Trafiłem też na ten problem.

Niestety nie czułem się komfortowo korzystając z żadnego z rozwiązań przedstawionych w tym wątku, więc poszedłem sprawdzić takie, które by mnie satysfakcjonowało.

Oto, co wymyśliłem i działa bez problemów:

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

W istocie działa podobnie do większości przedstawionych tutaj rozwiązań, ale zamiast wykonywać pracę ręczną, używa selektora xpath, aby wybrać wszystkie elementy w treści i połączyć ich kod HTML.

Nikola Petkanski
źródło
Jak wszystkie tutaj rozwiązania, nie działa to w każdym przypadku: jeśli załadowany ciąg nie zaczynał się od znacznika, <p> </p> został dodany, to twój kod nie działa, ponieważ doda <p> </p> znaczniki w zapisanej treści
copndz
Szczerze mówiąc, nie testowałem tego z surowym tekstem, ale teoretycznie powinno działać. W konkretnym przypadku może zajść potrzeba zmiany ścieżki xpath na coś podobnego descendant-or-self::body/p/*.
Nikola Petkanski
0

mój serwer ma php 5.3 i nie mogę zaktualizować, więc te opcje

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD

nie są dla mnie.

Aby rozwiązać ten problem, mówię funkcji SaveXML, aby wydrukowała element Body, a następnie po prostu zamień „body” na „div”

oto mój kod, mam nadzieję, że komuś pomaga:

<? 
$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 służy do obsługi języka hebrajskiego.

Tomer Ofer
źródło
0

Odpowiedź Alexa jest poprawna, ale może powodować następujący błąd na pustych węzłach:

Argument 1 przekazany do DOMNode :: removeChild () musi być instancją DOMNode

Oto mój mały mod:

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

Dodanie trim () jest również dobrym pomysłem, aby usunąć białe znaki.

redaxmedia
źródło
0

Może za późno. Ale może ktoś (jak ja) nadal ma ten problem.
Więc żadna z powyższych nie działała dla mnie. Ponieważ $ dom-> loadHTML również zamyka otwarte tagi, nie tylko dodaje tagi html i body.
Więc dodaj element <div> nie działa dla mnie, ponieważ czasami mam 3-4 niezamknięte div w kawałku html.
Moje rozwiązanie:

1.) Dodaj znacznik do wycięcia, a następnie załaduj kawałek HTML

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

2.) rób co chcesz z dokumentem
3.) zapisz html

$new_html_piece = $dom->saveHTML();

4.) zanim go zwrócisz, usuń tagi <p> ​​</ p> ze znacznika, dziwne jest to tylko na [MARK], ale nie na [/ MARK] ...!?

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

5.) usuń wszystko przed i po markerze

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

6.) zwrócić go

return $new_html_piece;

Byłoby dużo łatwiej, gdyby LIBXML_HTML_NOIMPLIED działał dla mnie. Powinien, ale tak nie jest. PHP 5.4.17, libxml wersja 2.7.8.
Wydaje mi się naprawdę dziwne, używam parsera HTML DOM, a potem, aby naprawić tę "rzecz", muszę użyć wyrażenia regularnego ... Chodziło o to, aby nie używać wyrażenia regularnego;)

Joe
źródło
Wygląda na niebezpieczne to, co tutaj robisz, stackoverflow.com/a/29499718/367456 powinno wykonać to za Ciebie.
hakre
Niestety to ( stackoverflow.com/questions/4879946/… ) nie zadziała dla mnie. Jak powiedziałem: „Więc dodaj element <div> nie działa dla mnie, ponieważ czasami mam 3-4 niezamknięte div w kawałku html” Z jakiegoś powodu DOMDocument chce zamknąć wszystkie „niezamknięte” elementy. W każdym przypadku dostanę zwolnienie w krótkim kodzie lub innym znaczniku, usunę zwolnienie i chcę manipulować drugim fragmentem dokumentu, kiedy skończę, wstawię zwolnienie z powrotem.
Joe
Powinno być możliwe pozostawienie elementu div i operowanie na elemencie body po załadowaniu własnej treści. Element body należy dodać niejawnie podczas ładowania fragmentu.
hakre
Mój problem polega na tym, że mój identyfikator zawiera niezamknięty tag. Powinien pozostać niezamknięty, a DOMDocument zamknie te elementy. Fregment jak: < div >< div > ... < /div >. Ciągle szukam rozwiązań.
Joe
Hmm, myślę, że tagi DIV zawsze mają parę zamykającą. Być może Tidy sobie z tym poradzi, poradzi sobie też z fragmentami.
hakre
0

Dla każdego, kto korzysta z Drupala, jest do tego wbudowana funkcja:

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

Kod odniesienia:

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
źródło
Głosowano za. Użycie tej funkcji z interfejsu API Drupala działa dobrze na mojej stronie Drupal 7. Wydaje mi się, że osoby, które nie używają Drupala, mogą po prostu skopiować tę funkcję do swojej własnej witryny - ponieważ nie ma w tym nic specyficznego dla Drupala.
Free Radical
0

Możesz użyć porządku tylko z pokazem ciała:

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

Ale pamiętaj: porządnie usuń niektóre tagi, takie jak ikony Font Awesome: Problemy z wcięciem HTML (5) w PHP

Rafa Rodríguez
źródło
-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);
Dylan Maxey
źródło
Chcesz się podzielić, dlaczego -1?
Dylan Maxey