Comment enregistrer HTML de DOMDocument sans wrapper HTML?


116

Je suis la fonction ci-dessous, j'ai du mal à sortir le DOMDocument sans ajouter les wrappers de balises XML, HTML, body et p avant la sortie du contenu. La solution suggérée:

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

Ne fonctionne que lorsque le contenu ne contient aucun élément de niveau bloc. Cependant, quand c'est le cas, comme dans l'exemple ci-dessous avec l'élément h1, la sortie résultante de saveXML est tronquée à ...

<p> Si vous aimez </p>

On m'a signalé cet article comme une solution de contournement possible, mais je ne comprends pas comment l'implémenter dans cette solution (voir les tentatives commentées ci-dessous).

Aucune suggestion?

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

Réponses:


217

Toutes ces réponses sont maintenant fausses , car à partir de PHP 5.4 et Libxml 2.6 a loadHTMLmaintenant un $optionparamètre qui indique à Libxml comment il doit analyser le contenu.

Par conséquent, si nous chargeons le HTML avec ces options

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

en faisant, saveHTML()il y aura non doctype, non <html>et non <body>.

LIBXML_HTML_NOIMPLIEDdésactive l'ajout automatique d'éléments html / body implicites LIBXML_HTML_NODEFDTDempêche l'ajout d' un doctype par défaut quand on n'en trouve pas.

La documentation complète sur les paramètres Libxml est ici

