【问题标题】:How to use XPath on TXMLDocument which has namespace prefixes?如何在具有命名空间前缀的 TXMLDocument 上使用 XPath?
【发布时间】:2015-08-21 15:34:25
【问题描述】:

我从第三方网络服务器收到了一个 XML 数据包:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SomeResponse xmlns="http://someurl">
      <SomeResult>
        .....
      </SomeResult>
    </SomeResponse>
  </soap:Body>
</soap:Envelope>

为了能够跨平台,这个 XML 被加载到 Delphi 的IXMLDocument

XmlDoc.LoadFromXML(XmlString);

我是using a solution,正在使用 XPath 查找 XML 节点。该解决方案在其他情况下有效,但是当 XML 文档包含命名空间前缀时,它会失败。

我正在尝试访问路径:

/soap:Envelope/soap:Body/SomeResponse/SomeResult

来自链接的答案:

function selectNode(xnRoot: IXmlNode; const nodePath: WideString): IXmlNode;
var
  intfSelect : IDomNodeSelect;
  dnResult : IDomNode;
  intfDocAccess : IXmlDocumentAccess;
  doc: TXmlDocument;
begin
  Result := nil;
  if not Assigned(xnRoot) or not Supports(xnRoot.DOMNode, IDomNodeSelect, intfSelect) then
    Exit;
  dnResult := intfSelect.selectNode(nodePath);
  if Assigned(dnResult) then
  begin
    if Supports(xnRoot.OwnerDocument, IXmlDocumentAccess, intfDocAccess) then
      doc := intfDocAccess.DocumentObject
    else
      doc := nil;
    Result := TXmlNode.Create(dnResult, nil, doc);
  end;
end;

它在dnResult := intfSelect.selectNode(nodePath);EOleException 失败:Reference to undeclared namespace prefix: 'soap'

当节点名称有命名空间前缀时,我该如何进行这项工作?

【问题讨论】:

  • 您需要以某种方式告诉 XPath 处理器文档中使用的名称空间 URL。文档中使用的名称并不重要(这很好,因为未命名 SomeResponse 节点的命名空间)。一些 XML 库具有采用名称空间映射的函数;也许这个也是。然后为 http://someurl 命名空间选择一个名称(例如,foo),然后在 XPath 查询中使用相同的名称(例如,foo:SomeResponse)。您还需要包含其他命名空间的名称。
  • 获取SomeResult 的最简单方法是使用//SomeResponse/SomeResult 而不是使用完整路径。 XmlDoc 的SelectionNamespaces 属性也可能有所帮助。看here
  • @kobik:请注意,SelectionNamespaces 是特定于 msxml DOM 提供程序的,并且 OP 正在寻找 xplat 解决方案;)

标签: xml delphi soap xpath


【解决方案1】:

不要尝试在 XPath 查询中包含命名空间。 如果您想要的只是 SomeResult 节点的文本,那么您可以使用 '//SomeResult' 作为查询。出于某种原因,默认命名空间 xmlns="http://someurl" 上的默认 xml 实现 (msxml) barfs 在 SomeResponse 父节点上。但是,使用 OmniXML 作为 DOMVendor(= 跨平台并且从 XE7 开始有效 - 感谢@gabr)这样可以:

program Project3;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Xml.XmlIntf,
  Xml.XMLDoc,
  Xml.XMLDom,
  Xml.omnixmldom,
  System.SysUtils;

const
 xml = '<?xml version="1.0" encoding="utf-8"?>'+#13#10+
        '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'+#13#10+
        'xmlns:xsd="http://www.w3.org/2001/XMLSchema"'+#13#10+
        'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">'+#13#10+
        ' <soap:Body>'+#13#10+
        '  <SomeResponse xmlns="http://tempuri.org">'+#13#10+
        '   <SomeResult>1</SomeResult>'+#13#10+
        '  </SomeResponse>'+#13#10+
        ' </soap:Body>'+#13#10+
        '</soap:Envelope>';

function selectNode(xnRoot: IXmlNode; const nodePath: WideString): IXmlNode;
var
  intfSelect : IDomNodeSelect;
  dnResult : IDomNode;
  intfDocAccess : IXmlDocumentAccess;
  doc: TXmlDocument;
begin
  Result := nil;
  if not Assigned(xnRoot) or not Supports(xnRoot.DOMNode, IDomNodeSelect, intfSelect) then
    Exit;
  dnResult := intfSelect.selectNode(nodePath);
  if Assigned(dnResult) then
  begin
    if Supports(xnRoot.OwnerDocument, IXmlDocumentAccess, intfDocAccess) then
      doc := intfDocAccess.DocumentObject
    else
      doc := nil;
    Result := TXmlNode.Create(dnResult, nil, doc);
  end;
