ややめも

アプリ作りたい女子大学院生のめも💁‍♀️

技術メモ
日記
つくったもの
就活の話

AmazonのAPI(PA-API)がv5に移行したので、Goで書き換えてみた

AmazonのProduct Advertising API(以下PA-API)を使ったWebアプリを作っています💃 サーバーはGoで書いていたので、APIにリクエストを送る際にはこちらのライブラリを使用していました。

github.com

しかし先月、Amazonからこんなお知らせが入りました。

Product Advertising API (以下PA-API)の新しいバージョン(PA-API v5)についてお知らせいたします。

PA-API v5への移行を2019年11月30日までに実施しなかった場合、現在のPA-APIはご利用をいただけなくなります。

今まで使用していたライブラリは3年前から更新されておらず、バージョン移行は自分で行う必要がありそうです。また、PA-APIはGoのためのSDKは用意されていません(PHP・Java・Node.js・Pythonは有)。

そこで今回は、Goのhttp clientを用いてPA-APIにリクエストを送ってみました!

Product Advertising APIについて

その名の通り、Amazonの商品情報を取得することができるAPIです。 次のような情報を取得できます。

GetItems(v4ではItemLookup)

商品ID(ASIN)から商品情報を取得

SearchItems(v4ではItemSearch)

検索から商品情報を取得

GetBrowseNodes(v4ではBrowseNodeLookup)

BranchNodeID(各カテゴリのID)を入れると、そのノードの情報や親子関係を取得

GetVariations(v5から登場した新しいAPI)

商品ID(ASIN)から製品自体は同じではあるが、サイズや色や特典が違うものを取得

f:id:yaya-w-1026:20191009153523p:plain:w400
このようなイメージ

Scratchpadを使ってリクエストを送る

Amazonから提供されているScratchpadというツールを使い、実際にリクエストを送ってみましょう。

Product Advertising API 5.0 Scratchpad

GetItemsで商品取得をしてみます。

Common parametersの欄に認証情報を、Request parametersにはItemIds(今回の例はB00OYMY4S4)と、Resourcesから取得したい情報を選んでおきます。

f:id:yaya-w-1026:20191009115614p:plain

するとこんな感じでレスポンスが取得できました!(私の大好きなゲーム🎮) f:id:yaya-w-1026:20191009115941p:plain

詳しいレスポンスはJSON responseのタブで確認できます。

PA-APIでどのような情報を取得できるのかわかってきたと思います! 次からは今回Scratchpadで行ったリクエストをGoを使って実装していきます。

Goからリクエストを送る

Amazonに対するリクエストには、認証情報を用いて署名する必要があります。 一連の流れはこちらを参照してください。本コードでも4つの流れをStep1~Step4とコメントに記載してあります。

署名にはHMACを用いていて、ハッシュ関数にはSHA256を使います。 この署名アルゴリズム(hmacSHA256)とバイト文字列をハッシュ化して文字列にして返す部分(hashedString)は、 こちらのコードを利用させていただきました。(調べてたらクライアントライブラリを作っている方がいた!!すてき!!) github.com

リクエストに必要な情報がわからない場合(endpoint・region・servicenameなど)は、Scratchpadの結果をみながら確認してみると良いでしょう。

次のコードは主に公式のJavaのコードを参考にしています。 Goのバージョンは1.13です。

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "strings"
    "time"
)

const (
    Host          = "webservices.amazon.co.jp"
    Region        = "us-west-2"
    Path          = "/paapi5/getitems"
    AccessKey     = "AWS_ACCESS_KEY_ID"
    SecretKey     = "AWS_SECRET_ACCESS_KEY"
    AssociateTag  = "AWS_ASSOCIATE_TAG"
    HmacAlgorithm = "AWS4-HMAC-SHA256"
    Aws4Request   = "aws4_request"
    ServiceName   = "ProductAdvertisingAPI"
    ContentType   = "application/json; charset=UTF-8"
    AmzTarget     = "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems"
    Encoding      = "amz-1.0"
    PartnerType   = "Associates"
    MarketPlace   = "www.amazon.co.jp"
)

type Aws4Auth struct{}

func main() {
    // これは好きなものを設定
    ASIN := "B00OYMY4S4"
    payload, err := getItemsQuery(ASIN)
    if err != nil {
        log.Println("クエリを作成できませんでした ", err)
    }

    req, err := http.NewRequest("POST", "https://"+Host+Path, bytes.NewBuffer(payload))
    if err != nil {
        log.Println("リクエストの作成に失敗 ", err)
    }

    now := time.Now().UTC()

    aws4Auth := Aws4Auth{}

    // Step1: 署名バージョン 4 の正規リクエストを作成する
    canonicalURL := aws4Auth.prepareCanonicalRequest(payload, now)

    // Step2: 署名バージョン 4 の署名文字列を作成する
    stringToSign := aws4Auth.prepareStringToSign(now, canonicalURL)

    // Step3: AWS 署名バージョン 4 の署名を計算する
    sig := aws4Auth.signature(now, stringToSign)

    req.Header.Set("Host", Host)
    req.Header.Set("Content-Type", ContentType)
    req.Header.Set("X-Amz-Date", getTimeStamp(now))
    req.Header.Set("X-Amz-Target", AmzTarget)
    req.Header.Set("Content-Encoding", Encoding)

    // Step4: HTTP リクエストに署名を追加する
    req.Header.Set("Authorization", aws4Auth.buildAuthorizationString(sig, now))

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Println("リクエストに失敗しました ", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode == 200 {
        log.Println("Status Code ", resp.StatusCode)
        var result interface{}
        if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
            log.Println(err)
            return
        }
        log.Println(result)
    } else {
        log.Println("Status Code ", resp.StatusCode)
        var result interface{}
        if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
            log.Println(err)
            return
        }
    }
}

