Como salvar HTML de DOMDocument sem wrapper HTML?

116

Sou a função abaixo, estou lutando para gerar o DOMDocument sem anexar os wrappers de tag XML, HTML, body e p antes da saída do conteúdo. A correção sugerida:

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

Só funciona quando o conteúdo não tem elementos de nível de bloco dentro dele. No entanto, quando isso acontece, como no exemplo abaixo com o elemento h1, a saída resultante de saveXML é truncada para ...

<p> Se você gosta </p>

Eu fui apontado para esta postagem como uma possível solução alternativa, mas não consigo entender como implementá-la nesta solução (consulte as tentativas comentadas abaixo).

Alguma sugestão?

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
fonte

Respostas:

217

Todas essas respostas estão erradas , porque a partir do PHP 5.4 e da Libxml 2.6 loadHTMLagora tem um $optionparâmetro que instrui a Libxml sobre como ela deve analisar o conteúdo.

Portanto, se carregarmos o HTML com essas opções

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

ao fazer saveHTML(), haverá não doctype, não <html>e não <body>.

LIBXML_HTML_NOIMPLIEDdesativa a adição automática de elementos html / body implícitos LIBXML_HTML_NODEFDTDevita que um doctype padrão seja adicionado quando não for encontrado.

A documentação completa sobre os parâmetros Libxml está aqui

(Observe que os loadHTMLdocumentos dizem que a Libxml 2.6 é necessária, mas LIBXML_HTML_NODEFDTDsó está disponível na Libxml 2.7.8 e LIBXML_HTML_NOIMPLIEDestá disponível na Libxml 2.7.7)

