Как запросить XML с помощью пространств имен в Java с XPath?
когда мой 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>
какие идеи?
6 ответов:
во втором примере XML-файла элементы привязаны к пространству имен. Ваш XPath пытается адресовать элементы, привязанные к пространству имен по умолчанию "no namespace", поэтому они не совпадают.
предпочтительным методом является регистрация пространства имен с префиксом пространства имен. Это делает ваш 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
один из выводов, который они делают:
Итак, чтобы иметь возможность использовать XPath выражения для содержимого XML, определенного в пространство имен (по умолчанию), нам нужно укажите сопоставление префикса пространства имен
обратите внимание, что это не значит, что вы должны изменить свой источник документ в любом случае (хотя вы можете поместить туда префиксы пространства имен, если хотите). Звучит странно, правда? Что ты будет do-это создание сопоставления префиксов пространства имен в коде 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
переменной.предостережение: если вы анализируете свой XML как DOM со стандартными классами JAXP, обязательно вызовите
setNamespaceAware(true)
наDocumentBuilderFactory
. В противном случае, этот код не будет работать!
все пространства имен, которые вы собираетесь выбрать в исходном XML, должны быть связаны с префиксом на языке хоста. В Java / JAXP это делается путем указания URI для каждого префикса пространства имен с помощью экземпляра
javax.xml.namespace.NamespaceContext
. К сожалению, есть нет реализации наNamespaceContext
предусмотрено в SDK.к счастью, это очень легко написать собственное:
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);
обратите внимание, что даже если первый пространство имен не указывает префикс в исходном документе (т. е. это пространство имен по умолчанию)вы должны связать его с префиксом в любом случае. Затем ваше выражение должно ссылаться на узлы в этом пространстве имен с помощью выбранного префикса, например:
/main:workbook/main:sheets/main:sheet[1]
имена префиксов, которые вы хотите связать с каждым пространством имен, являются произвольными; они не должны соответствовать тому, что появляется в исходном XML. это сопоставление-это всего лишь способ говорить Механизм XPath, который заданное имя префикса в выражении коррелирует с определенным пространством имен в исходном документе.
Если вы используете Spring, он уже содержит org.springframework.утиль.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);
убедитесь, что вы ссылаетесь на пространство имен в вашем 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>
в качестве входных данных, гдеkey
- это префикс, аvalue
- это пространство имен.следует NamespaceContext spesification, и вы можете увидеть, как это работает в тесты.
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