2011年6月3日金曜日

iOSでXMLを読み込む(Libxml2-DOM)

iOSでXMLをパースするには標準で、
  • NSXMLParser
  • Libxml2
を使う方法があるようです。外部ライブラリもある様ですが、そのへんは未調査です。

それぞれ一長一短で、NSXMLParserはSAXタイプのパーサです。Libxml2はSAXとDOMタイプの両方を提供しています。
個人的にSAXはフラグを多用するので好きじゃないのとSAXタイプの解析方法は色々と参考に出来るサイトが多々存在したので、Libxml2/DOMでの解析を試してみました。

Xcodeの開発環境にLibxml2を引き込む

標準で使えると書きましたが、標準でライブラリは存在するが、標準でライブラリは引きこまれていません。個別に引き込む設定を行う必要があります。
Libxml2ライブラリの引き込み方は以下の方法が一番簡単でした。

プロジェクトを選択して「Build Phases」の設定画面を出します。「Link Binary With Libraries」を選択します。
後は図にあるとおり操作します。
#実はこのやり方、後から気付きました。この後に書いてある「Add Files・・・」の方法よりもこっちの方が分かりやすいのではないかと思うのです。


この操作で問題なく出来ましたが、Xodeの「File」-「Add Files・・・」のメニューからも追加出来ました。この際は、以下ディレクトリのライブラリを指定しました。
/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.3.sdk/usr/lib/

次にヘッダの引き込みパスを追加します。
下の図に示すとおり、「Build Settings」の「Search Paths」にある「Header Search Paths」に以下のパスを追加します。
/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.3.sdk/usr/include/libxml2


これで準備は完了。

解析対象のXML

サンプルとして以下のようなXMLをパースしてみます。

<?xml version='1.0' encoding='UTF-8'?>
<root>
    <entryList>
        <entry id="1">information1</entry>
        <entry id="2">information2</entry>
        <entry id="3">information3</entry>
        <entry id="4">information4</entry>
        <entry id="5">information5</entry>
    </entryList>
</root>

実装

全体的な流れとしては、

1)XMLファイルの読み込みパース(xmlDocのオブジェクトの生成)
2)Contextオブジェクトの生成
3)XPath表現により該当ノード情報の取得

となります。

注意点としては、libxmlはC言語である事です。libxmlのライブラリ呼び出し部分は全てC言語で記述する必要がありますし、データ型もC言語の型になっているので、Objective-cというかiOS SDKのクラスとやりとりするには変換が必要です。ですが、やりとりする情報は文字列で、NSStringにはC言語のポインタからオブジェクト型へ、逆にオブジェクト型からC言語のポインタへの変換メソッドが提供されているので、苦労はありません。

1)XMLファイルの読み込みパース(xmlDocのオブジェクトの生成)

*XMLをパースして、必要なデータ修得するクラスを作成したので、そのクラスを例に書きます。以下、そのクラスのヘッダ。

#import <Foundation/Foundation.h>
#include <libxml/parser.h>
#include <libxml/xpath.h>

@interface XmlParser : NSObject {
    NSString* filename;
    xmlDocPtr document;
}

- (id) initWithFileName: (NSString*)name;
- (NSDictionary*) getElementsByXPath: (NSString*)xpath;
- (BOOL) parse;
- (void) dealloc;

@property (retain, nonatomic) NSString* filename;

@end

XMLファイルの読み込みとパース
- (BOOL) parse {
    if (self.filename == nil) {
        return NO;
    }
    //XMLをパース
    //self.filenameはNSString型ですが、C言語のchar*型に変換する必要がります。
    document = xmlParseFile([self.filename cStringUsingEncoding:NSUTF8StringEncoding]);
    if (document == nil) {
        return NO;
    }
    return YES;
}

もし、URLからXMLを取得し、そのメモリからXMLを読み込ますには以下のようなイメージになると思います。
NSURL *url = [NSURL URLWithString:@"http://hogehoge/hoge.xml"];
NSString *xml = [NSString stringWithContentsOfURL:
                                url encoding:NSUTF8StringEncoding error:nil];

document = xmlParseMemory([xml UTF8String],
                          [xml lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);

2)Contextオブジェクトの生成

このへんはお決まりなのだと思います。

- (NSDictionary*) getElementsByXPath:(NSString *)xpath {
    if (document == nil) {
        NSLog(@"document is nil.");
        return nil;
    }
    if (xpath == nil || [xpath length] == 0) {
        NSLog(@"xpath is nil or size zero.");
        return nil;
    }
    
    xmlXPathContextPtr context;
    xmlXPathObjectPtr result;
    
    //新しいcontextを作成
    context = xmlXPathNewContext(document);
    if (context == nil) {
        NSLog(@"xmlxpathnewContext error.");
        return nil;
    }

// 以下省略
// 以下省略
// 以下省略

3)XPath表現により該当ノード情報の取得

