¿Cómo guardar HTML de DOMDocument sin contenedor HTML?

116

Soy la función a continuación, estoy luchando para generar el DOMDocument sin que agregue los envoltorios XML, HTML, body y p antes de la salida del contenido. La solución sugerida:

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

Solo funciona cuando el contenido no tiene elementos de nivel de bloque en su interior. Sin embargo, cuando lo hace, como en el ejemplo siguiente con el elemento h1, la salida resultante de saveXML se trunca a ...

<p> Si te gusta </p>

Me han señalado esta publicación como una posible solución alternativa, pero no puedo entender cómo implementarla en esta solución (consulte los intentos comentados a continuación).

¿Alguna sugerencia?

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
fuente

Respuestas:

217

Todas estas respuestas ahora son incorrectas , porque a partir de PHP 5.4 y Libxml 2.6 loadHTMLahora tiene un $optionparámetro que le indica a Libxml cómo debe analizar el contenido.

Por tanto, si cargamos el HTML con estas opciones

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

al hacerlo saveHTML()habrá no doctype, no <html>y no <body>.

LIBXML_HTML_NOIMPLIEDdesactiva la adición automática de elementos implícitos html / body LIBXML_HTML_NODEFDTDevita que se agregue un tipo de documento predeterminado cuando no se encuentra uno.

La documentación completa sobre los parámetros de Libxml está aquí

(Tenga en cuenta que los loadHTMLdocumentos dicen que se necesita Libxml 2.6, pero LIBXML_HTML_NODEFDTDsolo está disponible en Libxml 2.7.8 y LIBXML_HTML_NOIMPLIEDestá disponible en Libxml 2.7.7)