(Notez que la loadHTMLdocumentation indique que Libxml 2.6 est nécessaire, mais LIBXML_HTML_NODEFDTDn'est disponible que dans Libxml 2.7.8 et LIBXML_HTML_NOIMPLIEDest disponible dans Libxml 2.7.7)


10
Cela fonctionne comme un charme. Devrait être la réponse acceptée. Je viens d'ajouter un drapeau et tous mes maux de tête sont partis ;-)
Just Plain High

8
Cela ne fonctionne pas avec PHP 5.4 et Libxml 2.9. loadHTML n'accepte aucune option :(
Acyra

11
Notez que ce n'est pas tout à fait parfait. Voir stackoverflow.com/questions/29493678/…
Josh Levinson

4
Désolé, mais cela ne semble pas du tout être une bonne solution (du moins pas en pratique). Cela ne devrait vraiment pas être la réponse acceptée. Outre les questions mentionnées, il y a aussi un problème d'encodage méchant avec DOMDocumentqui affecte également le code dans cette réponse. Afaik, DOMDocumentinterprète toujours les données d'entrée comme latin-1 sauf si l'entrée spécifie un jeu de caractères différent . En d'autres termes: la <meta charset="…">balise semble être nécessaire pour les données d'entrée qui ne sont pas latin-1. Sinon, la sortie sera interrompue par exemple pour les caractères multi-octets UTF-8.
mermshaus

1
LIBXML_HTML_NOIMPLIED gâche également le code HTML en supprimant les tabulations, les retraits et les sauts de ligne
Zoltán Süle

72

Supprimez simplement les nœuds directement après avoir chargé le document avec loadHTML ():

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

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

c'est la réponse la plus propre pour moi.
KnF

39
il faut noter que cela fonctionne si <body> n'a qu'un seul nœud enfant.
Yann Milin

A très bien fonctionné. Je vous remercie! Beaucoup plus propre et plus rapide que l'autre réponse preg.
Ligemer

Merci pour ça! Je viens d'ajouter un autre snip en bas pour gérer les nœuds vides.
redaxmedia

2
Le code à supprimer <!DOCTYPE fonctionne. La deuxième ligne saute si <body>a plus d'une note enfant.
Free Radical

21

Utilisez à la saveXML()place et transmettez-lui le documentElement comme argument.

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

http://php.net/domdocument.savexml


C'est mieux, mais j'obtiens toujours <html><body> <p> encapsuler le contenu.
Scott B


2
Il convient de noter que saveXML () enregistrera XHTML, pas HTML.
alexantd le

@Scott: c'est vraiment étrange. Il montre ce que vous essayez de faire ici même dans la section des exemples. Êtes-vous sûr de ne pas avoir ce code HTML dans votre DOM? Quel est exactement le code HTML dans votre DOMDocument? Il se peut que nous ayons besoin d'accéder à un nœud enfant.
Jonah

@Jonah ce n'est pas étrange. Lorsque vous faites loadHTMLlibxml utilise le module d'analyseur HTML et qui insérera le squelette HTML manquant. Par conséquent, $dom->documentElementsera l'élément HTML racine. J'ai corrigé votre exemple de code. Il devrait maintenant faire ce que Scott demande.
Gordon le

19

Le problème avec la première réponse est qu'elle LIBXML_HTML_NOIMPLIEDest instable .

Il peut réorganiser les éléments (en particulier, déplacer la balise de fermeture de l'élément supérieur vers le bas du document), ajouter des pbalises aléatoires , et peut-être une variété d'autres problèmes [1] . Cela peut supprimer les balises htmlet bodypour vous, mais au prix d'un comportement instable. En production, c'est un drapeau rouge. En bref:

N'utilisez pasLIBXML_HTML_NOIMPLIED . Utilisez plutôtsubstr .


Pensez-y. Les longueurs de <html><body>et </body></html>sont fixes et aux deux extrémités du document - leurs tailles ne changent jamais, pas plus que leurs positions. Cela nous permet d'utiliser substrpour les couper:

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

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

( CE N'EST PAS LA SOLUTION FINALE CEPENDANT! Voir ci-dessous pour la réponse complète , continuez à lire pour le contexte)

On coupe 12le début du document parce que <html><body>= 12 caractères ( <<>>+html+body= 4 + 4 + 4), et on recule et on coupe 15 la fin parce que \n</body></html>= 15 caractères ( \n+//+<<>>+body+html= 1 + 2 + 4 + 4 + 4)

Notez que j'utilise toujours LIBXML_HTML_NODEFDTDomettre l' !DOCTYPEinclusion. Premièrement, cela simplifie la substrsuppression des balises HTML / BODY. Deuxièmement, nous ne supprimons pas le doctype avec substrparce que nous ne savons pas si le « default doctype» sera toujours quelque chose d'une longueur fixe. Mais, plus important encore, LIBXML_HTML_NODEFDTDempêche l'analyseur DOM d'appliquer un doctype non HTML5 au document - ce qui empêche au moins l'analyseur de traiter les éléments qu'il ne reconnaît pas comme du texte libre.

Nous savons pertinemment que les balises HTML / BODY ont des longueurs et des positions fixes, et nous savons que les constantes comme LIBXML_HTML_NODEFDTDne sont jamais supprimées sans un certain type d'avis de dépréciation, donc la méthode ci-dessus devrait bien se dérouler dans le futur, MAIS ...


... la seule mise en garde est que l'implémentation DOM pourrait changer la façon dont les balises HTML / BODY sont placées dans le document - par exemple, en supprimant la nouvelle ligne à la fin du document, en ajoutant des espaces entre les balises ou en ajoutant des nouvelles lignes.

Cela peut être résolu en recherchant les positions des balises d'ouverture et de fermeture bodyet en utilisant ces décalages comme pour nos longueurs à découper. Nous utilisons strposet strrpospour trouver les décalages de l'avant et de l'arrière, respectivement:

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

En conclusion, une répétition de la réponse finale et pérenne :

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

Pas de doctype, pas de balise html, pas de balise body. Nous ne pouvons qu'espérer que l'analyseur DOM recevra bientôt une nouvelle couche de peinture et que nous pourrons éliminer plus directement ces balises indésirables.


Excellente réponse, un petit commentaire, pourquoi pas $html = $dom -> saveHTML();au lieu de $dom -> saveHTML();plusieurs fois?
Steven

15

Une astuce intéressante consiste à utiliser loadXMLet ensuite saveHTML. Les balises htmlet bodysont insérées au loadstade, pas au savestade.

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

NB que c'est un peu piraté et que vous devriez utiliser la réponse de Jonah si vous pouvez la faire fonctionner.


4
Cela échouera pour le HTML non valide.
Gordon

1
@Gordon Exactement pourquoi j'ai mis la clause de non-responsabilité en bas!
lonesomeday

1
Quand j'essaye ceci, et echo $ dom-> saveHTML (), il renvoie juste une chaîne vide. Comme si loadXML ($ content) était vide. Quand je fais la même chose avec $ dom-> loadHTML ($ content), puis echo $ dom-> saveXML () j'obtiens le contenu comme prévu.
Scott B

Utiliser loadXML lorsque vous êtes prêt à charger HTMl est un pouce. Surtout parce que LoadXML ne sait pas gérer le HTML.
botenvouwer

15

utiliser DOMDocumentFragment

$html = 'what you want';
$doc = new DomDocument();
$fragment = $doc->createDocumentFragment();
$fragment->appendXML($html);
$doc->appendChild($fragment);
echo $doc->saveHTML();

3
La réponse la plus claire pour le pré php5.4.
Nick Johnson

Cela fonctionne pour moi, à la fois plus ancien et plus récent que la version Libxml 2.7.7. Pourquoi serait-ce uniquement pour le pré-php5.4?
RobbertT

Cela devrait avoir plus de votes. Excellente option pour les versions de libxml qui ne prennent pas en charge LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD. Merci!
Marty Mulligan

13

Nous sommes en 2017, et pour cette question de 2011, je n'aime aucune des réponses. Beaucoup de regex, de grandes classes, loadXML etc ...

Solution simple qui résout les problèmes connus:

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

Facile, simple, solide, rapide. Ce code fonctionnera avec les balises HTML et l'encodage comme:

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

Si quelqu'un trouve une erreur, veuillez le dire, je l'utiliserai moi-même.

Modifier , Autres options valides qui fonctionnent sans erreurs (très similaires à celles déjà données):

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

Vous pouvez ajouter du corps vous-même pour éviter toute chose étrange sur la furure.

Troisième option:

 $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
Vous devriez améliorer votre réponse en évitant le plus cher mb_convert_encodinget en ajoutant <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>et en modifiant en substrconséquence. Btw, la vôtre est la solution la plus élégante ici. J'ai voté pour.
Hlsg

10

Je suis un peu en retard dans le club mais je ne voulais pas partager une méthode que j'ai découverte. Tout d'abord, j'ai les bonnes versions pour loadHTML () pour accepter ces options intéressantes, mais LIBXML_HTML_NOIMPLIEDje n'ai pas fonctionné sur mon système. Les utilisateurs signalent également des problèmes avec l'analyseur (par exemple ici et ici ).

La solution que j'ai créée est en fait assez simple.

Le HTML à charger est placé dans un <div>élément de sorte qu'il a un conteneur contenant tous les nœuds à charger.

Ensuite, cet élément conteneur est supprimé du document (mais le DOMElement de celui-ci existe toujours).

Ensuite, tous les enfants directs du document sont supprimés. Cela inclut tout ajouté <html>, <head>et <body>balises (efficacement LIBXML_HTML_NOIMPLIEDoption) ainsi que la <!DOCTYPE html ... loose.dtd">déclaration (efficace LIBXML_HTML_NODEFDTD).

Ensuite, tous les enfants directs du conteneur sont à nouveau ajoutés au document et il peut être généré.

$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 fonctionne comme d'habitude, veillez simplement à ce qu'il y ait plusieurs éléments de document maintenant, donc pas un seul nœud racine:

$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 ~ precise + 2 (cli) (construit: 21 décembre 2014 20:28:53)

cela n'a pas fonctionné pour moi avec une source HTML plus complexe. Il a également supprimé une partie donnée du HTML.
Zoltán Süle

4

Aucune des autres solutions au moment de la rédaction de cet article (juin 2012) n'était en mesure de répondre complètement à mes besoins, j'en ai donc écrit une qui traite les cas suivants:

  • Accepte le contenu en texte brut sans balise, ainsi que le contenu HTML.
  • Ne pas ajouter des balises (y compris <doctype>, <xml>, <html>, <body>, et <p>tags)
  • Laisse tout enveloppé <p>seul.
  • Laisse le texte vide seul.

Voici donc une solution qui résout ces problèmes:

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

J'ai également écrit quelques tests qui vivraient dans cette même classe:

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
}

Vous pouvez vérifier que cela fonctionne pour vous-même. DomDocumentWorkaround::testAll()renvoie ceci:

    Succeeded
    Succeeded
    Succeeded
    Succeeded

1
HTML = / = XML, vous devez utiliser le chargeur HTML pour HTML.
hakre

4

D'accord, j'ai trouvé une solution plus élégante, mais c'est juste fastidieux:

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

D'accord, j'espère que cela n'oublie rien et aide quelqu'un?


2
Ne gère pas le cas où loadHTML charge une chaîne sans balisage
copndz

3

Utilisez cette fonction

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

13
Certains lecteurs sont peut-être tombés par hasard sur cet article via cet article , ont décidé de ne pas utiliser de regex pour analyser leur HTML et d'utiliser un analyseur DOM à la place, et finissent par avoir besoin d'une réponse regex pour parvenir à une solution complète ... ironique
Robbie Averill

Je ne comprends pas pourquoi noboy renvoie simplement le contenu de BODY. Cette balise n'est-elle pas supposée être toujours présente lorsque l'analyseur ajoute l'en-tête / doctype du document entier? Le regex ci-dessus serait même plus court.
sergio

@boksiora "ça fait le travail" - alors pourquoi utilisons-nous des méthodes d'analyseur DOM en premier lieu?
merci

@naomik je n'ai pas dit de ne pas utiliser un analyseur DOM, il y a bien sûr de nombreuses façons d'obtenir le même résultat, c'est à vous, au moment où j'ai utilisé cette fonction, j'avais un problème avec le dom php intégré parser, qui n'analysait pas correctement html5.
boksiora

1
J'ai dû utiliser preg_replaceparce que l'utilisation de méthodes basées sur DOMDocument pour supprimer les balises html et body ne préservait pas l'encodage UTF-8 :(
wizonesolutions

3

Si la solution d'indicateurs à laquelle Alessandro Vendruscolo a répondu ne fonctionne pas, vous pouvez essayer ceci:

$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 contiendra votre code HTML traité complet sans tous ces wraps HTML, à l'exception du <body> balise, qui est la racine de votre contenu. Ensuite, vous pouvez utiliser une expression régulière ou une fonction de découpage pour le supprimer de la chaîne finale (après saveHTML) ou, comme dans le cas ci-dessus, itérer sur tous ses enfants, en enregistrant leur contenu dans une variable temporaire $finalHtmlet en le renvoyant (ce que je crois être plus sûr).


3

Je suis tombé sur ce sujet pour trouver un moyen de supprimer le wrapper HTML. L'utilisation LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTDfonctionne très bien, mais j'ai un problème avec utf-8. Après beaucoup d'efforts, j'ai trouvé une solution. Je le poste ci-dessous car tout le monde a le même problème.

Le problème causé par <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

Le problème:

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

Solution 1:

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

Solution 2:

$dom->loadHTML($document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
utf8_decode($dom->saveHTML($dom->documentElement));

1
Je trouve agréable que vous partagiez vos résultats, mais la solution 2 est déjà présente avec ces questions exactes ici et la solution 1 est ailleurs. Pour le problème de la solution 1 également, la réponse donnée n'est pas claire. Je respecte vos bonnes intentions, mais sachez que cela peut créer beaucoup de bruit et empêcher les autres de trouver les solutions qu'ils recherchent, ce qui, je suppose, est un peu le contraire de ce que vous voulez obtenir avec votre réponse. Stackoverflow fonctionne mieux si vous gérez une question à la fois. Juste un indice.
hakre

3

J'ai du mal avec cela sur RHEL7 exécutant PHP 5.6.25 et LibXML 2.9. (Vieux trucs en 2018, je sais, mais c'est Red Hat pour vous.)

J'ai trouvé que la solution très votée suggérée par Alessandro Vendruscolo casse le HTML en réorganisant les balises. C'est à dire:

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

devient:

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

Cela vaut pour les deux options qu'il vous suggère d'utiliser: LIBXML_HTML_NOIMPLIEDet LIBXML_HTML_NODEFDTD.

La solution suggérée par Alex va à mi-chemin pour le résoudre, mais elle ne fonctionne pas si elle <body>a plus d'un nœud enfant.

La solution qui fonctionne pour moi est la suivante:

Tout d'abord, pour charger le DOMDocument, j'utilise:

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

Pour enregistrer le document après avoir massé le DOMDocument, j'utilise:

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

Je suis le premier à convenir que ce n’est pas une solution très élégante - mais cela fonctionne.


2

L'ajout de la <meta>balise déclenchera le comportement de correction de DOMDocument. La bonne partie est que vous n'avez pas du tout besoin d'ajouter cette balise. Si vous ne souhaitez pas utiliser un encodage de votre choix, passez-le simplement comme argument de constructeur.

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

Production

<div>Hello World</div>

Merci à @Bart


2

J'avais cette exigence aussi et j'aimais la solution publiée par Alex ci-dessus. Il y a cependant quelques problèmes: si l' <body>élément contient plus d'un élément enfant, le document résultant ne contiendra que le premier élément enfant de <body>, pas tous. De plus, j'avais besoin du décapage pour gérer les choses de manière conditionnelle - uniquement lorsque vous aviez un document avec les en-têtes HTML. Alors je l'ai affiné comme suit. Au lieu de le supprimer <body>, je l'ai transformé en un <div>, et j'ai supprimé la déclaration XML et <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

Tout comme les autres membres, je me suis d'abord délecté de la simplicité et de la puissance impressionnante de la réponse @Alessandro Vendruscolo. La possibilité de simplement transmettre certaines constantes marquées au constructeur semblait trop belle pour être vraie. Pour moi, ça l'était. J'ai les versions correctes de LibXML ainsi que de PHP, mais quoi qu'il en soit, cela ajouterait encore la balise HTML à la structure de nœud de l'objet Document.

Ma solution fonctionnait bien mieux que d'utiliser le ...

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

Drapeaux ou ....

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

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

Suppression de nœuds, qui devient désordonnée sans ordre structuré dans le DOM. Encore une fois, les fragments de code n'ont aucun moyen de prédéterminer la structure DOM.

J'ai commencé ce voyage en voulant un moyen simple de parcourir le DOM comme le fait JQuery ou du moins d'une manière qui avait un ensemble de données structurées soit un seul lien, soit doublement lié ou une traversée de nœuds arborescente. Je me fichais de savoir combien de temps je pouvais analyser une chaîne comme le fait HTML et avoir également l'incroyable puissance des propriétés de la classe d'entité de nœud à utiliser en cours de route.

Jusqu'à présent, DOMDocument Object m'a laissé vouloir ... Comme avec beaucoup d'autres programmeurs, il semble ... Je sais que j'ai vu beaucoup de frustration dans cette question, donc depuis que j'ai ENFIN .... (après environ 30 heures d'essais et d'échecs test de type) J'ai trouvé un moyen de tout obtenir. J'espère que ça aidera quelqu'un...

Tout d'abord, je suis cynique de TOUT ... lol ...

J'aurais passé toute ma vie avant d'être d'accord avec quiconque sur le fait qu'une classe tierce est de toute façon nécessaire dans ce cas d'utilisation. J'étais et je ne suis pas fan de l'utilisation d'une structure de classe tierce, mais je suis tombé sur un excellent analyseur. (environ 30 fois sur Google avant de céder, alors ne vous sentez pas seul si vous l'avez évité car cela avait l'air boiteux de quelque manière que ce soit ...)

Si vous utilisez des fragments de code et que vous avez besoin du code propre et non affecté par l'analyseur de quelque manière que ce soit, sans utiliser de balises supplémentaires, utilisez simplePHPParser .

C'est incroyable et agit beaucoup comme JQuery. Je ne suis pas souvent impressionné mais cette classe utilise beaucoup de bons outils et je n'ai pas encore eu d'erreurs d'analyse. Je suis un grand fan de pouvoir faire ce que fait cette classe.

Vous pouvez trouver ses fichiers à télécharger ici , ses instructions de démarrage ici , et son API ici . Je recommande vivement d'utiliser cette classe avec ses méthodes simples qui peuvent faire de .find(".className")la même manière qu'une méthode de recherche JQuery serait utilisée ou même des méthodes familières telles que getElementByTagName()ougetElementById() ...

Lorsque vous enregistrez une arborescence de nœuds dans cette classe, cela n'ajoute rien du tout. Vous pouvez simplement dire$doc->save(); et il renvoie l'arbre entier dans une chaîne sans aucun problème.

Je vais maintenant utiliser cet analyseur pour tous les projets à bande passante non plafonnée à l'avenir.


2

J'ai PHP 5.3 et les réponses ici n'ont pas fonctionné pour moi.

$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);remplacé tout le document par seulement le premier enfant, j'avais beaucoup de paragraphes et seul le premier était en cours d'enregistrement, mais la solution m'a donné un bon point de départ pour écrire quelque chose sans que regexje laisse quelques commentaires et je suis presque sûr que cela peut être amélioré, mais si quelqu'un a le même problème que moi cela peut être un bon point de départ.

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

Ensuite, nous pourrions l'utiliser comme ceci:

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

Notez que cela appendChildaccepte un DOMNodeafin que nous n'ayons pas besoin de créer de nouveaux éléments, nous pouvons simplement réutiliser ceux existants qui implémentent de DOMNodetels éléments DOMElementpeuvent être importants pour garder le code «sain» lors de la manipulation de plusieurs documents HTML / XML


Cela ne fonctionnera pas pour les fragments, mais uniquement pour un seul élément enfant dont vous voulez faire le premier enfant du document. Ceci est assez limité et ne fait effectivement pas le travail du LIBXML_HTML_NOIMPLIEDcar il ne le fait que partiellement. La suppression du doctype est efficace LIBXML_HTML_NODEFDTD.
hakre

2

Je fais face à 3 problèmes avec la DOMDocumentclasse.

1- Cette classe charge html avec le codage ISO et les caractères utf-8 ne s'affichent pas en sortie.

2- Même si on donne ‍‍‍LIBXML_HTML_NOIMPLIED à la méthode loadHTML, jusqu'à ce que notre html d'entrée ne contient pas de balise racine, il ne sera pas analysé correctement.

3- Cette classe considère les balises HTML5 invalides.

J'ai donc remplacé cette classe pour résoudre ces problèmes et j'ai changé certaines méthodes.

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

}

Maintenant, j'utilise DOMEditorau lieu de DOMDocumentet cela a bien fonctionné pour moi jusqu'à présent

        $editor = new DOMEditor();
        $editor->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        // works like a charm!
        echo $editor->saveHTML();

Votre point 1. est résolu en utilisant mb_convert_encoding ($ string, 'HTML-ENTITIES', 'UTF-8'); avant d'utiliser loadHTML () et le 2. et en ayant une balise DIV dans votre fonction d'assistance, autour de mb_convert_encoding () que vous utilisez par exemple. Ça a marché pour moi assez bien. En effet si aucun DIV n'est présent, alors il ajoute automatiquement un paragraphe dans mon cas ce qui est gênant car généralement ils ont une certaine marge appliquée (bootstrap ..)
trainoasis

0

Je suis également tombé sur ce problème.

Malheureusement, je ne me sentais pas à l'aise d'utiliser l'une des solutions fournies dans ce fil, alors je suis allé en vérifier une qui me satisferait.

Voici ce que j'ai inventé et cela fonctionne sans problème:

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

En substance, cela fonctionne de la même manière que la plupart des solutions fournies ici, mais au lieu de faire du travail manuel, il utilise le sélecteur xpath pour sélectionner tous les éléments du corps et concatène leur code html.


Comme toutes les solutions ici, cela ne fonctionne pas pour tous les cas: si la chaîne chargée ne commence pas par le balisage, <p> </p> a été ajouté, alors votre code ne fonctionne pas, car il ajoutera le <p> </p> balisage dans le contenu enregistré
copndz

Pour être juste, je ne l'ai pas testé avec du texte brut, mais en théorie, cela devrait fonctionner. Pour votre cas spécifique, vous devrez peut-être changer le xpath en quelque chose comme descendant-or-self::body/p/*.
Nikola Petkanski

0

mon serveur a php 5.3 et ne peut pas mettre à niveau donc ces options

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD

ne sont pas pour moi.

Pour résoudre ce problème, je dis à la fonction SaveXML d'imprimer l'élément Body, puis remplacez simplement le "body" par "div"

voici mon code, j'espère qu'il aide quelqu'un:

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

l'utf-8 est pour le support hébreu.


0

La réponse d'Alex est correcte, mais peut provoquer l'erreur suivante sur les nœuds vides:

L'argument 1 passé à DOMNode :: removeChild () doit être une instance de DOMNode

Voici mon petit 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;

L'ajout de trim () est également une bonne idée pour supprimer les espaces.


0

J'ai peut-être trop tard. Mais peut-être que quelqu'un (comme moi) a encore ce problème.
Donc, rien de ce qui précède n'a fonctionné pour moi. Comme $ dom-> loadHTML ferme également les balises ouvertes, ajoutez non seulement des balises html et body.
Donc, ajouter un élément <div> ne fonctionne pas pour moi, car j'ai parfois comme 3-4 div non fermés dans le morceau html.
Ma solution:

1.) Ajoutez un marqueur à couper, puis chargez le morceau html

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