XPathという表現を使い、欲しいノードを指定します。XPathに関して詳細は、ここを参照。
今回解析するXMLの場合XPathとして"/root/entryList/entry"を指定します。
サンプルでは- (NSDictionary*) getElementsByXPath:(NSString *)xpath メソッドの引数としてXPathが受け渡されてくるイメージです。

//2)からのつづき
//2)からのつづき
//2)からのつづき

    //XPathをCスタイルのポインタへ変換
    xmlChar *_xpath = (xmlChar*)[xpath cStringUsingEncoding:NSUTF8StringEncoding];
    //XMLを解析し、該当XPathに該当するデータを取得
    result = xmlXPathEvalExpression(_xpath, context);

    //contextの解放
    xmlXPathFreeContext(context);

    if (result != nil) {
        NSMutableArray *keyArray = [[NSMutableArray alloc] init];
        NSMutableArray *valueArray = [[NSMutableArray alloc] init];
        
        //解析結果からnodesetを取得
        xmlNodeSetPtr nodeset = result->nodesetval;

        //nodeset->nodeNrにはXPathに該当する全ノードの個数が格納されている
        for (int i=0; inodeNr; i++) {
            //nodeset->nodeTabの配列にノードの情報が格納されている。
            //型はxmlNodePtr型です。以下の処理で、XPathで指定した子ノードである
            //entryタグで囲まれたテキストノードの内容が取得できます。
            xmlChar *element = xmlNodeListGetString(
                                     document, nodeset->nodeTab[i]->xmlChildrenNode, 1);
            //以下の処理でentryタグのアトリビュートの取得をします。
            xmlChar *attr = xmlGetProp(nodeset->nodeTab[i], (const xmlChar*)"id");

            //log
            NSLog(@"element : %s", element);
            NSLog(@"id      : %s", attr);
            
            //データ返却用に解析結果を配列に格納
            NSString *_key = [[NSString alloc] initWithUTF8String:(const char*)attr];
            NSString *_value = [[NSString alloc] initWithUTF8String:(const char*)element];
            [keyArray addObject: _key];
            [valueArray addObject: _value];

            //free xml char
            if (element) {
                xmlFree(element);
            }
            if (attr) {
                xmlFree(attr);
            }

            [_key release];
            [_value release];
        }
        //結果データの解放
        xmlXPathFreeObject(result);
        
        NSDictionary *dictionary = 
                    [NSDictionary dictionaryWithObjects:valueArray forKeys:keyArray];
        [valueArray release];
        [keyArray release];
        
        return dictionary;
    }
    return nil;
}

- (void) dealloc {
    //documentの解放
    if (document != nil) {
        xmlFreeDoc(document);
    }
    //パーサの解放
    xmlCleanupParser();
    
    [filename release];
    [super dealloc];
}


参考までに、xmlNode型の定義を以下に、tree.hに定義されていました。
typedef struct _xmlNode xmlNode;
 typedef xmlNode *xmlNodePtr;
 struct _xmlNode {
 void           *_private; /* application data */
    xmlElementType   type; /* type number, must be second ! */
    const xmlChar   *name;      /* the name of the node, or the entity */
    struct _xmlNode *children; /* parent->childs link */
    struct _xmlNode *last; /* last child link */
    struct _xmlNode *parent; /* child->parent link */
    struct _xmlNode *next; /* next sibling link  */
    struct _xmlNode *prev; /* previous sibling link  */
    struct _xmlDoc  *doc; /* the containing document */
    
    /* End of common part */
    xmlNs           *ns;        /* pointer to the associated namespace */
    xmlChar         *content;   /* the content */
    struct _xmlAttr *properties;/* properties list */
    xmlNs           *nsDef;     /* namespace definitions on this node */
    void            *psvi; /* for type/PSVI informations */
    unsigned short   line; /* line number */
    unsigned short   extra; /* extra data for XPath/XSLT */
};

    typedef enum {
        XML_ELEMENT_NODE=  1,
        XML_ATTRIBUTE_NODE=  2,
        XML_TEXT_NODE=  3,
        XML_CDATA_SECTION_NODE= 4,
        XML_ENTITY_REF_NODE= 5,
        XML_ENTITY_NODE=  6,
        XML_PI_NODE=  7,
        XML_COMMENT_NODE=  8,
        XML_DOCUMENT_NODE=  9,
        XML_DOCUMENT_TYPE_NODE= 10,
        XML_DOCUMENT_FRAG_NODE= 11,
        XML_NOTATION_NODE=  12,
        XML_HTML_DOCUMENT_NODE= 13,
        XML_DTD_NODE=  14,
        XML_ELEMENT_DECL=  15,
        XML_ATTRIBUTE_DECL=  16,
        XML_ENTITY_DECL=  17,
        XML_NAMESPACE_DECL=  18,
        XML_XINCLUDE_START=  19,
        XML_XINCLUDE_END=  20
#ifdef LIBXML_DOCB_ENABLED
        ,XML_DOCB_DOCUMENT_NODE= 21
#endif
    } xmlElementType;

