在XML文档中查找所有命名空间声明 - xPath 1.0与xPath 2.0的区别

8
作为Java 6应用程序的一部分,我希望查找XML文档中的所有命名空间声明,包括任何重复的声明。
编辑:根据Martin的要求,这是我正在使用的Java代码:
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
XPathExpression xPathExpression = xPathExpression = xPath.compile("//namespace::*"); 
NodeList nodeList = (NodeList) xPathExpression.evaluate(xmlDomDocument, XPathConstants.NODESET);

假设我有这个XML文档:
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:ele="element.com" xmlns:att="attribute.com" xmlns:txt="textnode.com">
    <ele:one>a</ele:one>
    <two att:c="d">e</two>
    <three>txt:f</three>
</root>

为了找到所有的命名空间声明,我使用 xPath 1.0 将此 xPath 语句应用于 XML 文档:
//namespace::*

它找到了4个命名空间声明,这正是我期望(并希望)的:
/root[1]/@xmlns:att - attribute.com
/root[1]/@xmlns:ele - element.com
/root[1]/@xmlns:txt - textnode.com
/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace

但是,如果我改用xPath 2.0,那么我将得到16个命名空间声明(每个以前的声明都出现了4次),这不是我所期望(或希望)的:

/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace
/root[1]/@xmlns:att - attribute.com
/root[1]/@xmlns:ele - element.com
/root[1]/@xmlns:txt - textnode.com
/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace
/root[1]/@xmlns:att - attribute.com
/root[1]/@xmlns:ele - element.com
/root[1]/@xmlns:txt - textnode.com
/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace
/root[1]/@xmlns:att - attribute.com
/root[1]/@xmlns:ele - element.com
/root[1]/@xmlns:txt - textnode.com
/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace
/root[1]/@xmlns:att - attribute.com
/root[1]/@xmlns:ele - element.com
/root[1]/@xmlns:txt - textnode.com

即使我使用xPath语句的非缩写版本,仍然可以看到这种差异:

/descendant-or-self::node()/namespace::*

并且在oXygen中测试,可以看到它出现在各种XML解析器(LIBXML、MSXML.NET、Saxon)中。(编辑:如我后来在评论中提到的那样,这个说法是不正确的。虽然我认为我正在测试各种XML解析器,但实际上并不是。)

问题1:为什么从xPath 1.0到xPath 2.0有所区别?

问题2:使用xPath 2.0获得所需结果是否可行/合理?

提示:在xPath 2.0中使用distinct-values()函数无法返回所需结果,因为我想要所有命名空间声明,即使同一个命名空间被声明两次。例如,考虑以下XML文档:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <bar:one xmlns:bar="http://www.bar.com">alpha</bar:one>
    <bar:two xmlns:bar="http://www.bar.com">bravo</bar:two>
</root>

所需结果是:
/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace
/root[1]/bar:one[1]/@xmlns:bar - http://www.bar.com
/root[1]/bar:two[1]/@xmlns:bar - http://www.bar.com

James,请展示给我们寻找“declarations”命名空间的代码。在我看来,XPath“//namespace::*”找到的是所有命名空间节点,这与命名空间声明不同,因为命名空间节点存在于每个元素节点中,而且不在节点之间共享。因此,对于一个具有四个元素节点的XML文档,在根元素上有三个命名空间声明的情况下,该路径应该为每个四个元素找到四个命名空间节点。据我所知,这在XPath 1.0和2.0之间是相同的。此外,“/root[1]/@xmlns:txt”这样的表示方法可能会让人产生误解。 - Martin Honnen
/root[1]/@xmlns:txt 表示法来自 oXygen。这是它们节点列表中节点的表示方式,很好。 - james.garriss
Java代码已添加在上方,非常标准。感谢您的解释。 - james.garriss
1
我认为一个问题是你使用的Java API基于DOM节点模型,或者说将XPath/XSLT数据模型映射到DOM模型。DOM模型只有属性节点,其中一些是命名空间声明属性。XSLT/XPath模型具有属性节点和命名空间节点,并且命名空间声明在该模型中不是属性节点,因此例如<foo xmlns:ns1="http://example.com/ns1"/>中,foo元素在XPath/XSLT数据模型中没有属性节点,但在作用域命名空间节点中有两个(一个在标记中,另一个是xml命名空间的内置节点)。 - Martin Honnen
继续我的评论:问题在于你使用XPath //namespace::*选取了一些命名空间节点,但随后使用的API将结果表示为DOM节点。这种映射可能因实现而异。将XPath映射到DOM时存在其他已知问题,例如对于<foo><![CDATA[text 1]]>text2</foo>,在DOM中解析时/foo/text()[1]选择的内容是依赖于实现的,因为foo元素具有两个子节点,一个CDATA节和一个文本节点,而XPath模型仅有一个文本节点。 - Martin Honnen
詹姆斯,我完全忘记了你可能对XPath 2.0解决方案感兴趣。我已经更新了我的答案,使用XPath 2.0表达式选择XML文档中的所有“不同”命名空间节点,并生成它们可读的表示形式。 - Dimitre Novatchev
4个回答

