使用JavaScript的DOMParser在跨浏览器方式下如何检测XML解析错误?

32

所有主要浏览器似乎都实现了DOMParser API,以便将XML解析为DOM,然后使用XPath、getElementsByTagName等进行查询...

然而,检测解析错误似乎更加棘手。DOMParser.prototype.parseFromString总是返回一个有效的DOM。当发生解析错误时,返回的DOM包含一个<parsererror>元素,但在每个主要浏览器中略有不同。

示例JavaScript:

xmlText = '<root xmlns="http://default" xmlns:other="http://other"><child><otherr:grandchild/></child></root>';
parser = new DOMParser();
dom = parser.parseFromString(xmlText, 'application/xml');
console.log((new XMLSerializer()).serializeToString(dom));

在Opera中的结果:

DOM根节点是一个<parsererror>元素。

<?xml version="1.0"?><parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">Error<sourcetext>Unknown source</sourcetext></parsererror>

在Firefox中的结果:

DOM的根元素是一个<parsererror>元素。

<?xml-stylesheet href="chrome://global/locale/intl.css" type="text/css"?>
<parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">XML Parsing Error: prefix not bound to a namespace
Location: http://fiddle.jshell.net/_display/
Line Number 1, Column 64:<sourcetext>&lt;root xmlns="http://default" xmlns:other="http://other"&gt;&lt;child&gt;&lt;otherr:grandchild/&gt;&lt;/child&gt;&lt;/root&gt;
---------------------------------------------------------------^</sourcetext></parsererror>

Safari中的结果:

<root>元素解析正确,但包含一个不同命名空间下的嵌套<parsererror>,而Opera和Firefox的<parsererror>元素则不是。

<root xmlns="http://default" xmlns:other="http://other"><parsererror xmlns="http://www.w3.org/1999/xhtml" style="display: block; white-space: pre; border: 2px solid #c77; padding: 0 1em 0 1em; margin: 1em; background-color: #fdd; color: black"><h3>This page contains the following errors:</h3><div style="font-family:monospace;font-size:12px">error on line 1 at column 50: Namespace prefix otherr on grandchild is not defined
</div><h3>Below is a rendering of the page up to the first error.</h3></parsererror><child><otherr:grandchild/></child></root>
我是否错过了一种简单的、跨浏览器的方法来检测XML文档中是否发生解析错误?或者我必须查询每个可能由不同浏览器生成的<parsererror>元素的DOM吗?

你可以直接在根DOM节点上调用.getElementsByTagName("parseerror"),并假设如果返回的节点列表长度大于零,则存在错误吗? - Pointy
3
从技术上讲,我解析的 XML 文档可能包含“<parsererror>”元素,但仍然是完全有效的 XML(元素可能来自不同的命名空间)。因此,我需要根据每个浏览器的命名空间 URI 多次调用“.getElementsByTagNameNS(namespace, 'parsererror')”。 - cspotcode
嗯,HTML5规范对此的描述非常零碎,可以这么说。 - Pointy
3
我注意到这个 mozilla bug,它指向了这个 whatwg spec。我认为浏览器不使用异常是愚蠢的:正如你所写的,我们可能需要解析类似于错误返回的 XML 文档,而无法确定是否成功解析。解决问题的唯一方法是编写另一个解析器。 - Damien
看起来 Chrome 做了和 Safari 同样的事情。 - Tom Winter
4个回答

23

这是我想出的最佳解决方案。

我试图解析一个有意无效的XML字符串,并观察生成的<parsererror>元素的命名空间。然后,在解析实际的XML时,我可以使用getElementsByTagNameNS来检测相同类型的<parsererror>元素并抛出Javascript Error

// My function that parses a string into an XML DOM, throwing an Error if XML parsing fails
function parseXml(xmlString) {
    var parser = new DOMParser();
    // attempt to parse the passed-in xml
    var dom = parser.parseFromString(xmlString, 'application/xml');
    if(isParseError(dom)) {
        throw new Error('Error parsing XML');
    }
    return dom;
}

function isParseError(parsedDocument) {
    // parser and parsererrorNS could be cached on startup for efficiency
    var parser = new DOMParser(),
        errorneousParse = parser.parseFromString('<', 'application/xml'),
        parsererrorNS = errorneousParse.getElementsByTagName("parsererror")[0].namespaceURI;

    if (parsererrorNS === 'http://www.w3.org/1999/xhtml') {
        // In PhantomJS the parseerror element doesn't seem to have a special namespace, so we are just guessing here :(
        return parsedDocument.getElementsByTagName("parsererror").length > 0;
    }

    return parsedDocument.getElementsByTagNameNS(parsererrorNS, 'parsererror').length > 0;
};

请注意,此解决方案不包括Internet Explorer所需的特殊情况。但是,在IE中,事情要简单得多。XML使用loadXML方法进行解析,如果解析成功返回true,失败则返回false。有关示例,请参见http://www.w3schools.com/xml/xml_parser.asp

2
我在使用以下代码和Rast的函数时都遇到了如下错误:TypeError: 无法读取未定义的属性'namespaceURI'。 - Timothy Harding

18
当我第一次来到这里时,我为原始答案(由)点赞,然而,在Firefox中它不起作用。由于生成的文档结构,导致结果命名空间始终为“null”。 我进行了一些研究(检查代码here)。想法是不使用
invalidXml.childNodes[0].namespaceURI

但是

invalidXml.getElementsByTagName("parsererror")[0].namespaceURI

然后按照原始答案,通过命名空间选择“parsererror”元素。但是,如果您有一个有效的XML文档,并且<parsererror>标签与浏览器使用的相同命名空间位于同一命名空间中,则会出现错误警报。