2.) faites ce que vous voulez avec le document
3.) enregistrez html

$new_html_piece = $dom->saveHTML();

4.) avant de le renvoyer, supprimez les balises <p> </ p> du marqueur, étrangement elles n'apparaissent que sur [MARK] mais pas sur [/ MARK] ...!?

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

5.) tout supprimer avant et après le marqueur

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

6.) le renvoyer

return $new_html_piece;

Ce serait beaucoup plus facile si LIBXML_HTML_NOIMPLIED fonctionnait pour moi. Cela devrait, mais ce n'est pas le cas. PHP 5.4.17, version de libxml 2.7.8.
Je trouve vraiment étrange, j'utilise l'analyseur HTML DOM et ensuite, pour corriger ce "truc", je dois utiliser regex ... Le but était de ne pas utiliser de regex;)


Cela semble dangereux ce que vous faites ici, stackoverflow.com/a/29499718/367456 devrait faire le travail pour vous.
hakre

Malheureusement, cela ( stackoverflow.com/questions/4879946/… ) ne fonctionnera pas pour moi. Comme je l'ai dit: "Donc, ajouter un élément <div> ne fonctionne pas pour moi, parce que j'ai parfois comme 3-4 div non fermé dans le morceau html" Pour une raison quelconque, le DOMDocument veut fermer tous les éléments "non fermés". Dans certains cas, j'obtiendrai un fregment dans un shortcode ou un autre marqueur, supprimerai le fregment et je veux manipuler l'autre partie du document, quand j'en aurai fini avec cela, je réinsérerai le fregment.
Joe