Alessandro Vendruscolo
fuente
10
Esto funciona a las mil maravillas. Debería ser la respuesta aceptada. Acabo de agregar una bandera y todos mis dolores de cabeza desaparecieron ;-)
Just Plain High
8
Esto no funciona con PHP 5.4 y Libxml 2.9. loadHTML no acepta ninguna opción :(
Acyra
11
Tenga en cuenta que esto no es del todo perfecto. Ver stackoverflow.com/questions/29493678/…
Josh Levinson
4
Lo siento, pero esta no parece ser una buena solución en absoluto (al menos no en la práctica). Realmente no debería ser la respuesta aceptada. Además de las cuestiones mencionadas, también hay un problema de codificación desagradable con el DOMDocumentque también afecta el código en esta respuesta. Afaik, DOMDocumentsiempre interpreta los datos de entrada como latin-1 a menos que la entrada especifique un juego de caracteres diferente . En otras palabras: la <meta charset="…">etiqueta parece ser necesaria para datos de entrada que no son latin-1. De lo contrario, la salida se interrumpirá para, por ejemplo, caracteres multibyte UTF-8.
mermshaus
1
LIBXML_HTML_NOIMPLIED también estropea el código HTML al eliminar las pestañas, las sangrías y los saltos de línea
Zoltán Süle
72

Simplemente elimine los nodos directamente después de cargar el documento con loadHTML ():

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

# remove <html><body></body></html> 
$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
Alex
fuente
esta es la respuesta más limpia para mí.
KnF
39
Cabe señalar que esto funciona si <body> tiene solo un nodo hijo.
Yann Milin
Funcionó muy bien. ¡Gracias! Mucho más limpio y rápido que la otra respuesta preg.
Ligemer
¡Gracias por esto! Acabo de agregar otro recorte en la parte inferior para manejar los nodos vacíos.
redaxmedia
2
El código para eliminar <!DOCTYPE funciona. La segunda línea se rompe si <body>tiene más de una nota secundaria.
Radical libre
21

Úselo en su saveXML()lugar y pásele el elemento documentElement como argumento.

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

http://php.net/domdocument.savexml

Jonás
fuente
Eso es mejor, pero sigo recibiendo <html><body> <p> envolviendo el contenido.
Scott B
2
Cabe señalar que saveXML () guardará XHTML, no HTML.
alexantd
@Scott: eso es realmente extraño. Muestra lo que está intentando hacer en la sección de ejemplos. ¿Estás seguro de que no tienes ese HTML en tu DOM? ¿Qué HTML hay exactamente en su DOMDocument? Podría ser que necesitemos acceder a un nodo hijo.
Jonás
@Jonah no es extraño. Cuando lo haga, loadHTMLlibxml usa el módulo analizador HTML y eso insertará el esqueleto HTML que falta. En consecuencia, $dom->documentElementserá el elemento HTML raíz. He arreglado tu código de ejemplo. Ahora debería hacer lo que Scott está pidiendo.
Gordon
19

El problema con la respuesta principal es que LIBXML_HTML_NOIMPLIEDes inestable .

Puede reordenar elementos (en particular, mover la etiqueta de cierre del elemento superior a la parte inferior del documento), agregar petiquetas aleatorias y tal vez una variedad de otras cuestiones [1] . Puede eliminar las etiquetas htmly bodypor usted, pero a costa de un comportamiento inestable. En producción, eso es una señal de alerta. En breve:

No lo useLIBXML_HTML_NOIMPLIED . En su lugar, utilicesubstr .


Piénsalo. Las longitudes de <html><body>y </body></html>son fijas y en ambos extremos del documento; sus tamaños nunca cambian, ni tampoco sus posiciones. Esto nos permite usar substrpara cortarlos:

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

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

( ¡SIN EMBARGO, ESTA NO ES LA SOLUCIÓN FINAL! Vea a continuación la respuesta completa , siga leyendo para conocer el contexto)

Cortamos 12desde el inicio del documento porque <html><body>= 12 caracteres ( <<>>+html+body= 4 + 4 + 4), y retrocedemos y cortamos 15 del final porque \n</body></html>= 15 caracteres ( \n+//+<<>>+body+html= 1 + 2 + 4 + 4 + 4)

Tenga en cuenta que todavía uso LIBXML_HTML_NODEFDTDomitir la !DOCTYPEinclusión. Primero, esto simplifica la substreliminación de las etiquetas HTML / BODY. En segundo lugar, no eliminamos el tipo de documento con substrporque no sabemos si " default doctype" siempre tendrá una longitud fija. Pero, lo más importante, LIBXML_HTML_NODEFDTDevita que el analizador DOM aplique un tipo de documento que no sea HTML5 al documento, lo que al menos evita que el analizador trate los elementos que no reconoce como texto suelto.

Sabemos con certeza que las etiquetas HTML / BODY son de longitudes y posiciones fijas, y sabemos que las constantes como LIBXML_HTML_NODEFDTDnunca se eliminan sin algún tipo de aviso de desaprobación, por lo que el método anterior debería aplicarse en el futuro, PERO ...


... la única advertencia es que la implementación DOM podría cambiar la forma en que las etiquetas HTML / BODY se colocan dentro del documento, por ejemplo, eliminando la nueva línea al final del documento, agregando espacios entre las etiquetas o agregando nuevas líneas.

Esto puede remediarse buscando las posiciones de las etiquetas de apertura y cierre body, y usando esas compensaciones para nuestras longitudes para recortar. Usamos strposy strrpospara encontrar las compensaciones de la parte delantera y trasera, respectivamente:

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

Para terminar, una repetición de la respuesta final a prueba de futuro :

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

Sin doctype, sin etiqueta html, sin etiqueta de cuerpo. Solo podemos esperar que el analizador DOM reciba una nueva capa de pintura pronto y podamos eliminar más directamente estas etiquetas no deseadas.

Super gato
fuente
Gran respuesta, un pequeño comentario, ¿por qué no en $html = $dom -> saveHTML();lugar de $dom -> saveHTML();repetidamente?
Steven
15

Un buen truco es usar loadXMLy luego saveHTML. Las etiquetas htmly bodyse insertan en el loadescenario, no en el saveescenario.

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

Tenga en cuenta que esto es un poco hacky y debería usar la respuesta de Jonah si puede hacer que funcione.

día solitario
fuente
4
Sin embargo, esto fallará para HTML no válido.
Gordon
1
@Gordon ¡Exactamente por qué puse el descargo de responsabilidad al final!
solitario
1
Cuando intento esto, y echo $ dom-> saveHTML (), solo devuelve una cadena vacía. Como si loadXML ($ content) estuviera vacío. Cuando hago lo mismo con $ dom-> loadHTML ($ content), luego echo $ dom-> saveXML () obtengo el contenido como se esperaba.
Scott B
Usar loadXML cuando esté dispuesto a cargar HTMl es fácil. Especialmente porque LoadXML no sabe cómo manejar HTML.
botenvouwer
15

utilizar DOMDocumentFragment

$html = 'what you want';
$doc = new DomDocument();
$fragment = $doc->createDocumentFragment();
$fragment->appendXML($html);
$doc->appendChild($fragment);
echo $doc->saveHTML();
jcp
fuente
3
La respuesta más limpia para pre php5.4.
Nick Johnson
Esto funciona para mí, tanto más antiguo como más nuevo que la versión Libxml 2.7.7. ¿Por qué sería esto solo para pre php5.4?
RobbertT
Esto debería tener más votos. Excelente opción para versiones de libxml que no admiten LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD. ¡Gracias!
Marty Mulligan
13

Estamos en 2017 y para esta pregunta de 2011 no me gusta ninguna de las respuestas. Muchas expresiones regulares, clases grandes, loadXML, etc.

Fácil solución que resuelve los problemas conocidos:

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

Fácil, simple, sólido, rápido. Este código funcionará con respecto a las etiquetas HTML y la codificación como:

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

Si alguien encuentra un error, dígalo, lo usaré yo mismo.

Editar , Otras opciones válidas que funcionan sin errores (muy similares a las ya dadas):

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

Puede agregar cuerpo usted mismo para evitar cualquier cosa extraña en el furure.

Treinta opción:

 $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
fuente
3
Debe mejorar su respuesta evitando los más costosos mb_convert_encodingy, en su lugar, agregando <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>y modificando en substrconsecuencia. Por cierto, la tuya es la solución más elegante aquí. Voto a favor.
Hlsg
10

Llegué un poco tarde al club, pero no quería no compartir un método que descubrí. En primer lugar, tengo las versiones correctas para que loadHTML () acepte estas buenas opciones, pero LIBXML_HTML_NOIMPLIEDno funcionó en mi sistema. Además, los usuarios informan problemas con el analizador (por ejemplo, aquí y aquí ).

La solución que creé en realidad es bastante simple.

El HTML que se va a cargar se coloca en un <div>elemento para que tenga un contenedor que contiene todos los nodos que se van a cargar.

Luego, este elemento contenedor se elimina del documento (pero el elemento DOME todavía existe).

Luego, se eliminan todos los hijos directos del documento. Esto incluye cualquier etiqueta agregada <html>, <head>y <body>( LIBXML_HTML_NOIMPLIEDopción efectiva ) así como la <!DOCTYPE html ... loose.dtd">declaración (efectiva LIBXML_HTML_NODEFDTD).

Luego, todos los hijos directos del contenedor se agregan nuevamente al documento y se puede generar.

$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 funciona como de costumbre, solo tenga cuidado de que ahora haya varios elementos del documento, por lo que no un solo nodo raíz:

$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 ~ precisa + 2 (cli) (construido: 21 de diciembre de 2014 20:28:53)
hakre
fuente
no funcionó para mí con una fuente HTML más compleja. También eliminó una parte determinada del HTML.
Zoltán Süle
4

Ninguna de las otras soluciones en el momento de escribir este artículo (junio de 2012) pudo satisfacer completamente mis necesidades, así que escribí una que maneja los siguientes casos:

  • Acepta contenido de texto sin formato que no tiene etiquetas, así como contenido HTML.
  • No añade ninguna etiqueta (incluyendo <doctype>, <xml>, <html>, <body>, y <p>etiquetas)
  • Deja todo envuelto <p>solo.
  • Deja solo el texto vacío.

Entonces, aquí hay una solución que soluciona esos problemas:

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

También escribí algunas pruebas que vivirían en esa misma clase:

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
}

Puedes comprobar que funciona por ti mismo. DomDocumentWorkaround::testAll()devuelve esto:

    Succeeded
    Succeeded
    Succeeded
    Succeeded
arador
fuente
1
HTML = / = XML, debe utilizar el cargador HTML para HTML.
hakre
4

De acuerdo, encontré una solución más elegante, pero es tediosa:

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

Muy bien, ¿con suerte esto no omite nada y ayuda a alguien?

rclai
fuente
2
No maneja el caso cuando loadHTML carga una cadena sin marcado
copndz
3

Usa esta función

$layout = preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $layout);
boksiora
fuente
13
Es posible que algunos lectores se hayan tropezado con esta publicación a través de esta publicación , hayan decidido no usar expresiones regulares para analizar su HTML y usar un analizador DOM en su lugar, y terminen potencialmente necesitando una respuesta de expresiones regulares para lograr una solución completa ... irónico
Robbie Averill
No entiendo por qué noboy simplemente devuelve el contenido de BODY. ¿No se supone que esa etiqueta está siempre presente cuando el analizador agrega el encabezado / tipo de documento completo? La expresión regular de arriba sería incluso más corta.
sergio
@boksiora "hace el trabajo" - entonces, ¿por qué estamos usando métodos de analizador DOM en primer lugar?
Gracias
@naomik No he dicho que no use un analizador DOM, por supuesto, hay muchas formas diferentes de lograr el mismo resultado, depende de usted, en el momento en que usé esta función, tuve un problema con el php dom incorporado parser, que no estaba analizando html5 correctamente.
boksiora
1
Tuve que usar preg_replaceporque el uso de métodos basados ​​en DOMDocument para eliminar las etiquetas html y body no conservaba la codificación UTF-8 :(
wizonesolutions
3

Si la solución de banderas respondida por Alessandro Vendruscolo no funciona, puede intentar esto:

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

$bodyTagcontendrá su código HTML procesado completo sin todas esas envolturas HTML, excepto la <body>etiqueta, que es la raíz de su contenido. Luego puede usar una expresión regular o una función de recorte para eliminarlo de la cadena final (después saveHTML) o, como en el caso anterior, iterar sobre todos sus hijos, guardar su contenido en una variable temporal $finalHtmly devolverlo (lo que creo que es más seguro).

José Ricardo Júnior
fuente
3

Estoy luchando con esto en RHEL7 con PHP 5.6.25 y LibXML 2.9. (Cosas viejas en 2018, lo sé, pero eso es Red Hat para ti).

Descubrí que la solución sugerida por Alessandro Vendruscolo, muy votada, rompe el HTML al reorganizar las etiquetas. Es decir:

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

se convierte en:

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

Esto se aplica a las dos opciones que sugiere que uses: LIBXML_HTML_NOIMPLIEDy LIBXML_HTML_NODEFDTD.

La solución sugerida por Alex va a mitad de camino para resolverlo, pero no funciona si <body>tiene más de un nodo hijo.

La solución que me funciona es la siguiente:

Primero, para cargar DOMDocument, uso:

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

Para guardar el documento después de masajear el DOMDocument, uso:

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

Soy el primero en estar de acuerdo en que esta no es una solución muy elegante, pero funciona.

Radicales libres
fuente
2

Agregar la <meta>etiqueta activará el comportamiento de reparación de DOMDocument. Lo bueno es que no es necesario que agregue esa etiqueta en absoluto. Si no desea utilizar una codificación de su elección, simplemente páselo como un argumento de constructor.

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

Salida

<div>Hello World</div>

Gracias a @Bart

botenvouwer
fuente
2

Yo también tenía este requisito y me gustó la solución publicada por Alex arriba. Sin embargo, hay un par de problemas: si el <body>elemento contiene más de un elemento secundario, el documento resultante solo contendrá solo el primer elemento secundario de <body>, no todos. Además, necesitaba la eliminación para manejar las cosas de manera condicional, solo cuando tenía un documento con los encabezados HTML. Así que lo refiné de la siguiente manera. En lugar de eliminarlo <body>, lo transformé en a <div>, y eliminé la declaración XML y <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
fuente
2

Al igual que otros miembros, primero me deleité con la simplicidad y el asombroso poder de la respuesta de @Alessandro Vendruscolo. La capacidad de simplemente pasar algunas constantes marcadas al constructor parecía demasiado buena para ser verdad. Para mi lo fue. Tengo las versiones correctas tanto de LibXML como de PHP, sin embargo, sin importar qué, todavía agregaría la etiqueta HTML a la estructura de nodo del objeto Document.

Mi solución funcionó mucho mejor que usar el ...

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

Banderas o ....

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

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

Eliminación de nodos, que se complica sin un orden estructurado en el DOM. Una vez más, los fragmentos de código no tienen forma de predeterminar la estructura DOM.

Comencé este viaje queriendo una forma simple de hacer un recorrido de DOM como lo hace JQuery o al menos de alguna manera que tuviera un conjunto de datos estructurados, ya sea un solo enlace, doblemente vinculado o un nodo transversal de árbol. No me importaba cuánto tiempo pudiera analizar una cadena de la forma en que lo hace HTML y también tener el increíble poder de las propiedades de la clase de entidad de nodo para usar en el camino.

Hasta ahora, DOMDocument Object me ha dejado con ganas ... Al igual que con muchos otros programadores, parece ... Sé que he visto mucha frustración en esta pregunta, así que desde FINALMENTE ... (después de aproximadamente 30 horas de intentar y fallar pruebas de tipo) He encontrado una manera de obtenerlo todo. Espero que esto ayude a alguien...

En primer lugar, soy cínico con TODO ... jajaja ...

Habría pasado toda la vida antes de estar de acuerdo con cualquier persona en que se necesita una clase de terceros en este caso de uso. Yo era y NO soy un fanático de usar ninguna estructura de clases de terceros, sin embargo, me encontré con un gran analizador. (unas 30 veces en Google antes de que me rindiera, así que no se sienta solo si lo evitó porque parecía poco oficial o poco oficial de alguna manera ...)

Si está utilizando fragmentos de código y necesita el código limpio y no se ve afectado por el analizador de ninguna manera, sin que se utilicen etiquetas adicionales, utilice simplePHPParser .

Es asombroso y se parece mucho a JQuery. No me impresiona a menudo, pero esta clase hace uso de muchas buenas herramientas y todavía no he tenido errores de análisis. Soy un gran admirador de poder hacer lo que hace esta clase.

Puede encontrar sus archivos para descargar aquí , sus instrucciones de inicio aquí y su API aquí . Recomiendo encarecidamente usar esta clase con sus métodos simples que pueden hacer de .find(".className")la misma manera que se usaría un método de búsqueda de JQuery o incluso métodos familiares como getElementByTagName()o getElementById()...

Cuando guarda un árbol de nodos en esta clase, no agrega nada en absoluto. Simplemente puede decir $doc->save();y genera todo el árbol en una cadena sin ningún problema.

Ahora usaré este analizador para todos los proyectos de ancho de banda sin límite en el futuro.

GoreDefex
fuente
2

Tengo PHP 5.3 y las respuestas aquí no funcionaron para mí.

$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);reemplacé todo el documento con solo el primer hijo, tenía muchos párrafos y solo se estaba guardando el primero, pero la solución me dio un buen punto de partida para escribir algo sin regexdejar algunos comentarios y estoy bastante seguro de que esto se puede mejorar, pero si alguien tiene el mismo problema que yo, puede ser un buen punto de partida.

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

