# 楽天ジャンル検索APIをPythonのオブジェクトシリアライザであるPickleを使ってローカルにキャッシュ
楽天市場のランキングAPIを使って取得したデータを使って、👇のAlgoliaのUnified InstantSearch E-Commerce(Beta)のデモを作っているのですが、ランキングAPIのジャンルIDを元に、カテゴリの文字列を取得しようとする場合、楽天ジャンル検索APIを叩く必要があります。
playing with @algolia's Unified InstantSearch E-Commerce w/ #Rakuten Ranking API data🔎https://t.co/9kDnejHeOU
— Eiji Shinohara (@shinodogg) May 16, 2020
楽天のランキングAPIで取ってきたデータをAlgoliaのUnified InstantSearchに食わせて遊んでる :) #AlgoliaJP pic.twitter.com/cWS2XiifO0
# 楽天ジャンル検索API
楽天のジャンルは階層構造になっていて、ジャンルIDで検索をかけると👇のような形でデータが取れてきます。
- 親ジャンル
- 自ジャンル
- 兄弟ジャンル
- 子ジャンル
例えばジャンルID 304587 で検索すると👇のようなレスポンス。とは言え、Unified InstantSearch E-Commerceの階層カテゴリは2つまでなので、自分的には『genreLevel:1』と『genreLevel:2』のgenreNameだけ欲しいな、と。
{
"parents": [
{
"parent": {
"genreLevel": 1,
"genreName": "水・ソフトドリンク",
"genreId": 100316
}
}
],
"current": {
"genreLevel": 2,
"genreName": "水・ミネラルウォーター",
"genreId": 201351
},
〜以下略〜
楽天市場ランキングAPIは最大で34ページ(1ページ30件)なので約1000件のデータ取れてくるのですが、1000回ジャンル検索APIを叩かなければならなくなってしまうのはちょっとしんどいな、と。。 ジャンル検索APIは1秒に1回以上叩くとスロットリングしてしまうので、1000回叩いて合計1000秒待つのはちょっと辛すぎる。
# PythonのPickleを使って辞書データをローカルにキャッシュ
楽天のジャンルは定期的に変わるので、どこかのタイミングで取得し直さなければならないのですが、とりあえずは一回取得したジャンルIDとgenreLevel1とgenreLevel2のデータはローカルのファイルに保存しておくことにしました。
普通のアスキーなフラットファイルで都度シークして、あったら使うし、なかったら追記、、みたいに書いても大したことないのですが、Pythonには便利なシリアライザなPickleというライブラリがあって、これを使うことにしました。
具体的には、
👇のようにインポートしてあげて
import pickle
👇最初にローカルに置いてあるcategoryっていうファイルから読み込んで変数に詰め込む。後から出てくるけどPickleでシリアライズしたデータは wb っていうバイナリでの書き込みになるので、ここでの読み込みも rb になるのが注意。(rで読み込んだら『UnicodeDecodeError: 'utf-8' codec can't decode』とか言われてハニャ?っとなりました。笑)
category_cache = ''
with open('category', 'rb') as fi:
category_cache = pickle.load(fi)
fi.close
👇取得したジャンルIDで上記のキャッシュからデータを取ってきて、あったらソレ使う
if (genre_id in category_cache):
categories = category_cache[genre_id]
👇ローカルファイルのキャッシュにカテゴリデータが存在しなかった場合はAPIをコールして取りに行く。(もうちょっとキレイな実装をしたい人生でしたが…笑)
まずはAPI叩いて結果をローカルのファイルに改行とかインデントがある方法で保存(parentとかcurrentとかどうでもよくて上から2つのgenreNameを取りたいだけなのでgrep的なことすることにした…。いや、genreLevelの順番が保証された形でJSONでレスポンスされなかったら、、とかあるかもしれないけど、とりあえずデモなので。。。)
else:
genre_url = "https://app.rakuten.co.jp/services/api/IchibaGenre/Search/20140222?format=json&applicationId=『自分のアプリケーションID』&genreId=" + genre_id
r_get = requests.get(genre_url)
genre_data = r_get.json()
f = open('genre.json', 'w', encoding='utf8')
json.dump(genre_data, f, indent=2, ensure_ascii=False)
👇 genreNameで引っ掛けてきて上から2つを取得。Unified InstantSearch E-Commerceの階層型リストは2つまでなのでそれようのカウントを持っておく。なんていうか、この涙ぐましいやつ…笑
ジャンルの名前で取ってきて、トリムして、前後の要らない文字列を取り除いてカテゴリの1階層と2階層を配列に詰める、と。。。
categories_count = 0
f = open('genre.json', 'r')
line = f.readline()
while line:
if re.search(r'genreName', line):
stripped_line = line.strip()
front_removed = stripped_line.replace('\"genreName\": \"', '')
back_removed = front_removed.replace('\",', '')
categories.append(back_removed)
categories_count += 1
if categories_count == 2:
break
line = f.readline()
f.close()
で、カテゴリの第1階層と第2階層が入ったリストはローカルのファイルにキャッシュとして追加。ここでpickle.dumpしたヤツは wb ってことでバイナリで保存するのよ、と。
category_cache[genre_id] = categories
with open('category', 'wb') as fo:
pickle.dump(category_cache, fo)
fo.close
そして、ジャンル検索APIを叩いた場合は次の処理でスロットリングされてしまわないように念の為1秒待つ、と。
time.sleep(1)
ここまで来ると categories 配列にはキャッシュから取れた場合、もしくは、APIをコールした場合のどちらかで値は入ってるはずだから、Algoliaの階層型ファセットに渡してやる用に👇こんな風にしてやる。
AlgoliaのHierarchical facetsの仕様で2階層目は > が無いといけないので注意。
categories_lvl0 = categories[0]
categories_lvl1 = categories[0] + " > " + categories[1]
# Algoliaにインデクシング
ランキングAPIの各ページ毎にAlgolia用のJSONファイルを作ってあげて、後はそれをAlgoliaに放り込むだけ。 algolia-cliこういう時便利。
# これからのTODO
今のところ処理は👇のようになっている
- cURLでランキングAPIの各ページのデータを取ってきてファイルに保存するシェル
- Pythonで上記1.で配置したファイルを読んでAlgolia用のJSONを作成(ジャンルIDからカテゴリファセット用の文字列抽出)
- algolia-cliを使って上記2.で生成されたJSONをインデクシング
このランキングデータは日時でかなり変わってくるので、上記3.を一時的なインデックスにして、一時的なインデックスの作成が終わったらそのインデックスデータを本物用にコピーしてあげるようにすれば、表向きにメンテナンス的なものを出さなくてもイイのかなぁ。
そして、上記をServerlessで行って、ホスティングはS3(の手前にCloudFront+ACM)な構成に今週中に出来たらめでたく公開という流れにさせていただきたいと思いますmm
# Unified InstantSearch E-CommerceのUIも、、
まだ表記がドルのままだったりするところがあるし、Unified InstantSearch E-Commerceは既存のWebサイトに抵触しないように、オーバーレイさせる仕様になっているのですが、デモサイトであるがゆえ、最初からコンテンツが出て欲しいので👇のようにやっちゃってるとことかあるので、もうちょっとキレイにしないと、、
const [isOverlayShowing, setIsOverlayShowing] = React.useState(
// Object.keys(searchState).length > 0
true
);
そして、こんな時に限ってアレやコレやと仕事のタスクが押し寄せてくるのねん。。。