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


116

我是下面的函数,我正在努力输出DOMDocument,而没有在内容输出之前附加XML,HTML,bodyp标签包装器。建议的修复方法:

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

仅当内容中没有任何块级元素时才起作用。但是,这样做时,如下面的示例中带有h1元素的示例一样,saveXML的结果输出将被截断为...

<p>如果你喜欢</ p>

我已经指出此帖子是一种可能的解决方法,但我不明白如何将其实现到此解决方案中(请参阅下面的注释尝试)。

有什么建议?

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

Answers:


217

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

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

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

这样做的saveHTML()时候不会doctype,不会<html>,也不会<body>

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

有关Libxml参数的完整文档在这里

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


10
这就像一个魅力。应该是公认的答案。我只添加了一个标志,所有的头痛都消失了;-)
只是普通高

8
这不适用于PHP 5.4和Libxml 2.9。loadHTML不接受任何选项:(
Acyra 2014年

11
请注意,这不是很完美。见stackoverflow.com/questions/29493678/...
乔什-莱文森

4
抱歉,但这似乎根本不是一个好的解决方案(至少在实践中不是这样)。这确实不应该是公认的答案。除了提到的问题,还有一个令人讨厌的编码问题DOMDocument它也影响此答案中的代码。Afaik DOMDocument始终将输入数据解释为latin-1,除非输入指定了其他字符集。换句话说:<meta charset="…">对于不是latin-1的输入数据,似乎需要标签。否则,输出将被打乱,例如UTF-8多字节字符。
mermshaus

1
LIBXML_HTML_NOIMPLIED还通过移除标签,缩进和换行符弄乱的HTML代码
佐尔坦SULE

72

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

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

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

这是给我更干净的答案。
2013年

39
应当注意,如果<body>仅具有一个子节点,则此方法有效。
Yann Milin 2013年

很棒。谢谢!比其他preg答案更清洁,更快捷。
Ligemer 2014年

这次真是万分感谢!我只是在底部添加了另一个片段来处理空节点。
redaxmedia 2014年

2
该代码删除<!DOCTYPE 作品。如果<body>有多个子注释,第二行将中断。
Free Radical '18

21

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

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

http://php.net/domdocument.savexml


更好,但是我仍然得到<html> <body> <p>包装内容。
Scott B


2
应该注意的是,saveXML()将保存XHTML,而不是HTML。
2011年

@斯科特:真是奇怪。它在示例部分中显示了您正在尝试执行的操作。您确定DOM中没有该HTML吗?您的DOMDocument中到底有什么HTML?可能是我们需要访问一个子节点。
乔纳

@乔纳并不奇怪。当您执行loadHTMLlibxml时,将使用HTML解析器模块,这将插入缺少的HTML框架。因此,$dom->documentElement将成为HTML根元素。我已经修复了您的示例代码。现在,它应该可以执行Scott的要求。
Gordon

18

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

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

不要使用LIBXML_HTML_NOIMPLIED而是使用substr


想一想。的长度<html><body></body></html>固定,并在文档的两端-它们的大小不会改变,也不做他们的位置。这使我们可以使用substr它们来删除它们:

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

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

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

我们12从文档的开头剪掉了,因为<html><body>= 12个字符(<<>>+html+body= 4 + 4 + 4),而我们又向后切掉了15个结尾,因为\n</body></html>= 15个字符(\n+//+<<>>+body+html = 1 + 2 + 4 + 4 + 4)

请注意,我仍然使用LIBXML_HTML_NODEFDTD忽略!DOCTYPE被包括在内。首先,这简化了substrHTML / BODY标签的删除。其次,我们不删除带有的doctype,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 -> saveHTML();代替的$dom -> saveHTML();反复?
史蒂文·

15

一个巧妙的窍门是使用loadXML然后saveHTML。在htmlbody标签插入到load舞台,没有save舞台。

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

注意,这有点不可靠,如果可以使用,请使用乔纳的答案。


4
但是,这对于无效的HTML将失败。
Gordon

1
@Gordon就是为什么我将免责声明放在最底下!
lonesomeday '02

1
当我尝试此操作并回显$ dom-> saveHTML()时,它仅返回一个空字符串。好像loadXML($ content)为空。当我对$ dom-> loadHTML($ content)进行相同操作时,然后回显$ dom-> saveXML()即可得到预期的内容。
Scott B

愿意加载HTM1时使用loadXML。特别是因为LoadXML不知道如何处理HTML。
botenvouwer

15

使用DOMDocumentFragment

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

3
php5.4之前版本的最干净答案。
尼克·约翰逊

这适用于我,与Libxml 2.7.7版本相比,它的版本都旧。为什么这将仅用于pre php5.4?
RobbertT 2015年

这应该有更多的选票。不支持LIBXML_HTML_NOIMPLIED的libxml版本的绝佳选择| LIBXML_HTML_NODEFDTD。谢谢!
Marty Mulligan

13

现在是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());

3
您应该避免使用更昂贵的软件mb_convert_encoding,而应进行相应的添加<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>和修改substr,以改善答案。顺便说一句,您的是这里最优雅的解决方案。已投票。
Hlsg '18

10

我有点晚了俱乐部,但不想共享,我发现了一个方法。首先,我为loadHTML()找到了正确的版本,可以接受这些不错的选项,但是LIBXML_HTML_NOIMPLIED在我的系统上不起作用。用户还会报告解析器的问题(例如,herehere)。

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

将要加载的HTML放在 <div>元素中,因此它具有一个包含所有要加载的节点的容器。

然后,将该容器元素从文档中删除(但它的DOMElement仍然存在)。

然后,将删除文档中的所有直接子级。这包括任何添加<html><head><body>标签(有效LIBXML_HTML_NOIMPLIED选项),以及该<!DOCTYPE html ... loose.dtd">声明(有效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的给定部分。
佐尔坦SULE

4

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

  • 接受没有标签的纯文本内容以及HTML内容。
  • 不附加任何标签(包括<doctype><xml><html><body>,和<p>标签)
  • 将任何东西<p>单独包裹。
  • 留下空文本。

因此,这里是解决这些问题的解决方案:

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

1
HTML = / = XML,您应该将HTML加载程序用于HTML。
hakre 2015年

4

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

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

好吧,希望这不会遗漏任何东西并对某人有所帮助?


2
当loadHTML加载没有标记的字符串时无法处理这种情况
copndz

3

使用这个功能

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

13
可能有些读者通过这篇文章无意中发现了这篇文章,决定不再使用正则表达式来解析其HTML,而是使用DOM解析器,最终可能需要使用正则表达式来获得完整的解决方案……具有讽刺意味的是
Robbie Averill

我不明白为什么noboy只会返回BODY的内容。解析器添加整个文档标头/文档类型时,不是假定该标签始终存在吗?上面的正则表达式会更短。
塞尔吉奥2015年

@boksiora“它能完成任务”-那么为什么我们首先使用DOM解析器方法?
谢谢您

@naomik我没有说过不使用DOM解析器,当然有很多不同的方法可以达到相同的结果,这取决于您,在我使用此功能时,内置php dom出现了问题解析器,该解析器无法正确解析html5。
boksiora

1
我不得不使用,preg_replace因为使用基于DOMDocument的方法来删除html和body标签不会保留UTF-8编码:(
wizonesolutions

3

如果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代码,除了<body>标记(即内容的根)以外,没有所有这些HTML换行。然后,您可以使用regex或trim函数将其从最终字符串(之后saveHTML)删除,或者像上面的情况一样,遍历其所有子项,将其内容保存到临时变量中$finalHtml并返回(我相信更安全)。


3

我在运行PHP 5.6.25和LibXML 2.9的RHEL7上为此而苦苦挣扎。(我知道2018年的旧东西,但这就是给您的Red Hat。)

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

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

变成:

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

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

Alex建议的解决方案解决了一半,但如果<body>有多个子节点,则该解决方案不起作用。

对我有效的解决方案是:

首先,要加载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);

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


2

添加<meta>标签会触发固定行为DOMDocument。好消息是您根本不需要添加该标签。如果您不想使用选择的编码,只需将其作为构造函数参数传递即可。

http://php.net/manual/zh/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


2

我也有这个要求,并且喜欢上面Alex发布的解决方案。但是,有两个问题-如果该<body>元素包含多个子元素,那么生成的文档将仅包含的第一个子元素<body>,而不是全部。另外,我需要剥离来有条件地处理事情-仅当您具有带有HTML标题的文档时。因此,我将其细化如下。<body>我没有将其删除,而是将其转换为<div>,并去除了XML声明和<html>

function strip_html_headings($html_doc)
{
    if (is_null($html_doc))
    {
        // might be better to issue an exception, but we silently return
        return;
    }

    // remove <!DOCTYPE 
    if (!is_null($html_doc->firstChild) &&
        $html_doc->firstChild->nodeType == XML_DOCUMENT_TYPE_NODE)
    {
        $html_doc->removeChild($html_doc->firstChild);     
    }

    if (!is_null($html_doc->firstChild) &&
        strtolower($html_doc->firstChild->tagName) == 'html' &&
        !is_null($html_doc->firstChild->firstChild) &&
        strtolower($html_doc->firstChild->firstChild->tagName) == 'body')
    {
        // we have 'html/body' - replace both nodes with a single "div"        
        $div_node = $html_doc->createElement('div');

        // copy all the child nodes of 'body' to 'div'
        foreach ($html_doc->firstChild->firstChild->childNodes as $child)
        {
            // deep copies each child node, with attributes
            $child = $html_doc->importNode($child, true);
            // adds node to 'div''
            $div_node->appendChild($child);
        }

        // replace 'html/body' with 'div'
        $html_doc->removeChild($html_doc->firstChild);
        $html_doc->appendChild($div_node);
    }
}

2

与其他成员一样,我首先陶醉于@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次访问,因此如果您回避它,不要感到孤单,因为它看起来以任何形式都是la脚的……)