Alessandro Vendruscolo
fonte
10
Isso funciona como um encanto. Deve ser a resposta aceita. Acabei de adicionar um sinalizador e todas as minhas dores de cabeça foram embora ;-)
Just Plain High
8
Isso não funciona com PHP 5.4 e Libxml 2.9. loadHTML não aceita nenhuma opção :(
Acyra
11
Observe que isso não é totalmente perfeito. Consulte stackoverflow.com/questions/29493678/…
Josh Levinson
4
Desculpe, mas esta não parece ser uma boa solução (pelo menos não na prática). Realmente não deveria ser a resposta aceita. Além das questões mencionadas, há também um problema de codificação desagradável com DOMDocumentque também afeta o código nesta resposta. Afaik, DOMDocumentsempre interpreta os dados de entrada como latin-1 , a menos que a entrada especifique um conjunto de caracteres diferente . Em outras palavras: a <meta charset="…">tag parece ser necessária para dados de entrada que não sejam latin-1. Caso contrário, a saída será interrompida para, por exemplo, caracteres UTF-8 multibyte.
mermshaus
1
LIBXML_HTML_NOIMPLIED também bagunça o código HTML removendo as guias, recuos e quebras de linha
Zoltán Süle
72

Basta remover os nós diretamente após carregar o documento com loadHTML ():

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

# remove <html><body></body></html> 
$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
Alex
fonte
esta é a resposta mais limpa para mim.
KnF
39
deve-se observar que isso funciona se <body> tiver apenas um nó filho.
Yann Milin
Funcionou muito bem. Obrigado! Muito mais limpo e mais rápido do que a outra resposta preg.
Ligemer
Obrigado por isso! Acabei de adicionar outro recorte na parte inferior para lidar com nós vazios.
redaxmedia
2
O código a ser removido <!DOCTYPE funciona. A segunda linha é quebrada se <body>tiver mais de uma nota filha.
Free Radical de
21

Em saveXML()vez disso, use e passe documentElement como um argumento para ele.

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

http://php.net/domdocument.savexml

Jonah
fonte
Está melhor, mas ainda estou <html><body> <p> embrulhando o conteúdo.
Scott B
2
Deve-se observar que saveXML () salvará XHTML, não HTML.
alexantd
@Scott: isso é muito estranho. Mostra o que você está tentando fazer ali na seção de exemplos. Tem certeza de que não tem esse HTML em seu DOM? Exatamente o que HTML está em seu DOMDocument? Pode ser que precisemos acessar um nó filho.
Jonah
@Jonah não é estranho. Ao fazer, loadHTMLlibxml usa o módulo analisador HTML e isso irá inserir o esqueleto HTML ausente. Conseqüentemente, $dom->documentElementserá o elemento HTML raiz. Eu consertei seu código de exemplo. Agora deve fazer o que Scott está pedindo.
Gordon
19

O problema com a resposta principal é que ela LIBXML_HTML_NOIMPLIEDé instável .

Ele pode reordenar os elementos (particularmente, movendo a tag de fechamento do elemento superior para a parte inferior do documento), adicionar ptags aleatórias e talvez uma variedade de outros problemas [1] . Pode remover o htmlebody tags para você, mas ao custo de um comportamento instável. Na produção, é uma bandeira vermelha. Em resumo:

Não useLIBXML_HTML_NOIMPLIED . Em vez disso, usesubstr .


Pense nisso. Os comprimentos de <html><body>e </body></html>são fixos e em ambas as extremidades do documento - seus tamanhos nunca mudam e nem suas posições. Isso nos permite usar substrpara cortá-los:

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

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

( ESTA NÃO É A SOLUÇÃO FINAL NO ENTANTO! Veja abaixo a resposta completa , continue lendo para o contexto)

Cortamos 12desde o início do documento porque <html><body>= 12 caracteres ( <<>>+html+body= 4 + 4 + 4), e voltamos e cortamos 15 no final porque \n</body></html>= 15 caracteres (\n+//+<<>>+body+html = 1 + 2 + 4 + 4 + 4)

Observe que eu ainda uso LIBXML_HTML_NODEFDTDomitir !DOCTYPEde ser incluído. Primeiro, isso simplifica a substrremoção das tags HTML / BODY. Segundo, não removemos o doctype com substrporque não sabemos se o ' default doctype' sempre terá um comprimento fixo. Mas mais importante,LIBXML_HTML_NODEFDTD impede o analisador DOM de aplicar um doctype não HTML5 ao documento - o que pelo menos evita que o analisador trate os elementos que não reconhece como texto solto.

Sabemos com certeza que as tags HTML / BODY têm comprimentos e posições fixas, e sabemos que constantes como LIBXML_HTML_NODEFDTDnunca são removidas sem algum tipo de aviso de depreciação, então o método acima deve rolar bem no futuro, MAS ...


... a única ressalva é que a implementação do DOM pode mudar a maneira como as tags HTML / BODY são colocadas no documento - por exemplo, removendo a nova linha no final do documento, adicionando espaços entre as tags ou adicionando novas linhas.

Isso pode ser remediado pesquisando as posições das tags de abertura e fechamento bodye usando esses deslocamentos como para nossos comprimentos para aparar. Usamos strpose strrpospara encontrar os deslocamentos da frente e de trás, 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 encerrar, uma repetição da resposta final, preparada para o 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);

Sem doctype, sem tag html, sem tag body. Podemos apenas esperar que o analisador DOM receba uma nova camada de tinta em breve e possamos eliminar mais diretamente essas tags indesejadas.

Super Gato
fonte
Ótima resposta, um pequeno comentário, por que não em $html = $dom -> saveHTML();vez de $dom -> saveHTML();repetidamente?
Steven de
15

Um truque bacana é usar loadXMLe então saveHTML. As tags htmle bodysão inseridas no loadpalco, não no savepalco.

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

Observe que isso é um pouco hacky e você deve usar a resposta de Jonah se puder fazer funcionar.

solitário
fonte
4
Porém, isso falhará para HTML inválido.
Gordon
1
@Gordon Exatamente por isso que coloquei o aviso de isenção de responsabilidade no final!
lonesomeday
1
Quando tento fazer isso e echo $ dom-> saveHTML (), ele apenas retorna uma string vazia. Como se loadXML ($ content) estivesse vazio. Quando faço o mesmo com $ dom-> loadHTML ($ content), echo $ dom-> saveXML () obtenho o conteúdo conforme o esperado.
Scott B
Usar loadXML quando quiser carregar HTMl é thumb. Especialmente porque o LoadXML não sabe como lidar com HTML.
botenvouwer
15

use DOMDocumentFragment

$html = 'what you want';
$doc = new DomDocument();
$fragment = $doc->createDocumentFragment();
$fragment->appendXML($html);
$doc->appendChild($fragment);
echo $doc->saveHTML();
jcp
fonte
3
A resposta mais limpa para pré php5.4.
Nick Johnson,
Isso funciona para mim, tanto mais antigo quanto mais novo que a versão Libxml 2.7.7. Por que isso seria apenas para pré php5.4?
RobbertT
Isso deve ter mais votos. Ótima opção para versões de libxml que não suportam LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD. Obrigado!
Marty Mulligan
13

É 2017 e, para esta questão de 2011, não gosto de nenhuma das respostas. Muitos regex, grandes classes, loadXML etc ...

Solução fácil que resolve os problemas conhecidos:

$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, simples, sólido, rápido. Este código funcionará em relação a tags HTML e codificação como:

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

Se alguém encontrar um erro, diga, eu mesmo usarei.

Editar , Outras opções válidas que funcionam sem erros (muito semelhantes às já fornecidas):

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

Você mesmo poderia adicionar corpo para prevenir qualquer coisa estranha no furure.

Trinta opção:

 $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
fonte
3
Você deve melhorar sua resposta evitando o mais caro mb_convert_encodinge, em vez disso, adicionando <html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>e modificando de substracordo. Aliás, a sua solução é a mais elegante aqui. Votos positivos.
Hlsg
10

Estou um pouco atrasado no clube, mas não queria deixar de compartilhar um método que descobri. Em primeiro lugar, tenho as versões certas de loadHTML () para aceitar essas opções legais, mas LIBXML_HTML_NOIMPLIEDnão funcionou no meu sistema. Também os usuários relatam problemas com o analisador (por exemplo, aqui e aqui ).

A solução que criei é bem simples.

O HTML a ser carregado é colocado em um <div> elemento para que tenha um contêiner contendo todos os nós a serem carregados.

Em seguida, esse elemento de contêiner é removido do documento (mas o DOMElement dele ainda existe).

Em seguida, todos os filhos diretos do documento são removidos. Isto inclui qualquer adicionado <html>, <head>e <body>tags (efetivamente LIBXML_HTML_NOIMPLIEDopção), bem como a <!DOCTYPE html ... loose.dtd">declaração (efetivamente LIBXML_HTML_NODEFDTD).

Em seguida, todos os filhos diretos do contêiner são adicionados ao documento novamente e ele pode ser gerado.

$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 normalmente, apenas tome cuidado para que haja vários elementos de documento agora, portanto, não um único nó raiz:

$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) (construído: 21 de dezembro de 2014 20:28:53)
hakre
fonte
não funcionou para mim com fontes de HTML mais complexas. Ele também removeu parte do HTML.
Zoltán Süle de
4