Il devrait être possible de laisser l'élément div de côté et d'opérer sur l'élément body après avoir chargé votre propre contenu à la place. L'élément body doit être ajouté implicitement lorsque vous chargez un fragment.
hakre

Mon problème est que mon fregment contient une balise non fermée. Il doit rester non fermé et DOMDocument fermera ces éléments. Fregment comme: < div >< div > ... < /div >. Je cherche toujours des solutions.
Joe

Hmm, je pense que les balises div ont toujours une paire de fermeture. Peut-être que Tidy peut gérer cela, il peut également fonctionner avec des fragments.
hakre

0

Pour toute personne utilisant Drupal, il existe une fonction intégrée pour le faire:

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

Code de référence:

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

J'ai voté pour. Utiliser cette fonction de l'API Drupal fonctionne correctement sur mon site Drupal 7. Je suppose que ceux qui n'utilisent pas Drupal peuvent simplement copier la fonction dans leur propre site - car il n'y a rien de spécifique à Drupal à ce sujet.
Free Radical

0

Vous pouvez utiliser tidy avec show-body-only:

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

Mais rappelez-vous: supprimez certaines balises comme les icônes Font Awesome: Problèmes d'indentation de HTML (5) avec 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);

Voulez-vous partager pourquoi le -1?
Dylan Maxey le

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.