やはりきちんと実力のあるベンチャーが評価されるべきですよね。最近流行に便乗しただけのようなベンチャーが大量に資金を集めてたりするのはなんか違うな〜って思います。まあ日本のベンチャーを育てようってのはいい傾向なんですけどね。
んでバーコードバトラーアプリの制作過程の第5回目です。
最近Androidアプリの開発ばっかやってるんで、ちょっとObjective-C忘れ気味ですがいきましょう!
本日は楽天から取得したXMLをパースして、商品画像と商品タイトルを取得するとこまでやります。
1 XMLパーサー(SAX)
まずはXMLパーサーについて少し解説します。パーサーってのはXML文章をプログラムで扱うための方法で、XMLから要素を取得したり、値を挿入したりすることができます。
パーサーには大きく分けて二つの種類があります。
DOMとSAXです。
■DOM
DOMってのは簡単に説明すると、XMLの要素を全部読み込んで、要素のツリーを構築します。このツリー内を自由に行き来できて、要素を削除したり追加したりできるというメリットがあります。しかし一方で、XMLを全部一気に読み込むのでメモリの消費が大きいらしいです。
■SAX
SAXはXML文章を先頭から最後まで順番に読み込んでいき、タグを見つけるたびにメソッドが呼ばれる方式です。
例えば以下のようなXMLがあった場合
<itemName>PHP逆引きレシピ</itemName>
<catchcopy>【送料無料】</catchcopy>
<itemCode>book:13217617</itemCode>
<itemPrice>2730</itemPrice>
こんな感じになります
〜SAXのイメージ〜
パーススタート
↓
<itemName> → 開始タグ発見メソッドが呼ばれる
↓
PHP逆引きレシピ → 要素発見メソッドが呼ばれる
↓
</itemName> → 終了タグ発見メソッドが呼ばれる
↓
<catchcopy> → 開始タグ発見メソッドが呼ばれる
↓
【送料無料】 → 要素発見メソッドが呼ばれる
↓
</catchcopy> → 終了タグ発見メソッドが呼ばれる
↓
<itemCode> → 開始タグ発見メソッドが呼ばれる
↓
book:13217617 → 要素発見メソッドが呼ばれる
↓
</itemCode> → 終了タグ発見メソッドが呼ばれる
↓
<itemPrice> → 開始タグ発見メソッドが呼ばれる
↓
2730 → 要素発見メソッドが呼ばれる
↓
</itemPrice> → 終了タグ発見メソッドが呼ばれる
↓
パース終了
とこんな感じで、XMLを先頭から最後まで一つひとつ見ていき、「タグ発見メソッド」「要素発見メソッド」「終了タグ発見メソッド」がアホほど呼ばれるのがSAXパーサーです。
これはメモリの消費も少なく、高速らしいので、限られたリソース内で稼働させるアプリに関しては、SAXパーサーを使うのが良さそうです。
2 SAXのフォーマット
それでは実装をする前にフォーマットを見てみましょう。
まずはこんな感じでリクエストを作成します。NSString型の「requestUrl」がURLになります。以下のコードでリクエストを送信します。(今回はバーコード読み取り直後)
//リクエストの送信方法
//----------------------------------------------------------------------------
NSString *requestUrl = @"http〜〜〜〜〜〜〜〜〜";
NSURL *url = [NSURL URLWithString:requestUrl];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req addValue:@"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[req addValue:0 forHTTPHeaderField:@"Content-Length"];
[req setHTTPMethod:@"GET"];
conn = [[NSURLConnection alloc] initWithRequest:req delegate:self];
if (conn)
{
webData = [[NSMutableData data] retain];
}
//----------------------------------------------------------------------------
そしたら以下の三つのメソッドを実装しましょう。それぞれ
「レスポンスを受信したとき」
「データを受け取ったとき」
「データの転送中にエラーがあったとき」
に呼ばれます。
//レスポンスを受信したとき呼ばれるメソッド
//----------------------------------------------------------------------------
didReceiveResponse:(NSURLResponse *) response
{
[webData setLength: 0];
}
//データを受け取ったとき呼ばれるメソッド
//----------------------------------------------------------------------------
didReceiveData:(NSData *) data
{
[webData appendData:data];
}
//データの転送中にエラーがあったとき
//----------------------------------------------------------------------------
-(void) connection:(NSURLConnection *) connection
didFailWithError:(NSError *) error
{
[webData release];
[connection release];
}
そしてデータの受信がすべて完了したときに呼ばれるメソッドが以下の「connectionDidFinishLoading]です。
ここでXMLパーサーの実装をします。
//データ受信終了後に呼ばれる(パースをここで開始する
//----------------------------------------------------------------------------
-(void) connectionDidFinishLoading:(NSURLConnection *) connection
{
[[NSURLCache sharedURLCache] setMemoryCapacity:0];
[[NSURLCache sharedURLCache] setDiskCapacity:0];
xmlParser = [[NSXMLParser alloc] initWithData: webData];
[xmlParser setDelegate: self];
[xmlParser setShouldResolveExternalEntities:YES];
[xmlParser parse];
[xmlParser release];
////////////////////////////
//ここにパース終了後の記述(D)
////////////////////////////
}
//パーサーがスタートタグを見つけたら呼び出される(A)
//----------------------------------------------------------------------------
-(void) parser:(NSXMLParser *) parser
didStartElement:(NSString *) elementName
namespaceURI:(NSString *) namespaceURI
qualifiedName:(NSString *) qName
attributes:(NSDictionary *) attributeDict
{
}
//パーサーが要素のテキストを検出すると呼び出される(B)
//----------------------------------------------------------------------------
-(void)parser:(NSXMLParser *) parser foundCharacters:(NSString *)string
{
}
//パーサーがエンドタグを見つけたら呼び出される(C)
//----------------------------------------------------------------------------
-(void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
}
上記のABC三つのメソッドはタグ、要素を見つけるたびに何度も呼ばれるメソッドです。
このABC内に取得したい情報を入手する内容を記述して、最後に(D)の部分でパース終了後の内容を記述します。全体的な流れはこんな感じです。
2 楽天APIからのXMLをパース
いざ実装開始です。前回まで使用してきたコードに書き加えます。
まずは、
今回使用するXMLパーサー xmlParser
パースの最中にフラグの役割を果たす elementkey
そのデリゲート NSXMLParserDelegate
取得した情報を一時的に保管する soapResultArray
最終的に取得した商品タイトルと商品画像URLを格納する itemName、imageUrl
を宣言します。こんな感じ。
//MonsterShowViewController.h
//----------------------------------------------------------------------------
@interface MonsterShowViewController : UIViewController <NSXMLParserDelegate>
{
IBOutlet UIImageView *imageView;
IBOutlet UILabel *itemNameLabel;
NSString *jancode;
NSMutableData *webData;
NSURLConnection *conn;
NSXMLParser *xmlParser;
int elementkey;
NSMutableArray *soapResultArray;
NSString *itemName;
NSString *imageUrl;
}
@property(nonatomic,retain)UIImageView *imageView;
@property(nonatomic,retain)NSString *jancode;
@property(nonatomic,retain)UILabel *itemNameLabel;
-(void) parseTag:(NSString*)requestUrl;
@end
//----------------------------------------------------------------------------
これで宣言はオーケー
次に実装です。先ほど示したフォーマットに、楽天のXMLから商品名と画像URLを取得するコードを書いていきます。
まずは楽天へのリクエストURLをブラウザで直接見てみましょう。
itemNameタグの中に商品タイトルが、
mediumImageUrlタグの中に、サムネイル画像へのリンクURLが
それぞれ入っていることがわかります。
ここで注意点があります。要素を発見した際に呼ばれる「foundCharacters」メソッドですが、発見した要素によっては、数回にわかれて呼ばれることがあります。
どういうことかと言いますと。例えば書籍タイトルが「PHP逆引きレシピ」だった場合には、「PHP」と「逆引きレシピ」にわかれて「foundCharacters」が呼ばれます。これを知らないと、なぜか取得したタイトルが「逆引きレシピ」だけになってしまうという自体に陥ります。僕はずっとこの謎が解けずに悩んでいた時期がありました。
取得したタイトルを配列に保存することで、この問題は解決します。
よって実装は以下のようにします。
始めに楽天にリクエストを投げる部分の処理は前回を見て下さい。
//MonsterShowViewController.h
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
-(void) connectionDidFinishLoading:(NSURLConnection *) connection
{
[[NSURLCache sharedURLCache] setMemoryCapacity:0];
[[NSURLCache sharedURLCache] setDiskCapacity:0];
//パーサー生成
xmlParser = [[NSXMLParser alloc] initWithData: webData];
[xmlParser setDelegate: self];
[xmlParser setShouldResolveExternalEntities:YES];
soapResultArray = [[NSMutableArray alloc]init];
imageUrl = [[NSString alloc]init];
itemName = [[NSString alloc]init];
//パース開始
[xmlParser parse];
[xmlParser release];
//ここにパース終了後の記述(D)
//ラベルと画像を描画
itemNameLabel.text = itemName;
imageView.image = [self getImage:imageUrl];
}
//パーサーがスタートタグを見つけたら呼び出される(A)
//----------------------------------------------------------------------------
-(void) parser:(NSXMLParser *) parser
didStartElement:(NSString *) elementName
namespaceURI:(NSString *) namespaceURI
qualifiedName:(NSString *) qName
attributes:(NSDictionary *) attributeDict
{
//パーサーが開始タグ「itemName」「mediumImageUrl」を見つけたらフラグ「mediumImageUrl」を1に
if([elementName isEqualToString:@"itemName"] | [elementName isEqualToString:@"mediumImageUrl"])
{
elementkey = 1;
[soapResultArray removeAllObjects];
printf("発見\n");
}
}
//パーサーが要素のテキストを検出すると呼び出される(B)
//----------------------------------------------------------------------------
-(void)parser:(NSXMLParser *) parser foundCharacters:(NSString *)string
{
if (elementkey == 1) {
//開始タグを見つけた直後に要素を発見した場合、文字列を保管
//要素によっては、このメソッドが数回呼ばれることがあるので、
//配列に保管する。こうしないとタイトルが切れて保存されることも…
[soapResultArray addObject:string];
}
}
//パーサーがエンドタグを見つけたら呼び出される(C)
//----------------------------------------------------------------------------
-(void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
//エンドタグ「itemName」を見つけ、文字列をitemNameに格納
if ([elementName isEqualToString:@"itemName"]) {
NSString *str = @"";
for (int i = 0; i<[soapResultArray count]; i++) {
//配列に入ってる要素を文字列に直す処理
str = [NSString stringWithFormat:@"%@%@",str, [soapResultArray objectAtIndex:i]];
}
NSLog(@"取得した要素%@",str);
itemName = str;
}else if ([elementName isEqualToString:@"mediumImageUrl"]){
//上記と同様。画像URlを取得
NSString *str = @"";
for (int i = 0; i<[soapResultArray count]; i++) {
NSLog(@"%d",[soapResultArray count]);
str = [NSString stringWithFormat:@"%@%@",str, [soapResultArray objectAtIndex:i]];
}
NSLog(@"取得した要素%@",str);
imageUrl = str;
}
elementkey = 0;
}
//NADataから画像を取得するメソッド
- (UIImage *)getImage:(NSString *)url {
return [UIImage imageWithData:[self getData:url]];
}
//画像URLからNSDataを取得するメソッド
- (NSData *)getData:(NSString *)url {
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:[url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]
cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0];
NSURLResponse *response;
NSError *error;
NSData *result = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
if (result == nil) {
NSLog(@"NSURLConnection error %@", error);
}
return result;
}
@end
これでバーコド画像を撮影した後に、商品タイトルと画像が表示されるようになったと思います。