end;

function XPathQuery(Doc : IXMLDocument; Query : String) : String;

var
 Node : IXMLNode;

begin
 Result := '';
 Node := SelectNode(Doc.DocumentElement, Query);
 if Assigned(Node) then
  Result := Node.Text
end;

var
 Doc : IXMLDocument;

begin
 DefaultDOMVendor := sOmniXmlVendor;
 Doc := TXMLDocument.Create(nil);
 try
  Doc.LoadFromXML(Xml);
  Writeln(Doc.XML.Text);
  Writeln(XPathQuery(Doc, '//SomeResult'));
 except
  on E: Exception do
   Writeln(E.ClassName, ': ', E.Message);
 end;
 Doc := nil;
 Readln;
end.

【讨论】:

  • @JerryDodge:我删除了 COM 初始化例程和单元,因为它们是测试的剩余部分,在这里不需要。
  • OmniXML 对 XPath 的支持非常有限。即使有简单的X&lt;&gt;Y 条件,我也必须修补库。 Kluug 的 OXML 站点对 Delphi 的一些不同 XML 库进行了快速测试,因此可能是探索替代方案的起点。
【解决方案2】:

当我几年前尝试这个时,我发现 XPath 中的命名空间查找在不同的 xml 提供程序之间是不同的。

如果我没记错的话,Msxml 允许您使用在 xml 文件中定义的命名空间前缀。

ADOM 4 提供程序要求您将 XPath 查询中使用的名称空间前缀解析为实际的名称空间,而与 xml 文件中使用的名称空间映射无关。为此目的,有一个方法指针 OnOx4XPathLookupNamespaceURI。然后你可以有一个这样的名字查找函数:

procedure TTestXmlUtil.EventLookupNamespaceURI(
  const AContextNode: IDomNode; const APrefix: WideString;
  var ANamespaceURI: WideString);
begin
  if APrefix = 'soap' then
    ANamespaceURI := 'http://schemas.xmlsoap.org/soap/envelope/'
  else if APrefix = 'some' then
    ANamespaceURI := 'http://someurl'
end;

