Event-Driven XML Programming Guide--事件驱动解析XML

来源:互联网 发布:scp默认端口 编辑:程序博客网 时间:2024/05/04 11:34

转自http://supershll.blog.163.com/blog/static/37070436201251393157364/



Event-Driven XML Programming Guide

Cocoa提供了NSXMLParser类使用事件驱动解析XML文档和使用DTD声明的文档。他把其发现的东西报告给代理,让代理去处理数据。这个文档解释了如何使用NSXMLParser。

1,有两种方法可以解析和处理XML,每种都有自己的API:
1)基于树的API:包括DOM。
2)事件驱动API:SAX是最熟知的例子。也叫作流解析(streaming parser)

NSXMLParser采用了event-driven。但是取代回调,NSXMLParser是发送消息给其代理,随着parser遇到不同的item--包括element,attribute,declaration,entity reference,等等--他本身不对这些item做任何事情。

假设我们有下面的这个XML文件:
<?xml version= "1.0" encoding="UTF8">
<article author="John Doe">
    <para>This is a very short article.</para>
</article>

parser会报告下面的一系列事件给代理:
1)开始解析文档
2)找到 article 元素的开始标签
3)找到article元素的 author 属性,value为“John Doe"
4)找到para 元素的开始标签
5)找到characters "This is a very short article."
6) 找到 para元素的结束标签
7)找到article 元素的结束标签
8)结束解析文档

无论是基于树的还是基于事件驱动的解析都有优点和缺点。建立一个内存树需要可以想像得到的内存,尤其是大文档。
如果你只考虑性能而不修改,那么使用事件驱动是不错的选择。事件驱动解析不适合修改和扩展数据和写回文件。NSXMLParser还不支持验证文档结构是否遵循DTD或其他schema指定的规则。所有这些你需要一个DOM。当然,你也可以使用NSXMLParser构建你自己的树。

NSXMLParser验证XML或DTD是否结构良好。例如,他检查一个开始标签是否匹配有一个结束标签,或者一个属性是否有指定值。如果他遇到这些语法错误,他将停止解析并通知代理。

虽然parser只理解XML和DTD标记语言,但是它也能解析任何基于XML语言的Schema,例如RELAX NG和XML Schema。

2,使用NSXMLParser解析XML文档,你需要做:
1)定位XML:下面显示了从文件系统浏览器选择一个XML文件的代码:

Listing 1  Opening an XML file
- (void)openXMLFile {
    NSArray *fileTypes = [NSArray arrayWithObject:@"xml"];
    NSOpenPanel *oPanel = [NSOpenPanel openPanel];
    NSString *startingDir = [[NSUserDefaults standardUserDefaults] objectForKey:@"StartingDirectory"];
    if (!startingDir)
        startingDir = NSHomeDirectory();
    [oPanel setAllowsMultipleSelection:NO];
    [oPanel beginSheetForDirectory:startingDir file:nil types:fileTypes
      modalForWindow:[self window] modalDelegate:self
      didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:)
      contextInfo:nil];
}

 

- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
    NSString *pathToFile = nil;
    if (returnCode == NSOKButton) {
        pathToFile = [[[sheet filenames] objectAtIndex:0] copy];
    }
    if (pathToFile) {
        NSString *startingDir = [pathToFile stringByDeletingLastPathComponent];
        [[NSUserDefaults standardUserDefaults] setObject:startingDir forKey:@"StartingDirectory"];
        [self parseXMLFile:pathToFile];
    }
}

你可能从其他对象,如property-list(例如NSDictionary)或网络比特流接收到XML,在这些情况下,你需要首先转换XML为NSData。

2)创建并初始化NSXMLParser的实例,确保设备一个代理。如下所示:

Listing 2  Creating and initializing a NSXMLParser instance
- (void)parseXMLFile:(NSString *)pathToFile {
    BOOL success;
    NSURL *xmlURL = [NSURL fileURLWithPath:pathToFile];
    if (addressParser) // addressParser is an NSXMLParser instance variable
        [addressParser release];
    addressParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
    [addressParser setDelegate:self];
    [addressParser setShouldResolveExternalEntities:YES];
    success = [addressParser parse]; // return value not used
                // if not successful, delegate is informed of error
}

还可以使用NSData进行初始化:
addressParser = [[NSXMLParser alloc] initWithData:xmlData];

3)实现代理方法:(可以只实现那些你感兴趣的方法)
所有的解析操作从parserDidStartDocument:开始,以parserDidEndDocument:结束

4)处理任何解析错误:
如果parser遇到错误,它会停止解析并调用代理方法 parser:parseErrorOccurred:

在你解析XML时,内存管理变得非常重要。解析XML通常要求你创建很多对象;你应该不要让这些对象不再有用时累积到内存中。一种技术是在解析数据开始时创建一个自动释放池,结束时释放他。


3,处理XML元素和属性:
一般你解析XML文档,更多的是处理元素和元素相关的东西,如属性和其文本内容。发送的消息至少有3种:
1)parser:didStartElement:namespaceURI:qualifiedName:attributes:
2)parser:foundCharacters:
3)parser:didEndElement:namespaceURI:qualifiedName:

parser可能对一个element多次发送parser:foundCharacters:消息;然而,如果characters只由空白字符组成(空格,新行,tab,和类似字符),parser会发送
parser:foundIgnorableWhitespace:消息。

当你解析XML元素时,一个高级技巧是使用多个代理,每个代理知道如何处理特定类型的元素。

4,设计要素:
在面向对象的环境中,一个通常的策略是map them to objects。根元素和其他顶级元素经常等价于NSDictionary和NSArray。其他元素可能为一个或多个model对象。

然而,不是所有的元素都能很好的用对象表示。一些低级和特殊的”叶"元素更多的是其父节点逻辑上的属性。你应该让这些属性成为model对象的属性。

5,处理一个元素的例子:
XML文件内容如下:

Listing 1  Some of the sample XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE addresses SYSTEM "addresses.dtd">
<addresses owner="swilson">
    <person>
        <lastName>Doe</lastName>
        <firstName>John</firstName>
        <phone location="mobile">(201) 345-6789</phone>
        <email>jdoe@foo.com</email>
        <address>
            <street>100 Main Street</street>
            <city>Somewhere</city>
            <state>New Jersey</state>
            <zip>07670</zip>
        </address>
    </person>
    <!-- more person elements go here -->
</addresses>

让我们看一下这些元素的前3个元素是如何被处理的。

Listing 2  Implementing parser:didStartElement:namespaceURI:qualifiedName:attribute:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
    if ( [elementName isEqualToString:@"addresses"]) {
        // addresses is an NSMutableArray instance variable
       if (!addresses)
             addresses = [[NSMutableArray alloc] init];
        return;
    }
    if ( [elementName isEqualToString:@"person"] ) {
        // currentPerson is an ABPerson instance variable
        currentPerson = [[ABPerson alloc] init];
        return;
    }
    if ( [elementName isEqualToString:@"lastName"] ) {
        [self setCurrentProperty:kABLastNameProperty];
        return;
    }
    // .... continued for remaining elements ....
}

重要的动作是有一个方法来追踪当前元素。一个原因是parser:foundCharacters:可能被一个元素多次调用。在这个方法中,代理应该append相应的element的字符。如下所示:

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    if (!currentStringValue) {
        // currentStringValue is an NSMutableString instance variable
        currentStringValue = [[NSMutableString alloc] initWithCapacity:50];
    }
    [currentStringValue appendString:string];
}

如果parser遇到一些空白字符组成的内容,他发送 parser:foundIgnorableWhitespace:来给代理机会保留任何空白字符。
最终,parser遇到元素的结尾,他发送 parser:didEndElement:namespaceURI:qualifiedName: 消息给代理。如下所示:

Listing 4  Implementing parser:didEndElement:namespaceURI:qualifiedName:
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
    // ignore root and empty elements
    if (( [elementName isEqualToString:@"addresses"]) ||
        ( [elementName isEqualToString:@"address"] )) return;
    if ( [elementName isEqualToString:@"person"] ) {
        // addresses and currentPerson are instance variables
        [addresses addObject:currentPerson];
        [currentPerson release];
        return;
    }
    NSString *prop = [self currentProperty];
    // ... here ABMultiValue objects are dealt with ...
    if (( [prop isEqualToString:kABLastNameProperty] ) ||
        ( [prop isEqualToString:kABFirstNameProperty] )) {
        [currentPerson setValue:(id)currentStringValue forProperty:prop];
    }
    // currentStringValue is an instance variable
    [currentStringValue release];
    currentStringValue = nil;
}


6,处理属性:
仍然是在parser:didStartElement:namespaceURI:qualifiedName:attributes: 这个函数中,attributes:参数是一个NSDictionary,查询其相应的key来获得属性值。
如下所示:

Listing 5  Handling an attribute of an element
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
    if ( [elementName isEqualToString:@"addresses"]) {
        // addresses is an NSMutableArray instance variable
        if (!addresses)
            addresses = [[NSMutableArray alloc] init];
        NSString *thisOwner = [attributeDict objectForKey:@"owner"];
        if (thisOwner)
            [self setOwner:thisOwner forAddresses:addresses];
        return;
    // ... continued ...
}}


