emahiro's ProgrammingBlog

勉強記録と書評とたまに長めの呟きを書きます

Amazon Product Advertising APIを使ってISBNコードから書籍情報を取得する

やったこと

  • Amazon Product Advertising APIを使って書籍情報を取得すること
  • 書籍情報の検索フックにはISBNコードを使ったこと
  • 利用する際にハマったこと

事前準備

以下は事前にやってあるものとします。

  • Amazon Associate への登録
    • Associate TagにこのIDを使う
  • AcessKeyIdとAccessSecretIdを使う

refs:

blog.apitore.com

実装

※ 書籍のAPIを取得するところのみ抜粋

ディレクトリ構造

- PROJECT
  - conf
    - token_cred.json
  - src
    - project
      - main.go
      - handler/
          - amazon.go

amazon.go の実装は下記

const (
    EC_SERVICE_ENDPOINT = "webservices.amazon.co.jp"
    EC_SERVICE_URI      = "/onca/xml"
)

func readConf() ([]byte, error) {
    f, err := os.Open("./conf/token_cred.json")
    if err != nil {
        fmt.Printf("token_cred.json open error: err; %v", err)
        return nil, err
    }
    b, err := ioutil.ReadAll(f)
    if err != nil {
        fmt.Printf("json file read error: err; %v", err)
        return nil, err
    }
    return b, nil
}

