如何使用XPath中的Java命名空间查询XML?
当我的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>
有任何想法吗?
在第二个示例XML文件中,元素被绑定到一个名称空间。 您的XPath正在尝试处理绑定到默认“no namespace”命名空间的元素,因此它们不匹配。
首选方法是使用名称空间前缀注册名称空间。 这使得您的XPath更易于开发,读取和维护。
但是,注册名称空间并在您的XPath中使用名称空间前缀并不是强制性的。
您可以制定一个XPathexpression式,该expression式对元素和谓词filter使用通用匹配,以限制匹配所需的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可以匹配错误的元素并select错误的内容:
你的问题是默认的命名空间。 查阅这篇文章,了解如何处理XPath中的命名空间: http : //www.edankert.com/defaultnamespaces.html
他们得出的结论之一是:
因此,为了能够在(默认)名称空间中定义的XML内容上使用XPathexpression式,我们需要指定一个名称空间前缀映射
请注意,这并不意味着您必须以任何方式更改您的源文档(尽pipe如果您愿意,可以自由地将名称空间前缀放在那里)。 听起来很奇怪,对吧? 你要做的是在你的java代码中创build一个命名空间前缀映射,并在你的XPathexpression式中使用前缀。 在这里,我们将创build一个从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
variables中了。
警告:如果您使用标准JAXP类将XMLparsing为DOM,请务必在您的DocumentBuilderFactory
上调用setNamespaceAware(true)
。 否则,这个代码将无法正常工作!
您打算在源XML中select的所有名称空间都必须与主机语言的前缀关联。 在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);
请注意,即使第一个命名空间没有在源文档中指定前缀(即它是默认的命名空间 ), 您仍然必须将其与前缀相关联 。 然后,您的expression式应该使用您select的前缀引用该名称空间中的节点,如下所示:
/main:workbook/main:sheets/main:sheet[1]
您select与每个名称空间关联的前缀名称是任意的; 它们不需要匹配源XML中出现的内容。 这种映射只是告诉XPath引擎,expression式中的给定前缀名称与源文档中的特定名称空间相关联的一种方式。
确保您在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" >
我已经写了一个简单的NamespaceContext
实现( 这里 ),它将一个Map<String, String>
作为input,其中key
是一个前缀,并且该value
是一个名称空间。
它遵循NamespaceContext规范,你可以看到它在unit testing中是如何工作的。
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