Amazonの書籍情報をテーブルビューに表示する
Amazon Web Serviceで書籍情報を取得する(2008/12/20)のつづき。iPhoneで書籍を扱うアプリを作りたいなぁと思ってるってはなし。
Amazon APIを使えば、キーワードを使って書籍情報を検索し、検索結果をXMLで取得することができる。前はPythonで書いたのだが、Objective-Cで書いてみた。検索ボックスにキーワードを入力してSearchボタンを押すとAmazonの書籍タイトルをテーブルビューに表示するみたいなのを作った。まあ基本形。
Amazonが返すXMLをパースしなくちゃならないのだが、iPhoneではNSXMLParserというSAXパーサ(頭からシーケンシャルに読み込む方法)しか使えないらしい。DOM(木構造を作る方法)が使えないのはメモリをたくさん使うからじゃないかと思う。SAXは使ったことなかったのでSeismicXMLというiPhone付属のサンプルコードを調べて勉強した。後で使うと思うのでちょっとまとめておこう。まず、本の情報を格納するクラスを作る。
@interface Book : NSObject { @private NSString *_ASIN; // ASIN NSString *_webLink; // 詳細情報へのURL NSString *_author; // 著者 NSString *_manufacturer; // 出版社 NSString *_title; // タイトル } @property (nonatomic, retain) NSString *ASIN; @property (nonatomic, retain) NSString *webLink; @property (nonatomic, retain) NSString *author; @property (nonatomic, retain) NSString *manufacturer; @property (nonatomic, retain) NSString *title; @end
次にSAXのイベントハンドラを5つ定義する。
- パース開始時に呼ばれる
- タグの開始時に呼ばれる
- タグの終了時に呼ばれる
- 文字列で呼ばれる
- パース終了時に呼ばれる
ここでは、特定のタグで囲まれた文字列を取得したい。Amazonが返す本の情報は、実際もっとごちゃごちゃしてるけど
<Item> <Title>本のタイトル</Title> <Author>著者名</Author> </Item>
みたいなXMLになっている。ここで欲しいのは「本のタイトル」や「著者名」の文字列部分だ。上の5つのイベントハンドラでどうやって抽出するのかSAXを使ったことがなくて謎だったのだがSeismicXMLを調べてわかった。基本的な戦略は、
- 本のタグItemの開始時に空のBookオブジェクトを作成
- TitleやAuthorタグの開始時に空のプレースホルダオブジェクト(例ではNSMutableString)を作成
- 文字列ハンドラでプレースホルダオブジェクトが存在すれば文字列(本のタイトルとか著者名)を追加
- TitleやAuthorのタグの終了時にプレースホルダオブジェクトの文字列をBookのインスタンス変数に格納
ミソはNSMutableStringを使って文字列ハンドラでappendString:する点だ。これなら改行とか空白文字があってもそのまま追加される。上の戦略をコードで書くと下のようになる。
@interface XMLReader : NSObject { // XMLパース時に使用 Book *_currentBookObject; NSMutableString *_contentOfCurrentBookProperty; NSMutableArray *_bookList; } - (void)parseXMLFileAtURL:(NSURL *)URL ToList:(NSMutableArray *)list; @property (nonatomic, retain) Book *currentBookObject; @property (nonatomic, retain) NSMutableString *contentOfCurrentBookProperty; @property (nonatomic, retain) NSMutableArray *bookList; @end - (void)parseXMLFileAtURL:(NSURL *)URL ToList:(NSMutableArray *)list { self.bookList = [list retain]; // XMLを取得しパース // URLにはAmazon APIの検索URLが入る NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:URL]; [parser setDelegate:self]; [parser parse]; [parser release]; } // XMLパーサのイベントハンドラ - (void)parserDidStartDocument:(NSXMLParser *)parser { NSLog(@"パース開始"); } // タグの開始 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { if (qName) { elementName = qName; } // 空のBookオブジェクトを追加 if ([elementName isEqualToString:@"Item"]) { self.currentBookObject = [[Book alloc] init]; [self.bookList addObject:self.currentBookObject]; return; } // ASINタグで囲まれた文字列を格納するNSMutableStringを作成 // foundCharactersで格納される if ([elementName isEqualToString:@"ASIN"]) { self.contentOfCurrentBookProperty = [NSMutableString string]; } else if ([elementName isEqualToString:@"DetailPageURL"]) { self.contentOfCurrentBookProperty = [NSMutableString string]; } else if ([elementName isEqualToString:@"Author"]) { self.contentOfCurrentBookProperty = [NSMutableString string]; } else if ([elementName isEqualToString:@"Manufacturer"]) { self.contentOfCurrentBookProperty = [NSMutableString string]; } else if ([elementName isEqualToString:@"Title"]) { self.contentOfCurrentBookProperty = [NSMutableString string]; } else { // 指定したタグ以外は無視する self.contentOfCurrentBookProperty = nil; } } // タグの終了 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { if (qName) { elementName = qName; } if ([elementName isEqualToString:@"ASIN"]) { self.currentBookObject.ASIN = self.contentOfCurrentBookProperty; } else if ([elementName isEqualToString:@"DetailPageURL"]) { self.currentBookObject.webLink = self.contentOfCurrentBookProperty; } else if ([elementName isEqualToString:@"Author"]) { self.currentBookObject.author = self.contentOfCurrentBookProperty; } else if ([elementName isEqualToString:@"Manufacturer"]) { self.currentBookObject.manufacturer = self.contentOfCurrentBookProperty; } else if ([elementName isEqualToString:@"Title"]) { self.currentBookObject.title = self.contentOfCurrentBookProperty; } } // 文字列 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { // 領域が確保されていたら格納 // appendString:するのがミソ(空白文字があってもつなげてくれる) if (self.contentOfCurrentBookProperty) { [self.contentOfCurrentBookProperty appendString:string]; } } - (void)parserDidEndDocument:(NSXMLParser *)parser { NSLog(@"パース終了"); NSLog(@"%d冊の情報を取得しました。", [self.bookList count]); for (Book *book in self.bookList) { NSLog(@"%@", book); } }
とりあえず書籍情報はWebから取得できるようになった。次はもっといろいろな情報(書影とか)を表示させてみよう!