Nenhuma das outras soluções até o momento em que este livro foi escrito (junho de 2012) foi capaz de atender completamente às minhas necessidades, então escrevi uma que lida com os seguintes casos:

  • Aceita conteúdo de texto simples sem tags, bem como conteúdo HTML.
  • Não acrescentar quaisquer etiquetas (incluindo <doctype>, <xml>, <html>, <body>, e<p> etiquetas)
  • Deixa tudo embrulhado em <p> sozinho.
  • Deixa o texto vazio sozinho.

Portanto, aqui está uma solução que corrige esses 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;
    }
}

Também escrevi alguns testes que viveriam nessa mesma 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
}

Você pode verificar se funciona sozinho. DomDocumentWorkaround::testAll()retorna este:

    Succeeded
    Succeeded
    Succeeded
    Succeeded
lavrador
fonte
1
HTML = / = XML, você deve usar o carregador de HTML para HTML.
hakre,
4

Ok, encontrei uma solução mais elegante, mas é entediante:

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

Tudo bem, espero que isso não omita nada e ajude alguém?

rclai
fonte
2
Não
resolve
3

Use esta função

$layout = preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $layout);
Boksiora
fonte
13
Pode haver alguns leitores que se depararam com esta postagem através desta postagem , decidiram não usar regex para analisar seu HTML e usar um analisador DOM em vez disso, e acabam precisando potencialmente de uma resposta regex para obter uma solução completa ... irônico
Robbie Averill
Eu não entendo por que noboy apenas retorna o conteúdo de BODY. Essa tag não deve estar sempre presente quando o analisador adiciona todo o cabeçalho / doctype do documento? O regex acima seria ainda mais curto.
sergio
@boksiora "ele faz o trabalho" - então por que estamos usando métodos de analisador DOM em primeiro lugar?
Obrigado
@naomik, eu não disse para não usar um analisador DOM, é claro que existem muitas maneiras diferentes de obter o mesmo resultado, cabe a você decidir, no momento em que usei essa função, tive um problema com o php dom integrado analisador, que não estava analisando o html5 corretamente.
boksiora 01 de
1
Tive de usar preg_replaceporque usar métodos baseados em DOMDocument para remover as tags html e body não estavam preservando a codificação UTF-8 :(
wizonesolutions
3

Se a solução de sinalizadores respondida por Alessandro Vendruscolo não funcionar, você pode tentar o seguinte:

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

$bodyTagconterá seu código HTML totalmente processado sem todos os envoltórios de HTML, exceto para a <body>tag, que é a raiz do seu conteúdo. Então você pode usar um regex ou uma função trim para removê-lo da string final (depois saveHTML) ou, como no caso acima, iterar sobre todos os seus filhos, salvando seu conteúdo em uma variável temporária $finalHtmle retornando-o (o que eu acredito ser mais seguro).

José Ricardo Júnior
fonte
3

Estou lutando com isso no RHEL7 executando PHP 5.6.25 e LibXML 2.9. (Coisas antigas em 2018, eu sei, mas isso é Red Hat para você.)

Eu descobri que a solução muito votada sugerida por Alessandro Vendruscolo quebra o HTML ao reorganizar as tags. Ie:

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

torna-se:

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

Isso vale para ambas as opções que ele sugere que você use: LIBXML_HTML_NOIMPLIEDe LIBXML_HTML_NODEFDTD.

A solução sugerida por Alex vai no meio do caminho para resolvê-lo, mas não funciona se <body>tiver mais de um nó filho.

A solução que funciona para mim é a seguinte:

Primeiro, para carregar o DOMDocument, eu uso:

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

Para salvar o documento depois de massagear o DOMDocument, eu 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);

