当responseText包含有效的XML时,IXMLHttpRequest.responseXml为空且没有解析错误。

7

我正在从政府网站获取一些XML数据:

http://www.bankofcanada.ca/stats/assets/rates_rss/noon/en_all.xml

我正在使用下面这段相当简单的代码:

var
   szUrl: string;
   http: IXMLHTTPRequest;
begin
   szUrl := 'http://www.bankofcanada.ca/stats/assets/rates_rss/noon/en_all.xml';

   http := CoXMLHTTP60.Create;
   http.open('GET', szUrl, False, '', '');
   http.send(EmptyParam);

   Assert(http.Status = 200);

   Memo1.Lines.Add('HTTP/1.1 '+IntToStr(http.status)+' '+http.statusText);
   Memo1.Lines.Add(http.getAllResponseHeaders);
   Memo1.Lines.Add(http.responseText);

我不会展示返回的整个主体,但它确实在responseText中返回有效的XML:

HTTP/1.1 200 OK
Cache-Control: max-age=5
Connection: keep-alive
Connection: Transfer-Encoding
Date: Fri, 30 Mar 2012 14:50:50 GMT
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
Expires: Fri, 30 Mar 2012 14:50:55 GMT
Server: Apache/2.2.16 (Unix) PHP/5.3.3 mod_ssl/2.2.16 OpenSSL/1.0.0d mod_perl/2.0.4 Perl/v5.12.0
X-Powered-By: PHP/5.3.3


<?xml version="1.0" encoding="ISO-8859-1"?>
<rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns="http://purl.org/rss/1.0/"
    xmlns:cb="http://www.cbwiki.net/wiki/index.php/Specification_1.1"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:dcterms="http://purl.org/dc/terms/"
    xmlns:xsi="http://www.w3c.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.w3c.org/1999/02/22-rdf-syntax-ns#rdf.xsd">
    <channel rdf:about="http://www.bankofcanada.ca/stats/assets/rates_rss/noon/en_ALL.xml">
        <title xml:lang="en">Bank of Canada: Noon Foreign Exchange Rates</title>
        <link>http://www.bankofcanada.ca/rates/exchange/noon-rates-5-day/</link>

好的,很好,里面有有效的 xml。我知道它是有效的,因为...嗯,只需要看一下就行了。但我也知道它是有效的,通过解析它:

var
   ...
   szXml: WideString;
   doc: DOMDocument60;
begin
   ...
   szXml := http.responseText;
  
   doc.loadXML(szXml);
   Assert(doc.parseError.errorCode = 0);

   Memo1.Lines.Add('============parsed xml');
   Memo1.Lines.Add(doc.xml);

原来的 IXmlHttpRequest 包含一个 responseXml 属性。来自MSDN的解释如下:

表示已解析的响应实体主体。

如果响应实体主体不是有效的XML,则此属性返回DOMDocument,以便您访问错误。此属性不会返回IXMLDOMParseError本身,但可以从DOMDocument中访问它。

在我的情况下,responseXml属性存在,就像它应该的那样:

Assert(http.responseXml <> nil);

并且 responseText 没有解析错误:

doc := http.responseXml as DOMDocument60;
Assert(doc.parseError.errorCode = 0);

XML是有效的,因此应该存在。

但是当我查看http.responseXml文档对象时,它为空:

   Memo1.Lines.Add('============responseXml');
   Memo1.Lines.Add(doc.xml);

当IXMLHttpRequest(和IXMLServerHttpRequest)返回空的XML文档时,可能存在以下原因:

  • 存在XML文件
  • XML文件是有效的
  • 不存在解析错误

简单来说:

uses
    msxml2_tlb;

procedure TForm1.Button1Click(Sender: TObject);
var
    szUrl: string;
    http: IXMLHTTPRequest;
    doc: DOMDocument60;
begin
    szUrl := 'http://www.bankofcanada.ca/stats/assets/rates_rss/noon/en_all.xml';

    http := CoXMLHTTP60.Create; //or CoServerXmlHttpRequest.Create
    http.open('GET', szUrl, False, '', '');
    http.send(EmptyParam);

    Assert(http.Status = 200);

    doc := http.responseXml as DOMDocument60;
    Assert(doc.parseError.errorCode = 0);

    ShowMessage('"'+doc.xml+'"');