Entonces podríamos usarlo así:

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

Tenga en cuenta que appendChildacepta una DOMNodepara que no necesitemos crear nuevos elementos, simplemente podemos reutilizar los existentes que implementan DOMNode, como DOMElementesto puede ser importante para mantener el código "sano" al manipular múltiples documentos HTML / XML

Ladrillo inmutable
fuente
Esto no funcionará para fragmentos, solo para un único elemento secundario que desee convertir en el primer elemento secundario del documento. Esto es bastante limitado y efectivamente no hace el trabajo del LIBXML_HTML_NOIMPLIEDya que lo hace solo parcialmente. Eliminar el doctype es eficaz LIBXML_HTML_NODEFDTD.
hakre
2

Me encontré con este tema para encontrar una manera de eliminar el contenedor HTML. Usar LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTDfunciona muy bien, pero tengo un problema con utf-8. Después de mucho esfuerzo encontré una solución. Lo publico a continuación para que cualquiera tenga el mismo problema.

El problema causado por <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

El problema:

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

Solución 1:

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

Solución 2:

$dom->loadHTML($document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
utf8_decode($dom->saveHTML($dom->documentElement));
Panagiotis Koursaris
fuente
1
Me parece agradable que comparta sus hallazgos, pero la Solución 2 ya está presente con estas preguntas exactas aquí y la Solución 1 está en otra parte. Además, para el problema de la solución 1, la respuesta dada no es clara. Honro sus buenas intenciones, pero tenga en cuenta que puede crear mucho ruido y dificultar que otros encuentren las soluciones que están buscando, lo que supongo que es un poco lo contrario de lo que desea lograr con su respuesta. Stackoverflow funciona mejor si maneja una pregunta a la vez. Solo una pista.
hakre
2

Me enfrento a 3 problemas con DOMDocument clase.

1- Esta clase carga html con codificación ISO y caracteres utf-8 que no se muestran en la salida.

2- Incluso si damos ‍‍‍LIBXML_HTML_NOIMPLIEDbandera con el método loadHtml, hasta que nuestro HTML de entrada no contiene una etiqueta raíz, no será analizar correctamente.

3- Esta clase considera inválidas las etiquetas HTML5.

Así que anulé esta clase para resolver estos problemas y cambié algunos de los métodos.

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

}

