Как запросить XML с помощью пространств имен в Java с XPath?
Когда мой XML выглядит так (no 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>
Любые идеи?
Ответы
Ответ 1
Во втором примере 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 мог бы совпадение с неправильными элементами и выбор неправильного содержимого:
Ответ 2
Ваша проблема - пространство имен по умолчанию. Ознакомьтесь с этой статьей, чтобы узнать, как обращаться с пространствами имен в вашем XPath: http://www.edankert.com/defaultnamespaces.html
Один из сделанных ими выводов:
Итак, чтобы использовать XPath выражения на XML-содержимом, определенные в (по умолчанию), нам нужно указать сопоставление префикса пространства имен
Обратите внимание, что это не означает, что вам нужно каким-либо образом изменить исходный документ (хотя вы можете разместить префиксы пространства имен там, если захотите). Звучит странно, правда? Что вы сделаете, так это создать сопоставление префикса пространства имен в вашем Java-коде и использовать указанный префикс в выражении XPath. Здесь мы создадим сопоставление от spreadsheet
к вашему пространству имен по умолчанию.
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
// there 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
. В противном случае этот код не будет работать!
Ответ 3
Все пространства имен, которые вы собираетесь выбрать из исходного XML, должны быть связаны с префиксом на языке хоста. В Java/JAXP это делается путем указания URI для каждого префикса пространства имен с использованием экземпляра javax.xml.namespace.NamespaceContext
. К сожалению, в 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);
Обратите внимание, что хотя первое пространство имен не указывает префикс в исходном документе (т.е. это пространство имен по умолчанию), вы все равно должны связывать его с префиксом. Ваше выражение должно затем ссылаться на узлы в этом пространстве имен, используя префикс, который вы выбрали, например:
/main:workbook/main:sheets/main:sheet[1]
Имена префикса, которые вы хотите связать с каждым пространством имен, являются произвольными; им не нужно сопоставлять то, что появляется в исходном XML. Это сопоставление - это просто способ сообщить движку XPath, что данное префиксное имя в выражении коррелирует с определенным пространством имен в исходном документе.
Ответ 4
Если вы используете 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);
Ответ 5
Убедитесь, что вы ссылаетесь на пространство имен в 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" >
Ответ 6
Я написал простую реализацию NamespaceContext
(здесь), которая принимает вход Map<String, String>
, где key
является префиксом, а value
- пространством имен.
Это следует из NamespaceContext spesification, и вы можете увидеть, как он работает в 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
Ответ 7
Две вещи, которые нужно добавить к существующим ответам:
-
Я не знаю, был ли это тот случай, когда вы задали вопрос: в Java 10 ваш XPath действительно работает для второго документа, если вы не используете setNamespaceAware(true)
на фабрике компоновщика документов (по умолчанию false
).
-
Если вы хотите использовать 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: я только что нашел здесь, что на самом деле есть метод Node.lookupNamespaceURI(String prefix)
, так что вы можете использовать его вместо поиска атрибута:
public String getNamespaceURI(String prefix) {
return documentElement.lookupNamespaceURI(prefix.isEmpty() ? null : prefix);
}
Также обратите внимание, что пространства имен могут быть объявлены для элементов, отличных от элемента документа, и они не будут распознаны (ни в одной из версий).
Ответ 8
Удивительно, если я не установил factory.setNamespaceAware(true);
тогда упомянутый вами xpath работает как с пространством имен, так и без него. Вы просто не можете выбирать вещи "с указанным пространством имен" только общие xpaths. Пойди разберись. Так что это может быть вариант:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);