如何在具有名称空间前缀的TXMLDocument上使用XPath?

时间:2015-06-06 21:13:32

标签: xml delphi soap xpath

我收到了从第三方Web服务器收到的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);EOleExceptionReference to undeclared namespace prefix: 'soap'

时失败

如果节点名称具有名称空间前缀?

,如何使其工作

5 个答案:

答案 0 :(得分:2)

不要尝试在XPath查询中包含名称空间。 如果您想要的只是SomeResult节点的文本,那么您可以使用'// SomeResult'作为查询。由于某种原因,xmlns="http://someurl"父节点上的默认命名空间SomeResponse上的默认xml实现(msxml)barfs。但是,使用OmniXML作为DOMVendor(= Crossplatform并且从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.

答案 1 :(得分: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查询。

答案 2 :(得分:0)

一种解决方案可能是在开始处理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处理函数)

答案 3 :(得分:0)

正如其他人指出的那样,不同的供应商对命名空间的处理方式也不同。 这是一个使用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 :(得分:0)

OmniXML解决方案:

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

但是:

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

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

您可以很容易地实现一个循环,从而无需花费太多精力即可删除或规范化文档中的所有名称空间标签。