如果您使用的是代码片段,并且需要解析器以任何方式保持代码干净且不受任何影响,而无需使用额外的标记,请使用simplePHPParser

太神奇了,其行为类似于JQuery。我并不经常被打动,但是这个类使用了很多很好的工具,而且到目前为止我还没有解析错误。我非常热衷于能够完成本课程的工作。

你可以找到它的文件下载在这里,它的启动指令在这里,它的API 在这里。我强烈建议将该类及其简单方法一起使用,这些方法可以.find(".className")使用与使用JQuery查找方法相同的方法,甚至可以使用熟悉的方法,例如getElementByTagName()getElementById() ...

当您在此类中保存节点树时,它根本不会添加任何内容。你可以简单地说$doc->save();,它将整棵树输出为字符串,而不必大惊小怪。

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


2

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

$doc->replaceChild($doc->firstChild->firstChild->firstChild, $doc->firstChild);我只用第一个孩子替换了所有文档,我有很多段落,只有第一个被保存了,但是解决方案为我提供了一个很好的起点,可以编写一些东西而无需regex留下任何评论,我很确定这可以改进,但是如果有人和我有同样的问题,这可能是一个很好的起点。

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接受了a,DOMNode因此我们无需创建新元素,我们只需重用已实现的现有元素即可,DOMNode例如,DOMElement在处理多个HTML / XML文档时,这对于保持代码“健全”非常重要。


