在iPhone上使用NSXMLParser解析HTML实体

17

我认为我已经阅读了与这个问题有关的每一个网页,但我仍然找不到解决方案,所以我来到这里。

我有一个HTML网页不在我的控制之下,我需要从我的iPhone应用程序中解析它。下面是我所说的网页的示例:

<HTML>
  <HEAD>
    <META http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
  </HEAD>
  <BODY>
    <LI class="bye bye" rel="hello 1">
      <H5 class="onlytext">
        <A name="morning_part">morning</A>
      </H5>
      <DIV class="mydiv">
        <SPAN class="myclass">something about you</SPAN> 
        <SPAN class="anotherclass">
          <A href="http://www.google.it">Bye Bye &egrave; un saluto</A>
        </SPAN>
      </DIV>
    </LI>
  </BODY>
</HTML>

我正在使用NSXMLParser,一切进展顺利,直到遇到è HTML实体。它调用foundCharacters:方法处理“Bye Bye”字符串,然后调用resolveExternalEntityName:systemID::方法,传入一个实体名称为“egrave”的参数。

在这个方法中,我只返回由字符“è”转换而成的NSData,然后foundCharacters方法会再次被调用,将字符串“è”添加到之前的“Bye Bye ”字符串中,然后解析器引发NSXMLParserUndeclaredEntityError错误。

我没有DTD,并且无法更改正在解析的HTML文件。您有关于此问题的任何想法吗?

更新(12/03/2010)。在Griffo的建议下,我最终得出以下解决方案:

data = [self replaceHtmlEntities:data];
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser parse];

其中 replaceHtmlEntities:(NSData *) 大致如下:

- (NSData *)replaceHtmlEntities:(NSData *)data {
    
    NSString *htmlCode = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding];
    NSMutableString *temp = [NSMutableString stringWithString:htmlCode];
    
    [temp replaceOccurrencesOfString:@"&amp;" withString:@"&" options:NSLiteralSearch range:NSMakeRange(0, [temp length])];
    [temp replaceOccurrencesOfString:@"&nbsp;" withString:@" " options:NSLiteralSearch range:NSMakeRange(0, [temp length])];
    ...
    [temp replaceOccurrencesOfString:@"&Agrave;" withString:@"À" options:NSLiteralSearch range:NSMakeRange(0, [temp length])];

    NSData *finalData = [temp dataUsingEncoding:NSISOLatin1StringEncoding];
    return finalData;
    
}

但我仍在寻找解决此问题的最佳方法。我将在接下来的几天尝试TouchXml,但我仍然认为应该有一种使用NSXMLParser API来完成这个问题的方法。如果您知道如何做,请随意在这里写出来。


PS:我知道NSXMLParser是一种XML解析器而不是HTML解析器,但我读到libxml2存在同样的问题。NSXMLParser似乎比libxml2更容易学习,所以我首先尝试了这个,希望它能正常工作。如果没有解决方案,那么我将不得不转向libxml2... - Roberto
如Griffo所建议的,我用适当的字符替换了文本中的每个HTML实体,然后使用NSXMLParser解析它。现在它可以工作了,但我真的很想知道解决这种问题的更好方法是什么。 - Roberto
我注意到在处理多个“foundCharacters”调用时,使用&实体表示和号字符“&”会很麻烦。 - William T. Mallard
6个回答

10

经过探索多种替代方案,发现NSXMLParser将不支持除标准实体&lt;, &gt;, &apos;, &quot;和&amp;之外的实体。

下面的代码会导致NSXMLParserUndeclaredEntityError错误。


// Create a dictionary to hold the entities and NSString equivalents
// A complete list of entities and unicode values is described in the HTML DTD
// which is available for download http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent


NSDictionary *entityMap = [NSDictionary dictionaryWithObjectsAndKeys: 
                     [NSString stringWithFormat:@"%C", 0x00E8], @"egrave",
                     [NSString stringWithFormat:@"%C", 0x00E0], @"agrave", 
                     ...
                     ,nil];

NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser setShouldResolveExternalEntities:YES];
[parser parse];

