如何检测以跨浏览器方式使用 Javascript 的 DOMParser 时的 XML 解析错误

How do I detect XML parsing errors when using Javascript's DOMParser in a cross-browser way?

本文关键字:DOMParser Javascript 时的 XML 错误 方式使 何检测 检测 浏览器      更新时间:2023-12-05

似乎所有主流浏览器都实现了 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));

歌剧结果:

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

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

结果在火狐:

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> 元素解析正确,但包含与 Opera 和 Firefox 的 <parsererror> 元素不同的命名空间中的嵌套<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 文档中的任何位置是否发生了解析错误? 还是我必须向 DOM 查询不同浏览器可能生成的每个可能的<parsererror>元素?

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

我尝试解析有意无效的 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。

当我第一次来到这里时,我投票支持原始答案(通过cspotcode(,但是,它在Firefox中不起作用。由于生成的文档的结构,生成的命名空间始终为"null"。我做了一些研究(检查这里的代码(。这个想法是使用不

invalidXml.childNodes[0].namespaceURI

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

然后像原始答案一样按命名空间选择"解析器错误"元素。但是,如果您有一个有效的 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或其他特定指标。只是空。

回到 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

在当前的浏览器中,当给定格式错误的XML时,DOMParser似乎有两种可能的行为:

  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(,上述技术不会检测到错误,因此需要执行另一个步骤。

我们可以利用只插入一个<parsererror>的事实,即使在源的不同位置发现了多个错误。通过再次解析源字符串,此时附加语法错误,我们可以确保触发 (2( 行为,然后检查<parsererror>元素的数量是否发生了变化——如果没有,第一个parseFromString结果已经包含一个真正的<parsererror>

例:

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应该通过,因为它们的DOMParser的行为似乎与Firefox的行为相似,而当前的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;
}