这不适用于片段,仅适用于您要成为文档的第一个子元素的单个子元素。这是相当有限的,实际上并没有完成LIBXML_HTML_NOIMPLIED它的工作,因为它只是部分地做到了。删除doctype是有效的LIBXML_HTML_NODEFDTD
hakre

2

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

引起的问题是由于 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

问题:

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

1
我很高兴您分享您的发现,但是解决方案2已经在此处提出了这些确切的问题,而解决方案1在其他地方。同样对于解决方案1的问题,给出的答案还不清楚。我尊重您的良好意愿,但请注意,它可能会产生很多噪音,并会阻碍其他人找到他们正在寻找的解决方案,我认为这与您希望通过答案实现的目标相反。如果您一次处理一个问题,Stackoverflow效果最好。只是一个提示。
hakre

2

我在DOMDocument上课时遇到3个问题。

1-此类加载具有ISO编码和utf-8字符的html,但不会在输出中显示。

2-即使我们给‍‍‍LIBXML_HTML_NOIMPLIED标志loadHtml方法,直到我们的输入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,则在我的情况下会自动添加一个段落,这
很不

0

我也遇到了这个问题。

不幸的是,我不满意使用此线程中提供的任何解决方案,因此我去检查了一个可以让我满意的解决方案。

这是我组成的,可以正常使用:

$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代码连接起来。


像这里的所有解决方案一样,它并非在每种情况下都有效:如果加载的字符串不是以标记开头,则添加了<p> </ p>,则您的代码将无法正常工作,因为它将添加保存的内容中的<p> </ p>标记
copndz

公平地说,我没有用原始文本对其进行过测试,但理论上应该可以。对于您的特定情况,您可能需要将xpath更改为descendant-or-self::body/p/*
Nikola Petkanski

0

我的服务器安装了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支持希伯来语。


0

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

传递给DOMNode :: removeChild()的参数1必须是DOMNode的实例

我的小模组来了:

    $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()也是删除空白的好主意。


0

我可能为时已晚。但是也许有人(像我)仍然有这个问题。
因此,以上都不对我有用。因为$ dom-> loadHTML也会关闭打开的标签,所以不仅添加html和body标签。
所以添加<div>元素对我不起作用,因为我有时喜欢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.)在将其返回之前,请从标记中删除<p> </ p>标签,奇怪的是,它仅显示在[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解析器,然后要修复此“问题”,我必须使用regex ...重点是,不要使用regex;)


您在这里做什么看起来很危险,stackoverflow.com / a / 29499718/367456应该为您完成这项工作。
hakre '16

不幸的是,这个(stackoverflow.com/questions/4879946/…)对我不起作用。正如我说的:“因此添加<div>元素对我来说不起作用,因为我有时喜欢html片段中的3-4个未关闭的div”。出于某种原因,DOMDocument希望关闭所有“未关闭”的元素。在可能的情况下,我将在短代码或其他标记中获得一个片段,删除该片段,然后我要操作文档的另一部分,完成后,我将片段插入回去。

在加载您自己的内容之后,应该可以将div元素保留下来并在body元素上进行操作。加载片段时,应隐式添加body元素。
hakre

我的问题是,我的食物包含未关闭的标签。应该保持未关闭状态,而DOMDocument将关闭那些元素。片段喜欢:< div >< div > ... < /div >。我仍在寻找解决方案。

嗯,我认为div标签总是有一个封闭的对。也许Tidy可以解决这个问题,它也可以处理片段。
hakre

0

对于使用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 API的此功能可以在我的Drupal 7网站上正常工作。我猜那些不使用Drupal的用户可以将功能复制到自己的站点中-因为没有关于Drupal的特定内容。
Free Radical '18


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

为何要分享-1?
Dylan Maxey

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.