// NSXMLParser delegate method
- (NSData *)parser:(NSXMLParser *)parser resolveExternalEntityName:(NSString *)entityName systemID:(NSString *)systemID {
    return [[entityMap objectForKey:entityName] dataUsingEncoding: NSUTF8StringEncoding];
}

尝试通过在HTML文档前添加实体声明来声明实体将会成功,但是扩展的实体不会传递回到parser:foundCharacters,并且字符è和à会被删除。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
[
  <!ENTITY agrave "à">
  <!ENTITY egrave "è">
]>

在另一个实验中,我创建了一个带有内部DTD的完全有效的XML文档。
<?xml version="1.0" standalone="yes" ?>
<!DOCTYPE author [
    <!ELEMENT author (#PCDATA)>
    <!ENTITY js "Jo Smith">
]>
<author>&lt; &js; &gt;</author>

我实现了 parser:foundInternalEntityDeclarationWithName:value:; 代理方法,清楚地看到解析器获取了实体数据,但是 parser:foundCharacters 只对预定义的实体调用。
2010-03-20 12:53:59.871 xmlParsing[1012:207] Parser Did Start Document
2010-03-20 12:53:59.873 xmlParsing[1012:207] Parser foundElementDeclarationWithName: author model: 
2010-03-20 12:53:59.873 xmlParsing[1012:207] Parser foundInternalEntityDeclarationWithName: js value: Jo Smith
2010-03-20 12:53:59.874 xmlParsing[1012:207] didStartElement: author type: (null)
2010-03-20 12:53:59.875 xmlParsing[1012:207] parser foundCharacters Before: 
2010-03-20 12:53:59.875 xmlParsing[1012:207] parser foundCharacters After: <
2010-03-20 12:53:59.876 xmlParsing[1012:207] parser foundCharacters Before: <
2010-03-20 12:53:59.876 xmlParsing[1012:207] parser foundCharacters After: < 
2010-03-20 12:53:59.877 xmlParsing[1012:207] parser foundCharacters Before: < 
2010-03-20 12:53:59.878 xmlParsing[1012:207] parser foundCharacters After: <  
2010-03-20 12:53:59.879 xmlParsing[1012:207] parser foundCharacters Before: <  
2010-03-20 12:53:59.879 xmlParsing[1012:207] parser foundCharacters After: <  >
2010-03-20 12:53:59.880 xmlParsing[1012:207] didEndElement: author with content: <  >
2010-03-20 12:53:59.880 xmlParsing[1012:207] Parser Did End Document

我发现了一个关于使用LibXML SAX接口的教程的链接Using the SAX Interface of LibXML。被NSXMLParser使用的xmlSAXHandler允许定义getEntity回调函数。在调用getEntity之后,实体的展开会传递给characters回调函数。 NSXMLParser在此处缺少功能。应该发生的是NSXMLParser或其delegate存储实体定义并将它们提供给xmlSAXHandler中的getEntity回调函数。显然这并没有发生。我将提交一个错误报告。
同时,如果您的文档很小,先前的字符串替换答案完全可行。请查看上述提到的SAX教程以及来自Apple的XMLPerformance示例应用程序,以确定是否值得自己实现libxml解析器。
这很有趣。

:( 这个没有起作用。它继续引发 NSXMLParserUndeclaredEntityError = 26 的错误。 :( 我使用了你的代码。它进入了 resolveExternalEntityName 方法,然后引发了异常... - Roberto
你能包含URL吗?我有另一个理论想要测试。 - falconcreek
仍在寻找解决方案。找到了一个可能的答案http://www.cocoabuilder.com/archive/cocoa/218098-nsxmlparser-and-character-entities.html,但它使用的NSAttributedString在当前的iPhone OS上不可用。 - falconcreek
哎呀 :(( 与此同时,我尝试了TouchXml并阅读了其他解析器的相关信息...但似乎这是你应该自己完成的任务。:\ - Roberto
哇!你的回答真的很完整!你真的把所有的东西都放在了这里,我感谢你。 解释得非常好。所以故事的结局就是NSXMLParser很糟糕 :) - Roberto

2

一种可能更好的解决方案是用一个本地修改过的DTD替换所有外部实体声明。

这是我的做法:

首先,找到并用本地文件替换文档的DTD声明。例如,将其替换为:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html><body><a href='a.html'>hi!</a><br><p>Hello</p></body></html>

使用这个:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "file://localhost/Users/siuying/Library/Application%20Support/iPhone%20Simulator/6.1/Applications/17065C0F-6754-4AD0-A1EA-9373F6476F8F/App.app/xhtml1-transitional.dtd">
<html><body><a href='a.html'>hi!</a><br><p>Hello</p></body></html>

```

从W3C URL下载DTD并将其添加到应用程序包中。 您可以使用以下代码查找文件的路径:

NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSString* path = [[bundle URLForResource:@"xhtml1-transitional" withExtension:@"dtd"] absoluteString];

打开DTD文件,查找任何外部实体引用:
<!ENTITY % HTMLlat1 PUBLIC
   "-//W3C//ENTITIES Latin 1 for XHTML//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent">
%HTMLlat1;      
用实体文件的内容替换它(在上面的例子中为http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent)。
在替换所有外部引用后,NSXMLParser应该能够正确处理实体,而无需每次解析XML文件时下载每个远程DTD / 外部实体。

0

自从我刚开始做iOS开发以来,我一直在寻找同样的东西,并找到了一个相关的邮件列表条目: http://www.mail-archive.com/cocoa-dev@lists.apple.com/msg17706.html

- (NSData *)parser:(NSXMLParser *)parser resolveExternalEntityName: (NSString *)entityName systemID:(NSString *)systemID {       
    NSAttributedString *entityString = [[[NSAttributedString alloc] initWithHTML:[[NSString stringWithFormat:@"&%@;", entityName] dataUsingEncoding:NSUTF8StringEncoding] documentAttributes:NULL] autorelease];

    NSLog(@"resolved entity name: %@", [entityString string]);

    return [[entityString string] dataUsingEncoding:NSUTF8StringEncoding];
}

这与您原来的解决方案非常相似,也会导致解析器错误NSXMLParserErrorDomain error 26;但它在此之后继续解析。问题是,当然,更难以区分真正的错误 ;-)


0

在使用NSXMLParser解析数据之前,您可以对数据进行字符串替换。据我所知,NSXMLParser仅支持UTF-8编码。


是的,我刚在考虑这个问题,但我并不能认为这是一个真正的解决方案...因为有一个名为resolveExternalEntityName:systemID的方法,文档中说:“代理可以解析外部实体(例如,定位和读取外部声明的DTD),并将结果作为NSData对象提供给解析器对象。”因此,应该有一种方法来使用它来解析实体并将其翻译给解析器...可能我在NSXMLParser的逻辑上漏掉了什么... - Roberto
但我看到 NSXMLDocument 在 iPhone 开发中不可用,这是真的吗? - Roberto
NSXMLDocument在TouchXML中可用。请参见此处:http://code.google.com/p/touchcode/wiki/TouchXML - Hoang Pham
谢谢,我一定会尝试的。 但是我仍然在考虑如何使用SDK代码正确处理这种情况... - Roberto

0

我认为您将在此示例中遇到另一个问题,因为它不是有效的XML,而NSXMLParser正在查找的正是XML。

上述问题确切在于标签META、LI、HTML和BODY没有关闭,因此解析器一直查找其结束标记。

我所知道的唯一解决方法是,如果您无法更改HTML,则使用插入了闭合标签的镜像。


抱歉...示例中的HTML代码只是文件的第一部分。那是我的错。该文件已经正确关闭了每个标签。 - Roberto

0

我建议尝试使用不同的解析器,比如libxml2 - 理论上来说,它应该能够处理较差的HTML。


1
我看到libxml2有一个HTML解析器,但是我找不到关于它的教程、文档或示例,这就是为什么我首先尝试使用NSXMLParser的原因。 - Roberto

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