使用PhpWord转换HTML时出现错误 - DOMDocument :: loadXML():实体中未定义命名空间前缀o on p。

9

我试图将使用Php word格式化的HTML进行转换。

我使用summernote创建了一个HTML表单。Summernote允许用户格式化文本。此文本带有HTML标签保存到数据库中。

接下来,使用phpWord,我想将捕获的信息输出到Word文档中。请参见下面的代码:

$rational = DB::table('rationals')->where('qualificationheader_id',$qualId)->value('rational');

 $wordTest = new \PhpOffice\PhpWord\PhpWord();
        $newSection = $wordTest->addSection();
        $newSection->getStyle()->setPageNumberingStart(1);


    \PhpOffice\PhpWord\Shared\Html::addHtml($newSection,$rational);
    $footer = $newSection->addFooter();
    $footer->addText($curriculum->curriculum_code.'-'.$curriculum->curriculum_title);



    $objectWriter = \PhpOffice\PhpWord\IOFactory::createWriter($wordTest,'Word2007');
    try {
        $objectWriter->save(storage_path($curriculum->curriculum_code.'-'.$curriculum->curriculum_title.'.docx'));
    } catch (Exception $e) {
    }

    return response()->download(storage_path($curriculum->curriculum_code.'-'.$curriculum->curriculum_title.'.docx'));

保存在数据库中的文本看起来像这样:

<p class="MsoNormal"><span lang="EN-GB" style="background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial;"><span style="font-family: Arial;">The want for this qualification originated from the energy crisis in
South Africa in 2008 together with the fact that no existing qualifications
currently focuses on energy efficiency as one of the primary solutions.  </span><span style="font-family: Arial;">The fact that energy supply remains under
severe pressure demands the development of skills sets that can deliver the
necessary solutions.</span><span style="font-family: Arial;">  </span><o:p></o:p></span></p><p class="MsoNormal"><span lang="EN-GB" style="background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; font-family: Arial;">This qualification addresses the need from Industry to acquire credible
and certified professionals with specialised skill sets in the energy
efficiency field. The need for this skill set has been confirmed as a global
requirement in few of the International commitment to the reduction of carbon

我收到了以下错误信息:
ErrorException (E_WARNING) DOMDocument::loadXML(): Namespace prefix o on p is not defined in Entity, line: 1
1个回答

23

问题

解析器报错,因为您的文本在元素标签中包含命名空间,更具体地说,标签<o:p>(其中o:是前缀)上的前缀。它似乎是Word的某种格式

重现问题

为了重现这个问题,我需要挖掘一下,因为抛出异常的不是PHPWord,而是PHPWord正在使用的DOMDocument。下面的代码使用了相同的解析方法,应该输出关于代码的所有警告和通知。

# Make sure to display all errors
ini_set("display_errors", "1");
error_reporting(E_ALL);

$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';

# Set up and parse the code
$doc = new DOMDocument();
$doc->loadXML($html); # This is the line that's causing the warning.
# Print it back
echo $doc->saveXML();

分析

对于格式良好的HTML结构,可以在声明中包含命名空间,从而告诉解析器这些前缀实际上是什么。但由于它似乎只是要解析的HTML代码的一部分,所以不可能。

可以将DOMXPath与命名空间一起提供,以便PHPWord可以利用它。不幸的是,DOMXPath不是公共的API,因此不可能。

相反,最好的方法似乎是从标记中删除前缀,并使警告消失。

编辑2018-10-04:我后来发现了一种方法,可以保留标记中的前缀,同时使错误消失,但执行效果并不理想。如果有人能提供更好的解决方案,请随时编辑我的帖子或留下评论。

解决方案

基于分析,解决方案是移除前缀,因此我们必须预先解析代码。由于PHPWord使用DOMDocument, 我们也可以使用它,并确保我们不需要安装任何(额外的)依赖项。

PHPWord使用loadXML解析HTML,该函数会抱怨格式。在这种方法中,可以抑制错误消息,我们必须在两个解决方案中都这样做。这是通过loadXMLloadHTML函数传递一个附加参数来完成的。

解决方案1:将代码预先解析为XML并移除前缀

第一种方法将html代码解析为XML并递归遍历树,并删除标记名称上的任何前缀。

我创建了一个类来解决这个问题。

class TagPrefixFixer {

    /**
      * @desc Removes all prefixes from tags
      * @param string $xml The XML code to replace against.
      * @return string The XML code with no prefixes in the tags.
    */
    public static function Clean(string $xml) {
        $doc = new DOMDocument();
        /* Load the XML */
        $doc->loadXML($xml,
            LIBXML_HTML_NOIMPLIED | # Make sure no extra BODY
            LIBXML_HTML_NODEFDTD |  # or DOCTYPE is created
            LIBXML_NOERROR |        # Suppress any errors
            LIBXML_NOWARNING        # or warnings about prefixes.
        );
        /* Run the code */
        self::removeTagPrefixes($doc);
        /* Return only the XML */
        return $doc->saveXML();
    }