end;

我该如何让 XmlHttpRequest (更重要的是 ServerXMLHTTP60)的行为符合文档说明?

Delphi版本信息在涉及RTL和标准库的所有问题中都是关键。哪个版本? - Warren P
我没有使用 Delphi 的任何组件,但是使用的是 Delphi 5。 - Ian Boyd
相关:http://stackoverflow.com/questions/8925798/delphi-2007-ixmlhttprequest-time-out-issue - 注意Keepalive评论。尝试过了吗?另外,IE版本和MS XML版本是什么,因为这些在这些情况下也很重要。我相信MS XML的HTTP方法使用WinInet,它有一些有趣的错误,并且当您更新IE时会进行更新。 - Warren P
@WarrenP 我尝试了超时;它并没有改变结果(也不应该,因为我得到了有效的响应)。IE9,MSXML 6.0。如果您复制粘贴最终简化的8行版本,您是否会得到相同的行为? - Ian Boyd
我认为这与您使用DOMDocument60而不是http.responseText有关。 - Warren P
@WarrenP 我想你漏掉了一些东西;我想要使用document(而不是responseText - Ian Boyd
4个回答

4

I我找到了问题

我使用Fiddler将HTTP响应保存到文本文件中。然后,我可以修改响应文件,并指示Fiddler提供我的手工替代品,而不是访问原始网站。

enter image description here

经过3小时的摸索,我成功追踪到原始HTTP响应头中的问题:

HTTP/1.1 200 OK
Cache-Control: max-age=5
Connection: keep-alive
Connection: Transfer-Encoding
Date: Fri, 30 Mar 2012 14:50:50 GMT
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
Expires: Fri, 30 Mar 2012 14:50:55 GMT
Server: Apache/2.2.16 (Unix) PHP/5.3.3 mod_ssl/2.2.16 OpenSSL/1.0.0d mod_perl/2.0.4 Perl/v5.12.0
X-Powered-By: PHP/5.3.3

应该是:

HTTP/1.1 200 OK
Cache-Control: max-age=5
Connection: keep-alive
Connection: Transfer-Encoding
Date: Fri, 30 Mar 2012 14:50:50 GMT
Transfer-Encoding: chunked
Content-Type: text/xml; charset=UTF-8
Expires: Fri, 30 Mar 2012 14:50:55 GMT
Server: Apache/2.2.16 (Unix) PHP/5.3.3 mod_ssl/2.2.16 OpenSSL/1.0.0d mod_perl/2.0.4 Perl/v5.12.0
X-Powered-By: PHP/5.3.3

一旦我找到了问题,我就能够查找相关行为的文档:文档解释了这个问题

MSXML 6.0支持的MIME类型包括:

  • "text/xml"
  • "application/xml"
  • 或以 "+xml" 结尾的任何类型,例如 "application/rss+xml"

我正在获取的RSS源实际上是一个资源定义格式(RDF)源,其内容类型应该是:

application/rdf+xml

他们的使用方法:
text/html

这种行为存在很多问题。

我遇到的行为是有意设计的,尽管令人沮丧——因为没有简单的方法来确定responseXml是否“有效”。

  • responseXml对象将被赋值
  • parseError对象将被赋值
  • parseError.ErrorCode为零
  • responseXml.documentElement将为空

我的猜测是,由于不支持"text/html",底层的XML解析器甚至没有解析任何数据,这将使"errorCode"设置为零,但不会创建"documentElement"对象。如果"responseText"不为空,但"documentElement"为空,那么你就知道出了什么问题,可以进行测试。 - Remy Lebeau
1
该网站上的许多Noon feeds都将“Content-Type”设置为“text/html”,但是其中一些,如“en_USD.xml”和“en_MXN.xml”,则使用“application/xml”。 - Remy Lebeau
那该怎么办呢?通过一些字符串操作来黑客他们的XML吗? - Warren P
1
@WarrenP 我现在采用 if http.responseXml.documentElement = nil then result := GetXmlObjectFromXmlString(http.responseText) else result := http.responseXml; 的退化方案。虽然这不是完全通用的解决方案,因为有时候可能会出现没有 documentElement 的xml文档。但在这种情况下它已经足够好了,因为如果真的为空的话,那么他们肯定做了些傻瓜事。 - Ian Boyd

3
我遇到了与YouTube服务相同的问题。 responseXml对象取决于响应的内容类型/MIME。您可以检查响应的Content-Type,例如:如果http.getResponseHeader('Content-Type')包含text/xmlapplication/xml,那么只有在这种情况下才能引用http.responseXml,否则它将为空(请参阅MSDN的备注)。还要注意,出于安全原因,responseXml解析器验证功能始终处于关闭状态。
但是,无论响应中的内容类型是什么,http.responseText始终具有xml文本,因此您可以始终使用新实例的DOMDocument来加载xml,例如:
...
http := CoXMLHTTP60.Create; // or CoServerXmlHttpRequest.Create 
http.open('GET', szUrl, False, '', '');
http.send(EmptyParam);
Assert(http.Status = 200);

doc := CreateOleObject('Msxml2.DOMDocument.6.0') as DOMDocument60; 
doc.async := False;
doc.loadXML(http.responseText); // <- load XmlHttpRequest.responseText into DOMDocument60 and use it
Assert(doc.parseError.errorCode = 0);

// do useful things with doc object...

我不使用DOMDocument对象(严格来说,我也不使用XmlHttpRequest对象),因为一些蠢材企业认为阻止人们访问互联网是个好主意(即我必须配置代理 - 只能使用IServerXmlHttpRequest完成)。此外,你不能仅仅检查text/xml,还需要检查application/xmlanything/anything+xml(充满了我甚至不想关心的边缘情况)。 - Ian Boyd
2
仔细阅读我的回答。我在说,只有当响应明确设置为text/xml时,您才会拥有一个有效的responseXml.documentElement对象。我是在说不要依赖于responseXml,并且始终像第一个代码示例中那样使用responseTextDOMDocument。当响应内容类型不是text/xml时,IServerXmlHttpRequest的行为与XmlHttpRequest相同。 - kobik
我更倾向于让XmlHttpRequest处理响应XML文本的解析;否则,XML必须经过另一个编码循环(编码为UTF-16 BSTR,然后再次解析)。即使内容类型为“text/xml”,documentElement可能为空 - 这是一个陷阱。 - Ian Boyd
我也更喜欢那样。但这是有意为之的。 :/ - kobik

0

您正在从DOMDocument对象本身检索xml,但您应该从文档树中的第一个节点获取它,例如:

doc := http.responseXml as DOMDocument60; 
Assert(doc.parseError.errorCode = 0); 
ShowMessage('"' + doc.DocumentElement.childNodes.Item(0).xml + '"'); 

微软在DOMDocumentxml属性的文档中提供了自己的示例,展示了这种逻辑。


在这种情况下,documentElement 是空的。 - Ian Boyd
那么这就解释了为什么“xml”是空的——在“DOMDocument”中没有任何内容。 - Remy Lebeau
至少可以说它是具有欺骗性的:DOMDocument中什么都没有,但它却是有效的XML。 - Ian Boyd

0

这个在 Delphi XE 和 Delphi 7 中可以运行:

procedure TForm1.Button1Click(Sender: TObject);
var
    szUrl: string;
    http: IXMLHTTPRequest;
    doc: {$ifndef UNICODE}WideString{$else}string{$endif};
begin
    szUrl := 'http://www.bankofcanada.ca/stats/assets/rates_rss/noon/en_all.xml';

    http := CoXMLHTTP60.Create; //or CoServerXmlHttpRequest.Create
    http.open('GET', szUrl, False, '', '');
    http.setRequestHeader('Content-Type', 'text/xml;charset=UTF-8');
    http.send(EmptyParam);

    Assert(http.Status = 200);

    doc := UTF8Encode(http.responseText);

    Memo1.Lines.text := doc;
//  ShowMessage('"'+doc.xml+'"');
end;

希望它在Delphi 5中也能为您工作。当然,在非Unicode Delphi版本中,任何Unicode字符都将变成问号。

respoonseText 可以正常工作,但 responseXml 是空的(虽然没有解析错误)。 - Ian Boyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接