Net::Amazon の謎

最近ちょっとした興味から、 http://tvtwi.com/ というのを書いて、番組内容連動広告っぽいのをアイディアとして実装してみたときのメモ。Twitter ユーザにはゴールデンタイムとアニメが人気あるみたい。

Amazon のキーワード検索を非同期で使いたいと思い立ってあれこれ考えた

worker で AE::T::S を使っているので、非同期で書けないとツイートが詰まる。
AE::Net::Amazon 的なモジュールが無いので、 Net::Amazon を使って Amazon にリクエストするサーバを別に立てて AE::HTTP からそのサーバに対してリクエストを投げれば非同期でデータ取得できる、という結論にいたったが、実際に書いてみたら 900MB の swap までも使い果たしてまともにレスポンスができる状況でないほどにメモリを大量に食った。(サーバは Starman, Starlet ともに試した)

コード

こんな感じでリクエストを受け、レスポンスを受け取り、 JSON にして返す。
途中で深追いするのはやめたのであまり詳しくは追ってないけれど何かがおかしいみたい。

sub amazon {
    my $env = shift;
    my $request = Plack::Request->new($env);

    my $keyword = $request->param('keyword');

    unless ($keyword) {
        return [500, [], ['']];
    }

    my $ua = Net::Amazon->new(
        locale => 'jp',
        %$amazon_key,
    );

    my $req = Net::Amazon::Request::Blended->new(
        page => 1,
        blended => $keyword,
    );

    my $res = $ua->request($req);

    unless ($res->is_success) {
        return [500, [], ['']];
    }

    my $hash = {};
    for my $prop ($res->properties) {
        if (my $rank = $prop->SalesRank) {
            $hash->{$rank} = {
                name => $prop->ProductName,
                price => $prop->OurPrice,
                asin => $prop->ASIN,
                image => $prop->ImageUrlSmall,
            };
        } else {
            #noop;
        }
    }

    my %result;
    for my $rank (sort {$a <=> $b} keys %$hash) {
        $result{$rank} = {
            name => $hash->{$rank}{name},
            price => $hash->{$rank}{price},
            asin  => $hash->{$rank}{asin},
            image => $hash->{$rank}{image}
        };
    }

    return [200, ['Content-Type' => 'application/json'], [JSON::encode_json(\%result)]];

};

worker 側で処理するようにした。

結局は自前でリクエスト生成部分を書いた
モジュール化したいけれど Amazon のドキュメントがイマイチすぎて API 仕様を把握しきれないので悩ましい上に、 Net::Amazon を非同期にするのは一人でやるには量が多すぎる
最終的にはこんな風に書けばレスポンス帰ってくる。

sub search_amazon {
    my %args = @_;

    my $keywords = $args{keywords};
    my $type     = 'Blended'; #$args{type};
    my $page     = 1;         #$args{page};

    my %req = (
        Operation     => 'ItemSearch',
        SearchIndex   => $type,
        ResponseGroup => 'Images,ItemAttributes',
        Keywords      => $keywords,
        ItemPage      => $page,

        Service        => 'AWSECommerceService',
        AWSAccessKeyId => $amazon_config->{token},
        Version        => '2009-03-31',
        Timestamp      => POSIX::strftime("%Y-%m-%dT%TZ", gmtime),
    );

    my $uri  = URI->new('http://ecs.amazonaws.jp/onca/xml');

    my $qstr = join '&', map { "$_=" . URI::Escape::uri_escape_utf8($req{$_}, "^A-Za-z0-9\-_.~")} sort keys %req;
    my $signme = join "\n", "GET", $uri->host, $uri->path, $qstr;

    my $sig = Digest::SHA::hmac_sha256_base64($signme, $amazon_config->{secret_key});
    $sig .= '=' while length($sig) % 4;
    $sig = URI::Escape::uri_escape_utf8($sig, "^A-Za-z0-9\-_.~");

    $qstr .= "&Signature=$sig";
    $uri->query($qstr);

    AnyEvent::HTTP::http_get $uri->as_string, sub {
        say Dumper \@_;
        return if ($_[1]->{Status} ne '200');

        my $res = XML::Simple::XMLin($_[0], SuppressEmpty => undef, ForceArray => ['Item']);
        say Dumper $res;

        my $data = {};
        my $count = 0;
        for my $item (@{$res->{Items}{Item}}) {
            ...
        }
    };

}