如何在没有 HTML 包装器的情况下保存 DOMDocument 的 HTML?

Posted

技术标签:

【中文标题】如何在没有 HTML 包装器的情况下保存 DOMDocument 的 HTML?【英文标题】:How to saveHTML of DOMDocument without HTML wrapper? 【发布时间】:2011-06-20 06:42:12 【问题描述】:

我是下面的函数,我正在努力输出 DOMDocument 而不在输出之前附加 XML、htmlbodyp 标记包装器内容。建议的修复:

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

仅当内容中没有块级元素时才有效。但是,当它这样做时,如下例中使用 h1 元素的示例,saveXML 的结果输出将被截断为...

如果你喜欢

有人指出这篇文章是一种可能的解决方法,但我不明白如何将它实施到这个解决方案中(请参阅下面注释掉的尝试)。

有什么建议吗?

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;

【问题讨论】:

【参考方案1】:

在阅读了有关该主题的大量代码后,我最终得到了以下解决方案,该解决方案对我来说非常有效且易于理解。

它修复了不需要的 Doctype 和 &lt;html&gt;&lt;body&gt; 以及编码问题。

此代码假定$htmlContent 编码为utf-8

$htmlContent = "<h1>This is a heading</h1><p>This is a paragraph</p>";

// 1.) Load the html
$dom = new DOMDocument();
$dom->loadHTML("<meta http-equiv='Content-Type' content='charset=utf-8' /><div>$htmlContent</div>");

// 2.) Do you logic
$dom->getElementsByTagName('h1')[0]->setAttribute('class', 'happy');

// 3.) Render the html
$wrapperNode = $dom->getElementsByTagName('div')[0];
$renderedHtml = $dom->saveHTML($wrapperNode);
// If you want to keep the wrapper div
echo $renderedHtml;
// Or remove the wrapper <div>
echo substr(trim($renderedHtml), 5, -6);

关键要点是:

loadHTML 假定内容为 iso-8859-1,如果不是这种情况,则需要添加编码信息。 将您的 html 代码包装在一个 div 中并仅呈现此 div,如果您不想保留它,可以使用子字符串将其删除。

【讨论】:

【参考方案2】:

我的通用解决方案独立于 HTML 的加载方式:

function getNodeHtml(DOMNode $node, $outer = true) 
    $doc = new DOMDocument();
    $node = $node instanceof DOMDocument ? $node->documentElement : $node;
    foreach(($outer ? array($node) : $node->childNodes) as $n) 
        $doc->appendChild($doc->importNode($n->cloneNode(true), true));
    
    return $doc->saveHTML();

示例结果:

&lt;p&gt;foo bar &lt;/p&gt;━▶&lt;p&gt;foo bar &lt;/p&gt; &lt;p&gt;foo&lt;/p&gt;&lt;p&gt;bar&lt;/p&gt;━▶&lt;p&gt;foo&lt;/p&gt;&lt;p&gt;bar&lt;/p&gt; &lt;p&gt;foo &lt;/p&gt; &lt;p&gt; bar&lt;/p&gt;━▶&lt;p&gt;foo &lt;/p&gt; &lt;p&gt; bar&lt;/p&gt; Hello!━▶Hello! &lt;html&gt;&lt;body&gt;&lt;b&gt;foo&lt;/b&gt;&lt;/body&gt;&lt;/html&gt;━▶&lt;html&gt;&lt;body&gt;&lt;b&gt;foo&lt;/b&gt;&lt;/body&gt;&lt;/html&gt;

【讨论】:

这完全没有任何作用...为什么不直接使用 "$doc->saveHTML()" ?【参考方案3】:

这是帮助我的解决方案:

$content = str_replace(array('<html>','</html>') , '' , $doc->saveHTML());

【讨论】:

【参考方案4】:

您可以将 tidy 与 show-body-only 一起使用:

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

但是,请记住:整理删除一些标签,例如 Font Awesome 图标:Problems Indenting HTML(5) with php

【讨论】:

【参考方案5】:

我在DOMDocument 课程中遇到 3 个问题。

1- 此类加载带有 ISO 编码和 utf-8 字符的 html 未显示在输出中。

2- 即使我们给 loadHtml 方法提供了 ‍‍‍LIBXML_HTML_NOIMPLIED 标志,直到我们输入的 html 不包含根标签,它也不会被正确解析。

3- 此类认为 HTML5 标记无效。

所以我重写了这个类来解决这些问题,我改变了一些方法。

