Как сделать запрос XML с использованием пространств имен в Java с XPath? - PullRequest
61 голосов
/ 17 июня 2011

Когда мой 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>

Есть идеи?

Ответы [ 7 ]

65 голосов
/ 18 июня 2011

Во втором примере 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 может совпадать с неправильными элементами и выбирать неправильный контент:

57 голосов
/ 17 июня 2011

Ваша проблема - пространство имен по умолчанию. Прочтите эту статью о том, как обращаться с пространствами имен в вашем XPath: http://www.edankert.com/defaultnamespaces.html

Один из выводов, которые они делают:

Итак, чтобы иметь возможность использовать XPath выражения на XML-контент, определенный в пространство имен (по умолчанию), нам нужно указать отображение префикса пространства имен

Обратите внимание, что это не означает, что вы должны каким-либо образом изменять исходный документ (хотя вы можете свободно добавлять туда префиксы пространства имен, если хотите). Звучит странно, правда? *1010* сделает то, что создаст отображение префикса пространства имен в вашем 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. В противном случае этот код не будет работать!

36 голосов
/ 18 июня 2011

Все пространства имен, которые вы намереваетесь выбрать в исходном 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, что указанное имя префикса в выражении коррелирует с конкретным пространством имен в исходном документе.

3 голосов
/ 30 января 2018

Если вы используете 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);
1 голос
/ 28 сентября 2015

Я написал простую реализацию NamespaceContext ( здесь ), которая принимает Map<String, String> в качестве ввода, где key - это префикс, а value - это пространство имен.

Это следует за NamespaceContext , и вы можете увидеть, как оно работает в модульных тестах .

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

1 голос
/ 17 июня 2011

Убедитесь, что вы ссылаетесь на пространство имен в вашем 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"       >
0 голосов
/ 07 февраля 2019

Поразительно, если я не установлю factory.setNamespaceAware(true);, то упомянутый вами xpath работает с пространствами имен и без них при воспроизведении. Вы просто не можете выбирать вещи "с указанным пространством имен" только общие xpaths. Пойди разберись. Так что это может быть вариант:

 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
 factory.setNamespaceAware(false);
...