7,处理解析错误
当parser遇到语法错误或其他错误,它停止解析并发送
parser:parseErrorOccurred:消息给代理。在实现方法中,应该显示给用户错误原因。解析错误是致命的(不可恢复的),所以通知用户是你实际上能做的所有事情。通过这个信息,用户可能修正XML文件。
下面显示了如何实现parser:parseErrorOccurred:函数:

Listing 1  Handling parsing errors
- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
    NSWindow *modWin = [self windowForSheet];
    if (!modWin) modWin = [NSApp mainWindow];
    NSAlert *parserAlert = [[NSAlert alloc] init];
    [parserAlert setMessageText:@"Parsing Error!"];
    [parserAlert setInformativeText:[NSString stringWithFormat:@"Error %i,
        Description: %@, Line: %i, Column: %i", [parseError code],
        [[parser parserError] localizedDescription], [parser lineNumber],
        [parser columnNumber]]];
    [parserAlert addButtonWithTitle:@"OK"];
    [parserAlert beginSheetModalForWindow:modWin modalDelegate:self
        didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
        contextInfo:nil];
    [parserAlert release];
}

 

- (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo { }

这段代码的关键是NSAlert的提示文本的建立。这个文本包括error code(一个NSXMLParserErrorenum常量),一个本地描述,一个line number和column。在例子中,代理从两个源头获取信息:从parser本身和NSError对象。从parser对象也可以获得NSError对象。

然而,NSError的默认的本地描述是基本的,你可能想提供你自己的描述。有时解析错误可能需要一个特定于应用程序的解释。要实现这个目的,你可以使用NSXMLParserError常量来决定如何显示错误信息。


8,使用多代理:
对于一些大型XML文档,只使用一个代理可能不是最好的选择。

作为一个例子,一个应用用它遇到的元素创建一个DOM-style tree。从根元素开始,一个元素创建一个子元素并且传递控制给他--通过将其设置为代理。子元素再创建其子元素(继续),每次都重置代理。如果一个元素没有子节点,或者它是一个复合元素,它accumulates(累积,积攒)为自己累积文本内容。最终,当解析器遇到元素的结束标签,元素设置代理给其父元素。下面显示了相关的实现代码:

Listing 1  Resetting the delegate for the next element
- (void)parser:(NSXMLParser *)parser
        didStartElement:(NSString *)elementName
        namespaceURI:(NSString *)namespaceURI
        qualifiedName:(NSString *)qualifiedName
        attributes:(NSDictionary *)attributeDict {
    // Element is a custom class for object representing element nodes
    // Creation of element sets child as delegate (see below)
    [self addChild:[Element elementWithName:elementName
        attributes:attributeDict parent:self children:nil parser:parser]];
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    [self appendString:string];
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
    Element *parent = [self parent];
    [parser setDelegate:parent]; // RESET DELEGATE TO PARENT
}

+ (id)elementWithName:(NSString *)elementName attributes:(NSDictionary *)attributes parent:(Element *)parent children:(NSArray *)children parser:(NSXMLParser *)parser {
    return [[[[self class] alloc] initWithName:elementName
        attributes:attributes parent:parent children:children
        parser:parser] autorelease];
}

- (id)initWithName:(NSString *)elementName attributes:(NSDictionary *)attributes parent:(id)parent children:(NSArray *)children parser:(NSXMLParser *)parser {
    self = [super init];
    if (self) {
         [self setName:elementName];
         if (attributes) {
               [self addAttributes:attributes];
         }
         [self setParent:parent];
         if (children) {
              [self addChildren:children];
         }
         [parser setDelegate:self]; // CHILD SET AS DELEGATE
    }
    return self;
}

另外一个管理多代理的技巧是在一个集合中如NSDictionary对象中,维持若干代理对象,每个都有其特定的role(规则)。这些对象应该知道在给定的上下文中 谁是他们的父或子元素,so能够在其当前元素解析完后,为下一个元素设置代理(使用合适的dictionary key)。

9,创建XML Tree 结构:
一般如果你希望添加或修改XML文档的内容,你必须创建一个静态树结构--能完全表示元素和文档其他的结构。如果你打算验证DTD(或其他语言的schema,那种预描述文档结构的)的XML文档结构,树表现形式也是关键。
当血多开发者想构建一个DOM-style树表示XML文档时,他们使用一个tree-based解析器,而不是流解析器如NSXMLParser。(然而,Tree-based解析引擎通常建立在流解析上)。尽管如此,这并不意味着你不能使用NSXMLParser创建树结构。虽然这篇文档并不深入探讨使用NSXMLParser创建XML树结构,但是他仍然是一个你可以选择的普遍方法。

注意:DOM(for Document Object Model)是W3C指定的一个模型标准来描述XML和HTML文档。它也定义了访问和操作这些对象的接口,这些对象代表了文档的元素和其相关的属性。下面讨论的程序并不只针对DOM,虽然他们很相似。

你可以表示任何XML文档为一个hierarchical(层次)tree --他的节点是元素显示了父子的关系。每个元素可以有1或多个子元素,并且除了根元素,每个元素都有且只有一个父元素。Tree通过一个root element(唯一没有父元素)定位,叶节点表示只包含有文本的元素,虽然他们也可以是混合元素或空元素。

例子,考虑下面的文档:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE addresses SYSTEM "addresses.dtd">
<addresses>
    <person idnum="0123">
        <lastName>Doe</lastName>
        <firstName>John</firstName>
        <phone location="mobile">(201) 345-6789</phone>
        <email>jdoe@foo.com</email>
        <address>
            <street>100 Main Street</street>
            <city>Somewhere</city>
            <state>New Jersey</state>
            <zip>07670</zip>
        </address>
    </person>
</addresses>

addresses 是根元素,person是第一个子元素,lastName,firstName,phone,email,address都是其子元素。street,city...又是address的子元素。

有几种办法可以通过使用NSXMLParser来构建XML文档的一个tree表现形式。这篇文章采用面向对象的方法--动态转换代理负责处理文档的元素,是一个递归。代码的结果是双链接列表的对象和对象数组;抽象的结果是一个文档的树表示形式。

构建树的步骤如下:
1) 创建一个类--其实例表示XML文档的一个元素。这个类应该定义元素的名字,和他的父和子的关系;他还应该封装相关的元素属性。作为一个速记符号,我们称这个类为MyElement。

