ChatGPT解决这个技术问题 Extra ChatGPT

如何通过 XPath 在 Java 中使用名称空间查询 XML?

当我的 XML 看起来像这样(没有 xmlns)时,我可以使用像 /workbook/sheets/sheet[1] 这样的 XPath 轻松查询它

<?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>

有任何想法吗?

您如何在第二个示例中访问它?
请发布您目前拥有的 Java 源代码

M
Mads Hansen

在第二个示例 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 可能会在错误的元素并选择了错误的内容:


我不明白为什么我需要在我的 XPath 中关联命名空间 URI 和命名空间前缀?在 XML 文档中,已经存在这样的关联,如原始问题中的 xmlns:r="schemas.openxmlformats.org/officeDocument/2006/relationships" 。在那里,前缀 r 绑定到命名空间 URI。按照我的阅读方式,我将被迫在我的 XPath 中(或以编程方式)重新建立此连接。
我建议反对这种做法。如果可能的话,不要按本地名称和命名空间进行匹配,这会使您的代码混乱,并且快速的哈希速度查找将不起作用。 @nokul:那是因为 XPath 可以对任何文档进行操作,并且命名空间前缀可以不同,但命名空间不能。如果您将 xmlns:xx 绑定到命名空间 aaa,并且文档在同一命名空间中有 <yy:foo>,则 xpath 表达式 xx:foo 将选择该节点。
以下 xpath 在我们的案例中不起作用:/NotifyShipment/DataArea/Shipment/ShipmentHeader/Status/Code/text() 根据上述答案,此 xpath 似乎有所帮助: (/*[local-name()='NotifyShipment ']/*[local-name()='DataArea']/*[local-name()='Shipment']/*[local-name()='ShipmentHeader']/*[local-name()= '状态']/*[本地名称()='代码']/文本())。我们可能会提出另一种方法,但感谢您的精彩说明!
s
stevevls

您的问题是默认命名空间。查看这篇文章,了解如何处理 XPath 中的命名空间:http://www.edankert.com/defaultnamespaces.html

他们得出的结论之一是:

因此,为了能够在(默认)命名空间中定义的 XML 内容上使用 XPath 表达式,我们需要指定命名空间前缀映射

请注意,这并不意味着您必须以任何方式更改源文档(尽管如果您愿意,您可以自由地将名称空间前缀放在那里)。听起来很奇怪,对吧?你做的是在你的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 变量中。

警告:如果您使用标准 JAXP 类将 XML 解析为 DOM,请务必在您的 DocumentBuilderFactory 上调用 setNamespaceAware(true)。否则,此代码将不起作用!


如何仅使用 Java SDK 来做到这一点?我没有 SimpleNamespaceContext 也不想使用外部库。
@lnez 看看...我更新了我的答案,以展示如何使用标准 jdk 类来做到这一点。
+1 for setNamespaceAware(true) ..xpath 让我发疯,然后我发现问题不在于注册 NS 或 xpath 语句本身,而是在更早的时候!
回复:“如果您使用标准 JAXP 类将 XML 解析为 DOM,请务必在 DocumentBuilderFactory 上调用 setNamespaceAware(true)。” OMG Java 太笨了。 2小时就这个。
如果您有默认名称空间(xmlns="http://www.default.com/..." 以及前缀名称 xmlns:foo="http://www.foo.com/..."),那么您还需要为默认名称提供映射,以便您的 XPath 表达式能够使用默认名称空间定位元素(例如,它们不'没有前缀)。对于上面的示例,只需向 getNamespaceURI 添加另一个条件,例如 else if ("default".equals(prefix)) return "http://www.default.com/...";。花了我一点时间来解决这个问题,希望可以为其他人节省一些工程时间。
W
Wayne

您打算从源 XML 中选择的所有名称空间都必须与宿主语言中的前缀相关联。在 Java/JAXP 中,这是通过使用 javax.xml.namespace.NamespaceContext 的实例为每个名称空间前缀指定 URI 来完成的。遗憾的是,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);

请注意,即使第一个命名空间没有在源文档中指定前缀(即 default namespace您也必须将其与前缀相关联。然后,您的表达式应该使用您选择的前缀引用该命名空间中的节点,如下所示:

/main:workbook/main:sheets/main:sheet[1]

您选择与每个命名空间关联的前缀名称是任意的;它们不需要匹配源 XML 中出现的内容。这种映射只是告诉 XPath 引擎表达式中的给定前缀名称与源文档中的特定名称空间相关联的一种方式。


我找到了另一种使用命名空间的方法,但你给了我提示——谢谢。
@vikingsteve 你能发布你的“另一种方式”吗?
道歉@Stephan,我不记得我在那里做了什么,但这让我走上了正确的轨道。
+1 用于整洁的 NamespaceContext 实现。您应该强调 setNamespaceAware(true) 是在 DocumentBuilderFactory 上设置的,就像@stevevls 所做的那样。否则,此代码将不起作用!这并不容易弄清楚。基本上,如果一个带有名称空间的 xml 并且不让 DBF NS 知道,那么 xpath 就会默默地变得无用,并且只能使用 local-name() 进行搜索。
如果您有默认名称空间(xmlns="http://www.default.com/..." 以及前缀名称 xmlns:foo="http://www.foo.com/..."),那么您还需要为默认名称提供映射,以便您的 XPath 表达式能够使用默认名称空间定位元素(例如,它们不'没有前缀)。对于上面的示例,只需向 getNamespaceURI 添加另一个条件,例如 else if ("default".equals(prefix)) return "http://www.default.com/...";。花了我一点时间来解决这个问题,希望可以为其他人节省一些工程时间。
k
kasi

如果您使用的是 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);

t
tomaj

我编写了一个简单的 NamespaceContext 实现 (here),它以 Map<String, String> 作为输入,其中 key 是前缀,value 是命名空间。

它遵循 NamespaceContext 规范,您可以在 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


c
cordsen

确保您在 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"       >

r
rogerdpack

令人吃惊的是,如果我没有设置 factory.setNamespaceAware(true);,那么您提到的 xpath 确实可以在使用和不使用命名空间的情况下工作。您只是无法选择“指定命名空间”的东西,只能选择通用 xpath。去搞清楚。所以这可能是一个选择:

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

j
joriki

要添加到现有答案中的两件事:

我不知道当您提出问题时是否是这种情况:使用 Java 10,如果您不在文档构建器工厂上使用 setNamespaceAware(true)(默认为 false),您的 XPath 实际上适用于第二个文档。

如果您确实想使用 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:我刚刚发现 here 实际上有一个方法 Node.lookupNamespaceURI(String prefix),所以您可以使用它来代替属性查找:

    public String getNamespaceURI(String prefix) {
        return documentElement.lookupNamespaceURI(prefix.isEmpty() ? null : prefix);
    }

另外,请注意,可以在文档元素以外的元素上声明命名空间,并且这些元素不会被识别(任一版本)。