如何使用XPath使用Java中的命名空间查询XML?

时间:2011-06-17 18:45:52

标签: java xml xpath xml-namespaces

当我的XML看起来像这样(没有xmlns)时,我可以轻松地使用XPath查询它,如/workbook/sheets/sheet[1]

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook>
  <sheets>
    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
  </sheets>
</workbook>

但是当它看起来像这样我就不能

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <sheets>
    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
  </sheets>
</workbook>

有什么想法吗?

8 个答案:

答案 0 :(得分:65)

在第二个示例XML文件中,元素绑定到命名空间。您的XPath正在尝试处理绑定到默认“无命名空间”命名空间的元素,因此它们不匹配。

首选方法是使用namespace-prefix注册命名空间。它使您的XPath更容易开发,阅读和维护。

但是,您必须注册命名空间并在XPath中使用namespace-prefix。

可以制定一个XPath表达式,该表达式使用元素的通用匹配和谓词过滤器,该过滤器限制所需local-name()namespace-uri()的匹配。例如:

/*[local-name()='workbook'
    and namespace-uri()='http://schemas.openxmlformats.org/spreadsheetml/2006/main']
  /*[local-name()='sheets'
      and namespace-uri()='http://schemas.openxmlformats.org/spreadsheetml/2006/main']
  /*[local-name()='sheet'
      and namespace-uri()='http://schemas.openxmlformats.org/spreadsheetml/2006/main'][1]

正如您所看到的,它产生了一个非常冗长且冗长的XPath语句,很难阅读(和维护)。

您也可以匹配元素的local-name()并忽略命名空间。例如:

/*[local-name()='workbook']/*[local-name()='sheets']/*[local-name()='sheet'][1]

但是,您存在匹配错误元素的风险。如果您的XML具有使用相同local-name()的混合词汇表(对于此实例可能不是问题),则XPath可以匹配错误的元素并选择错误的内容:

答案 1 :(得分:57)

您的问题是默认命名空间。查看本文,了解如何处理XPath中的命名空间:http://www.edankert.com/defaultnamespaces.html

他们得出的结论之一是:

  

所以,能够使用XPath   关于XML定义的XML内容的表达式   一个(默认)命名空间,我们需要   指定名称空间前缀映射

请注意,这并不意味着您必须以任何方式更改源文档(尽管您可以根据需要随意添加名称空间前缀)。听起来很奇怪吧? 做的是在java代码中创建名称空间前缀映射,并在XPath表达式中使用所述前缀。在这里,我们将创建从spreadsheet到默认命名空间的映射。

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();

// there's no default implementation for NamespaceContext...seems kind of silly, no?
xpath.setNamespaceContext(new NamespaceContext() {
    public String getNamespaceURI(String prefix) {
        if (prefix == null) throw new NullPointerException("Null prefix");
        else if ("spreadsheet".equals(prefix)) return "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
        else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
        return XMLConstants.NULL_NS_URI;
    }

    // This method isn't necessary for XPath processing.
    public String getPrefix(String uri) {
        throw new UnsupportedOperationException();
    }

    // This method isn't necessary for XPath processing either.
    public Iterator getPrefixes(String uri) {
        throw new UnsupportedOperationException();
    }
});

// note that all the elements in the expression are prefixed with our namespace mapping!
XPathExpression expr = xpath.compile("/spreadsheet:workbook/spreadsheet:sheets/spreadsheet:sheet[1]");

// assuming you've got your XML document in a variable named doc...
Node result = (Node) expr.evaluate(doc, XPathConstants.NODE);

瞧...现在你已经将你的元素保存在result变量中了。

警告:如果您使用标准JAXP类将XML解析为DOM,请务必在setNamespaceAware(true)上调用DocumentBuilderFactory。否则,此代码将无效!

答案 2 :(得分:35)

您要在源XML中选择的所有命名空间必须与宿主语言中的前缀相关联。在Java / JAXP中,这是通过使用javax.xml.namespace.NamespaceContext的实例为每个名称空间前缀指定URI来完成的。不幸的是,SDK中提供了{strong>没有实现的NamespaceContext

幸运的是,编写自己的内容非常简单:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.xml.namespace.NamespaceContext;

public class SimpleNamespaceContext implements NamespaceContext {

    private final Map<String, String> PREF_MAP = new HashMap<String, String>();

    public SimpleNamespaceContext(final Map<String, String> prefMap) {
        PREF_MAP.putAll(prefMap);       
    }

    public String getNamespaceURI(String prefix) {
        return PREF_MAP.get(prefix);
    }

    public String getPrefix(String uri) {
        throw new UnsupportedOperationException();
    }

    public Iterator getPrefixes(String uri) {
        throw new UnsupportedOperationException();
    }

}

像这样使用:

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
HashMap<String, String> prefMap = new HashMap<String, String>() {{
    put("main", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
    put("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
}};
SimpleNamespaceContext namespaces = new SimpleNamespaceContext(prefMap);
xpath.setNamespaceContext(namespaces);
XPathExpression expr = xpath
        .compile("/main:workbook/main:sheets/main:sheet[1]");
Object result = expr.evaluate(doc, XPathConstants.NODESET);

请注意,即使第一个命名空间未在源文档中指定前缀(即它是default namespace,您仍必须将其与前缀相关联。然后,您的表达式应使用您选择的前缀引用该命名空间中的节点,如下所示:

/main:workbook/main:sheets/main:sheet[1]

您选择与每个命名空间关联的前缀名称是任意的;它们不需要匹配源XML中显示的内容。这种映射只是告诉XPath引擎表达式中给定前缀名与源文档中特定名称空间相关的一种方式。

答案 3 :(得分:3)

如果您使用的是Spring,则它已包含org.springframework.util.xml.SimpleNamespaceContext。

        import org.springframework.util.xml.SimpleNamespaceContext;
        ...

        XPathFactory xPathfactory = XPathFactory.newInstance();
        XPath xpath = xPathfactory.newXPath();
        SimpleNamespaceContext nsc = new SimpleNamespaceContext();

        nsc.bindNamespaceUri("a", "http://some.namespace.com/nsContext");
        xpath.setNamespaceContext(nsc);

        XPathExpression xpathExpr = xpath.compile("//a:first/a:second");

        String result = (String) xpathExpr.evaluate(object, XPathConstants.STRING);

答案 4 :(得分:1)

确保您在XSLT中引用命名空间

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
             xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"       >

答案 5 :(得分:1)

我编写了一个简单的NamespaceContext实现(here),它以Map<String, String>作为输入,其中key是前缀,{ {1}}是名称空间。

遵循NamespaceContext规定,您可以在unit tests中看到它是如何运作的。

value

请注意,它依赖于Google Guava

答案 6 :(得分:0)

要添加到现有答案中的两件事:

  • 当您问以下问题时,我不知道是否是这种情况:对于Java 10,如果您未在文档构建器工厂上使用setNamespaceAware(true),则XPath实际上可用于第二个文档(默认为false

  • 如果您确实想使用setNamespaceAware(true),其他答案已经显示了如何使用名称空间上下文来执行此操作。但是,您不需要自己提供前缀到名称空间的映射,如以下答案所示:document元素中已经存在前缀,并且可以将其用于名称空间上下文:

import java.util.Iterator;

import javax.xml.namespace.NamespaceContext;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class DocumentNamespaceContext implements NamespaceContext {
    Element documentElement;

    public DocumentNamespaceContext (Document document) {
        documentElement = document.getDocumentElement();
    }

    public String getNamespaceURI(String prefix) {
        return documentElement.getAttribute(prefix.isEmpty() ? "xmlns" : "xmlns:" + prefix);
    }

    public String getPrefix(String namespaceURI) {
        throw new UnsupportedOperationException();
    }

    public Iterator<String> getPrefixes(String namespaceURI) {
        throw new UnsupportedOperationException();
    }
}

其余代码与其他答案相同。然后XPath /:workbook/:sheets/:sheet[1]产生工作表元素。 (您也可以像其他答案一样,为默认名称空间使用非空前缀,方法是将prefix.isEmpty()替换为prefix.equals("spreadsheet")并使用XPath /spreadsheet:workbook/spreadsheet:sheets/spreadsheet:sheet[1]。)

P.S。:我刚刚发现here实际上有一个方法Node.lookupNamespaceURI(String prefix),因此您可以使用它代替属性查找:

    public String getNamespaceURI(String prefix) {
        return documentElement.lookupNamespaceURI(prefix.isEmpty() ? null : prefix);
    }

另外,请注意,可以在document元素以外的元素上声明名称空间,而这些名称将无法被识别(任一版本)。

答案 7 :(得分:-1)

令人惊讶的是,如果我没有设置Future<void> checkIfUserHasSubscription() async { await Future < dynamic > remainingS = CloudFunctions.instance .call(functionName: 'isValid'); if (remaining == "true") hasSubscription = true; else { hasSubscription = false; remaining = int.parse(remaining); } } ,那么您提到的xpath可以在使用和不使用名称空间的情况下使用。您只是不能选择“指定了名称空间”的东西,而只能选择通用xpath。去搞清楚。因此,这可能是一个选择:

factory.setNamespaceAware(true);