    private static function removeTagPrefixes(DOMNode $domNode) {
        /* Iterate over each child */
        foreach ($domNode->childNodes as $node) {
            /* Make sure the element is renameable and has children */
            if ($node->nodeType === 1) {
                /* Iterate recursively over the children.
                 * This is done before the renaming on purpose.
                 * If we rename this element, then the children, the element
                 * would need to be moved a lot more times due to how 
                 * renameNode works. */
                if($node->hasChildNodes()) {
                    self::removeTagPrefixes($node);
                }
                /* Check if the tag contains a ':' */
                if (strpos($node->tagName, ':') !== false) {
                    print $node->tagName;
                    /* Get the last part of the tag name */
                    $parts = explode(':', $node->tagName);
                    $newTagName = end($parts);
                    /* Change the name of the tag */
                    self::renameNode($node, $newTagName);
                }
            }
        }
    }

    private static function renameNode($node, $newName) {
        /* Create a new node with the new name */
        $newNode = $node->ownerDocument->createElement($newName);
        /* Copy over every attribute from the old node to the new one */
        foreach ($node->attributes as $attribute) {
            $newNode->setAttribute($attribute->nodeName, $attribute->nodeValue);
        }
        /* Copy over every child node to the new node */
        while ($node->firstChild) {
            $newNode->appendChild($node->firstChild);
        }
        /* Replace the old node with the new one */
        $node->parentNode->replaceChild($newNode, $node);
    }
}

要使用这段代码,只需调用TagPrefixFixer::Clean函数即可。

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print TagPrefixFixer::Clean($xml);

输出

<?xml version="1.0"?>
<p>Foo <b>Bar</b></p>

解决方案2:预解析为HTML

我注意到,如果您使用loadHTML而不是loadXMLPHPWord正在使用它将在将HTML加载到类中时自动删除前缀。

这段代码要短得多。

function cleanHTML($html) {
    $doc = new DOMDocument();
    /* Load the HTML */
    $doc->loadHTML($html,
            LIBXML_HTML_NOIMPLIED | # Make sure no extra BODY
            LIBXML_HTML_NODEFDTD |  # or DOCTYPE is created
            LIBXML_NOERROR |        # Suppress any errors
            LIBXML_NOWARNING        # or warnings about prefixes.
    );
    /* Immediately save the HTML and return it. */
    return $doc->saveHTML();
}

要使用这段代码,只需要调用 cleanHTML 函数。

$html = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print cleanHTML($html);

输出

<p>Foo <b>Bar</b></p>

解决方案三:保留前缀并添加命名空间

我尝试在将数据馈送到解析器之前使用给定的Microsoft Office namespaces来包装代码,这也可以解决问题。讽刺的是,我没有找到一种方法可以在DOMDocument解析器中添加命名空间而不会引发原始警告。因此,这个解决方案的执行有点糟糕,我不建议使用它,而是建立你自己的解决方案。但是你明白了思路:

function addNamespaces($xml) {
    $root = '<w:wordDocument
        xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
        xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
        xmlns:o="urn:schemas-microsoft-com:office:office">';
    $root .= $xml;
    $root .= '</w:wordDocument>';
    return $root;
}

要使用此代码,只需调用addNamespaces函数

$xml = '<o:p>Foo <o:b>Bar</o:b></o:p>';
print addNamespaces($xml);

输出

<w:wordDocument
    xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml"
    xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint"
    xmlns:o="urn:schemas-microsoft-com:office:office">
    <o:p>Foo <o:b>Bar</o:b></o:p>
</w:wordDocument>

这段代码可以直接输入到PHPWord函数的addHtml中,不会引起任何警告。

可选解决方案(已弃用)

在之前的回答中,这些被提出作为(可选)解决方案,但为了解决问题,我将让它们在下面保留。请注意,这些都不是推荐的方法,应谨慎使用。

关闭警告

由于这只是一个警告而不是致命的停止异常,您可以关闭警告。您可以通过在脚本顶部包含此代码来实现。然而,这仍然会减慢您的应用程序速度,最好的方法始终是确保没有警告或错误。

// Show the default reporting except from warnings
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED & ~E_WARNING);

这些设置来自默认报告级别

使用正则表达式

可以通过在保存到数据库之前或在获取用于此函数的文本后对其进行正则表达式来消除(大多数)名称空间。 由于它已经存储在数据库中,因此最好在从数据库获取后使用下面的代码。 但是,正则表达式可能会错过某些出现情况,或者在最坏的情况下混乱HTML。

正则表达式:

$text_after = preg_replace('/[a-zA-Z]+:([a-zA-Z]+[=>])/', '$1', $text_before);

例子:

$text = '<o:p>Foo <o:b>Bar</o:b></o:p>';
$text = preg_replace('/[a-zA-Z]+:([a-zA-Z]+[=>])/', '$1', $text);
echo $text; // Outputs '<p>Foo <b>Bar</b></p>'

正则表达式用于HTML?不行!https://dev59.com/X3I-5IYBdhLWcg3wq6do - delboy1978uk
1
你是对的 @delboy1978uk。我已经用另一种方法重新制作了整个解决方案,这应该更加可持续。 - Johan
对于任何已经读到最后的人:我将尝试在解析数据之前添加命名空间标签,以查看是否可以解决问题而无需抑制任何警告,但我要等到今天晚些时候才能有时间这样做。 - Johan
跟进我的早前评论:保留前缀并且仍然能够解析代码而不产生任何警告/错误是可能的。我已经将结果添加为我的第三个解决方案,尽管它的执行并不是最优的。 - Johan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接