class DOMEditor extends DOMDocument

    /**
     * Temporary wrapper tag , It should be an unusual tag to avoid problems
     */
    protected $tempRoot = 'temproot';

    public function __construct($version = '1.0', $encoding = 'UTF-8')
    
        //turn off html5 errors
        libxml_use_internal_errors(true);
        parent::__construct($version, $encoding);
    

    public function loadHTML($source, $options = LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD)
    
        // this is a bitwise check if LIBXML_HTML_NOIMPLIED is set
        if ($options & LIBXML_HTML_NOIMPLIED) 
            // it loads the content with a temporary wrapper tag and utf-8 encoding
            parent::loadHTML("<$this->tempRoot>" . mb_convert_encoding($source, 'HTML', 'UTF-8') . "</$this->tempRoot>", $options);
         else 
            // it loads the content with utf-8 encoding and default options
            parent::loadHTML(mb_convert_encoding($source, 'HTML', 'UTF-8'), $options);
        
    

    private function unwrapTempRoot($output)
    
        if ($this->firstChild->nodeName === $this->tempRoot) 
            return substr($output, strlen($this->tempRoot) + 2, -strlen($this->tempRoot) - 4);
        
        return $output;
    

    public function saveHTML(DOMNode $node = null)
    
        $html = html_entity_decode(parent::saveHTML($node));
        if (is_null($node)) 
            $html = $this->unwrapTempRoot($html);
        
        return $html;
    

    public function saveXML(DOMNode $node = null, $options = null)
    
        if (is_null($node)) 
            return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . PHP_EOL . $this->saveHTML();
        
        return parent::saveXML($node);
    



现在我使用DOMEditor 而不是DOMDocument,到目前为止它对我来说效果很好

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

【讨论】:

你的观点 1. 通过使用 mb_convert_encoding($string, 'HTML-ENTITIES', 'UTF-8');在使用 loadHTML() 和 2.nd 之前,通过在您的辅助函数中使用 DIV 标记,例如您使用的 mb_convert_encoding() 周围。对我来说已经足够好了。实际上,如果不存在 DIV,那么它会在我的情况下自动添加一个段落,这很不方便,因为通常它们会应用一些边距(引导程序 ..)【参考方案6】:
#remove doctype tag
$doc->removeChild($doc->doctype); 

#remove html & body tags
$html = $doc->getElementsByTagName('html')[0];
$body = $html->getElementsByTagName('body')[0];
foreach($body->childNodes as $child) 
    $doc->appendChild($child);

$doc->removeChild($html);

【讨论】:

愿意分享为什么是-1?【参考方案7】:

我在运行 PHP 5.6.25 和 LibXML 2.9 的 RHEL7 上苦苦挣扎。 (我知道,2018 年的旧东西,但那是你的红帽。)

我发现 Alessandro Vendruscolo 建议的备受好评的解决方案通过重新排列标签来破坏 HTML。即:

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

变成:

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

这适用于他建议您使用的两个选项:LIBXML_HTML_NOIMPLIEDLIBXML_HTML_NODEFDTD

Alex 建议的解决方法是解决了一半,但是如果&lt;body&gt; 有多个子节点就不行了。

对我有用的解决方案如下:

首先,要加载 DOMDocument,我使用:

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

为了在按摩 DOMDocument 后保存文档,我使用:

// remove <!DOCTYPE 
$doc->removeChild($doc->doctype);  
$content = $doc->saveHTML();
// remove <html><body></body></html> 
$content = str_replace('<html><body>', '', $content);
$content = str_replace('</body></html>', '', $content);

我是第一个同意这不是一个非常优雅的解决方案 - 但它确实有效。

【讨论】:

这行不通 - 如果 $content 包含 信息,那么它会失败。【参考方案8】:

这个库使遍历/修改 DOM 变得简单,并且还为您删除了 doctype/html 包装器:

https://github.com/sunra/php-simple-html-dom-parser

【讨论】:

【参考方案9】:

现在是 2017 年,对于这个 2011 年的问题,我不喜欢任何答案。 很多正则表达式、大类、loadXML 等...

解决已知问题的简单解决方案:

$dom = new DOMDocument();
$dom->loadHTML( '<html><body>'.mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8').'</body></html>' , LIBXML_HTML_NODEFDTD);
$html = substr(trim($dom->saveHTML()),12,-14);

简单、可靠、快速。此代码适用于 HTML 标记和编码,例如:

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

如果有人发现错误,请告诉我,我会自己使用这个。

编辑,其他没有错误的有效选项(与已经给出的非常相似):

@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$saved_dom = trim($dom->saveHTML());
$start_dom = stripos($saved_dom,'<body>')+6;
$html = substr($saved_dom,$start_dom,strripos($saved_dom,'</body>') - $start_dom );

您可以自己添加身体,以防止任何奇怪的事情发生。

第三个选项:

 $mock = new DOMDocument;
 $body = $dom->getElementsByTagName('body')->item(0);
  foreach ($body->childNodes as $child)
     $mock->appendChild($mock->importNode($child, true));
  
$html = trim($mock->saveHTML());

【讨论】:

您应该避免使用更昂贵的mb_convert_encoding,而是添加&lt;html&gt;&lt;head&gt;&lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"&gt;&lt;/head&gt;&lt;body&gt; 并相应地修改substr,从而改进您的答案。顺便说一句,您的解决方案是这里最优雅的解决方案。赞成。【参考方案10】:

最佳答案的问题是 LIBXML_HTML_NOIMPLIED 不稳定

它可以对元素重新排序(特别是将顶部元素的结束标签移动到文档底部),添加随机的p 标签,也许还有其他各种问题[1]。它可能会为您删除htmlbody 标签,但代价是行为不稳定。在生产中,这是一个危险信号。简而言之:

不要使用LIBXML_HTML_NOIMPLIED改为使用substr


考虑一下。 &lt;html&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt; 的长度是固定的,并且位于文档的两端——它们的大小永远不会改变,它们的位置也不会改变。这允许我们使用substr 将它们删除:

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

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

这不是最终的解决方案!完整答案见下文,请继续阅读上下文)

我们将12 从文档的开头剪掉,因为&lt;html&gt;&lt;body&gt; = 12 个字符(&lt;&lt;&gt;&gt;+html+body = 4+4+4),我们向后剪掉15 个字符,因为\n&lt;/body&gt;&lt;/html&gt; = 15字符(\n+//+&lt;&lt;&gt;&gt;+body+html = 1 + 2 + 4 + 4 + 4)

请注意,我仍然使用 LIBXML_HTML_NODEFDTD 省略了 !DOCTYPE 被包含在内。首先,这简化了 HTML/BODY 标记的 substr 删除。其次,我们不会删除带有substr 的文档类型,因为我们不知道'default doctype' 是否总是固定长度的东西。但是,最重要的是,LIBXML_HTML_NODEFDTD 阻止 DOM 解析器将非 HTML5 文档类型应用于文档——这至少可以防止解析器将它无法识别的元素视为松散文本。

我们知道 HTML/BODY 标签的长度和位置是固定的,并且我们知道像 LIBXML_HTML_NODEFDTD 这样的常量在没有某种类型的弃用通知的情况下永远不会被删除,所以上述方法应该会很好地应用于未来, 但是...


...唯一需要注意的是 DOM 实现 可能 改变 HTML/BODY 标记在文档中的放置方式 - 例如,删除文档末尾的换行符,添加标签之间的空格,或添加换行符。

这可以通过搜索 body 的开始和结束标签的位置来解决,并使用这些偏移量作为我们的长度来修剪。我们使用strposstrrpos 分别求出前后偏移量:

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

$trim_off_front = strpos($dom->saveHTML(),'<body>') + 6;
// PositionOf<body> + 6 = Cutoff offset after '<body>'
// 6 = Length of '<body>'

$trim_off_end = (strrpos($dom->saveHTML(),'</body>')) - strlen($dom->saveHTML());
// ^ PositionOf</body> - LengthOfDocument = Relative-negative cutoff offset before '</body>'

echo substr($dom->saveHTML(), $trim_off_front, $trim_off_end);

最后,重复最终的、面向未来的答案

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

$trim_off_front = strpos($dom->saveHTML(),'<body>') + 6;
$trim_off_end = (strrpos($dom->saveHTML(),'</body>')) - strlen($dom->saveHTML());

echo substr($dom->saveHTML(), $trim_off_front, $trim_off_end);

没有 doctype,没有 html 标签,没有 body 标签。我们只能希望 DOM 解析器能尽快获得新的油漆,我们可以更直接地消除这些不需要的标签。

【讨论】:

很好的答案,一个小评论,为什么不重复$html = $dom -&gt; saveHTML(); 而不是$dom -&gt; saveHTML();【参考方案11】:

我遇到这个主题是为了找到一种删除 HTML 包装器的方法。使用 LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD 效果很好,但我对 utf-8 有疑问。经过一番努力,我找到了解决方案。我把它贴在下面,给有同样问题的人。

由于&lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"&gt;引起的问题

问题:

$dom = new DOMDocument();
$dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $document, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$dom->saveHTML();

解决方案 1:

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

解决方案 2:

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

【讨论】:

我很高兴您能分享您的发现,但解决方案 2 已经在此处提供了这些确切的问题,而解决方案 1 在其他地方。同样对于解决方案 1 的问题,给出的答案也不清楚。我尊重你的好意,但请注意,它会产生很多噪音,同时也会阻碍其他人找到他们正在寻找的解决方案,我猜这与你想要通过你的答案实现的目标有点相反。如果您一次处理一个问题,*** 效果最好。只是一个提示。【参考方案12】:

对于任何使用 Drupal 的人,有一个内置函数可以做到这一点:

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

参考代码:

function filter_dom_serialize($dom_document) 
  $body_node = $dom_document->getElementsByTagName('body')->item(0);
  $body_content = '';

  if ($body_node !== NULL) 
    foreach ($body_node->getElementsByTagName('script') as $node) 
      filter_dom_serialize_escape_cdata_element($dom_document, $node);
    

    foreach ($body_node->getElementsByTagName('style') as $node) 
      filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/');
    

    foreach ($body_node->childNodes as $child_node) 
      $body_content .= $dom_document->saveXML($child_node);
    
    return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content);
  
  else 
    return $body_content;
  

