iOSでXMLをパースする #2 KissXML編

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回のTBXMLに引き続き、今回はKissXMLを使ってみたいと思います。

KissXML

KissXMLはTouchXMLというライブラリをベースに作られたDOMタイプのパーサです。TBXMLとの大きな違いはXPathをサポートしていることと読書可であることです。ライセンスは修正BSDライセンス(New BSD License)です。今回も前回同様パースしたXMLをNSDictionaryに変換するロジックで試します。では早速使ってみましょう。

KissXMLを使ってみよう!

ここからは以下の環境を前提に説明します。尚、作成するサンプルは弊社開発ブログのRSS(https://dev.classmethod.jp/feed/)を解析してNSDictionaryに格納するというものです。ちなみにデータ取得にはAFNetworkingを使用します。

Mac OS X 10.8 Moutain lion
Xcode 4.6.2
iOS SDK 6.1

サンプルプロジェクトのダウンロード

今回紹介するiOSアプリのソースコードをGitHubにあげてあるのでダウンロードしてください。
hirai-yuki/KissXMLSample

サンプルアプリを実行すると、デバッグエリアにNSDictionaryに変換されたXMLデータが表示されるかと思います。

必要なフレームワーク・ライブラリの設定

CocoaPodsを利用する場合

KissXMLはCcocoaPodsからもインストール可能です。CocoaPodsを利用する場合は、Podfileに以下のように記述します。

pod 'KissXML'

KissXMLを直接インポートする場合

KissXMLはGitHubにあるrobbiehanson/KissXMLからダウンロードできます。

KissXMLのインポート

ダウンロードした以下のファイルをプロジェクトにインポートします。

  • KissXML/
    • Additions/DDXMLElementAdditions
    • Categories/NSString+DDXML
    • DDXML.h
    • DDXMLDocument
    • DDXMLElement
    • DDXMLNode
    • Private/DDXMLPrivate.h

ソースからインポートするのはDDXML.hだけです。

libxml2.dylibを追加

KissXMLではlibxml2.dylibを使用するのでプロジェクトで使用するように設定してください。

プロジェクトの設定

プロジェクトナビゲータよりプロジェクトを選択し「Build Settings」を開き、以下の設定を変更しましょう。

項目 設定値
Other Linker Flags -lxml2
Header Search Paths /usr/include/libxml2

KissXMLの使い方

XMLの解析

KissXMLもTBXMLと同様インスタンスの生成と同時に解析をしてくれる模様。使用できるイニシャライザは以下の通り。

  • - (id)initWithXMLString:(NSString *)string options:(NSUInteger)mask error:(NSError **)error;
  • - (id)initWithData:(NSData *)data options:(NSUInteger)mask error:(NSError **)error;

使用例:ViewController.m

・・・

- (void)viewDidLoad
{
    ・・・
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSDate *startDate = [NSDate date];
        
        NSError *error = nil;
        
        DDXMLDocument *doc = [[DDXMLDocument alloc] initWithData:responseObject options:0 error:&error];
        if (!error) {
            NSDictionary *xml = [doc.rootElement convertDictionary];

            NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:startDate];
            NSLog(@"実行時間 : %lf (sec)\n%@", interval, xml);
        } else {
            NSLog(@"%@ %@", [error localizedDescription], [error userInfo]);
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"%@ %@", [error localizedDescription], [error userInfo]);
    }];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operation];
}

@end

属性・要素へのアクセス

TBXMLとは違い、属性・要素へのアクセスはインスタンスを介して行います。KissXMLでは要素にアクセスするために以下のクラスが用意されています。

  • DDXMLDocument
  • DDXMLElement
  • DDXMLNode

DDXMLDocumentとDDXMLElementはDDXMLNodeのサブクラスとして定義されているため、これにより柔軟な実装ができます。また、KissXMLではXPathをサポートしているため、以下のように簡単に要素にアクセスできます。

// 以下のXMLを読み込む
// <?xml version="1.0" encoding="utf-8"?>
// <items>
//     <item>
//         <title>hoge</title>
//     </item>
// </items>
DDXMLDocument *doc = [[DDXMLDocument alloc] initWithData:responseObject options:0 error:&error];

// /items/item/titleにアクセス
NSArray *nodes = [doc nodesForXPath:@"/items/item/title" error:nil];
DDXMLNode *node = nodes[0];

// /items/item/titleの値を出力
NSLog(@"/items/item/title: %@", node.stringValue); // <- hogeが出力される
[/c]

<h3>読み込んだXMLの変更</h3>

<p>
読み込んだXMLを変更するのも簡単です。以下のように直接値を代入するだけです。
</p>