Sou o primeiro a concordar que esta não é uma solução muito elegante - mas funciona.

Radical Livre
fonte
2

Adicionar a <meta>tag irá desencadear o comportamento de fixação deDOMDocument . A parte boa é que você não precisa adicionar essa tag. Se você não quiser usar uma codificação de sua escolha, passe-a como um argumento do construtor.

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

Resultado

<div>Hello World</div>

Obrigado a @Bart

botenvouwer
fonte
2

Eu também tinha esse requisito e gostei da solução postada por Alex acima. No entanto, há alguns problemas - se o <body>elemento contiver mais de um elemento filho, o documento resultante conterá apenas o primeiro elemento filho de <body>, não todos eles. Além disso, eu precisava da remoção para lidar com as coisas condicionalmente - somente quando você tinha um documento com os cabeçalhos HTML. Então, eu o refinei da seguinte maneira. Em vez de removê-lo <body>, transformei-o em um <div>e retirei a declaração XML e <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
fonte
2

Assim como outros membros, eu primeiro me deliciei com a simplicidade e o incrível poder da resposta de @Alessandro Vendruscolo. A capacidade de simplesmente passar algumas constantes sinalizadas para o construtor parecia boa demais para ser verdade. Para mim foi. Eu tenho as versões corretas de LibXML e também de PHP, no entanto, não importa o que, ele ainda adicionaria a tag HTML à estrutura de nó do objeto Document.

Minha solução funcionou muito melhor do que usar o ...

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

Sinalizadores ou ....

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

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

Remoção de nó, que fica confusa sem uma ordem estruturada no DOM. Novamente, os fragmentos de código não têm como predeterminar a estrutura do DOM.

Comecei esta jornada querendo uma maneira simples de fazer a travessia do DOM como o JQuery faz ou pelo menos de alguma forma que tivesse um conjunto de dados estruturados tanto com link único, quanto com link duplo ou travessia de nó em árvore. Eu não me importava por quanto tempo eu poderia analisar uma string como o HTML e também ter o poder incrível das propriedades da classe de entidade de nó para usar ao longo do caminho.

Até agora, o Objeto DOMDocument me deixou com desejos ... Como muitos outros programadores, parece ... Eu sei que tenho visto muita frustração nesta questão, então, desde que FINALMENTE ... (após cerca de 30 horas de tentativas e fracassos teste de tipo) Eu encontrei uma maneira de conseguir tudo. Espero que isso ajude alguém...

Em primeiro lugar, sou cínico de TUDO ... rs ...

Eu teria passado uma vida inteira antes de concordar com alguém que uma classe de terceiros é de qualquer maneira necessária neste caso de uso. Eu era e NÃO sou um fã de usar qualquer estrutura de classe de terceiros, mas me deparei com um ótimo analisador. (cerca de 30 vezes no Google antes de eu ceder, então não se sinta sozinho se você evitou, porque parecia coxo ou não oficial de alguma forma ...)

Se você estiver usando fragmentos de código e precisar do, código limpo e não afetado pelo analisador de nenhuma forma, sem tags extras sendo usadas, use simplePHPParser .

