Подтвердить что ты не робот

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

Любые идеи?

4b9b3361

Ответ 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);