Как запросить 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 58

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