2)From a top-level object in the application,加载一个XML文档,创建一个NSXMLParser实例,指定顶级对象为代理,并且开始解析文档。
3)解析器遇到文档的根元素,并发送 parser:didStartElement:namespaceURI:qualifiedName:attributes: 给代理。代理创建一个MyElement对象表示根元素并设置其父为nil。对象的创建和初始化的方法也设置其为NSXMLParser实例的新代理。
4)parser遇到下一个元素--根元素的第一个子元素--并且再次发送parser:didStartElement:namespaceURI:qualifiedName:attributes:消息给代理。现在的代理是刚刚创建的表示根元素的MyElement对象。它创建另外一个MyElement对象表示新元素(设置新对象为代理,并设置自身为其父)并且添加新对象到其子列表中。
5)新大吃收到下一个 parser:didStartElement:namespaceURI:qualifiedName:attributes: 消息,表示其第一个子元素,并且他创建它并添加它到它的子列表中。
6)这个递归持续下去,直到遇到叶元素--只包含文本,混合内容或空元素。 If there is mixed content the descent is not truly over since parser:didStartElement:namespaceURI:qualifiedName:attributes: is sent to the delegate even after it receives parser:foundCharacters: for the current element.(不会翻译,看不懂)。。处理取决于元素的类型:
a)如果它是一个空元素,处理跳过到下一个步骤(end-element tag)
b)如果当前元素节点只有文本,代理响应 parser:foundCharacters:消息--通过累积文本
c)如果是混合内容,代理首先处理文本--自从它收到通知它元素开始和元素结束标签的消息之后。一种处理这种情况的方法就是wrap the text in special text-element对象,并且插入这些(使用正确的顺序)到元素的子列表中。
7)最终,parser发送 parser:didEndElement:namespaceURI:qualifiedName: 给代理,通知其代理元素完成。代理设置新代理为其父并返回。
8)如果父有更多的子元素,解析器发送下一个 parser:didStartElement:namespaceURI:qualifiedName:attributes: 消息,父MyElement对象创建MyElement实例表示其下一个子,并添加新创建的对象到其子列表。如果父没有更多的子(就是他收到parser:didEndElement:namespaceURI:qualifiedName:消息)它将设置新代理给其父,并返回。
9)程序继续--直到整个XML文档被处理并且所有的树的分支都被构建。

节点树应该能够打印为XML代码。你的应用应该也实现一个算法--请求对象打印自身到正确的文档序列中。

10,验证技巧和技术:
11,使用NSXMLParser处理DTD声明:NSXMLParser报告DTD声明给其代理。如果schema语言是DTD,NSXMLParser帮助你获得你需要的数据,或者验证或者用于其他目的,例如强制修正等等,
NSXMLParser定义了6个代理方法--当其遇到DTD声明 in a internal or external source.这些方法的格式为:
parser:foundTypeDeclarationWithName:...
第三个参数和其他后续的参数取决于声明的类型。

parser:foundElementDeclarationWithName:model:
例如: <!ELEMENT dictionary (documentation?, suite+)>
... (这里不翻译了,用到再补充)

12,XML术语:
原创粉丝点击