当我的 XML 看起来像这样(没有 xmlns
)时,我可以使用像 /workbook/sheets/sheet[1]
这样的 XPath 轻松查询它
<?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>
有任何想法吗?
在第二个示例 XML 文件中,元素绑定到命名空间。您的 XPath 正在尝试处理绑定到默认“无命名空间”命名空间的元素,因此它们不匹配。
首选方法是使用命名空间前缀注册命名空间。它使您的 XPath 更易于开发、阅读和维护。
但是,您不必在 XPath 中注册名称空间并使用名称空间前缀。
您可以制定一个 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 可能会在错误的元素并选择了错误的内容:
您的问题是默认命名空间。查看这篇文章,了解如何处理 XPath 中的命名空间:http://www.edankert.com/defaultnamespaces.html
他们得出的结论之一是:
因此,为了能够在(默认)命名空间中定义的 XML 内容上使用 XPath 表达式,我们需要指定命名空间前缀映射
请注意,这并不意味着您必须以任何方式更改源文档(尽管如果您愿意,您可以自由地将名称空间前缀放在那里)。听起来很奇怪,对吧?你将做的是在你的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,请务必在您的 DocumentBuilderFactory
上调用 setNamespaceAware(true)
。否则,此代码将不起作用!
xmlns="http://www.default.com/..."
以及前缀名称 xmlns:foo="http://www.foo.com/..."
),那么您还需要为默认名称提供映射,以便您的 XPath 表达式能够使用默认名称空间定位元素(例如,它们不'没有前缀)。对于上面的示例,只需向 getNamespaceURI
添加另一个条件,例如 else if ("default".equals(prefix)) return "http://www.default.com/...";
。花了我一点时间来解决这个问题,希望可以为其他人节省一些工程时间。
您打算从源 XML 中选择的所有名称空间都必须与宿主语言中的前缀相关联。在 Java/JAXP 中,这是通过使用 javax.xml.namespace.NamespaceContext
的实例为每个名称空间前缀指定 URI 来完成的。遗憾的是,SDK 中没有实现 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 引擎表达式中的给定前缀名称与源文档中的特定名称空间相关联的一种方式。
xmlns="http://www.default.com/..."
以及前缀名称 xmlns:foo="http://www.foo.com/..."
),那么您还需要为默认名称提供映射,以便您的 XPath 表达式能够使用默认名称空间定位元素(例如,它们不'没有前缀)。对于上面的示例,只需向 getNamespaceURI
添加另一个条件,例如 else if ("default".equals(prefix)) return "http://www.default.com/...";
。花了我一点时间来解决这个问题,希望可以为其他人节省一些工程时间。
如果您使用的是 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);
我编写了一个简单的 NamespaceContext
实现 (here),它以 Map<String, String>
作为输入,其中 key
是前缀,value
是命名空间。
它遵循 NamespaceContext 规范,您可以在 unit tests 中看到它是如何工作的。
Map<String, String> mappings = new HashMap<>();
mappings.put("foo", "http://foo");
mappings.put("foo2", "http://foo");
mappings.put("bar", "http://bar");
context = new SimpleNamespaceContext(mappings);
context.getNamespaceURI("foo"); // "http://foo"
context.getPrefix("http://foo"); // "foo" or "foo2"
context.getPrefixes("http://foo"); // ["foo", "foo2"]
请注意,它依赖于 Google Guava
确保您在 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" >
令人吃惊的是,如果我没有设置 factory.setNamespaceAware(true);
,那么您提到的 xpath 确实可以在使用和不使用命名空间的情况下工作。您只是无法选择“指定命名空间”的东西,只能选择通用 xpath。去搞清楚。所以这可能是一个选择:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
要添加到现有答案中的两件事:
我不知道当您提出问题时是否是这种情况:使用 Java 10,如果您不在文档构建器工厂上使用 setNamespaceAware(true)(默认为 false),您的 XPath 实际上适用于第二个文档。
如果您确实想使用 setNamespaceAware(true),其他答案已经展示了如何使用命名空间上下文来执行此操作。但是,您不需要自己提供前缀到命名空间的映射,就像这些答案所做的那样:它已经存在于文档元素中,您可以将其用于命名空间上下文:
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]
。)
PS:我刚刚发现 here 实际上有一个方法 Node.lookupNamespaceURI(String prefix)
,所以您可以使用它来代替属性查找:
public String getNamespaceURI(String prefix) {
return documentElement.lookupNamespaceURI(prefix.isEmpty() ? null : prefix);
}
另外,请注意,可以在文档元素以外的元素上声明命名空间,并且这些元素不会被识别(任一版本)。
xmlns:xx
绑定到命名空间aaa
,并且文档在同一命名空间中有<yy:foo>
,则 xpath 表达式xx:foo
将选择该节点。