// 以下のXMLを読み込む
// <?xml version="1.0" encoding="utf-8"?>
// <items>
//     <item>
//         <title>hoge</title>
//     </item>
// </items>
DDXMLDocument *doc = [[DDXMLDocument alloc] initWithData:responseObject options:0 error:&error];

// /items/item/titleにアクセス
NSArray *nodes = [doc nodesForXPath:@"/items/item/title" error:nil];
DDXMLNode *node = nodes[0];

// /items/item/titleの値を変更
node.stringValue = @"hogehoge";

// 以下のXMLが出力される
// <?xml version="1.0" encoding="utf-8"?>
// <items>
//     <item>
//         <title>hogehoge</title>
//     </item>
// </items>
NSLog(@"%@", doc.XMLString);

要素の追加は、DDXMLElementの- (void)addChild:(DDXMLNode *)childメソッドで行います。

// 以下のXMLを読み込む
// <?xml version="1.0" encoding="utf-8"?>
// <items>
//     <item>
//         <title>hoge</title>
//     </item>
// </items>
DDXMLDocument *doc = [[DDXMLDocument alloc] initWithData:responseObject options:0 error:&error];

// /items/testを追加
DDXMLElement *testElement = [DDXMLElement elementWithName:@"test"];
testElement.stringValue = @"テスト";
[doc.rootElement addChild:testElement];

// 以下のXMLが出力される
// <?xml version="1.0" encoding="utf-8"?>
// <items>
//     <item>
//         <title>hogehoge</title>
//     </item>
//     <test>テスト</test>
// </items>
NSLog(@"%@", doc.XMLString);

DDXMLElement+Dictionary

KissXMLで解析した結果をNSDictionaryに格納するカテゴリは以下の通りです。

#import "DDXMLElement+Dictionary.h"

NSString * const kTextNodeKey = @"text";

@implementation DDXMLElement (Dictionary)

- (NSDictionary *)convertDictionary
{
    NSMutableDictionary *elementDict = [NSMutableDictionary dictionary];
    
    // elementDictに属性をセット
    for (DDXMLNode *attribute in self.attributes) {
        elementDict[attribute.name] = attribute.stringValue;
    }
    
    // elementDictにネームスペースをセット
    for (DDXMLNode *namespace in self.namespaces) {
        elementDict[namespace.name] = namespace.stringValue;
    }
    
    if (self.childCount > 0) {
        // 子要素がある場合は子要素に対し再起的に+ dictionaryWithElement:メソッドを実行する。
        for (DDXMLNode *childNode in self.children) {
            if (childNode.kind == DDXMLElementKind) {
                DDXMLElement *childElement = (DDXMLElement *)childNode;
                
                NSString *childElementName = childElement.name;
                NSDictionary *childElementDict = [childElement convertDictionary];
                
                if (elementDict[childElementName] == nil) {
                    // elementDictにchildElementNameで指定された要素が存在しない場合、elementDictに要素を追加する
                    [elementDict addEntriesFromDictionary:childElementDict];
                } else if ([elementDict[childElementName] isKindOfClass:[NSArray class]]) {
                    // childElementNameで指定された要素が既存在しかつ配列の場合、その配列に子要素を追加する
                    NSMutableArray *items = [NSMutableArray arrayWithArray:elementDict[childElementName]];
                    [items addObject:childElementDict[childElementName]];
                    elementDict[childElementName] = [NSArray arrayWithArray:items];
                } else {
                    // childElementNameで指定された要素が既存在しかつ配列でない場合、新しく配列を生成して子要素を追加する
                    NSMutableArray *items = [NSMutableArray array];
                    [items addObject:elementDict[childElementName]];
                    [items addObject:childElementDict[childElementName]];
                    elementDict[childElementName] = [NSArray arrayWithArray:items];
                }
            } else if (childNode.stringValue != nil && childNode.stringValue.length > 0) {
                // テキストがあればセットする
                if (elementDict.count > 0) {
                    elementDict[kTextNodeKey] = childNode.stringValue;
                } else {
                    elementDict[self.name] = childNode.stringValue;
                }
            }
        }
    }
    
    NSDictionary *resultDict = nil;
    
    if (elementDict.count > 0) {
        if (elementDict[self.name]) {
            resultDict = [NSDictionary dictionaryWithDictionary:elementDict];
        } else {
            resultDict = [NSDictionary dictionaryWithObject:elementDict forKey:self.name];
        }
    }
    
    return resultDict;
}

@end

まとめ

個人的な感想ですが、要素に対応したクラスが用意されていることやXPathをサポートしていることからTBXMLよりいい感じです。次回はGDataXMLを使って解説します!
ちなみにKissXMLをNSDictionaryに変換する部分はDDXMLElement+Dictionary.hDDXMLElement+Dictionary.mになりますので、良かったら勝手に使ってください!