上記のxmlNode構造体定義から分かるように、基本的にはxmlNode型の構造体が取得出来てしまえは、後はその親、子ノードの取得が可能なので、好きなようにノードを移動してデータ取得が出来ます。

例えば、
xmlChar *element = xmlNodeListGetString(
document, nodeset->nodeTab[i]->xmlChildrenNode, 1);

というコードは、以下とほぼ同等です。TEXTノードのテキストを取得します。

xmlChar *element = nil;
 if (nodeset->nodeTab[i]->children->type == XML_TEXT_NODE) {
    element = nodeset->nodeTab[i]->children->content;
}

また、アトリビュート値を取得している次のコードは、

xmlChar *attr = xmlGetProp(nodeset->nodeTab[i], (const xmlChar*)"id");

以下とほぼ同等です。プロパティのノードを取得して、属性名が"id"となっている値を取得しています。

xmlChar *attr = nil;
if (!xmlStrcmp(nodeset->nodeTab[i]->properties->name, (const xmlChar*)"id")) {
    attr = nodeset->nodeTab[i]->properties->children->content;
}

ちなみに、呼び出し元のコードは以下のようになりますが、XMLファイルを配置した絶対パスを取得するのにはまりました。
NSHomeDirectory()でアプリケーションのホームディレクトリディレクトリを取得してパスを求めることで解決しました。 ←間違い。こっち(Resource Bundle)を参照。
まあ、本当はXMLファイルをローカルに置いて使うような場面よりは、WEB上のXMLファイルを参照する場面の方が多いと思いますが。

//アプリケーションのホームディレクトリからファイルパスを求める。
    NSString * xmlFilePath = [NSHomeDirectory() stringByAppendingPathComponent:@"XmlParseTest.app/test.xml"];

    //ファイルを指定して初期化
    XmlParser* _parser = [[XmlParser alloc] initWithFileName: xmlFilePath];

    //パース実行とその結果判定
    if ([_parser parse] == NO) {
        //parse error.
        NSLog(@"parse error");
    }
    
    //XPathを指定して結果を取得
    myData = [_parser getElementsByXPath:@"/root/entryList/entry"];
    
    [_parser release];

まとめ
本当に一部ですみませんが、登場してきた関数をまとめますと、

xmlDocPtr xmlParseFile(const char * filename)
ファイルを読み込んでパースを行う
filename:ファイル名。マニュアルにはZIP圧縮ファイルでも大丈夫と記載がある(未検証)
戻り値:xmlDocPtr型。パースしたドキュメントのポインタが返却される。失敗した場合はNULLが返却される。
備考:パースに成功した場合は、XML操作後にxmlFreeDocを呼び出してメモリを解放する必要があります。
xmlDocPtr xmlParseMemory (const char * buffer, int size)
メモリからXMLを読み込んでパースします。
buffer:バッファへの先頭ポインタ。
size:データサイズ。
戻り値:xmlDocPtr型。パースしたドキュメントのポインタが返却される。失敗した場合はNULLが返却される。
備考:パースに成功した場合は、XML操作後にxmlFreeDocを呼び出してメモリを解放する必要があります。
void xmlFreeDoc (xmlDtdPtr cur)
ドキュメントを解放します。
cur:ドキュメントへのポインタ。
戻り値:なし。
備考
void xmlCleanupParser (void)
パースで使用したライブラリで使用しているメモリをクリーンします。
void:なし。
戻り値:なし。
備考
xmlXPathContextPtr xmlXPathNewContext (xmlDocPtr doc)
新しいXPathContextを作成します。
doc:documentへのポインタ。
戻り値:XPathContext構造体へのポインタ。
備考:戻り値のcontextは使用後に解放する必要があります。xmlXPathFreeContext参照。
xmlXPathObjectPtr xmlXPathEvalExpression (const xmlChar * str, xmlXPathContextPtr ctxt)
XPath表現を評価します。
str:XPath。
xmlXPathContextPtr:XPathContext構造体へのポインタ。
戻り値:xmlXPathObjectPtr型のオブジェクト。
備考:戻り値のobjectは使用後に解放する必要があります。xmlXPathFreeObject参照。
void xmlXPathFreeContext (xmlXPathContextPtr ctxt)
作成したcontextを解放します。。
ctxt:contextへのポインタ。
戻り値:なし。
備考
void xmlXPathFreeObject (xmlXPathObjectPtr obj)
作成したobjectを解放します。。
obj:オブジェクト(XPathの評価結果)へのポインタ。
戻り値:なし。
備考

XMLの関数は思った以上に多く存在します。以下の参考URLを参考にしてみると良いでしょう(英語)


参考URL http://xmlsoft.org/

0 件のコメント :

コメントを投稿