Ahora estoy usando en DOMEditorlugar de DOMDocumenty me ha funcionado bien hasta ahora

        $editor = new DOMEditor();
        $editor->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        // works like a charm!
        echo $editor->saveHTML();
Señor Hosseini
fuente
Su punto 1. se resuelve usando mb_convert_encoding ($ string, 'HTML-ENTITIES', 'UTF-8'); antes de usar loadHTML () y el 2.nd teniendo una etiqueta DIV en su función auxiliar, alrededor de mb_convert_encoding () que usa, por ejemplo. Me funcionó lo suficientemente bien. De hecho, si no hay DIV presente, automáticamente agrega un párrafo en mi caso, lo cual es inconveniente ya que generalmente tienen algún margen aplicado (bootstrap ..)
trainoasis
0

También me encontré con este problema.

Desafortunadamente, no me sentí cómodo usando ninguna de las soluciones proporcionadas en este hilo, así que fui a ver una que me satisficiera.

Esto es lo que inventé y funciona sin problemas:

$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 esencia, funciona de manera similar a la mayoría de las soluciones proporcionadas aquí, pero en lugar de hacer trabajo manual, usa el selector xpath para seleccionar todos los elementos dentro del cuerpo y concatena su código html.

Nikola Petkanski
fuente
Como todas las soluciones aquí, no funciona para todos los casos: si la cadena cargada no comenzó con el marcado, se agregó <p> </p>, entonces su código no funciona, ya que agregará el <p> </p> marcado en el contenido guardado
copndz
Para ser justos, no lo he probado con texto sin formato, pero en teoría debería funcionar. Para su caso específico, es posible que deba cambiar el xpath a algo como descendant-or-self::body/p/*.
Nikola Petkanski
0

mi servidor tiene php 5.3 y no se puede actualizar, así que esas opciones

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD

no son para mi.

Para resolver esto, le digo a la función SaveXML que imprima el elemento Body y luego reemplace el "body" con "div"

aquí está mi código, espero que esté ayudando a alguien:

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

el utf-8 es para soporte en hebreo.

Tomer Ofer
fuente
0

La respuesta de Alex es correcta, pero puede causar el siguiente error en los nodos vacíos:

El argumento 1 pasado a DOMNode :: removeChild () debe ser una instancia de DOMNode

Aquí viene mi pequeño 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;

Agregar el recorte () también es una buena idea para eliminar los espacios en blanco.

redaxmedia
fuente
0

Quizás sea demasiado tarde. Pero tal vez alguien (como yo) todavía tenga este problema.
Entonces, nada de lo anterior funcionó para mí. Debido a que $ dom-> loadHTML también cierra etiquetas abiertas, no solo agrega etiquetas html y body.
Entonces, agregar un elemento <div> no me funciona, porque a veces tengo como 3-4 div sin cerrar en la pieza html.
Mi solución:

1.) Agregue marcador para cortar, luego cargue la pieza html

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

2.) haz lo que quieras con el documento
3.) guarda html

$new_html_piece = $dom->saveHTML();

4.) antes de devolverlo, elimine las etiquetas <p> </ p> del marcador, curiosamente solo aparece en [MARK] pero no en [/ MARK] ...!?

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

5.) eliminar todo antes y después del marcador

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

6.) devuélvelo

return $new_html_piece;

Sería mucho más fácil si LIBXML_HTML_NOIMPLIED funcionara para mí. Debería, pero no lo es. PHP 5.4.17, libxml Versión 2.7.8.
Me parece realmente extraño, uso el analizador HTML DOM y luego, para arreglar esta "cosa", tengo que usar expresiones regulares ... El punto era, no usar expresiones regulares;)

Joe
fuente
Parece peligroso lo que haces aquí, stackoverflow.com/a/29499718/367456 debería hacer el trabajo por ti.
hakre
Desafortunadamente, esto ( stackoverflow.com/questions/4879946/… ) no funcionará para mí. Como dije: "Agregar un elemento <div> no me funciona, porque a veces tengo como 3-4 div sin cerrar en la pieza html" Por alguna razón, DOMDocument quiere cerrar todos los elementos "no cerrados". En tal caso, obtendré un fragmento dentro de un código corto u otro marcador, eliminaré el fragmento y quiero manipular la otra parte del documento, cuando termine con eso, volveré a insertar el fragmento.
Joe
Debería ser posible dejar el elemento div fuera y operar en el elemento del cuerpo después de cargar su propio contenido. El elemento del cuerpo debe agregarse implícitamente cuando carga un fragmento.
hakre
Mi problema es que mi paquete contiene una etiqueta sin cerrar. Debe permanecer sin cerrar y DOMDocument cerrará esos elementos. Fregment como: < div >< div > ... < /div >. Sigo buscando soluciones.
Joe
Hmm, creo que las etiquetas div siempre tienen un par de cierre. Quizás Tidy pueda manejar eso, también puede funcionar con fragmentos.
hakre
0

Para cualquiera que use Drupal, hay una función incorporada para hacer esto:

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

Código de referencia:

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
fuente
Voto a favor. El uso de esta función de la API de Drupal funciona bien en mi sitio de Drupal 7. Supongo que aquellos que no usan Drupal pueden simplemente copiar la función en su propio sitio, ya que no hay nada específico de Drupal sobre esto.
Free Radical
0

Puede usar tidy con show-body-only:

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

Pero, recuerde: tidy elimine algunas etiquetas como Font Awesome icons: Problemas al sangrar HTML (5) con PHP

Rafa Rodríguez
fuente
-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
fuente
¿Te importaría compartir por qué el -1?
Dylan Maxey