func SearchISBN(w http.ResponseWriter, r *http.Request) {
    b, err := readConf()
    if err != nil {
        fmt.Printf("readConf error. err: %v", err)
        return
    }

    var cred model.AmazonTokenCred
    if err := json.Unmarshal(b, &cred); err != nil {
        fmt.Printf("json unmarshal error. err: %v", err)
        return
    }

    params := url.Values{}
    params.Set("Service", "AWSECommerceService")
    params.Set("Operation", "ItemLookup")
    params.Set("ItemId", "ISBNコード")
    params.Set("IdType", "ISBN")
    params.Set("SearchIndex", "Books")
    params.Set("Timestamp", time.Now().UTC().Format(time.RFC3339))
    params.Set("AWSAccessKeyId", cred.AccessKeyId)
    params.Set("AssociateTag", cred.AssociateTag)
    params.Set("ResponseGroup", "Images,ItemAttributes,Offers")

    // 署名
    canonical_params := params.Encode()
    strToSign := fmt.Sprintf("GET\n%v\n%v\n%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params)
    mac := hmac.New(sha256.New, []byte(cred.SecretKeyId))
    mac.Write([]byte(strToSign))
    signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
    canonical_params = fmt.Sprintf("%v&Signature=%v", canonical_params, signature)

    // http request
    res, err := http.Get(fmt.Sprintf("http://%v%v?%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params))
    if err != nil {
        fmt.Printf("response error. err: %v", err)
        return
    }
  // response はよしなに整形する
}

ハマったポイント

APIのDocs(https://images-na.ssl-images-amazon.com/images/G/09/associates/paapi/dg/index.html?RG_ItemAttributes.html の例のところ)に書いてある遠りのコード書いたら

  • The request must contain the parameter Timestamp.
  • The request must contain the parameter Signature.

の2つのエラーにぶち当たりました。
REST APIのドキュメントをそのまま叩いたのは以下

 curl -i "http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=XXXXXX&Operation=ItemLookup&ItemId=B00008OE6I"
HTTP/1.1 400 Bad Request
Date: Fri, 08 Dec 2017 19:10:17 GMT
Server: Apache-Coyote/1.1
Vary: Accept-Encoding,User-Agent
nnCoection: close
Transfer-Encoding: chunked

<?xml version="1.0"?>
<ItemLookupErrorResponse xmlns="http://ecs.amazonaws.com/doc/2005-10-05/"><Error><Code>MissingParameter</Code><Message>The request must contain the parameter Signature.</Message></Error><RequestID>831754e7-5761-4d0a-adae-6febc205949b</RequestID></ItemLookupErrorResponse>⏎

TimeStampについて

今の時刻をISO8601の形式で使います。
GOにおいては time.RFC3339 で求められてる形式で時刻を取得します。

署名(Signature)について

AmazonProduct Advertising APIで使用する署名(Signature)は 発行したSecretKeyIdを使ってhmacでハッシュ化されたものをbase64でencodeした値 です。

署名の作成は以下のphpのコードを参考にgoで書き直しました。

qiita.com

qiita.com

githubに以下の今回使おうとしているAPIのgoのクライアントを見つけたのでこちらも参考しました。

github.com

署名作成でハマったこと

書いたコードの中で署名を生成している箇所は以下

// 署名
canonical_params := params.Encode()
strToSign := fmt.Sprintf("GET\n%v\n%v\n%v", EC_SERVICE_ENDPOINT, EC_SERVICE_URI, canonical_params)
mac := hmac.New(sha256.New, []byte(cred.SecretKeyId))
mac.Write([]byte(strToSign))
signature := url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
canonical_params = fmt.Sprintf("%v&Signature=%v", canonical_params, signature)

url.Values でクエリパラメータをセットして Encode したら完了かと思っていたのですが、上記の署名生成のコードの最後の行で記載しているように Signatureは最後につけないといけない というところで思いっきりハマりました。

例は以下

# params.Encode()でqueryparamsを作成したときのクエリパラメータ
"AWSAccessKeyId=[AccessKeyId]&AssociateTag=[AssociateTag]&IdType=ISBN&ItemId=9784774193328&Operation=ItemLookup&ResponseGroup=Images,ItemAttributes,Offers&SearchIndex=Books&Service=AWSECommerceService&Signature=[生成した署名]&Timestamp=2017-12-10T06:16:16Z"
# -> 403 Forbidden

# 文字列結合でsignatureをつけたときのクエリパラメータ
"AWSAccessKeyId=[AccessKeyId]&AssociateTag=[AssociateTag]&IdType=ISBN&ItemId=9784774193328&Operation=ItemLookup&ResponseGroup=Images,ItemAttributes,Offers&SearchIndex=Books&Service=AWSECommerceService&Timestamp=2017-12-10T06:16:16Z&Signature=[生成した署名]"
# status OK

当初 params.Encode() で文字列作成をしてし待ってましたが、params.Encode() はqueryのkeyを自動でsortしてしまい、 Signature が末尾に来ません。 そのため、params.Encode() でクエリパラメータを生成し、署名内容とurlは文字列結合の時と同じでも、403を返して来てしまいました。

まとめ

Amazon Product Advertising APIは署名を自前で作らないといけなかったりして忘れかけたことを思い出すきっかけをくれたので久しぶりに触ってみてよかったです。
APIのテストで自動的にphpjavaのコードは生成してくれますが、こういうときにgoとかで書き直してみるのもいいなと思います。

とりあえず書いたコードはこちら

github.com

xmlのレスポンスをstructにマッピングしたりはまた次回!

os.Openするときの相対パスの書き方

goで os.Open(filename string) を実行するときに読み取りファイルを特定のディレクトリにまとめておいたときに os.Open で指定する相対パスがどう決まるかのメモです。

答えはmain.goからの相対パスになります。

例えば以下のようなディレクトリ構成のとき

- src
  - main.go
  - conf.json

conf.jsonを読み込むためには

f, err := os.Open("./conf.json")

となります。

次に以下のようなディレクトリ構成を考えます。

- Project
  - conf
    - conf.json
  - src
    - project
      - main.go

この場合は

f, err := os.Open("../../conf/conf.json")

となります。

main.goから見た相対パスだということを忘れていたので備忘録のために書きました。

[追記]

答えはmain.goからの相対パスになります。

ここが少し違っていてこれは実行ディレクトリから見たときの相対パスになります。

- Project
  - conf
    - conf.json
  - src
    - project
      - main.go

のようなディレクトリ構造の時

# ProjectRootで実行する
$ go run src/project/main.go

とする場合はconf以下のjsonを読み込もうと思ったら os.Open(./conf/conf.json) になります。
一方で

# projectディレクトリで実行する
$ go run main.go

と実行する場合は本エントリで書いたような相対パスになります。

どこで実行するかで書き方を変更する必要があります。

選んだ理由よりも「選ばなかった」理由を知りたいなという話

現在働いているチームの目指すべき姿の一つに、答えではなく「観点」を理解する ことで次回以降一人でその答えに辿り着けるようにする。というものがあります。 自分はこの言葉がすごく好きではあるのですが、答えにたどり着く観点の他に、その答えの他に考えていたいくつかの「答え」候補を選ばなかった理由も合わせて知ることが、考える力と判断する力を養う一つのアプローチになるのではないかと考えています。

選ばなかった理由を知ることの例えとして、技術選定のプロセスを考えます。
このプロセスでは 特定の技術を採用する というのが最終的なゴールになります。 特定の技術を採用するということは、候補として上がっていた他の技術を採用しなかった ということになるわけで、その技術たちを どうして採用しなかったのか ということの方が、採用した経緯を知るより学べる事が多いと感じています。

もう一つの例えとしてコードを書くことを考えて見ます。

「コードを書く瞬間の思考」にアドバイスを貰える

at-grandpa.hatenablog.jp

書かなかったコードや、なぜそれを残さなかったのかにも学びがある

コーディングにおいてはもっとわかりやすいですが、「書かなかった理由 = 選ばなかった理由」であり、できる人のコードの書く瞬間が一番勉強になります。
現に自分であれば書いたであろう1行を書かないわけですから、その意図や理由は非常に勉強になります。

技術選定にしろ、コードを書くことにろ、結局は「課題解決」の手段として捉えればできる人の意思決定を真似るには できる人が選ばないことを自分も選ばなければいい ということに落ち着くのではないかと考えています。

ある特定の問題解決を使用する際に、いくつかソリューションの候補はあげますが、いざ決める時にその人の「思考」が詰まっています。
その思考のことをノウハウというのではないかと考えてるようになりました。

何かの意思決定をすることは することを決める ことですが、これは しないことを決める と同義です。
しかしながら、することを決める ノウハウは様々な場で知見が共有される一方で、しないことを決める ノウハウは語られることは少なく、そもそも語られなかったりして、「なぜ選ばなかったのか?」「なぜしなかったのか?」ということはあまり知見として広まっていないように感じています。

意思決定の瞬間の思考を深掘りするときに、どうして選ばなかったのかをしっかり説明できるようになりたいし、意思決定する際には「しない」理由にも目を向けると、自身の価値判断の手数が増えていき、武器が増えていくように感じます。

たくさんの選ばない理由に触れ、吸収し続けることの大事さを最近感じる出来事があったので、備忘録としてまとめて見ました。

おわり

depでginを入れる

やったこと

depをつかってginをinstallして動かすまで。

depのinstall方法

以下を見て下さい。

ema-hiro.hatenablog.com

depでginを入れる

depでginをinstallします。

# projectのROOTにいるとする。
$ cd ./src/{project_name}
$ dep init
$ dep ensure -add github.com/gin-gonic/gin

終わり...

のはずだった。

しかし、goファイルがないよ!!! というエラーが出る事が分かったので、src/{project_name} 直下に main.go を置いてpackage mainを書いて再度 dep ensure -add をすることでginをDLする事ができます。

広告を非表示にする

go ✕ ajaxを書いてみた

サマリ

goで簡易的なajax通信するアプリを作ったのでそのメモ

構成

環境

ディレクトリ構成

- src
  - app
    - main.go
    - handler
      - handler.go
    - render
      - render.go
- templates
  - index.tmpl

sampleコード

main.go

var port = "8080"
func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", handler.Top).Methods("GET")
    {
        router.HandleFunc("/sample", handler.SamplePost).Methods("POST")
    }
    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), router); err != nil {
        log.Fatalf("err: %v", err)
    }
}

handler/handler.go

func SamplePost(w http.ResponseWriter, r *http.Request) {
    // postのリクエストを処理する
  client := http.Client{}
  req, err := http.NewRequest("POST", "http://sample.com/create", nil)
    if err != nil {
        log.Fatalf("build request error. err: %v", err)
    }
    // form をparseする
    if err := r.ParseForm(); err != nil {
        log.Fatalf("parse form error. err: %v", err)
    }

    // 何かしらrequestのbodyにparseしたURLのパラメータを入れ込む処理か何かが入る
  /*
    何かしらの処理
  */
  
    resp, err := client.Do(req)
    if resp.Status != "200 OK" {
        log.Fatalf("http request failed. code: %v", resp.Status)
    }
  json.NewEncoder(w).Encode(&resp)
}

index.tmpl のjsの部分

var url = $("form").attr("action");
var p = $("form").serialize()
$.ajax({
  url: url,
  type: "post",
  data: p,
}).done(function (data) {
  res = JSON.parse(data);
  // responseで返ってきたjsonを◯◯する
}).fail(function (err) {
  console.log(err)
});

ハマったところ

requestはそのままparseする

簡易的なajaxなので今回はjqueryを使ってさらっと書いてますが、goでPOSTリクエストを受けて、serializedされたパラメータを url.Values 型として扱うためには、ポインタとして受け渡される http.Request をそのままparseします。

// form をparseする
    if err := r.ParseForm(); err != nil {
        log.Fatalf("parse form error. err: %v", err)
    }

http.Request の持つ関数の中に PostFrom というのがありますが、これは新しく空の url.Values を作るだけで、フロントからリクエストされたrequestをparseしてくれるわけではないです。

直感的な名前がついていたために、間違って使っていて、どうしてもrequestを一度でparseできないと悩んでしまっていました。

まとめ

validationとか一切考えない超簡易的なajaxを書いてみましたが、標準のライブラリしか使っていないのに、案外簡単に書けました。

GoLandの設定をremoteで管理する

Golandの設定をremoteで管理したかったので、その設定方法をメモとして書いておきます。

背景

PC変えたりすると使っていたPCの設定が全て初期化されて1から作り直すのめんどくさいです。
PCやeditorくらいであればもしかしたら、設定ファイルをgithub等にあげて clone してくれば設定完了みたいなことはしていましたが、jetbrains系のIDEの設定までremoteで管理していなかったので、この機会にGoLandを使ってIDEのremoteめsettingをsyncの方法を記載します。

ツール

  • Settings Repository (Browse Jetbrains Plugin)
  • GithubのAccessToken (Settings -> Developer Applications -> Personal access token)

の2点を利用します。

手順

Settings Repository をDLしてきます。
Plugin -> Browse Jetbrains Plugin から Settings Repository をinstallします。

f:id:ema_hiro:20171125031928p:plain

次にGithubのAccessTokenを取得します。
個人のGithubのアカウントを作成しておき
* Settings -> Developer applications -> Personal access token に遷移してGoLand用にアクセストークンを取得します。

GoLandに戻ってきたら Settings Repository を起動します。
GithubリポジトリのURLとaccess token の入力を求められるので、上記手順で取得したgithubaccess tokenを入力します。

適当なプロジェクト開いて
「File」 -> 「Settings Repository」 に遷移します。

f:id:ema_hiro:20171125032249p:plain

この時に以下のようなwindowが表示されるので Override local でローカルを上書きします。

f:id:ema_hiro:20171125032420p:plain

※ どうも一度目は Override local をしなければならないみたいです。

あとはaccess tokenによってremoteで接続すべきGithubのレポジトリも繋がっているので、IDEを終了したタイミングでgithub上に作成したremoteのリポジトリと設定がsyncされます。
これで他端末でも同じ設定が使えます。

refs

Sharing Your IDE Settings - Help | IntelliJ IDEA

qiita.com

QiitaのAPIで遊ぶ

サマリ

APIで遊びながらgoの学習をするシリーズ第二弾で、Qiitaで記事を検索するクライアント goota をgoで書きました。

コードはこちら

github.com

demo

f:id:ema_hiro:20171124023348g:plain

ざっくり仕様

Requirement

  • tagを指定できる。
  • tagはカンマ区切りでOR条件で検索出来る。
  • ストック数が100以上の記事にする。
  • 簡易的なAjaxを使ったSPAとする

はまったところ

Qiitaの仕様が変わって、従来のStocksがQIitaでは「いいね」を指していたので、最初クエリを組み立てる時に likes_count を設定してやろうとしても一件も記事が返ってこなくて困ってた。

refs

qiita API Document

qiita.com

「Qiita APIで投稿一覧を取得するときに、検索クエリをORでつなぐ時の注意点」

qiita.com

広告を非表示にする