この記事でできること:
サードパーティ SteamSpy API の 推定オーナー数(owners) や タグ投票(tags) を活用し、規模感の把握/ジャンル傾向の可視化/分析の優先度付けに使う最小パターンを紹介します。
ただし 一部環境では Cloudflare により取得できないことがあるため、本記事は Optional(任意) です。コードは 失敗時に自動でフォールバック(公式ストアの appdetails/appreviews)へ切り替わる構成になっています。
全体像:👉 Steamガイド(入口ページ)
AppIDの取り方:👉 Step1:AppIDを見つける5つの方法
まずは 30秒の実行前チェック
- ブラウザで
https://steamspy.com/api.php?request=appdetails&appid=730を開く。
→ JSONが表示されれば“使える回線”の可能性が高い/「Just a moment…」等のHTMLならブロック中。 - Python のアクセスは1秒以上の間隔を空け、失敗時はスキップして続行(本記事のコードはその設計です)。
SteamSpyで取得できる主な項目(抜粋)
| キー | 意味 | メモ |
|---|---|---|
name, developer, publisher | 基本属性 | 表記揺れあり |
owners | 推定オーナー数(例: 200000..500000) | 範囲→中央値に寄せて近似する例を後述 |
ccu | 前日ピークCCU(推定) | 瞬間ではなく日次ピーク系の近似 |
positive, negative, userscore, score_rank | レビュー集計 | 傾向把握用 |
average_forever, median_forever | 累計平均/中央値プレイ時間(分) | やり込み度 |
price, initialprice, discount | 価格・割引 | スナップショット |
tags | タグ名→投票数(辞書) | 特徴量として有用 |
注意: いずれも推定値です。厳密な実数として扱わず、規模感や優先度付けの材料に使いましょう。
クイックスタート(失敗時は自動フォールバック)【requestsのみ】
まずは SteamSpy を試し、ダメなら 公式ストアAPI(appdetails)と レビュー集計(appreviews)へ切替。
単体の appid は 2246340(モンスターハンター ワイルズ)にしています。
import requests, json, time
APPID = 2246340 # ← 単体サンプルはここだけ差し替えればOK
UA = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124 Safari/537.36",
"Referer":"https://steamspy.com/"}
def owners_to_midpoint(owners_range: str) -> int | None:
try:
lo, hi = owners_range.split("..")
return (int(lo) + int(hi)) // 2
except Exception:
return None
def steamspy_appdetails_optional(appid: int, tries=2, sleep=1.2):
"""SteamSpyが取れたら dict、403/HTML/未収録なら None を返す(落ちない設計)"""
for i in range(tries):
try:
r = requests.get("https://steamspy.com/api.php",
params={"request":"appdetails","appid":appid},
headers=UA, timeout=30)
ct = (r.headers.get("Content-Type") or "").lower()
if r.ok and "json" in ct:
j = r.json()
return j if (j.get("name") or j.get("owners")) else None
if r.status_code in (429, 503):
time.sleep(sleep*(i+1)); continue
return None # 403/HTMLなどはNoneでスキップ
except requests.RequestException:
time.sleep(sleep*(i+1))
return None
def store_appdetails(appid: int, cc="jp", lang="japanese"):
"""公式ストアメタ(型/発売日/価格/ジャンル等)"""
r = requests.get("https://store.steampowered.com/api/appdetails",
params={"appids": appid, "cc": cc, "l": lang}, timeout=30)
node = (r.json().get(str(appid)) or {})
return node.get("data") if node.get("success") else None
def appreviews_summary(appid: int):
"""レビュー集計(総数/好評率)。本文は取得しない"""
r = requests.get(f"https://store.steampowered.com/appreviews/{appid}",
params={"json":1, "language":"all", "purchase_type":"all", "num_per_page":0},
timeout=30)
return (r.json().get("query_summary") or {}) if r.ok else {}
# --- 実行 ---
spy = steamspy_appdetails_optional(APPID)
meta = store_appdetails(APPID) or {}
rev = appreviews_summary(APPID)
print("=== BASIC (store appdetails) ===")
print("name:", meta.get("name"))
print("release:", (meta.get("release_date") or {}).get("date"), " type:", meta.get("type"))
print("price_overview:", json.dumps(meta.get("price_overview") or {}, ensure_ascii=False))
print("genres:", [g["description"] for g in (meta.get("genres") or [])])
print("\n=== REVIEWS SUMMARY (appreviews) ===")
print({k: rev.get(k) for k in ("total_reviews","total_positive","total_negative",
"review_score","review_score_desc")})
if spy:
owners_raw = spy.get("owners")
owners_mid = owners_to_midpoint(owners_raw or "")
top_tags = sorted((spy.get("tags") or {}).items(), key=lambda x: -x[1])[:5]
print("\n=== STEAMSPY (optional) ===")
print("owners(raw):", owners_raw, " owners(mid):", owners_mid)
print("top tags:", top_tags)
else:
print("\n[SteamSpyはブロック/未収録のためスキップしました]")
実行結果を確認
=== BASIC (store appdetails) ===
name: (タイトル名)
release: (発売日) type: game
price_overview: {"final_formatted": "¥x,xxx", ...}
genres: ['Action', 'Adventure', ...]
=== REVIEWS SUMMARY (appreviews) ===
{'total_reviews': 12345, 'total_positive': 10000, 'total_negative': 2345, 'review_score': 8, 'review_score_desc': 'Very Positive'}
[SteamSpyはブロック/未収録のためスキップしました]
公式ラッパを使う(steamspypi) ※同じくOptional
requests 直叩きより通るケースもありますが、同じ回線がブロックされていれば結果は出ません。失敗時はやさしく診断して終わります。
# %pip install steamspypi
import time, steamspypi
appid = 2246340 # ← 単体サンプル
def steamspy_appdetails_pi(appid: int, tries=2, sleep=1.0):
last = None
for i in range(tries):
try:
d = steamspypi.download({"request":"appdetails","appid":appid})
if d and (d.get("name") or d.get("owners")):
return {"ok": True, "data": d}
return {"ok": False, "reason": "not_found_or_empty"}
except Exception as e:
last = str(e)
time.sleep(sleep*(i+1))
return {"ok": False, "reason": "exception", "error": last}
res = steamspy_appdetails_pi(appid)
print(res if not res["ok"] else {k:res["data"].get(k) for k in ("name","owners")})
複数タイトルを一括取得 → DataFrame化(取れた分だけ)
[570, 730, 1172470, 2246340] を対象に、SteamSpyが取れたタイトルだけを表にします(owners_mid と 上位タグ3件を列に展開)。
import time, requests, pandas as pd
APPIDS = [570, 730, 1172470, 2246340] # 2246340 を追加
def owners_to_midpoint(owners_range: str) -> int | None:
try:
lo, hi = owners_range.split("..")
return (int(lo) + int(hi)) // 2
except Exception:
return None
def steamspy_optional(appid: int):
UA = {"User-Agent":"Mozilla/5.0", "Referer":"https://steamspy.com/"}
try:
r = requests.get("https://steamspy.com/api.php",
params={"request":"appdetails","appid":appid},
headers=UA, timeout=30)
ct = (r.headers.get("Content-Type") or "").lower()
if r.ok and "json" in ct:
j = r.json()
return j if (j.get("name") or j.get("owners")) else None
except requests.RequestException:
pass
return None
def fetch_many_optional(appids: list[int], sleep=0.6) -> pd.DataFrame:
rows = []
for aid in appids:
d = steamspy_optional(aid)
if d is None:
time.sleep(sleep); continue # 取れないIDはスキップ
tags = sorted((d.get("tags") or {}).items(), key=lambda x: -x[1])
rows.append({
"appid": aid,
"name": d.get("name"),
"owners_mid": owners_to_midpoint(d.get("owners","")),
"owners_raw": d.get("owners"),
"ccu_peak_yday": d.get("ccu"),
"userscore": d.get("userscore"),
"tag_top1": tags[0][0] if len(tags)>0 else None,
"tag_top2": tags[1][0] if len(tags)>1 else None,
"tag_top3": tags[2][0] if len(tags)>2 else None,
})
time.sleep(sleep)
return pd.DataFrame(rows)
df = fetch_many_optional(APPIDS)
print(df.head())
日次スナップショットで“推移化”(成功分だけ追記)
毎日1回だけ回し、取れたIDだけCSVに追記します。
(CCUの瞬間値は公式APIで別収集 → 👉 同時接続の自前収集と可視化(時系列))
"""
config:
APPIDS = [570, 730, 1172470, 2246340]
頻度 = 日次(cron/タスクスケジューラ/GitHub Actions など)
"""
import csv, time, requests, datetime as dt
from pathlib import Path
APPIDS = [570, 730, 1172470, 2246340]
OUT = Path("steamspy_daily.csv")
UA = {"User-Agent":"Mozilla/5.0", "Referer":"https://steamspy.com/"}
def owners_to_midpoint(owners_range: str) -> int | None:
try:
lo, hi = owners_range.split("..")
return (int(lo) + int(hi)) // 2
except Exception:
return None
def today_utc(): return dt.datetime.utcnow().date().isoformat()
def fetch_row_optional(appid:int):
try:
r = requests.get("https://steamspy.com/api.php",
params={"request":"appdetails","appid":appid},
headers=UA, timeout=30)
ct = (r.headers.get("Content-Type") or "").lower()
if not (r.ok and "json" in ct): return None
d = r.json()
if not (d.get("name") or d.get("owners")): return None
return {
"date_utc": today_utc(),
"appid": appid,
"name": d.get("name"),
"owners_mid": owners_to_midpoint(d.get("owners","")),
"owners_raw": d.get("owners"),
"ccu_peak_yday": d.get("ccu"),
"userscore": d.get("userscore")
}
except requests.RequestException:
return None
def append_daily():
is_new = not OUT.exists()
with OUT.open("a", newline="", encoding="utf-8") as f:
fields = ["date_utc","appid","name","owners_mid","owners_raw","ccu_peak_yday","userscore"]
w = csv.DictWriter(f, fieldnames=fields)
if is_new: w.writeheader()
for aid in APPIDS:
row = fetch_row_optional(aid)
if row: w.writerow(row)
time.sleep(0.8) # マナー
print("appended:", OUT)
append_daily()
「タグ×規模ウェイト」ランキング(取れた分だけ)
owners_mid × votes でざっくり重み付け。SteamSpyが返せたIDだけ集計します。
import pandas as pd, requests, time
APPIDS = [570, 730, 1172470, 2246340]
UA = {"User-Agent":"Mozilla/5.0", "Referer":"https://steamspy.com/"}
def owners_to_midpoint(owners_range: str) -> int | None:
try:
lo, hi = owners_range.split("..")
return (int(lo) + int(hi)) // 2
except Exception:
return None
def steamspy_optional(appid: int):
try:
r = requests.get("https://steamspy.com/api.php",
params={"request":"appdetails","appid":appid},
headers=UA, timeout=30)
ct = (r.headers.get("Content-Type") or "").lower()
if r.ok and "json" in ct:
j = r.json()
return j if (j.get("name") or j.get("owners")) else None
except requests.RequestException:
pass
return None
def tag_weight_table_optional(appids: list[int]) -> pd.DataFrame:
rows = []
for aid in appids:
d = steamspy_optional(aid)
if not d:
time.sleep(0.6); continue
owners_mid = owners_to_midpoint(d.get("owners",""))
for tag, votes in (d.get("tags") or {}).items():
rows.append({"tag": tag,
"votes": votes,
"owners_mid": owners_mid,
"weight": (owners_mid or 0)*votes})
time.sleep(0.6)
if not rows: return pd.DataFrame()
return (pd.DataFrame(rows)
.groupby("tag", as_index=False)
.agg(votes_sum=("votes","sum"),
owners_sum=("owners_mid","sum"),
weight_sum=("weight","sum"))
.sort_values("weight_sum", ascending=False))
tbl = tag_weight_table_optional(APPIDS)
print(tbl.head(10))
読み方・実務Tips
- 推定前提:
owners/ccuは推定。順位・規模感の当たりに使い、決定は複数指標で。 - 失敗はスキップ:ブロック/未収録は珍しくありません。落ちない実装で“取れた分だけ”前に進む。
- 日次キャッシュ:日次1回だけ試行→成功分だけ保存。長期で埋まっていきます。
- 代替指標:
appreviewsの総レビュー数/好評率、自前のCCU日次ピーク、appdetailsの genres/categories など。
次に読む(関連How-to)
- 【How-to】レビュー本文の取得と要約(
appreviews) 👉 /steam-appreviews-howto/ - 【How-to】同時接続の自前収集と可視化(時系列) 👉 /steam-ccu-logger/
- 【How-to】価格・タグ・発売情報を取る(
appdetails) 👉 /steam-appdetails-howto/ - 入口に戻る 👉 /steam-guide/ / これまで:Step1 👉 /steam-appid-howto/
免責とポリシー
本記事は公開情報の整理を目的としています。SteamSpyは第三者サービスであり、アクセス制限や仕様変更で取得できない場合があります。必ず利用規約・法令・引用ルールを遵守し、取得不能時は本記事のフォールバック手順をご利用ください。