8

我认为这将获取所有的命名空间,而且没有重复:

for $i in 1 to count(//namespace::*) return 
if (empty(index-of((//namespace::*)[position() = (1 to ($i - 1))][name() = name((//namespace::*)[$i])], (//namespace::*)[$i]))) 
then (//namespace::*)[$i] 
else ()

这就是!这个 xPath 2.0 将会找到所有命名空间声明,并且它适用于我在我的 OP 中给出的两个例子。这种方法的优雅之处在于它将命名空间处理为序列。干得好,@Roger。 - james.garriss

4

To find all namespace declarations, I applied this xPath statement to the XML document using xPath 1.0:

//namespace::* It finds 4 namespace declarations, which is what I expect (and desire):

/root[1]/@xmlns:att - attribute.com
/root[1]/@xmlns:ele - element.com 
/root[1]/@xmlns:txt - textnode.com 
/root[1]/@xmlns:xml - http://www.w3.org/XML/1998/namespace

您正在使用一个不符合标准(有缺陷的)XPath 1.0 实现

我使用所有的 XSLT 1.0 处理器都能得到不同且正确的结果。这个转换(只是评估 XPath 表达式并打印每个选定命名空间节点的一行):

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
     <xsl:for-each select="//namespace::*">
       <xsl:value-of select="concat(name(), ': ', ., '&#xA;')"/>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

当应用于提供的XML文档时:

<root xmlns:ele="element.com" xmlns:att="attribute.com" xmlns:txt="textnode.com">
    <ele:one>a</ele:one>
    <two att:c="d">e</two>
    <three>txt:f</three>
</root>

产生正确的结果:

xml: http://www.w3.org/XML/1998/namespace
ele: element.com
att: attribute.com
txt: textnode.com
xml: http://www.w3.org/XML/1998/namespace
ele: element.com
att: attribute.com
txt: textnode.com
xml: http://www.w3.org/XML/1998/namespace
ele: element.com
att: attribute.com
txt: textnode.com
xml: http://www.w3.org/XML/1998/namespace
ele: element.com
att: attribute.com
txt: textnode.com

在所有这些XSLT 1.0和XSLT 2.0处理器中,包括MSXML3、MSXML4、MSXML6、.NET XslCompiledTransform、.NET XslTransform、Altova(XML SPY)、Saxon 6.5.4、Saxon 9.1.07和XQSharp。
这里有一个简短的C#程序,确认在.NET中选择的节点数为16:
namespace TestNamespaces
{
    using System;
    using System.IO;
    using System.Xml.XPath;

    class Test
    {
        static void Main(string[] args)
        {
            string xml =
@"<root xmlns:ele='element.com' xmlns:att='attribute.com' xmlns:txt='textnode.com'>
    <ele:one>a</ele:one>
    <two att:c='d'>e</two>
    <three>txt:f</three>
</root>";
            XPathDocument doc = new XPathDocument(new StringReader(xml));

            double count = 
              (double) doc.CreateNavigator().Evaluate("count(//namespace::*)");

            Console.WriteLine(count);
        }
    }
}
结果为

16

更新

这是一个XPath 2.0表达式,它仅查找“distinct”命名空间节点,并为每个节点生成一行名称-值对:

for $i in distinct-values(
             for $ns in //namespace::*
               return
                  index-of(
                           (for $x in //namespace::*
                             return
                                concat(name($x), ' ', string($x))

                            ),
                            concat(name($ns), ' ', string($ns))
                          )
                          [1]
                                                  )
  return
    for $x in (//namespace::*)[$i]
     return
        concat(name($x), ' :', string($x), '&#xA;')

当我使用Java 6中的默认解析器并使用xPath API时,我得到4个节点。当我在oXygen 12.1中使用XSV、LIBXML、MSXML4.0、MSXML.NET和Saxon-EE应用xPath(作为1.0)时,我也得到了4个节点。当我在oXygen中使用你的XSLT和Xalan时,我得到了4个节点。但是,当我在oXygen中使用各种版本的Saxon和你的XSLT时,我得到了16个节点。我不明白为什么我在Saxon中得到了不同的答案。一定有些简单的东西我没注意到... - james.garriss
@james.garriss:添加了承诺的C#示例--16个节点。你认为为什么.NET XPath评估会产生不同的结果?显然,你得到不同结果的原因在于你的代码。 - Dimitre Novatchev
1
确实,问题出在我(错误地)使用了oXygen。下拉菜单允许我在解析器之间进行选择,但并不与xPath查询相关,而只与XSD验证相关。因此,我认为真正发生的是:当我选择xPath 1.0时,oXygen在幕后使用的是Xalan,而Xalan的实现不符合标准。当我选择xPath 2.0时,它使用了符合标准的Saxon,我得到了正确的答案(16个节点)。 - james.garriss
底线是:当你说我在使用“不兼容(有缺陷的)XPath 1.0实现”时,你是正确的。我在我的Java代码中使用了Xalan,在oXygen中也使用了Xalan。 - james.garriss
@james.garriss:很高兴我的回答有用。 - Dimitre Novatchev
显示剩余2条评论

3
如前面的线程所示,//namespace::*将返回所有命名空间节点,根据XPath 1.0和XPath 2.0实现,共有16个。如果您发现某个实现无法正确实现规范,这并不令人惊讶。
使用XPath 1.0或XPath 2.0通常无法找到所有命名空间声明(与命名空间节点不同),因为以下两个文档在数据模型级别上被认为是等价的:
文档A:
<a xmlns="one">
  <b/>
</a> 

文档B:

<a xmlns="one">
  <b xmlns="one"/>
</a>

但是,如果我们将“重要的命名空间声明”定义为存在于子元素上但不存在于其父元素上的命名空间,则可以尝试使用此XPath 2.0表达式:

for $e in //* return
  for $n in $e/namespace::* return
     if (not(some $p in $n/../namespace::* satisfies ($p/name() eq $e/name() and string($p) eq string($n)))) then concat($e/name(), '->', $n/name(), '=', string($n)) else ()

虽然我非常希望这个答案能够解决我的问题,因为它涉及到了我的核心问题,但是这个xPath并没有返回任何与我已经使用的(//namespace::*)不同的内容。不过还是谢谢你的尝试。 - james.garriss

0

这是我使用.NET的XPathDocument(XSLT / XPath 1.0数据模型)、XmlDocument(DOM数据模型)和MSXML 6的DOM实现XPath 1.0的结果;针对您的示例XML文档运行的测试代码如下:

    Console.WriteLine("XPathDocument:");
    XPathDocument xpathDoc = new XPathDocument("../../XMLFile4.xml");
    foreach (XPathNavigator nav in xpathDoc.CreateNavigator().Select("//namespace::*"))
    {
        Console.WriteLine("Node type: {0}; name: {1}; value: {2}.", nav.NodeType, nav.Name, nav.Value);
    }
    Console.WriteLine();

    Console.WriteLine("DOM XmlDocument:");
    XmlDocument doc = new XmlDocument();
    doc.Load("../../XMLFile4.xml");
    foreach (XmlNode node in doc.SelectNodes("//namespace::*"))
    {
        Console.WriteLine("Node type: {0}; name: {1}; value: {2}.", node.NodeType, node.Name, node.Value);
    }
    Console.WriteLine();


    Console.WriteLine("MSXML 6 DOM:");
    dynamic msxmlDoc = Activator.CreateInstance(Type.GetTypeFromProgID("Msxml2.DOMDocument.6.0"));
    msxmlDoc.load("../../XMLFile4.xml");
    foreach (dynamic node in msxmlDoc.selectNodes("//namespace::*"))
    {
        Console.WriteLine("Node type: {0}; name: {1}; value: {2}.", node.nodeType, node.name, node.nodeValue);
    }

它的输出是

XPathDocument:
Node type: Namespace; name: txt; value: textnode.com.
Node type: Namespace; name: att; value: attribute.com.
Node type: Namespace; name: ele; value: element.com.
Node type: Namespace; name: xml; value: http://www.w3.org/XML/1998/namespace.
Node type: Namespace; name: txt; value: textnode.com.
Node type: Namespace; name: att; value: attribute.com.
Node type: Namespace; name: ele; value: element.com.
Node type: Namespace; name: xml; value: http://www.w3.org/XML/1998/namespace.
Node type: Namespace; name: txt; value: textnode.com.
Node type: Namespace; name: att; value: attribute.com.
Node type: Namespace; name: ele; value: element.com.
Node type: Namespace; name: xml; value: http://www.w3.org/XML/1998/namespace.
Node type: Namespace; name: txt; value: textnode.com.
Node type: Namespace; name: att; value: attribute.com.
Node type: Namespace; name: ele; value: element.com.
Node type: Namespace; name: xml; value: http://www.w3.org/XML/1998/namespace.

DOM XmlDocument:
Node type: Attribute; name: xmlns:txt; value: textnode.com.
Node type: Attribute; name: xmlns:att; value: attribute.com.
Node type: Attribute; name: xmlns:ele; value: element.com.
Node type: Attribute; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespa
ce.
Node type: Attribute; name: xmlns:txt; value: textnode.com.
Node type: Attribute; name: xmlns:att; value: attribute.com.
Node type: Attribute; name: xmlns:ele; value: element.com.
Node type: Attribute; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespa
ce.
Node type: Attribute; name: xmlns:txt; value: textnode.com.
Node type: Attribute; name: xmlns:att; value: attribute.com.
Node type: Attribute; name: xmlns:ele; value: element.com.
Node type: Attribute; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespa
ce.
Node type: Attribute; name: xmlns:txt; value: textnode.com.
Node type: Attribute; name: xmlns:att; value: attribute.com.
Node type: Attribute; name: xmlns:ele; value: element.com.
Node type: Attribute; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespa
ce.

MSXML 6 DOM:
Node type: 2; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespace.
Node type: 2; name: xmlns:ele; value: element.com.
Node type: 2; name: xmlns:att; value: attribute.com.
Node type: 2; name: xmlns:txt; value: textnode.com.
Node type: 2; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespace.
Node type: 2; name: xmlns:ele; value: element.com.
Node type: 2; name: xmlns:att; value: attribute.com.
Node type: 2; name: xmlns:txt; value: textnode.com.
Node type: 2; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespace.
Node type: 2; name: xmlns:ele; value: element.com.
Node type: 2; name: xmlns:att; value: attribute.com.
Node type: 2; name: xmlns:txt; value: textnode.com.
Node type: 2; name: xmlns:xml; value: http://www.w3.org/XML/1998/namespace.
Node type: 2; name: xmlns:ele; value: element.com.
Node type: 2; name: xmlns:att; value: attribute.com.
Node type: 2; name: xmlns:txt; value: textnode.com.

所以这肯定不是XPath 1.0与XPath 2.0的问题。我认为您看到的问题是将XPath数据模型与命名空间节点映射到具有属性节点的DOM模型的缺陷。更熟悉Java XPath API的人需要告诉您,您看到的行为是否正确地依赖于实现,因为API规范对于将XPath命名空间轴映射到DOM模型的情况不够精确,或者这是一个错误。


我同意这不是一个xPath 1.0与2.0的问题,但我不倾向于认为这个问题在于Java 6中的xPath API(尽管它可能有各种缺陷),因为当我使用Saxon 9 HE替换Java 6中的默认XML解析器(Xalan),并且没有更改我的Java代码时,它可以工作(即,返回16个节点而不是4个)。这让我得出结论,Xalan的实现才是真正的原因。 - james.garriss

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