【讨论】:

赞成。在我的 Drupal 7 站点上使用 Drupal API 中的这个函数可以正常工作。我猜那些不使用 Drupal 的人可以将函数复制到他们自己的站点中——因为这与 Drupal 无关。【参考方案13】:

我有 PHP 5.3,这里的答案对我不起作用。

$doc-&gt;replaceChild($doc-&gt;firstChild-&gt;firstChild-&gt;firstChild, $doc-&gt;firstChild); 只用第一个孩子替换了所有文档,我有很多段落,只有第一个被保存,但是解决方案给了我一个很好的起点,可以在没有 regex 的情况下写一些东西我留下了一些 cmets,我我很确定这可以改进,但如果有人和我有同样的问题,这可能是一个很好的起点。

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

    // lets get all children inside the body tag
    foreach ($doc->firstChild->firstChild->childNodes as $k => $v) 
        if($k !== 0) // don't store the first element since that one will be used to replace the html tag
            $doc->appendChild( clone($v) ); // appending element to the root so we can remove the first element and still have all the others
        
    
    // replace the body tag with the first children
    $doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);
    return $doc;

那么我们可以这样使用它:

$doc = new DOMDocument();
$doc->encoding = 'UTF-8';
$doc->loadHTML('<p>Some html here</p><p>And more html</p><p>and some html</p>');
$doc = extractDOMContent($doc);

请注意,appendChild 接受 DOMNode,因此我们不需要创建新元素,我们可以重用实现 DOMNode 的现有元素,例如 DOMElement,这对于在以下情况下保持代码“健全”很重要处理多个 HTML/XML 文档

【讨论】:

这不适用于片段,仅适用于您想要制作文档的第一个子元素的单个子元素。这是非常有限的,实际上并没有完成LIBXML_HTML_NOIMPLIED 的工作,因为它只是部分完成了工作。删除文档类型实际上是LIBXML_HTML_NODEFDTD【参考方案14】:

如果 Alessandro Vendruscolo 回答的标志解决方案不起作用,您可以试试这个:

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

//do your stuff..

$finalHtml = '';
$bodyTag = $dom->documentElement->getElementsByTagName('body')->item(0);
foreach ($bodyTag->childNodes as $rootLevelTag) 
    $finalHtml .= $dom->saveHTML($rootLevelTag);

echo $finalHtml;

$bodyTag 将包含您完整处理的 HTML 代码,没有所有这些 HTML 包装,除了 &lt;body&gt; 标记,它是您的内容的根。然后,您可以使用正则表达式或修剪函数将其从最终字符串中删除(在 saveHTML 之后),或者像上面的情况一样,遍历其所有子项,将其内容保存到临时变量 $finalHtml 并返回它(我认为更安全)。

【讨论】:

【参考方案15】:

我可能为时已晚。但也许有人(比如我)仍然有这个问题。 所以,以上方法都不适合我。因为 $dom->loadHTML 也会关闭打开的标签,不仅添加 html 和 body 标签。 所以添加一个 元素对我不起作用,因为我有时在 html 片段中有 3-4 个未闭合的 div。 我的解决方案: 1.) 添加标记进行剪切,然后加载html片段

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

2.) 对文档做任何你想做的事情 3.) 保存html

$new_html_piece = $dom->saveHTML();

4.) 在你返回它之前,从标记中删除

标签,奇怪的是它只出现在 [MARK] 而不是出现在 [/MARK]...!?
$new_html_piece = preg_replace( "/<p[^>]*?>(\[MARK\]|\s)*?<\/p>/", "[MARK]" , $new_html_piece );

5.) 删除标记前后的所有内容

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

6.) 归还

return $new_html_piece;

