Amazonからデータを取得して、ハッシュに変換する

というモジュールを作ってみた。XML::SimpleやXML::LibXML::Simpleを使った方が楽なんだけど、

$xml->{BrowseNodeResponse}->{BrowseNodes}->{BrowseNode};

というふうに書くのが面倒なので、XML::LibXMLを使って任意のハッシュデータを作るようにしてみた。ただ、XPathのハッシュを書かなきゃいけないので、面倒くささは大して変わらない気もする。

package Amazon;

use strict;
use warnings;

use URI;
use Carp;
use LWP::Simple;
use XML::LibXML;

sub new {
    my ($class, $access_key, $associate_tag) = @_;
    unless ($access_key) {
        Carp::croak("required access key");
    }

    my $self = {
        aws_url  => "http://webservices.amazon.co.jp/onca/xml",
        params   => {
            Service        => "AWSECommerceService",
            Version        => "2008-04-07",
            AWSAccessKeyId => $access_key,
            AssociateTag   => $associate_tag,
        },
        parser   => XML::LibXML->new
    };
    
    return bless $self, $class;
}

sub request {
    my ($self, $params, $xpath, $force_array) = @_;

    my $uri = URI->new($self->{aws_url});
    $uri->query_form(
        %{$self->{params}},
        %{$params}
    );

    my $content = LWP::Simple::get($uri) or
                    Carp::croak("cannot get content from $uri");
    my $root = $self->{parser}->parse_string($content)->documentElement;

    my @errors = $root->findnodes(
        $self->replace_xpath("//Errors/Error/Message")
    );
    if (@errors) {
        Carp::croak(join ";", map { $_->textContent } @errors);
    }

    $xpath = {root => $xpath};
    $force_array ||= [];
    push @$force_array, "root" unless $force_array == 1;
    my $hash = $self->node2hash($root, $xpath, $force_array);
    return @{$hash->{root}};
}

sub node2hash {
    my ($self, $node, $xpath, $force_array) = @_;
    
    my $result = {};

    while (my ($key, $xp) = each %$xpath) {
        next if $key eq "root_xpath";

        my @values;
        if (ref $xp eq "HASH") {
            if (exists $xp->{root_xpath}) {
                map {push @values, $self->node2hash($_, $xp, $force_array)}
                        $node->findnodes(
                            $self->replace_xpath($xp->{root_xpath})
                        );
            } else {
                push @values, $self->node2hash($node, $xp, $force_array);
            }
        } else {
            @values = map {$_->textContent} 
                            $node->findnodes($self->replace_xpath($xp));
        }

        if (@values > 1 or $force_array == 1 
            or (ref $force_array eq"ARRAY" and
                    grep {$_ eq $key} @$force_array)) {
            $result->{$key} = \@values;
        } else {
            $result->{$key} = $values[0];
        }
    }
    return $result;
}

sub replace_xpath {
    my ($self, $xpath) = @_;
    $xpath =~ s!<([^/]+)>!*[local-name()='$1']!g;
    return $xpath;
}

1;
#!/usr/bin/perl

use strict;
use warnings;
use utf8;

use Amazon;
use YAML::Syck;

my $access_key = "1K6RFH5CWTBQWH0Y6702";
my $browse_node_id = "492332";

my $amazon = Amazon->new($access_key);

my $params = {
    Operation                         => "BrowseNodeLookup,ItemSearch",
    "BrowseNodeLookup.1.BrowseNodeId" => $browse_node_id,
    "ItemSearch.1.BrowseNode"         => $browse_node_id,
    "ItemSearch.1.SearchIndex"        => "Books"
};

my $xpath = {
    root_xpath   => "//<BrowseNodes>/<BrowseNode>",
    BrowseNodeId => "<BrowseNodeId>",
    Name         => "<Name>",
    Children     => {
        root_xpath   => "<Children>/<BrowseNode>",
        BrowseNodeId => "<BrowseNodeId>",
        Name         => "<Name>",
    },
    Ancestors    => {
        root_xpath  => "//<Ancestors>/<BrowseNode>",
        BrowseNodeId => "<BrowseNodeId>",
        Name         => "<Name>",
    },
    TotalItems => "//<TotalResults>",
    TotalPages => "//<TotalPages>"
};

my $force_array = ["Children", "Ancestors"];

my ($result) = $amazon->request($params, $xpath, $force_array);
print YAML::Syck::Dump($result);
--- 
Ancestors: 
  - 
    BrowseNodeId: 466298
    Name: コンピュータ・インターネット
  - 
    BrowseNodeId: 465610
    Name: ジャンル別
  - 
    BrowseNodeId: 465392
    Name: 本
BrowseNodeId: 492332
Children: 
  - 
    BrowseNodeId: 525588
    Name: インターネット入門書
  - 
    BrowseNodeId: 502720
    Name: ブラウザ
  - 
    BrowseNodeId: 492062
    Name: e-ビジネス
  - 
    BrowseNodeId: 502718
    Name: 通信・メール
  - 
    BrowseNodeId: 515206
    Name: Web開発
  - 
    BrowseNodeId: 542586
    Name: セキュリティー管理
  - 
    BrowseNodeId: 886896
    Name: ネット社会
  - 
    BrowseNodeId: 3431651
    Name: インターネット・Web開発 全般
Name: インターネット・Web開発
TotalItems: 7854
TotalPages: 786