É incrível e funciona muito como JQuery. Não fico impressionado com frequência, mas esta classe usa várias ferramentas boas e ainda não tive erros de análise. Eu sou um grande fã de ser capaz de fazer o que esta classe faz.

Você pode encontrar seus arquivos para download aqui , suas instruções de inicialização aqui e sua API aqui . Eu recomendo fortemente o uso desta classe com seus métodos simples que podem fazer .find(".className")da mesma maneira que um método find JQuery seria usado ou até mesmo métodos familiares como getElementByTagName()ou getElementById()...

Quando você salva uma árvore de nós nesta classe, ela não adiciona nada. Você pode simplesmente dizer $doc->save();e ele produz a árvore inteira em uma string sem qualquer problema.

Agora irei usar este analisador para todos os projetos de largura de banda sem limite no futuro.

GoreDefex
fonte
2

Tenho PHP 5.3 e as respostas aqui não funcionaram para mim.

$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);substituí todo o documento com apenas o primeiro filho, eu tinha muitos parágrafos e apenas o primeiro estava sendo salvo, mas a solução me deu um bom ponto de partida para escrever algo sem regexdeixar alguns comentários e tenho certeza que isso pode ser melhorado, mas se alguém tem o mesmo problema que eu, pode ser um bom ponto 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;
}

Então, poderíamos usá-lo assim:

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

Observe que appendChildaceita um, DOMNodeportanto, não precisamos criar novos elementos, podemos apenas reutilizar os existentes que implementam DOMNodecomo DOMElementeste pode ser importante para manter o código "são" ao manipular vários documentos HTML / XML

Tijolo Imutável
fonte
Isso não funcionará para fragmentos, apenas para um único elemento filho que você deseja tornar o primeiro filho do documento. Isso é bastante limitado e efetivamente não está fazendo o trabalho do, LIBXML_HTML_NOIMPLIEDjá que o faz apenas parcialmente. Remover o doctype é eficaz LIBXML_HTML_NODEFDTD.
hakre
2

Me deparei com este tópico para encontrar uma maneira de remover o wrapper HTML. Usar LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTDfunciona muito bem, mas tenho um problema com o utf-8. Depois de muito esforço, encontrei uma solução. Posto abaixo para quem tiver o mesmo problema.

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

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

Solução 1:

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

Solução 2:

$dom->loadHTML($document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
utf8_decode($dom->saveHTML($dom->documentElement));
Panagiotis Koursaris
fonte
1
Acho bom que você compartilhe suas descobertas, mas a Solução 2 já está presente com exatamente essas questões aqui e a Solução 1 está em outro lugar. Também para o problema da solução 1, a resposta dada não é clara. Eu honro suas boas intenções, mas esteja ciente de que isso pode criar muito ruído, além de impedir outras pessoas de encontrar as soluções que estão procurando, o que eu acho que é o oposto do que você deseja alcançar com sua resposta. Stackoverflow funciona melhor se você lidar com uma pergunta de cada vez. Apenas uma dica.
hakre
2

Eu enfrento 3 problemas com as DOMDocumentaulas.

1- Essa classe carrega html com codificação ISO e caracteres utf-8 não exibidos na saída.

2 Mesmo se dermos LIBXML_HTML_NOIMPLIEDbandeira para o método loadHTML, até o nosso html de entrada não contém uma tag raiz, não será analisado corretamente.

3- Esta classe considera as tags HTML5 inválidas.

Portanto, substituí esta classe para resolver esses problemas e alterei alguns dos 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);
    }

}

Agora estou usando em DOMEditorvez de DOMDocumente tem funcionado bem para mim até agora

        $editor = new DOMEditor();
        $editor->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        // works like a charm!
        echo $editor->saveHTML();
Sr. Hosseini
fonte
Seu ponto 1. é resolvido usando mb_convert_encoding ($ string, 'HTML-ENTITIES', 'UTF-8'); antes de usar loadHTML () e 2.nd por ter uma tag DIV em torno de sua função auxiliar, em torno de mb_convert_encoding () que você usa, por exemplo. Funcionou bem para mim. Na verdade, se nenhum DIV estiver presente, ele adiciona automaticamente um parágrafo no meu caso, o que é inconveniente, pois geralmente eles têm alguma margem aplicada (bootstrap ..)
trainoasis
0

Também me deparei com esse problema.

Infelizmente, não me senti confortável em usar nenhuma das soluções fornecidas neste tópico, então fui verificar uma que me satisfizesse.

Aqui está o que eu criei e funciona sem 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);
}