如果 LIBXML_HTML_NOIMPLIED 为我工作会容易得多。它应该,但事实并非如此。 PHP 5.4.17,libxml 版本 2.7.8。 我觉得很奇怪,我使用 HTML DOM 解析器,然后,为了修复这个“东西”,我必须使用正则表达式......重点是,不要使用正则表达式;)

【讨论】:

你在这里做的事情看起来很危险,***.com/a/29499718/367456 应该为你做这项工作。 不幸的是,这 (***.com/questions/4879946/…) 对我不起作用。正如我所说:“所以添加一个 元素对我不起作用,因为我有时在 html 片段中喜欢 3-4 个未关闭的 div” 出于某种原因,DOMDocument 想要关闭所有“未关闭”的元素。在可能的情况下,我会在短代码或其他标记中获得一个片段,删除该片段并且我想操作文档的另一部分,当我完成后,我将插入该片段。 应该可以不用div元素,在加载自己的内容后对body元素进行操作。加载片段时应隐式添加 body 元素。 我的问题是,我的片段包含未封闭的标签。它应该保持未关闭状态,并且 DOMDocument 将关闭这些元素。片段如:&lt; div &gt;&lt; div &gt; ... &lt; /div &gt;。我仍在寻找解决方案。 嗯,我认为 div 标签总是有一个结束对。也许 Tidy 可以处理这个问题,它也可以处理片段。【参考方案16】:

我在俱乐部有点晚了,但不想分享我发现的方法。首先,我有正确的 loadHTML() 版本来接受这些不错的选项,但是 LIBXML_HTML_NOIMPLIED 在我的系统上不起作用。用户还报告了解析器的问题(例如here 和here)。

我创建的解决方案实际上非常简单。

要加载的 HTML 放在 &lt;div&gt; 元素中,因此它有一个容器,其中包含要加载的所有节点。

然后这个容器元素从文档中移除(但是它的DOMElement仍然存在)。

然后从文档中删除所有直接子级。这包括任何添加的&lt;html&gt;&lt;head&gt;&lt;body&gt; 标签(实际上是LIBXML_HTML_NOIMPLIED 选项)以及&lt;!DOCTYPE html ... loose.dtd"&gt; 声明(实际上是LIBXML_HTML_NODEFDTD)。

然后将容器的所有直接子元素再次添加到文档中即可输出。

$str = '<p>Lorem ipsum dolor sit amet.</p><p>Nunc vel vehicula ante.</p>';

$doc = new DOMDocument();

$doc->loadHTML("<div>$str</div>");

$container = $doc->getElementsByTagName('div')->item(0);

$container = $container->parentNode->removeChild($container);

while ($doc->firstChild) 
    $doc->removeChild($doc->firstChild);


while ($container->firstChild ) 
    $doc->appendChild($container->firstChild);


$htmlFragment = $doc->saveHTML();

XPath 照常工作,只需注意现在有多个文档元素,而不是单个根节点:

$xpath = new DOMXPath($doc);
foreach ($xpath->query('/p') as $element)
   #                   ^- note the single slash "/"
    # ... each of the two <p> element

PHP 5.4.36-1+deb.sury.org~precise+2 (cli)(构建时间:2014 年 12 月 21 日 20:28:53)

【讨论】:

它不适用于更复杂的 HTML 源代码。它还删除了 HTML 的给定部分。【参考方案17】:

使用 loadHTML() 加载文档后直接删除节点即可:

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

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

【讨论】:

这是对我来说更清晰的答案。 应该注意,如果 只有一个子节点,则此方法有效。 效果很好。谢谢!比其他 preg 答案更清洁、更快。 谢谢你!我刚刚在底部添加了另一个片段来处理空节点。 删除&lt;!DOCTYPE 的代码有效。如果&lt;body&gt; 有多个子注释,则第二行换行。【参考方案18】:

与其他成员一样,我首先陶醉于@Alessandro Vendruscolo 答案的简单性和强大的功能。简单地将一些标记的常量传递给构造函数的能力似乎好得令人难以置信。对我来说是。我有 LibXML 和 PHP 的正确版本,但是无论如何它仍然会将 HTML 标记添加到 Document 对象的节点结构中。

我的解决方案比使用...

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

标志或....

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

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

节点移除,如果没有 DOM 中的结构化顺序,就会变得一团糟。同样,代码片段无法预先确定 DOM 结构。

我开始了这个旅程,想要一种简单的方法来执行 DOM 遍历,就像 JQuery 那样,或者至少以某种方式具有结构化数据集,或者单链接、双链接或树节点遍历。我不在乎我能像 HTML 那样解析一个字符串多久,并且还拥有节点实体类属性的惊人功能,可以在此过程中使用。