因此,这里有一个启发式方法来检查您的XML是否成功解析:

function tryParseXML(xmlString) {
    var parser = new DOMParser();
    var parsererrorNS = parser.parseFromString('INVALID', 'application/xml').getElementsByTagName("parsererror")[0].namespaceURI;
    var dom = parser.parseFromString(xmlString, 'application/xml');
    if(dom.getElementsByTagNameNS(parsererrorNS, 'parsererror').length > 0) {
        throw new Error('Error parsing XML');
    }
    return dom;
}

为什么DOMParser没有实现异常处理?

在当前情境下值得一提的是:如果你试图使用XMLHttpRequest获取XML文件,解析后的DOM将会被存储在responseXML属性中,如果XML文件内容无效,则为null。不是异常,也不是parsererror或其他特定指示符。只是null。


1
PhantomJS需要特殊处理。我已经尝试在编辑第一个答案时反映出来。 - cburgmer

8

回到2022年这个问题,DOMParser.parseFromString()方法的文档提供了一个更简单的解决方案:

const parser = new DOMParser();

const xmlString = "<warning>Beware of the missing closing tag";
const doc = parser.parseFromString(xmlString, "application/xml");
const errorNode = doc.querySelector('parsererror');
if (errorNode) {
  // parsing failed
} else {
  // parsing succeeded
}

尽管被接受的答案对我有用,但使用Document.querySelector()方法确实更简单,因为您不必确定parsererror元素的namespaceURI

1

在当前的浏览器中,DOMParser 在给出格式不正确的 XML 时似乎有两种可能的行为:

  1. 完全丢弃结果文档 - 返回一个带有错误详细信息的 <parsererror> 文档。Firefox 和 Edge 似乎总是采取这种方法;Chrome 家族的浏览器在大多数情况下也会这样做。

  2. 返回结果文档,并在根元素的第一个子节点插入一个额外的 <parsererror>。在能够生成根元素的情况下,Chrome 的解析器会在找到源 XML 中的错误时执行此操作。插入的 <parsererror> 可能具有命名空间,也可能没有。文档的其余部分似乎保持不变,包括注释等。请参阅 xml_errors.cc - 搜索 XMLErrors::InsertErrorMessageBlock

对于(1),检测错误的方法是向源字符串添加一个节点,进行解析,检查该节点是否存在于结果文档中,然后将其删除。据我所知,唯一可以在不影响结果的情况下实现这一点的方法是在源字符串末尾附加处理指令或注释。
示例:
let key = `a`+Math.random().toString(32);

let doc = (new DOMParser).parseFromString(src+`<?${key}?>`, `application/xml`);

let lastNode = doc.lastChild;
if (!(lastNode instanceof ProcessingInstruction)
    || lastNode.target !== key
    || lastNode.data !== ``)
{
    /* the XML was malformed */
} else {
    /* the XML was well-formed */
    doc.removeChild(lastNode);
}

如果出现情况(2),以上技术将无法检测到错误,因此需要另一步操作。
我们可以利用这样一个事实:即使在源代码中发现多个不同位置的错误,也只插入一个。通过再次解析源字符串,并附加一个语法错误,我们可以确保触发(2)行为,然后检查元素的数量是否发生了变化——如果没有,第一个parseFromString结果已经包含了真正的。
示例:
let errCount = doc.documentElement.getElementsByTagName(`parsererror`).length;
if (errCount !== 0) {
    let doc2 = parser.parseFromString(src+`<?`, `application/xml`);
    if (doc2.documentElement.getElementsByTagName(`parsererror`).length === errCount) {
        /* the XML was malformed */
    }
}

我制作了一个测试页面来验证这种方法:https://github.com/Cauterite/domparser-tests
它针对整个XML W3C一致性测试套件进行测试,再加上一些额外的样本以确保它能够区分包含<parsererror>元素的文档和DOMParser实际发出的错误。只有少数几个测试用例被排除在外,因为它们包含无效的Unicode序列。
明确一点,它只测试给定文档的结果是否与XMLHttpRequest.responseXML相同。
您可以在https://cauterite.github.io/domparser-tests/index.html上运行测试,但请注意它使用ECMAScript 2018。
在撰写本文时,最近版本的Firefox、Chrome、Safari和Android上的Firefox都通过了所有测试。 Edge和基于Presto的Opera应该会通过,因为它们的DOMParsers似乎 behave like Firefox's,而当前的Opera应该会通过,因为它是Chromium的分支。

如果您能找到任何反例或可能的改进,请告诉我。

对于懒惰的人,这里是完整的函数:

const tryParseXml = function(src) {
    /* returns an XMLDocument, or null if `src` is malformed */

    let key = `a`+Math.random().toString(32);

    let parser = new DOMParser;

    let doc = null;
    try {
        doc = parser.parseFromString(
            src+`<?${key}?>`, `application/xml`);
    } catch (_) {}

    if (!(doc instanceof XMLDocument)) {
        return null;
    }

    let lastNode = doc.lastChild;
    if (!(lastNode instanceof ProcessingInstruction)
        || lastNode.target !== key
        || lastNode.data !== ``)
    {
        return null;
    }

    doc.removeChild(lastNode);

    let errElemCount =
        doc.documentElement.getElementsByTagName(`parsererror`).length;
    if (errElemCount !== 0) {
        let errDoc = null;
        try {
            errDoc = parser.parseFromString(
                src+`<?`, `application/xml`);
        } catch (_) {}

        if (!(errDoc instanceof XMLDocument)
            || errDoc.documentElement.getElementsByTagName(`parsererror`).length
                === errElemCount)
        {
            return null;
        }
    }

    return doc;
}

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