Steam データ活用

SteamSpyの使い方(Optional/取得できない場合あり・フォールバック付き)【推定オーナー数・タグ集計・CCU指標】Python最小サンプル

2025年10月17日

この記事でできること:
サードパーティ SteamSpy API推定オーナー数(owners)タグ投票(tags) を活用し、規模感の把握/ジャンル傾向の可視化/分析の優先度付けに使う最小パターンを紹介します。
ただし 一部環境では Cloudflare により取得できないことがあるため、本記事は Optional(任意) です。コードは 失敗時に自動でフォールバック(公式ストアの appdetailsappreviews)へ切り替わる構成になっています。

全体像:👉 Steamガイド(入口ページ)
AppIDの取り方:👉 Step1:AppIDを見つける5つの方法


まずは 30秒の実行前チェック

  1. ブラウザで https://steamspy.com/api.php?request=appdetails&appid=730 を開く。
    JSONが表示されれば“使える回線”の可能性が高い/「Just a moment…」等のHTMLならブロック中。
  2. 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 を試し、ダメなら 公式ストアAPIappdetails)と レビュー集計appreviews)へ切替。
単体の appid2246340(モンスターハンター ワイルズ)にしています。

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日次ピークappdetailsgenres/categories など。

次に読む(関連How-to)


免責とポリシー

本記事は公開情報の整理を目的としています。SteamSpyは第三者サービスであり、アクセス制限や仕様変更で取得できない場合があります。必ず利用規約・法令・引用ルールを遵守し、取得不能時は本記事のフォールバック手順をご利用ください。

-Steam, データ活用
-, , , , , ,