到目前为止,DOMDocument 对象让我一直想要......就像许多其他程序员一样......我知道我在这个问题上看到了很多挫败感,所以自从我终于......(经过大约 30 小时尝试并失败类型测试)我找到了一种方法来获得这一切。我希望这对某人有所帮助...

首先,我对一切都持怀疑态度......哈哈......

在与任何人同意在此用例中无论如何都需要第三方类之前,我会花费一生的时间。我非常喜欢并且不喜欢使用任何第三方类结构,但是我偶然发现了一个很棒的解析器。 (在我放弃之前在 Google 中大约 30 次,所以如果你避免它,不要感到孤单,因为它看起来很不正式......)

如果您正在使用代码片段并且需要代码干净且不受解析器以任何方式影响,并且不使用额外的标签,请使用simplePHPParser。

这太棒了,而且很像 JQuery。我并没有经常给我留下深刻的印象,但是这门课使用了很多好的工具,到目前为止我还没有解析错误。我非常喜欢能够做这门课所做的事情。

你可以找到它的文件下载here,它的启动说明here,以及它的APIhere。我强烈推荐使用这个类及其简单的方法,可以像使用 JQuery find 方法一样执行 .find(".className"),甚至可以使用熟悉的方法,例如 getElementByTagName()getElementById()...

当您在此类中保存节点树时,它根本不会添加任何内容。你可以简单地说$doc-&gt;save();,它会毫不费力地将整个树输出到一个字符串中。

我现在将在未来将这个解析器用于所有非上限带宽的项目。

【讨论】:

【参考方案19】:

Alex 的答案是正确的,但可能会在空节点上导致以下错误:

传递给 DOMNode::removeChild() 的参数 1 必须是 DOM节点

我的小模组来了:

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

添加 trim() 也是删除空格的好主意。

【讨论】:

【参考方案20】:

我的服务器安装了 php 5.3,无法升级,所以这些选项

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD

不适合我。

为了解决这个问题,我告诉 SaveXML 函数打印 Body 元素,然后将“body”替换为“div”

这是我的代码,希望对某人有所帮助:

<? 
$html = "your html here";
$tabContentDomDoc = new DOMDocument();
$tabContentDomDoc->loadHTML('<?xml encoding="UTF-8">'.$html);
$tabContentDomDoc->encoding = 'UTF-8';
$tabContentDomDocBody = $tabContentDomDoc->getElementsByTagName('body')->item(0);
if(is_object($tabContentDomDocBody))
    echo (str_replace("body","div",$tabContentDomDoc->saveXML($tabContentDomDocBody)));

?>

utf-8 用于支持希伯来语。

【讨论】:

【参考方案21】:

使用 DOMDocumentFragment

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

【讨论】:

php5.4 之前最干净的答案。 这适用于我,无论是旧版本还是新版本 Libxml 2.7.7。为什么这仅适用于 php5.4 之前的版本? 这应该有更多的选票。不支持 LIBXML_HTML_NOIMPLIED 的 libxml 版本的绝佳选择 | LIBXML_HTML_NODEFDTD。谢谢! 谢谢,这是最好的解决方案【参考方案22】:

所有这些答案现在都错误,因为从 PHP 5.4 和 Libxml 2.6 开始,loadHTML 现在有一个 $option 参数,用于指示 Libxml 如何解析内容。

因此,如果我们使用这些选项加载 HTML

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

当做saveHTML()时,不会有doctype,没有&lt;html&gt;,也没有&lt;body&gt;

LIBXML_HTML_NOIMPLIED 关闭隐含 html/body 元素的自动添加 LIBXML_HTML_NODEFDTD 防止在找不到默认文档类型时添加默认文档类型。

有关 Libxml 参数的完整文档是 here

(请注意,loadHTML 文档说需要 Libxml 2.6,但 LIBXML_HTML_NODEFDTD 仅在 Libxml 2.7.8 中可用,LIBXML_HTML_NOIMPLIED 在 Libxml 2.7.7 中可用)

【讨论】:

这就像一个魅力。应该是公认的答案。我刚刚添加了一个标志,我所有的头痛都消失了;-) 这不适用于 PHP 5.4 和 Libxml 2.9。 loadHTML 不接受任何选项 :( 请注意,这并不完美。见***.com/questions/29493678/… 对不起,但这似乎根本不是一个好的解决方案(至少在实践中不是)。这真的不应该是公认的答案。除了提到的问题之外,还有一个 nasty encoding issue 和 DOMDocument 也会影响此答案中的代码。 Afaik,DOMDocument 总是将输入数据解释为 latin-1,除非输入指定不同的字符集。换句话说:&lt;meta charset="…"&gt; 标签似乎是非 latin-1 输入数据所必需的。否则 e 的输出将被破坏。 G。 UTF-8 多字节字符。 这将导致问题:'

First Paragraph

Second Paragraph

' 将被转换为 '

First Paragraph

第二段不要去掉两个段间标签。
【参考方案23】:

我也有这个要求,并且喜欢上面 Alex 发布的解决方案。但是有几个问题 - 如果&lt;body&gt; 元素包含多个子元素,则生成的文档将仅包含&lt;body&gt; 的第一个子元素,而不是全部。另外,我需要剥离来有条件地处理事情——只有当你有带有 HTML 标题的文档时。所以我将其细化如下。我没有删除&lt;body&gt;,而是将其转换为&lt;div&gt;,并删除了XML 声明和&lt;html&gt;

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

【讨论】:

【参考方案24】:

添加&lt;meta&gt; 标签将触发DOMDocument 的修复行为。好的部分是您根本不需要添加该标签。如果您不想使用您选择的编码,只需将其作为构造函数参数传递即可。

http://php.net/manual/en/domdocument.construct.php

$doc = new DOMDocument('1.0', 'UTF-8');
$node = $doc->createElement('div', 'Hello World');
$doc->appendChild($node);
echo $doc->saveHTML();

输出

<div>Hello World</div>

感谢@Bart

【讨论】:

【参考方案25】:

我也遇到过这个问题。

不幸的是,我对使用此线程中提供的任何解决方案感到不舒服,所以我去检查一个能让我满意的解决方案。

这是我编造的,它可以正常工作:

$domxpath = new \DOMXPath($domDocument);

/** @var \DOMNodeList $subset */
$subset = $domxpath->query('descendant-or-self::body/*');

$html = '';
foreach ($subset as $domElement) 
    /** @var $domElement \DOMElement */
    $html .= $domDocument->saveHTML($domElement);

本质上,它的工作方式与此处提供的大多数解决方案类似,但它使用 xpath 选择器选择正文中的所有元素并连接它们的 html 代码,而不是手动操作。

【讨论】:

像这里的所有解决方案一样,它并不适用于所有情况:如果加载的字符串不是以标记开头,则

已添加,那么您的代码不会工作,因为它会在保存的内容中添加

标记
公平地说,我没有用原始文本测试过,但理论上应该可以。对于您的具体情况,您可能需要将 xpath 更改为 descendant-or-self::body/p/*【参考方案26】:

好的,我找到了一个更优雅的解决方案,但它很乏味:

$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

好的,希望这不会遗漏任何内容并对某人有所帮助?

【讨论】:

不处理 loadHTML 加载没有标记的字符串时的情况【参考方案27】:

使用此功能

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

【讨论】:

可能有一些读者通过this post 偶然发现了这篇文章,他们决定不使用正则表达式来解析他们的 HTML 而是使用 DOM 解析器,最终可能需要正则表达式来回答实现一个完整的解决方案...讽刺 我不明白为什么 noboy 只是返回 BODY 的内容。当解析器添加整个文档标题/文档类型时,该标签是否不应该始终存在?上面的正则表达式甚至会更短。 @boksiora “它完成了这项工作”——那我们为什么首先使用 DOM 解析器方法? @naomik 我没有说过不要使用 DOM 解析器,当然有很多不同的方法可以达到相同的结果,这取决于你,在我使用这个功能时,我有一个内置 php dom 解析器的问题,它没有正确解析 html5。 我不得不使用preg_replace,因为使用基于 DOMDocument 的方法来删除 html 和 body 标签并没有保留 UTF-8 编码:(【参考方案28】:

在撰写本文时(2012 年 6 月),没有其他解决方案能够完全满足我的需求,因此我编写了一个处理以下情况的解决方案:

接受没有标签的纯文本内容,以及 HTML 内容。 不附加任何标签(包括&lt;doctype&gt;&lt;xml&gt;&lt;html&gt;&lt;body&gt;&lt;p&gt;标签) 将任何东西单独包裹在 &lt;p&gt; 中。 保留空白文本。

所以这里有一个解决这些问题的解决方案:

class DOMDocumentWorkaround

    /**
     * Convert a string which may have HTML components into a DOMDocument instance.
     *
     * @param string $html - The HTML text to turn into a string.
     * @return \DOMDocument - A DOMDocument created from the given html.
     */
    public static function getDomDocumentFromHtml($html)
    
        $domDocument = new DOMDocument();

        // Wrap the HTML in <div> tags because loadXML expects everything to be within some kind of tag.
        // LIBXML_NOERROR and LIBXML_NOWARNING mean this will fail silently and return an empty DOMDocument if it fails.
        $domDocument->loadXML('<div>' . $html . '</div>', LIBXML_NOERROR | LIBXML_NOWARNING);

        return $domDocument;
    

    /**
     * Convert a DOMDocument back into an HTML string, which is reasonably close to what we started with.
     *
     * @param \DOMDocument $domDocument
     * @return string - The resulting HTML string
     */
    public static function getHtmlFromDomDocument($domDocument)
    
        // Convert the DOMDocument back to a string.
        $xml = $domDocument->saveXML();

        // Strip out the XML declaration, if one exists
        $xmlDeclaration = "<?xml version=\"1.0\"?>\n";
        if (substr($xml, 0, strlen($xmlDeclaration)) == $xmlDeclaration) 
            $xml = substr($xml, strlen($xmlDeclaration));
        

        // If the original HTML was empty, loadXML collapses our <div></div> into <div/>. Remove it.
        if ($xml == "<div/>\n") 
            $xml = '';
        
        else 
            // Remove the opening <div> tag we previously added, if it exists.
            $openDivTag = "<div>";
            if (substr($xml, 0, strlen($openDivTag)) == $openDivTag) 
                $xml = substr($xml, strlen($openDivTag));
            

            // Remove the closing </div> tag we previously added, if it exists.
            $closeDivTag = "</div>\n";
            $closeChunk = substr($xml, -strlen($closeDivTag));
            if ($closeChunk == $closeDivTag) 
                $xml = substr($xml, 0, -strlen($closeDivTag));
            
        

        return $xml;
    

我还写了一些测试,这些测试可以放在同一个类中:

public static function testHtmlToDomConversions($content)

    // test that converting the $content to a DOMDocument and back does not change the HTML
    if ($content !== self::getHtmlFromDomDocument(self::getDomDocumentFromHtml($content))) 
        echo "Failed\n";
    
    else 
        echo "Succeeded\n";
    


public static function testAll()

    self::testHtmlToDomConversions('<p>Here is some sample text</p>');
    self::testHtmlToDomConversions('<div>Lots of <div>nested <div>divs</div></div></div>');
    self::testHtmlToDomConversions('Normal Text');
    self::testHtmlToDomConversions(''); //empty

您可以检查它是否适合您自己。 DomDocumentWorkaround::testAll() 返回:

    Succeeded
    Succeeded
    Succeeded
    Succeeded

【讨论】:

HTML =/= XML,您应该为 HTML 使用 HTML 加载器。【参考方案29】:

改用saveXML(),并将 documentElement 作为参数传递给它。

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

echo $innerHTML;

http://php.net/domdocument.savexml

【讨论】:

这更好,但我仍然得到

包装内容。

As of PHP 5.3.6, you can use saveHTML as well (example) 需要注意的是saveXML()会保存XHTML,而不是HTML。 @Scott:这真的很奇怪。它在示例部分中显示了您正在尝试执行的操作。您确定您的 DOM 中没有该 HTML 吗?您的 DOMDocument 中究竟是什么 HTML?可能是我们需要访问一个子节点。 @Jonah 这并不奇怪。当您执行 loadHTML 时,libxml 使用 HTML 解析器模块,这将插入缺少的 HTML 框架。因此,$dom-&gt;documentElement 将成为根 HTML 元素。我已经修复了您的示例代码。它现在应该满足 Scott 的要求了。【参考方案30】:

一个巧妙的技巧是使用loadXML,然后使用saveHTMLhtmlbody 标签插入到 load 阶段,而不是 save 阶段。

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

注意,这有点 hacky,如果你可以让它工作,你应该使用 Jonah 的答案。

【讨论】:

这会因为无效的 HTML 而失败。 @Gordon 正是我将免责声明放在底部的原因! 当我尝试这个并回显 $dom->saveHTML() 时,它只返回一个空字符串。好像 loadXML($content) 是空的。当我对 $dom->loadHTML($content) 执行相同操作时,然后 echo $dom->saveXML() 我得到了预期的内容。 在愿意加载 HTMl 时使用 loadXML 是拇指。特别是因为 LoadXML 不知道如何处理 HTML。

以上是关于如何在没有 HTML 包装器的情况下保存 DOMDocument 的 HTML?的主要内容,如果未能解决你的问题,请参考以下文章

Vue渲染功能:在没有包装器的情况下将插槽包含到子组件中

如何在 iPhone 上保存对特定照片的引用以便在没有选择器的情况下加载?

NHibernate 可以在没有迭代器的情况下保存集合吗?

在不使用过滤器和包装器的情况下删除 URL 中的 JSESSIONID

Jquery 循环 - 在不破坏缩略图分页器的情况下将图像包装在 div 标签中

是否可以在不使用文本包装器的情况下使用不同的列表编号字体大小和列表内部文本? [复制]