iOSでXMLをパースする #1 TBXML編

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

今回、案件でXMLを触る機会があったのでメモ。

Mac OS X用アプリの開発からObjective-Cに入った私にとって、
XMLをパースなんてNSXMLDocument使えばいいんでない?
と思っていましたが、iOSでは使えない模様。iOSでどうやっているのか調べたころ、結構たくさんありました。

そんなわけで今回から代表的な手法をピックアップして使い方などを比較してみようかと思います。
あと、これらを比較するにあたって、せっかくなので解析したXMLをNSDictionary(連想配列)にするロジックを作成しようと思います。面倒臭がりの方は使ってみてください(できれば実案件では横着せずにちゃんと解析しましょう!)。そして最後の回で実行時間や機能などから、大本命はどのライブラリかを考察していきたいと思います!

おさらい:XML解析のためのAPI

本題に入る前に、一般的なXML解析のためのAPIについて軽く触れておきます。

一般的に、XML解析のためのAPIとしてSAXDOMが利用されています。これらのAPIはそれぞれ以下のような特徴を持っています。

SAX

SAXはXMLデータを先頭から順に読み込み、要素の開始や要素の終わりを都度に通知する方法です。実装者はそれらの通知を受け取ったときの処理を定義しておき、適宜処理を行なっていきます。解析後の情報はメモリ上に保持しないので、扱うXMLデータの容量が大きい場合に効果を発揮します。

DOM

一方DOMは、XMLデータを一気に解析しメモリ上にDOMツリーとして保持します。解析後どの要素にもいつでもアクセスすることが可能です。DOMは、XMLデータの構造を変更したい場合に効果を有効です。

iOSのXMLパーサ

iOS開発で利用できるパーサの代表的なものとして以下のようなライブラリがあります。

iOS SDK標準のパーサ

NSXMLParser

Objective-Cで書かれているSAXタイプのパーサ
libxml2
C言語ベースのAPIで、でDOMとSAXの両方をサポートしている。

サードパーティ製のライブラリ

TBXML
メモリをできるだけ使用しないように設計された非常に軽量なDOMタイプのパーサ。XPathはサポートしていない。DOMタイプではあるけれど読取専用。
KissXML
TouchXMLというライブラリをベースに作られたDOMタイプのパーサ。読書可。
GDataXML
Googleが開発したDOMタイプのXMLパーサ。読書可。XPathクエリ対応

代表的なものではこの辺でしょうか。では早速使ってみましょう。今回はタイトル通りTBXMLを使って解説します。

TBXML

TBXMLは軽量かつ高速であることを売りにしているDOMタイプのパーサです。メモリの使用も極力抑えるように設計されています。しかし、その代償としてDOMタイプであるにも関わらず、XMLの生成や解析したXMLへの変更、XMLの検証などは行えません。なので、NSXMLParserlibxml2などのSAXタイプのパーサは使いにくくて嫌だけど、KissXMLGDataXMLほどの機能は必要ない!という方におすすめです。ライセンスはMITライセンスです。

TBXMLを使ってみよう!

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

Mac OS X 10.8 Moutain lion
Xcode 4.5.2
iOS SDK 6.0

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

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

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

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

TBXMLのダウンロード

TBXMLはGitHubにある71squared / TBXMLからダウンロードします。

TBXMLのインポート

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

  • TBXML.h
  • TBXML.m
  • TBXML+Compression.h(任意)
  • TBXML+Compression.m(任意)
  • TBXML+HTTP.h(任意)
  • TBXML+HTTP.m(任意)

TBXML+Compression.h/.mとTBXML+HTTP.h/.mは必要なければインポートする必要はありませんが、TBXML+Compression.h/.mをインポートする場合は、libz.dylibをインクルードする必要があります。

xxx-Prefix.pchの編集

ちょっと面倒ですが、xxx-Prefix.pchに以下のコードを追記する必要があります。追記しないとコンパイル時に怒られてしまいます。

#import <Availability.h>

#ifndef __IPHONE_5_0
#warning "This project uses features only available in iOS SDK 5.0 and later."
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>

    #if __has_feature(objc_arc) && __clang_major__ >= 3
    #define ARC_ENABLED 1
    #endif // __has_feature(objc_arc)
#endif

TBXMLの使い方

XMLの解析

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

  • - (id)initWithXMLString:(NSString*)aXMLString error:(NSError **)error;
  • - (id)initWithXMLData:(NSData*)aData error:(NSError **)error;
  • - (id)initWithXMLFile:(NSString*)aXMLFile error:(NSError **)error;
  • - (id)initWithXMLFile:(NSString*)aXMLFile fileExtension:(NSString*)aFileExtension error:(NSError **)error;

ViewController.m

・・・

- (void)viewDidLoad
{
    ・・・
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSDate *startDate = [NSDate date];
        
        NSError *error = nil;

        // レスポンスデータからTBXMLインスタンスを生成
        TBXML *tbxml = [[TBXML alloc] initWithXMLData:responseObject error:&error];
        
        if (!error) {
            NSDictionary *xml = [TBXML dictionaryWithElement:tbxml.rootXMLElement];
            
            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

属性・要素へのアクセス

最近のiOSライブラリではあまり見られませんが、構造体とクラスメソッドを駆使して属性・要素へのアクセスします。Objective-Cというより気持ちC言語よりではありますが、慣れてしまえば割と簡単に書けてしまうと思います。

#import "TBXML+Dictionary.h"

NSString * const kTextNodeKey = @"text";

@implementation TBXML (Dictionary)

+ (NSDictionary *)dictionaryWithElement:(TBXMLElement *)element
{
    NSMutableDictionary *elementDict = [NSMutableDictionary dictionary];
    
    NSString *elementName = [TBXML elementName:element];
    
    // elementDictに属性をセット
    TBXMLAttribute *attribute = element->firstAttribute;
    if (attribute) {
        while (attribute) {
            elementDict[[TBXML attributeName:attribute]] = [TBXML attributeValue:attribute];
            attribute = attribute->next;
        }
    }
    
    TBXMLElement *childElement = element->firstChild;
    if (childElement) {
        // 子要素がある場合は子要素に対し再起的に+ dictionaryWithElement:メソッドを実行する。
        while (childElement) {
            NSString *childElementName = [TBXML elementName:childElement];
            NSDictionary *childElementDict = [TBXML dictionaryWithElement:childElement];
            
            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];
            }
            
            childElement = childElement->nextSibling;
        }
    } else if ([TBXML textForElement:element] != nil && [TBXML textForElement:element].length > 0) {
        // テキストがあればセットする
        if (elementDict.count > 0) {
            elementDict[kTextNodeKey] = [TBXML textForElement:element];
        } else {
            elementDict[elementName] = [TBXML textForElement:element];
        }
    }
    
    NSDictionary *resultDict = nil;
    
    if (elementDict.count > 0) {
        if (elementDict[elementName]) {
            resultDict = [NSDictionary dictionaryWithDictionary:elementDict];
        } else {
            resultDict = [NSDictionary dictionaryWithObject:elementDict forKey:elementName];
        }
    }
    
    return resultDict;
}

@end

まとめ

まぁ他のやつを使ってみないと何とも言えませんが、XMLを読み込むだけであればとても有用なのではないでしょうか。次回は個人的に大本命であるKissXMLを使って解説します!
ちなみにTBXMLをNSDictionaryに変換する部分はTBXML+Dictionary.hTBXML+Dictionary.mになりますので、良かったら勝手に使ってください!