使用这个查找函数和 selectNode 函数(看起来像是我曾经在 Delphi 论坛上发布过的东西,取自 https://github.com/Midiar/adomxmldom/blob/master/xmldocxpath.pas),我可以进行以下测试(在字符串常量中使用您的 xml):

procedure TTestXmlUtil.SetUp;
begin
  inherited;
  DefaultDOMVendor := sAdom4XmlVendor;
  docFull := LoadXmlData(csSoapXml);

  OnOx4XPathLookupNamespaceURI := EventLookupNamespaceURI;
end;

procedure TTestXmlUtil.Test_selectNode;
var
  xn: IXmlNode;
begin
  xn := selectNode(docFull.DocumentElement, '/soap:Envelope/soap:Body/some:SomeResponse/some:SomeResult');
  CheckNotNull(xn, 'selectNode returned nil');
end;

我不得不针对默认命名空间稍微修改您的 XPath 查询。

【讨论】:

  • 看起来很有希望,好像这是合适的解决方案。然而,Whosrdaddy 的上述解决方案更简单、更清洁。
【解决方案3】:

正如其他人所指出的,不同的供应商处理命名空间的方式不同。 这是一个使用 MSXML(Windows 默认)DOMVendor 的示例:(我确实意识到这并不是 OP 所要求的,但我觉得值得记录)

XML:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SomeResponse xmlns="http://someurl">
      <SomeResult>
        Some result here
      </SomeResult>
    </SomeResponse>
  </soap:Body>
</soap:Envelope>

选择代码(为了完整性)

// From a post in Embarcadero's Delphi XML forum.
function selectNode(xnRoot: IXmlNode; const nodePath: WideString): IXmlNode;
var
  intfSelect : IDomNodeSelect;
  dnResult : IDomNode;
  intfDocAccess : IXmlDocumentAccess;
  doc: TXmlDocument;
begin
  Result := nil;
  if not Assigned(xnRoot) or not Supports(xnRoot.DOMNode, IDomNodeSelect, intfSelect) then
    Exit;
  dnResult := intfSelect.selectNode(nodePath);
  if Assigned(dnResult) then
  begin
    if Supports(xnRoot.OwnerDocument, IXmlDocumentAccess, intfDocAccess) then
      doc := intfDocAccess.DocumentObject
    else
      doc := nil;
    Result := TXmlNode.Create(dnResult, nil, doc);
  end;
end;

XML 搜索命名空间的实际设置:

uses Winapi.MSXMLIntf; // NOTE: Use this version of the interface. MSXML2_TLB won't work.
...
procedure TForm1.DoExampleSearch;
var fnd:IXmlNode;
    doc:IXmlDomDocument2;
    msdoc:TMSDOMDocument;
const searchnames = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '+
                    'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '+
                    'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" '+
                    'xmlns:some="http://someurl"';

begin
  if Xmldocument1.DOMDocument is TMSDOMDocument then
  begin
    msdoc:=Xmldocument1.DOMDocument as TMSDOMDocument;
    doc:=(msdoc.MSDocument as IXMLDOMDocument2);
    doc.setProperty('SelectionLanguage', 'XPath');
    doc.setProperty('SelectionNamespaces',searchNames);
  end;
  fnd:=selectNode(XmlDocument1.DocumentElement,'/soap:Envelope/soap:Body/some:SomeResponse/some:SomeResult');
  if (fnd=nil) then showmessage('Not found') else showmessage('Found: '+fnd.Text);
end;

值得注意的几件事:一旦您将命名空间添加到混合中,Xpath 似乎在所有事情上都坚持使用它们。请注意,我为搜索条件添加了一个“某些”命名空间,因为 SomResult 从其父级继承了它,而且我还没有让 XPath 隐式处理默认命名空间。

【讨论】:

    【解决方案4】:

    一种解决方案是在开始处理 XML 之前删除所有命名空间:

    class function TXMLHelper.RemoveNameSpaces(XMLString: String): String;
    const
      // An XSLT script for removing the namespaces from any document.
      // From http://wiki.tei-c.org/index.php/Remove-Namespaces.xsl
      cRemoveNSTransform =
        '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' +
        '<xsl:output method="xml" encoding="utf-8"/>' +
    
        '<xsl:template match="/|comment()|processing-instruction()">' +
        '    <xsl:copy>' +
        '      <xsl:apply-templates/>' +
        '    </xsl:copy>' +
        '</xsl:template>' +
    
        '<xsl:template match="*">' +
        '    <xsl:element name="{local-name()}">' +
        '      <xsl:apply-templates select="@*|node()"/>' +
        '    </xsl:element>' +
        '</xsl:template>' +
    
        '<xsl:template match="@*">' +
        '    <xsl:attribute name="{local-name()}">' +
        '      <xsl:value-of select="."/>' +
        '    </xsl:attribute>' +
        '</xsl:template>' +
    
        '</xsl:stylesheet>';
    
    var
      Doc, XSL, Res: IXMLDocument;
      UTF8: UTF8String;
    begin
       try
         Doc := LoadXMLData(XMLString);
         XSL := LoadXMLData(cRemoveNSTransform);
         Res := NewXMLDocument;
         Doc.Node.TransformNode(XSL.Node,Res);  // Param types IXMLNode, IXMLDocument
         Res.SaveToXML(Utf8);      // This ensures that the encoding remains utf-8
         Result := String(UTF8);
       except
         on E:Exception do Result := E.Message;
       end;
    end; { RemoveNameSpaces }
    

    TXMLHelper 是一个辅助类,我有一些有用的 XML 处理函数)

    【讨论】:

    • 暂时离开我的电脑,很快就会尝试,但似乎是一个奇怪的解决方法。
    • 是的,但就我而言,在处理传入的 XML 时根本不需要命名空间。它甚至简化了解析/调试/日志记录,因为所有“混乱”都消失了。
    【解决方案5】:

    OmniXML 解决方案:

    我绝对可以确认 OmniXML XPath 本身不支持命名空间。

    但是:

    由于它将节点名视为文字,因此“soap:Envelope”将在查询中工作,只要 xml 文档中的名称为 soap:Envelope。 因此,在 OP 示例中,OmniXML 搜索路径 '/soap:Envelope/soap:Body/SomeResponse/SomeResult' 会起作用。

    请注意,您绝对不能依赖继承或默认命名空间,OmniXML 匹配文字节点名。

    您可以相当轻松地实现一个循环来删除或规范化文档中的所有命名空间标签,而无需太多努力。

    【讨论】:

      猜你喜欢
      • 2013-11-29
      • 2011-04-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-08
      • 1970-01-01
      • 2015-03-25
      • 2011-02-01
      相关资源
      最近更新 更多