Claude Codeで楽天商品検索APIの競合価格を価格帯表にする
楽天市場の商品検索APIで競合商品の価格を取得し、Claude Codeに取得スクリプトと価格帯の集計を書かせる手順です。1ページ30件の上限、送料別の扱い、利用規約の確認点まで扱います。
楽天市場の競合価格は、商品検索API(Rakuten Ichiba Item Search API)にキーワードかジャンルIDを渡せば、JSONで取れます。Claude Codeには、取得スクリプトと価格帯の集計スクリプトを書かせ、実行して出力を読ませます。この記事では、そのための準備、2本のスクリプト、集計で価格を読み違えやすい点、使う前に読んでおきたい利用規約の条文を順に扱います。
画像から価格を拾う手順は競合店のチラシ画像で価格調査をClaudeに任せる手順にあります。ここではAPIで機械的に取れる側を扱います。
1回のリクエストで何が取れるか
エンドポイントは https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20260701 で、末尾の数字がAPIのバージョンです。旧版として2026-04-01の版も別に載っています。コードを書かせる前に、Claude Codeへ渡すURLの版を揃えておきます。
対象は楽天市場に出品されている商品で、共同出品・オークション・フリマ・C2Cの商品は含まれません。検索の入口は4つあり、キーワード・ジャンルID・商品コード・ショップコードのどれか1つが必須です。
取得件数に関わる上限
1ページの件数(hits)
1〜30
既定は30
ページ番号(page)
1〜100
pageCountの最大も100
keywordの長さ
128
半角文字数
1ページは最大30件、ページは最大100です。単純に掛けると、1つの検索条件で辿れるのは30件×100ページで3,000件までになります。ヒット件数がそれを超える広いキーワードは、後述のとおり価格帯で条件を割って取ります。
価格比較で使う項目は次のとおりです。
| 項目 | 内容 |
|---|---|
itemPrice | 内容商品の価格 |
itemPriceMin1〜itemPriceMax3 | 内容価格の最小・最大(全商品 / 検索可能 / 購入可能の3種類) |
postageFlag | 内容0が送料込み、1が送料別 |
taxFlag | 内容0が税込、1が税別 |
availability | 内容0が在庫なし、1が購入可能 |
shopName / shopCode | 内容出品ショップ |
reviewCount / reviewAverage | 内容レビュー件数と平均評価 |
pointRate | 内容商品単位のポイント倍率。24時間以内に終わる倍率は出ない |
genreId | 内容商品のジャンルID |
価格帯の条件は minPrice と maxPrice で渡せます。どちらも0より大きく999,999,999より小さい整数で、maxPriceはminPriceより大きくします。sort には +itemPrice(安い順)や -reviewCount(レビュー数の多い順)が指定できます。+ はURLエンコードが必要なので、%2BitemPrice と書きます。
返す項目は elements=itemName,itemPrice,itemUrl のようにカンマ区切りで絞れます。formatVersion=2 を付けると、items[0].item.itemName ではなく items[0].itemName で参照できる形になります。後のスクリプトはこちらの形を前提にしています。
準備 — キーを渡す側とClaude Codeの権限
リクエストには applicationId と accessKey の両方が要ります。accessKey はヘッダーでもクエリでも渡せる項目で、仕様の表に「NEW」と付いています。取得方法は商品検索APIのページから「Check here」のリンクで案内されています。
2つのキーは、リポジトリに書かずシェルの環境変数にします。
export RAKUTEN_APP_ID="(アプリID)"
export RAKUTEN_ACCESS_KEY="(アクセスキー)"
claudeClaude Codeには、キーが書かれたファイルを読ませない設定も入れておきます。.env に置いている場合は、プロジェクトの .claude/settings.json に Read の拒否ルールを書きます。
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)"]
}
}Read のdenyルールが対象にするのは、Claudeのファイルツールによる読み取りです。cat などシェルコマンド経由の読み取りまで止まるとは書かれていないため、キー本体は環境変数に置き、ファイルには書かない形にします。
規約で先に確認しておく条文
APIの利用には楽天会員としての登録が前提で、利用規約(英訳は参考で、日本語の原文が正)に禁止行為が並んでいます。競合価格を集める用途で、読み合わせておきたい条文は次のとおりです。
- 10条(7): 取得した情報(商品情報を含む)を、楽天が別に定めた目的以外に使うこと、定めた方法以外で複製・加工すること
- 10条(9): 取得した情報を、不特定多数と共有できる場所に保存すること
- 10条(10): 特定の人しかアクセスできない環境でAPIを使うこと(楽天が明示的に許可した場合を除く)
- 10条(4): 楽天アフィリエイト以外の方法でAPIから収入を得ること
- 10条(6): 楽天グループと競合しうるサービスを、APIを使って提供すること
- 7条3項: アクセス数・頻度などの利用制限を楽天が別に定められること
競合価格の表を社内の会議資料に使う、というだけの用途がこれらのどれに当たるかは、条文の文面だけでは判断できません。とくに(10)は「特定の人しか入れない環境」を対象にしているので、社内ツールに組み込む前にアプリの登録内容と照らし合わせます。不明な点は楽天ウェブサービス側に問い合わせるのが確実です。
取得した価格のCSVをGitHubの公開リポジトリやチャットの共有チャンネルに置く運用は、(9)の「不特定多数と共有できる場所」に近づきます。表は手元のローカルに置き、共有するのは集計結果の数字にとどめる運用が、条文から読める範囲では無難です。また、アプリに楽天の商品情報を表示する場合は、楽天市場へのリンクを置くことが8条4項で求められ、13条は楽天が求めたときに提供元の表示を出すと定めています。
Claude Codeに渡す指示とCLAUDE.md
最初の依頼は、「スクリプトを書いて」だけにしないほうが安全です。仕様の制約を先に渡すと、書き直しが減ります。CLAUDE.md に、次のような断片を置きます。
## 楽天商品検索API(競合価格)
- 認証は環境変数 RAKUTEN_APP_ID / RAKUTEN_ACCESS_KEY。値をコードやログに出さない
- エンドポイントは .../IchibaItem/Search/20260701 、formatVersion=2 を使う
- hits は最大30、page は最大100。同じURLを短時間に連打しない(リクエスト間に1秒以上あける)
- 取得結果は data/ 配下のCSVに保存する。リポジトリにコミットしない(.gitignore に追加)
- 取得日 fetchedAt を必ず列に残す。価格は日々変わる
- 送料は postageFlag で別管理し、itemPrice に足して比較しない依頼文は、取得と集計を分けて出します。
楽天商品検索APIから「(競合商品のキーワード)」の在庫あり商品を取得し、
data/items.csv に保存する fetch_rakuten.py を作って。
列は itemCode, shopCode, shopName, itemName, itemPrice, postageFlag,
availability, reviewCount, reviewAverage, pointRate, itemUrl, fetchedAt。
429 と 503 は待ってリトライ、404 は0件として扱う。
まだ集計はしない。作ったら少ないページ数で実行して、先頭5行を見せて。取得スクリプトが固まってから、集計を任せます。取得と集計を同じ依頼にすると、数字が変なとき、APIの取りこぼしか計算の誤りかを切り分けられません。
取得スクリプトの例
次は、上の指示で書かせる形の一例です。標準ライブラリだけで動き、商品コードで重複を除き、取得日を列に残します。
import csv, json, os, sys, time, datetime
import urllib.error, urllib.parse, urllib.request
ENDPOINT = "https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20260701"
FIELDS = ["fetchedAt", "itemCode", "shopCode", "shopName", "itemName",
"itemPrice", "postageFlag", "availability", "reviewCount",
"reviewAverage", "pointRate", "itemUrl"]
def call(params):
q = {**params, "applicationId": os.environ["RAKUTEN_APP_ID"],
"accessKey": os.environ["RAKUTEN_ACCESS_KEY"],
"format": "json", "formatVersion": 2}
url = ENDPOINT + "?" + urllib.parse.urlencode(q, quote_via=urllib.parse.quote)
for wait in (5, 15, 45):
try:
with urllib.request.urlopen(url, timeout=30) as r:
return json.load(r)
except urllib.error.HTTPError as e:
if e.code == 404:
return {"items": [], "pageCount": 0}
if e.code in (429, 500, 503):
time.sleep(wait)
continue
sys.exit(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')}")
sys.exit("リトライ上限に達しました")
def main(keyword, out, max_pages=10, **extra):
now = datetime.date.today().isoformat()
seen, rows = set(), []
for page in range(1, max_pages + 1):
data = call({"keyword": keyword, "hits": 30, "page": page,
"availability": 1, **extra})
for it in data.get("items", []):
if it["itemCode"] in seen:
continue
seen.add(it["itemCode"])
rows.append({"fetchedAt": now, **{k: it.get(k, "") for k in FIELDS[1:]}})
if page >= data.get("pageCount", 0):
break
time.sleep(1)
with open(out, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=FIELDS)
w.writeheader()
w.writerows(rows)
print(f"{len(rows)} 件を {out} に保存しました")
if __name__ == "__main__":
kw, out = sys.argv[1], sys.argv[2]
pages = int(sys.argv[3]) if len(sys.argv) > 3 else 10
main(kw, out, pages)このコードは、仕様の表(パラメータ名・上限・エラーコード)に合わせて組んだ例です。実行前に、RAKUTEN_APP_ID と RAKUTEN_ACCESS_KEY の2つを渡して、まず python3 fetch_rakuten.py "(キーワード)" data/items.csv 1 のようにページ数1で試します。最初から10ページ回すと、キーの誤りやパラメータの不備に気づくのが遅れます。
エラーの見分け方は次のとおりです。
- 400:
specify valid applicationId(キー未指定)、またはkeyword parameter is not valid(1文字だけのキーワードなど) - 404: 該当データなし。0件のときもこれが返る
- 429: リクエスト過多。時間をおいて再試行する
- 500・503: 楽天側の内部エラー・メンテナンス
スクリプトは404を0件として扱うので、キーワードのつづりを間違えても「エラーにならず0件」になります。0件のときは、件数を表示して気づけるようにしておきます。
価格帯表に集計する
集計は、件数・四分位・価格帯ごとの件数・出品数の多いショップを出す形にしました。区切りの金額は、競合商品の価格感に合わせて EDGES を書き換えます。
import csv, statistics, sys
from collections import Counter
EDGES = [0, 1000, 2000, 3000, 5000, 10000, float("inf")]
def label(lo, hi):
return f"{lo:,}円以上" if hi == float("inf") else f"{lo:,}〜{hi - 1:,}円"
def main(path):
with open(path, encoding="utf-8") as f:
rows = list(csv.DictReader(f))
if not rows:
sys.exit("0件です")
prices = sorted(int(r["itemPrice"]) for r in rows)
q = statistics.quantiles(prices, n=4) if len(prices) >= 2 else [prices[0]] * 3
print(f"件数 {len(prices)} / 最安 {prices[0]:,} / 第1四分位 {q[0]:,.0f} / "
f"中央値 {q[1]:,.0f} / 第3四分位 {q[2]:,.0f} / 最高 {prices[-1]:,}")
print("\n| 価格帯 | 件数 | 割合 | 送料別の件数 |\n|---|---|---|---|")
for lo, hi in zip(EDGES, EDGES[1:]):
inb = [r for r in rows if lo <= int(r["itemPrice"]) < hi]
ship = sum(1 for r in inb if r["postageFlag"] == "1")
print(f"| {label(lo, hi)} | {len(inb)} | {len(inb) / len(rows):.0%} | {ship} |")
print("\n出品数の多いショップ")
for shop, n in Counter(r["shopName"] for r in rows).most_common(5):
print(f"- {shop}: {n}件")
if __name__ == "__main__":
main(sys.argv[1])集計の実行は、取得したCSVを渡すだけです。
python3 price_bands.py data/items.csv出力は「件数・最安・四分位・最高」の1行と、価格帯ごとの件数の表、出品数の多いショップです。この表を会議資料に貼る前に、Claudeへ「CSVの件数と表の合計が一致するか」「最安値の行のitemUrlを見せて」と聞かせると、集計の取りこぼしを拾えます。
条件が広すぎるときは価格帯で割る
3,000件を超えるキーワードは、minPrice と maxPrice で価格帯ごとに分けて取ります。たとえば0〜2,999円、3,000〜5,999円のように条件を分け、それぞれで30件×100ページの範囲に収めます。count に総ヒット数が返るので、まず1ページ目だけ取って件数を見て、分け方を決めます。
ジャンルIDで絞るなら、ジャンル検索API(Rakuten Ichiba Genre Search API)の genreId=0 で最上位のジャンルが引け、children に子ジャンルが返ります。商品検索側は genreInformationFlag=1 を付けると、各ジャンルの商品数も返ります。競合の商品がどのジャンルに属するかは、genreId を決めてからキーワードを足すと、検索対象が揃います。
価格の読み違えを防ぐ見方
APIの価格は、そのまま競合の売価と読めないことがあります。
落とし穴
送料込みと送料別が混ざった価格帯表になる
itemPrice は商品の価格で、送料は含みません。postageFlag が1(送料別)の商品と0(送料込み)の商品を同じ表に並べると、実際の支払額の順位が入れ替わります。集計スクリプトに「送料別の件数」の列を入れているのは、このためです。送料込みだけに絞るなら、リクエストに postageFlag=1 を付けます。リクエストの1は「送料込み・送料無料のみ」で、レスポンスの1(送料別)とは意味が逆です。
sort=+itemPrice で安い順に3ページだけ取ると、最安の90件だけの価格帯表になります。全体の分布を見たいときは標準順で多めに取り、最安値を調べたいときだけ安い順にします。
startTime と endTime が返る商品は、時間限定セールの設定があるものです。取得日の価格が、セール期間だけの価格かもしれません。
ポイント還元を含めた実質価格は、APIの値だけでは比べられません。pointRate は商品単位のポイント倍率で、ショップ単位の倍率は返りません。
キーワード検索の結果には、型番違いや付属品違いが混じります。自社商品との対応は、商品名だけでClaudeに任せず、itemCode(ショップコード:商品番号の形)で人が確定した対応表を渡します。この判断はチラシの価格調査でも、人が受け持つ部分として残しています。
自社の価格表示が景品表示法の面で適切かは、別の話です。点検の観点は二重価格表示を景品表示法の観点でClaudeに点検させるにまとめています。
週次で再取得する
価格は日々動くので、同じ条件で定期的に取り直し、取得日付きのCSVを残すと推移が見られます。手元で実行するなら、claude -p で非対話実行にできます。許可するツールは --allowedTools で絞ります。
claude -p "fetch_rakuten.py で取得し、price_bands.py の結果を前回と比べて差分を3行で" \
--allowedTools "Read,Bash(python3 fetch_rakuten.py *),Bash(python3 price_bands.py *)"--allowedTools は、プロンプトを出さずに使えるツールを指定する仕組みです。上のように実行コマンドの形まで書いておけば、それ以外のシェルコマンドは自動承認されません。スクリプトの引数を変える場合は、許可パターンの * に収まる形に保ちます。
取得の頻度は、同じURLへの短時間の連続アクセスが応答停止を招くという注意書きに合わせます。1ページごとに間をあけ、同じ条件を続けて取り直さないようにします。
つまずきやすい点
- 日本語のキーワードは、値の部分だけをUTF-8でURLエンコードします。リクエスト全体をエンコードしません。例に使ったスクリプトは
urllib.parse.quoteで値ごとに変換しています - 複数キーワードは半角スペースで区切り、既定はAND検索です。OR検索は
orFlag=1で、「(A AND B) OR C」のような複合条件は指定できません - 除外したい語は
NGKeywordで渡せます。キーワードと同じ書式です - 1文字のキーワードは400になります。ひらがな・カタカナ・記号は2文字以上が必要です
attributeFlag=1を使うときは、0以外のgenreIdが要りますasurakuFlagは2024年7月1日以降、常に0として扱われます。「あす楽」での絞り込みは使えません
仕様は変わりうるものです。規約には、楽天がWebサービスの仕様をいつでも変更できると書かれています。スクリプトが急に動かなくなったときは、まず商品検索APIのページでバージョンと入力パラメータの変更を見直します。