人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

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を調べてわかった。基本的な戦略は、

  1. 本のタグItemの開始時に空のBookオブジェクトを作成
  2. TitleやAuthorタグの開始時に空のプレースホルダオブジェクト(例ではNSMutableString)を作成
  3. 文字列ハンドラでプレースホルダオブジェクトが存在すれば文字列(本のタイトルとか著者名)を追加
  4. 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から取得できるようになった。次はもっといろいろな情報(書影とか)を表示させてみよう!

関連リンク