Em essência, ele funciona de maneira semelhante à maioria das soluções fornecidas aqui, mas em vez de fazer trabalho manual, ele usa o seletor xpath para selecionar todos os elementos dentro do corpo e concatena seu código html.

Nikola Petkanski
fonte
Como todas as soluções aqui, não funciona para todos os casos: se a string carregada não começou com marcação, <p> </p> foi adicionado, então seu código não funciona, pois adicionará o <p> </p> marcação no conteúdo salvo
copndz de
Para ser justo, não testei com texto bruto, mas em teoria deve funcionar. Para o seu caso específico, você pode precisar alterar o xpath para algo como descendant-or-self::body/p/*.
Nikola Petkanski
0

meu servidor tem php 5.3 e não pode atualizar essas opções

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD

não são para mim.

Para resolver isso, digo à função SaveXML para imprimir o elemento Body e, em seguida, apenas substituir o "corpo" por "div"

aqui está meu código, espero que esteja ajudando alguém:

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

o utf-8 é para suporte em hebraico.

Tomer Ofer
fonte
0

A resposta de Alex está correta, mas pode causar o seguinte erro em nós vazios:

O argumento 1 passado para DOMNode :: removeChild () deve ser uma instância de DOMNode

Aí vem meu pequeno 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;

Adicionar o trim () também é uma boa ideia para remover os espaços em branco.

redaxmedia
fonte
0

Talvez seja tarde demais. Mas talvez alguém (como eu) ainda tenha esse problema.
Portanto, nenhuma das opções acima funcionou para mim. Porque $ dom-> loadHTML também fecha as tags abertas, não apenas adiciona as tags html e body.
Portanto, adicionar um elemento <div> não está funcionando para mim, porque às vezes tenho 3-4 div não fechado na parte html.
Minha solução:

1.) Adicione um marcador para cortar e, em seguida, carregue a peça html

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

2.) faça o que quiser com o documento
3.) salve o html

$new_html_piece = $dom->saveHTML();

4.) antes de devolvê-lo, remova as tags <p> </ p> do marcador, estranhamente ele só aparece em [MARK], mas não em [/ MARK] ...!?

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

5.) remova tudo antes e depois do marcador

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

6.) devolvê-lo

return $new_html_piece;

Seria muito mais fácil se LIBXML_HTML_NOIMPLIED funcionasse para mim. Schould, mas não é. PHP 5.4.17, libxml Versão 2.7.8.
Eu acho muito estranho, eu uso o analisador HTML DOM e então, para consertar essa "coisa" eu tenho que usar regex ... A questão toda era, não usar regex;)

Joe
fonte
Parece perigoso o que você faz aqui, stackoverflow.com/a/29499718/367456 deve fazer o trabalho para você.
hakre
Infelizmente, isso ( stackoverflow.com/questions/4879946/… ) não funcionará para mim. Como eu disse: "Portanto, adicionar um elemento <div> não está funcionando para mim, porque às vezes tenho 3-4 div não fechado no pedaço html" Por alguma razão, o DOMDocument deseja fechar todos os elementos "não fechados". Em caso, vou pegar um fregment dentro de um shortcode ou outro marcador, remover o fregment e quero manipular a outra parte do documento, quando terminar, vou inserir o fregment de volta.
Joe
Deve ser possível deixar o elemento div de fora e operar no elemento body após carregar seu próprio conteúdo. O elemento do corpo deve ser adicionado implicitamente quando você carrega um fragmento.
hakre 01 de
Meu problema é que meu fregment contém tag não fechada. Deve permanecer não fechado e DOMDocument fechará esses elementos. Fregment como: < div >< div > ... < /div >. Ainda estou procurando soluções.
Joe
Hmm, acho que as tags div sempre têm um par de fechamento. Talvez o Tidy consiga lidar com isso, mas também pode trabalhar com fragmentos.
hakre
0

Para qualquer pessoa que use o Drupal, há uma função integrada para fazer isso:

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

Código para referência:

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
fonte
Votos positivos. Usar esta função da API Drupal funciona bem no meu site Drupal 7. Eu acho que aqueles que não usam o Drupal podem simplesmente copiar a função em seu próprio site - já que não há nada específico do Drupal sobre isso.
Free Radical de
0

Você pode usar tidy com show-body-only:

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

Mas, lembre-se: remova cuidadosamente algumas tags como Ícones impressionantes de fonte: Problemas de recuo HTML (5) com PHP

Rafa Rodríguez
fonte
-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
fonte
Importa-se de compartilhar por que o -1?
Dylan Maxey