func (a Aws4Auth) prepareCanonicalRequest(payload []byte, time time.Time) string {
    request := []string{"POST", Path, "", "content-encoding:" + Encoding, "host:" + Host,
        "x-amz-date:" + getTimeStamp(time), "x-amz-target:" + AmzTarget, "",
        "content-encoding;host;x-amz-date;x-amz-target", hashedString(payload)}
    return strings.Join(request, "\n")
}

func (a Aws4Auth) buildAuthorizationString(signature string, time time.Time) string {
        // 認証情報は環境変数から取得
    cre := strings.Join([]string{os.Getenv(AccessKey), getDate(time), Region, ServiceName, Aws4Request}, "/")
    authHeader := []string{HmacAlgorithm, " Credential=", cre,
        ", SignedHeaders=content-encoding;host;x-amz-date;x-amz-target, Signature=", signature}
    return strings.Join(authHeader, "")
}

func (a Aws4Auth) prepareStringToSign(time time.Time, url string) string {
    return strings.Join(
        []string{
            HmacAlgorithm, getTimeStamp(time),
            strings.Join([]string{getDate(time), Region, ServiceName, Aws4Request}, "/"),
            hashedString([]byte(url)),
        },
        "\n")
}

func (a Aws4Auth) signature(time time.Time, signed string) string {
    kSecret := "AWS4" + os.Getenv(SecretKey)
    kDate := hmacSHA256([]byte(kSecret), []byte(getDate(time)))
    kRegion := hmacSHA256(kDate, []byte(Region))
    kService := hmacSHA256(kRegion, []byte(ServiceName))
    kSinging := hmacSHA256(kService, []byte(Aws4Request))
    return hex.EncodeToString(hmacSHA256(kSinging, []byte(signed)))
}

func hmacSHA256(key, data []byte) []byte {
    hasher := hmac.New(sha256.New, key)
    hasher.Write(data)
    return hasher.Sum(nil)
}

func hashedString(data []byte) string {
    sum := sha256.Sum256(data)
    return hex.EncodeToString(sum[:])
}

func getTimeStamp(time time.Time) string {
    const format = "20060102T150405Z"
    return time.Format(format)
}

func getDate(time time.Time) string {
    const format = "20060102"
    return time.Format(format)
}

func getItemsQuery(ASIN string) ([]byte, error) {
    itemIds := []string{ASIN}
    resources := []string{
        "Images.Primary.Medium", "ItemInfo.ByLineInfo", "ItemInfo.ContentInfo",
        "ItemInfo.ContentRating", "ItemInfo.Classifications", "ItemInfo.ExternalIds",
        "ItemInfo.Features", "ItemInfo.ManufactureInfo", "ItemInfo.ProductInfo",
        "ItemInfo.TechnicalInfo", "ItemInfo.Title", "ItemInfo.TradeInInfo"}

    q := &GetItems{
        Marketplace: MarketPlace,
        PartnerTag:  os.Getenv(AssociateTag),
        PartnerType: PartnerType,
        Resources:   resources,
        ItemIds:     itemIds,
    }
    payload, err := json.Marshal(q)
    if err != nil {
        log.Println("フォーマットに失敗しました ", err)
        return payload, err
    }

    return payload, nil
}

type GetItems struct {
    Marketplace string
    PartnerTag  string
    PartnerType string
    ItemIds     []string `json:",omitempty"`
    ItemIdType  string   `json:",omitempty"`
    Resources   []string `json:",omitempty"`
}

少しコメントします。

   now := time.Now().UTC()

タイムゾーンをUTCに変更しておきます。 ここが他のタイムゾーンになっているとうまくいきません。

    req.Header.Set("Host", Host)
    req.Header.Set("Content-Type", ContentType)
    req.Header.Set("X-Amz-Date", getTimeStamp(now))
    req.Header.Set("X-Amz-Target", AmzTarget)
    req.Header.Set("Content-Encoding", Encoding)

    // Step4: HTTP リクエストに署名を追加する
    req.Header.Set("Authorization", aws4Auth.buildAuthorizationString(sig, now))

リクエストの際にはこの6つのヘッダーが必須になります。

        log.Println("Status Code ", resp.StatusCode)
        var result interface{}
        if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
            log.Println(err)
            return
        }
        log.Println(result)

本来であればJSONのレスポンスを構造体で定義しておいて、resultの型にしてデコードするのが良いですが、 今回は出力を確認できればいいのでこの形式にしました。JSONを構造体の形にするにはこのサイトが便利なので使ってみてください。

JSON-to-Go: Convert JSON to Go instantly

リクエストがうまく行かない場合

署名が上手くいかず

Code:InvalidSignature Message:The request has not been correctly signed.(略)

のようなメッセージがでてくることがあります。

その場合はこちら他の言語を使用して署名キーを取得にあるテストが通るかを確認すると良いです。 そうすることで、署名のコードが間違っているのか、他の部分が間違っているのか確認することができます。

まとめ

タイムゾーン設定し忘れて数日間溶かしたあああ。PA-APIv5は先月更新されたばかりなので情報がほぼ公式ドキュメントしかなくて、いつも他の解説に逃げちゃうマンなので少し大変でした。

ついでに私の好きなゲームもおすすめしときます!!! バブル全盛期が舞台の龍が如く0!!名作!!!!