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

时间:2012-07-19 14:56:07

标签: javascript xml

似乎所有主流浏览器都实现了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, 'text/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>元素正确解析,但在与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文档中是否发生了解析错误?或者我必须查询不同浏览器可能生成的每个可能<parsererror>元素的DOM吗?

4 个答案:

答案 0 :(得分:17)

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

我尝试解析故意无效的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, 'text/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('<', 'text/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中的事情要简单得多。使用loadXML方法解析XML,如果解析成功或失败,则返回true或false。有关示例,请参阅http://www.w3schools.com/xml/xml_parser.asp

答案 1 :(得分:13)

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

invalidXml.childNodes[0].namespaceURI

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

然后在原始答案中按名称空间选择“parsererror”元素。但是,如果您在浏览器使用的命名空间中具有<parsererror>标记的有效XML文档,则最终会出现误报。 所以,这是一个启发式方法来检查你的XML是否成功解析:

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

为什么不在DOMParser中实现异常?

在当前上下文中值得一提的有趣之处:如果您尝试使用XMLHttpRequest获取XML文件,则解析的DOM将存储在responseXML属性或null中,如果XML文件内容为无效。也不例外,不是parsererror或其他特定指标。只是null。

答案 2 :(得分:0)

在当前浏览器中,当给定格式错误的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>元素的数量是否已更改-如果没有,则第一个{{1 }}结果已经包含真实的parseFromString

示例:

<parsererror>

我整理了一个测试页以验证此方法:https://github.com/Cauterite/domparser-tests

它对整个XML W3C Conformance Test Suite进行测试,外加一些示例,以确保它可以将包含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 */ } } 元素的文档与DOMParser发出的实际错误区分开。仅排除了少数测试用例,因为它们包含无效的unicode序列。

要清楚,它只是测试给定文档的结果是否与<parsererror>相同。

您可以在https://cauterite.github.io/domparser-tests/index.html上自行运行测试,但请注意,它使用ECMAScript 2018。

在撰写本文时,所有测试均通过了Firefox,Chrome,Safari和Android上的Firefox的最新版本。 基于Edge和Presto的Opera应该通过,因为它们的DOMParsers看起来像Firefox一样,而当前Opera应该通过,因为它是Chromium的分支。


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

对于懒惰者来说,这是完整的功能:

XMLHttpRequest.responseXML

答案 3 :(得分:-1)

我的网络平台是将HTML5用作XML(application / xhtml + xml),并且没有任何无效的内容被允许保存。我最近确定我丢失了代码,因为在Rich Editor和XML Editor之间切换时,它的格式错误。尽管也不太困难,但跨各种渲染引擎捕获格式错误的错误并不统一。尽管所有呈现引擎仍会按需进行,但Gecko仍会以格式错误的XML错误污染console。经过测试:

  • Gecko / Waterfox 56
  • Presto / Opera 12.1
  • Trident / IE 11
  • WebKit / Safari 12.1
  • 闪烁/ Chrome 55/75

我还包括了id_()entities()xml_add()函数,这些函数在防止Unicode字符本身变形(如果数据库不兼容)方面大有帮助。从2019年开始,您将要使用MariaDB并将数据库的编码设置为utf8mb4_unicode_520_ci。我的entities()函数非常具有攻击性(使用Unicode编码非常低的数字实体)。 Rick James有一个非常深入的MySQL utf8 Collations比较页面,该页面显然与MariaDB兼容。在某些时候,将取代520个 ,因此我建议您每年(每年)添加提醒,以检查可用的最高编码是什么。

当您将XML导入DOM浏览器时,所有这些内容几乎涵盖了 ,而将检查重复的id属性/值!在我的平台上,大多数情况下,我只是删除页面层。如果要导入的页面两次或多次具有相同的id,我也要注意。如果您的代码两次包含相同的id,则浏览器将选择第一个或第二个实例。如果您认为代码的某些 other 部分存在错误,可能会非常发疯。严格总是 优于松散代码,而纯JavaScript总是优于框架和库。

try
{
 if (!id_('xml_temp')) {xml_add('after', 'editor_rich', '<div class="hidden" id="xml_temp"></div>');}
 var f = id_('xml_temp').appendChild(new DOMParser().parseFromString(entities('<div xmlns="http://www.w3.org/1999/xhtml">'+id_('post_xml').value+'</div>'),'application/xml').childNodes[0]);
}
catch (err) {var f = false}

if (!f || f.childNodes.length == 0 || f.childNodes[0].nodeName == 'parsererror') {dialog.alert(error);}
else
{
 //Proceed with compliant XML.
}

以上代码在平台上使用的前提条件。

function id_(id) {return (document.getElementById(id)) ? document.getElementById(id) : false;}


function entities(s)
{
 var i = 0;
 var r = '';

 while (i<=s.length)
 {
  if (!isNaN(s.charCodeAt(i)))
  {
   if (s.charCodeAt(i)<127) {r += s.charAt(i);}
   else {r += '&#'+s.charCodeAt(i)+';';}
  }
  i++;
 }

 return r;
}

function xml_add(pos, e, xml)
{
 e = (typeof e == 'string' && id_(e)) ? id_(e) : e;

 if (e.nodeName)
 {
  if (pos=='after') {e.parentNode.insertBefore(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true),e.nextSibling);}
  else if (pos=='before') {e.parentNode.insertBefore(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true),e);}
  else if (pos=='inside') {e.appendChild(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true));}
  else if (pos=='replace') {e.parentNode.replaceChild(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true),e);}
  //Add fragment and have it returned.
 }
}