先のエントリで紹介した amazlet.com のカテゴリ別ランキングですが、実際には AWS 3.0 の BrowseNodeSearch を使って実現しています。その商品の BrowseID を引数に BrowseNodeSearch を売り上げ順ソートで実行して、その結果を出力、で完了です。ただ、いろいろバッドな対処が必要なところがありました。
一つ目は Net::Amazon で BrowseId が取得できない点。これはどうも仕様みたいです。
AWS 3.0 の AsinSearch (の type=heavy) では応答の XML の中に BrowseList というエレメントが含まれています。(例)
<BrowseList>
<BrowseNode>
<BrowseId>575120</BrowseId>
<BrowseName>ジャンル別 - 外国映画 - アクション - パニック・スペタクル</BrowseName>
</BrowseNode>
<BrowseNode>
<BrowseId>3713711</BrowseId>
<BrowseName>ストア - COOP - 20世紀FOXストア - ドラマ</BrowseName>
</BrowseNode>
<BrowseNode>
<BrowseId>3713741</BrowseId>
<BrowseName>ストア - COOP - 20世紀FOXストア - アクション</BrowseName>
</BrowseNode>
...
</BrowseList>
こんな具合です。今回の実装においてはこの BrowseList の情報が必要なわけですが、Net::Amazon においては BrowseList の子要素である BrowseId を取得する方法が用意されていません。で、なぜか BrowseName は取得できます。
#!/usr/local/bin/perl
use strict;
use warnings;
use Net::Amazon;
use Encode;
use constant DEV_TOKEN => 'DM1XQEXM3YQU8';
my $ua = Net::Amazon->new(
token => DEV_TOKEN,
locale => 'jp',
);
my $asin = shift or die "need asin";
my $response = $ua->search( asin => $asin );
if ($response->is_error) {
die $response->message;
}
for my $prop ($response->properties) {
print encode('euc-jp', $_)."\n"
for ($prop->browse_nodes);
}
こんなスクリプトを書いてやって実行すると、
[naoya@judy naoya]$ perl browsenode.pl B0001A7D22 ジャンル別 - 外国映画 - アクション - パニック・スペタクル ストア - COOP - 20世紀FOXストア - ドラマ ストア - COOP - 20世紀FOXストア - アクション ストア - COOP - ストア別全ASIN - 20世紀FOXストア ストア - バーゲンコーナー - 外国映画 ユーズドDVD - 外国映画 ユーズドDVD - 外国映画 - アクション ユーズドDVD - 外国映画 - アドベンチャー
という具合に BrowseName の値はリストで返ってくるのですが、BrowseId が取得できず。変な仕様です。そこで、Hack しようということになるのですが Net::Amazon のコードに直接手を入れてしまうと後々めんどいので、自前のクラスで拡張してみました。
Net::Amazon::PropertyExt というクラスを作成。このクラスは Net::Amazon::Property の Decorator として実装しました。また、ひとつひとつの BrowseNode は Net::Amazon::BrowseNode というクラスのインスタンスとして実装しました。
for my $prop ($response->properties) {
my $propext = Net::Amazon::PropertyExt->new($prop);
# $node は Net::Amazon::BrowseNode
for my $node ($propext->browselist) {
sprintf("%d %s\n" $node->BrowseId, $node->BrowseName);
}
}
こんな感じで使えば、BrowseList からそれぞれの BrowseNode の Id と Name をスマートに引っ張り出せるという代物です。
Net::Amazon::BrowseNode はただの入れ物なので、setter/getter があるだけのシンプルな実装です。
package Net::Amazon::BrowseNode;
use strict;
sub new {
my ($class, $args) = @_;
my $self = bless {}, $class;
$self->{BrowseId} = $args->{BrowseId};
$self->{BrowseName} = $args->{BrowseName};
$self;
}
sub BrowseId {
my $self = shift;
@_ ? $self->{BrowseId} = shift : $self->{BrowseId};
}
sub BrowseName {
my $self = shift;
@_ ? $self->{BrowseName} = shift : $self->{BrowseName};
}
1;
Net::Amazon::PropertyExt は Decorator パターンで Net::Amazon::Property を内包します。AUTOLOAD と再bless による Decorator のどちらにしようかと思いましたが、なんとなく再bless でやってみました。
package Net::Amazon::PropertyExt;
use strict;
use base qw(Net::Amazon::Property);
use Net::Amazon::BrowseNode;
sub new {
my $class = shift;
my $prop = shift or die;
my $self = bless $prop, $class;
$self->_set_browselist;
return $self;
}
sub _set_browselist {
my $self = shift;
my $browse_nodes = $self->{xmlref}->{BrowseList}->{BrowseNode};
if(ref($browse_nodes) eq "ARRAY") {
my @nodes = map {
Net::Amazon::BrowseNode->new({
BrowseName => $_->{BrowseName},
BrowseId => $_->{BrowseId},
}); } @{ $browse_nodes };
$self->browselist(\@nodes);
} elsif (ref($browse_nodes) eq "HASH") {
$self->browselist([ Net::Amazon::BrowseNode->new({
BrowseName => $browse_nodes->{BrowseName},
BrowseId => $browse_nodes->{BrowseId},
}) ]);
} else {
$self->browse_nodes([ ]);
}
}
sub browselist {
my $self = shift;
@_ ? $self->{__browselist} = shift: $self->{__browselist};
}
1;
これで BrowseNode をオブジェクトとして取り出すことができます。取り出した BrowseNode の Id を引数に BrowseNodeSearch を実行すれば、その BrowseNode の売れ筋が得られます。
ひとつの商品に対して、BrowseNode は複数定義される場合があるのですが amazlet.com ではそのうちひとつの BrowseNode に絞ってランキングを出したかったので、Id でソートして一番若い BrowseNode のそれを使うようにしました。なんとなく Id が若いものが一番しっくりくる BrowseNode の気がしたので。
また、「パニック・スペタクル関連商品の売れ筋商品はこちら!」といった感じでラベルを出したいのですが、BrowseName は「ジャンル別 - 外国映画 - アクション - パニック・スペタクル」と長く使いにくいラベルになってるので、ここは力技で正規表現により一番後のジャンルを表示しています。
結構適当な仕様ですが、いざ動かしてみると案外それなりのラベルになってくれました。しかし、AWS 3.0 はそろそろ離れて ECS 4.0 の方で Hack していきたいところです。