凛「今日集まってもらったのは他でもないにゃ」
海未「他も何も・・・」
希「思い当たる節が何もないんやけど?」
凛「こ、細かいことはどうでもいいにゃ!とにかく、ちょっとやりたいことがあってAmazonの商品情報を扱うAPIを叩くプログラムを作ってたんだよ」
希「なんでまた・・・」
凛「最近新商品の買い逃しが多いにゃ。だから、発売日に知らせてくれる仕組みがほしいなって思って」
海未「まあ、その需要は分かりますが・・・」
凛「それで、Amazon Product Advertising APIっていうのがあるから、Elixirから試しに使ってみたコードを紹介するにゃ」
凛「Mixでプロジェクト作るよ。mix new paatest
とかして、まずはmix.exs
に依存関係の追加にゃ」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
defmodule Paatest.Mixfile do use Mix.Project def project do [app: :paatest, version: "0.0.1", elixir: "~> 1.1", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps] end # Configuration for the OTP application # # Type "mix help compile.app" for more information def application do [applications: [:httpoison, :tzdata]] end # Dependencies can be Hex packages: # # {:mydep, "~> 0.3.0"} # # Or git/path repositories: # # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} # # Type "mix help deps" for more examples and options defp deps do [ {:httpoison, "~> 0.8.0"}, {:timex, "~> 0.19.2"} ] end end |
凛「HTTPoisonっていうのがHTTP通信のパッケージで、他にもHTTPotionなんていうのもあったけど、なんとなく強そうだからHTTPoisonにしたよ」
海未「その判断根拠には色々と突っ込みたいところはありますが」
凛「もう1つは時間を扱うTimexっていうパッケージを使うにゃ」
希「ああ、時計メーカーの・・・」
凛「それ凛も思ったよ・・・」
凛「次はメインロジックの実装にゃ。AWSのアカウントとかは各自用意でお願いね」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
defmodule Paatest do require Record Record.defrecord :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl") Record.defrecord :xmlText, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl") use Timex @key "********************" @sec "************************************" @tag "kotorisonoda" def get_product asins do asin = Enum.join asins, "," url = "http://webservices.amazon.co.jp/onca/xml?AWSAccessKeyId=#{@key}&AssociateTag=#{@tag}&IdType=ASIN&ItemId=#{asin}&Operation=ItemLookup&ResponseGroup=ItemAttributes%2CItemIds&Service=AWSECommerceService" signed_url = url |> URI.parse |> timestamp |> sign case HTTPoison.get! signed_url do %{status_code: 200, body: body} -> parse_response body %{status_code: code} -> IO.puts "error: #{code}" end end defp timestamp url do timestamped = url.query |> URI.decode_query |> Map.put_new("Timestamp", DateFormat.format!(Date.now, "{ISOz}")) |> URI.encode_query Map.put url, :query, timestamped end defp sign url do ordered_query = url.query |> URI.decode_query |> Enum.sort |> URI.encode_query sig = :crypto.hmac(:sha256, @sec, Enum.join(["GET", url.host, url.path, ordered_query], "\n")) |> Base.encode64 |> URI.encode_www_form signed_query = "#{url.query}&Signature=#{sig}" Map.put url, :query, signed_query end defp parse_response res do {doc, []} = res |> :binary.bin_to_list |> :xmerl_scan.string items = :xmerl_xpath.string('/ItemLookupResponse/Items/Item', doc) Enum.map items, fn item -> [title_node] = :xmerl_xpath.string('//ItemAttributes/Title', item) [title_text] = xmlElement title_node, :content title = xmlText title_text, :value IO.puts title [rel_node] = :xmerl_xpath.string('//ItemAttributes/ReleaseDate', item) [rel_text] = xmlElement rel_node, :content rel = xmlText rel_text, :value [y, m, d] = Enum.map(String.split(List.to_string(rel), "-"), fn x -> String.to_integer x end) rel = Date.from({{y, m, d}, {0, 0, 0}}, :local) IO.inspect rel end end end |
凛「APIの仕様は公式ドキュメント見てもらうとして・・・」
Amazon Product Advertising API
凛「厄介なのがリクエストへの署名にゃ」
海未「ええと・・・TimestampとSignatureが必要で、SignatureはAWS秘密キーを元にしたbase64エンコードのHMAC_SHA256署名・・・確かに、難しそうです」
凛「それぞれtimestamp
関数とsign
関数に実装してあるけど、タイムスタンプの方はそんなに難しくないにゃ。Timexを使うとDate
とかDateFormat
とかのモジュールが使えるから」
海未「それで現在時刻を付けてやればいいのですね」
凛「ここで付ける時刻、日本時間じゃなくてGMTだから注意にゃ。最初Date.local
でやってて、ずっとはまってて・・・」
希「となると、難題は署名の方やね」
凛「まずは、署名するときはパラメータがソートされてないといけないから、一旦マップにしてEnum.sort
でソートしてからクエリストリングに戻してるんだよ」
凛「最終的にはこういう形のものが署名対象になるにゃ」
1 2 3 4 5 |
GET webservices.amazon.co.jp /onca/xml AWSAccessKeyId=*******************&AssociateTag=kotorisonoda&IdType=ASIN&ItemId=B0191R6HL8%2C4048656198%2CB017X8K1AK&Operation=ItemLookup&ResponseGroup=ItemAttributes%2CItemIds&Service=AWSECommerceService&Timestamp=2015-12-17T14%3A08%3A08.284Z |
凛「ここから署名を生成するのが:crypto.hmac
で、秘密鍵を使ってSHA256で署名を生成してるにゃ。あとはBASE64でエンコードしてやれば署名のできあがり」
希「んん、そこまで難しわけでもないんかな。分かってしまえば」
凛「ちなみに全般的にURIモジュール使ってるけど、これはURL文字列とそれを分解したマップの相互変換みたいなのをやってくれるモジュールにゃ。Elixirの標準機能だからドキュメントにも載ってるよ」
凛「結果がXMLで返ってくるんだけど、これを読む方が大変だったような気がするにゃ」
希「うん、全然読めへんわ」
凛「Erlangのxmerlっていうのを使ってるんだ。基本はDOMで、:xmerl_scan.string
で文字列からDOMツリーを構築できるよ。けど、ここでいう文字列は文字リスト、シングルクォートで囲う方の文字列にゃ」
海未「そういえば、文字列は2種類あるのでした」
凛「このxmerlで使う文字列は全部文字リストだと思えばよさそうにゃ」
凛「あとは、目的の要素をXPathで・・・:xmerl_xpath.string
を使うと、XPathを文字列で指定してノードのリストを取得できるよ」
海未「それで、ここのXPathもシングルクォートなのですね」
凛「うん。で、xmlElement
とかxmlText
とかxmerlに用意されてる関数を使って、中身を読み出してあげるにゃ」
海未「最初の方でdefrecord
しているものですね」
凛「xmlElement <node>, :content
みたいにすると、その要素の内容が取れるから、例えば<Title>
ノードに入ってる値がほしければこれを使うとテキストノードが取れるよ」
希「まだテキスト自体ではないんやったね」
凛「テキストノードからテキストを読み出すのはxmlText <node>, :value
みたいにするにゃ」
凛「一応これで、ASINを指定すると商品名と発売日を取得できるようになったにゃ」
海未「なるほど、これを膨らませていけば、最初に言っていた通知の仕組みも作れそうですね」
凛「そのへんはまた、うまくできたらお話しするね」