Steam データ活用

SteamSpyの使い方|推定オーナー数・タグ・CCU指標をPythonで取得

2025年10月17日

この記事では、第三者サービスである SteamSpy のAPIを使って、Steamゲームの推定オーナー数、タグ、CCU指標などをPythonで取得する方法を解説します。

Steamレビュー分析では、レビュー本文やストア情報だけでなく、「そのゲームがどの程度の規模なのか」「どのようなタグで見られているのか」を補助的に把握したい場面があります。SteamSpyを使うと、推定オーナー数やタグ投票数などを取得できる場合があります。

ただし、SteamSpyはSteam公式APIではありません。環境や時期によって取得できない場合があるため、本記事では 任意の補助データ として扱い、取得できない場合は appdetailsappreviews の情報で代替する方針にしています。

この記事でできること

  • SteamSpyで取得できる主な情報を理解する
  • 推定オーナー数を取得する
  • タグ情報を取得して上位タグを確認する
  • SteamSpyのCCU指標を取得する
  • 複数タイトルのSteamSpy情報をDataFrame化する
  • 取得できない場合に落ちない実装を作る
  • appdetailsappreviews を使った代替方法を理解する

想定読者

  • Steamゲームの規模感を補助指標として見たい方
  • 推定オーナー数やタグ情報をPythonで取得したい方
  • レビュー分析対象の優先度を決めたい方
  • Steamレビュー分析にストア情報以外の補助データを加えたい方
  • 取得失敗を前提に安全なコードを書きたい方

SteamSpyとは

SteamSpyは、Steamゲームに関する推定データを提供している第三者サービスです。API経由で、推定オーナー数、タグ、レビュー集計、CCU指標などを取得できる場合があります。

代表的なAPI URLは以下です。

https://steamspy.com/api.php?request=appdetails&appid=730

ただし、SteamSpyはSteam公式のWeb APIではありません。アクセス制限、仕様変更、Cloudflare等によるブロック、対象タイトルの未収録などにより、取得できない場合があります。

そのため、本記事ではSteamSpyを必須データ源ではなく、取得できた場合に使う補助データとして扱います。

事前準備

この記事では、SteamのAppIDが分かっている前提で進めます。AppIDの確認方法は以下の記事で解説しています。

SteamのAppIDを確認する方法|URL・検索・API・Python一括取得

また、SteamSpyが取得できない場合の代替情報として、以下の記事で扱う appdetailsappreviews を使います。

SteamSpyで取得できる主な項目

SteamSpyの appdetails で取得できる主な項目は以下です。

項目意味見方
nameタイトル名表記ゆれがある場合があります
developer開発元ストア情報と異なる場合があります
publisher販売元表記ゆれに注意します
owners推定オーナー数200000 .. 500000 のような範囲で返る場合があります
ccuCCU系指標公式APIの現在値とは意味が異なる可能性があります
positive好評数レビュー規模の参考になります
negative不評数レビュー規模の参考になります
userscoreユーザースコア補助的な評価指標として扱います
average_forever平均プレイ時間分単位で返る場合があります
median_forever中央値プレイ時間やり込み度の参考になります
tagsタグと投票数ゲーム特徴の補助指標として使えます

これらは推定値や第三者サービス上の集計値です。厳密な実数として扱うのではなく、規模感や比較の補助として使うのが安全です。

使用ライブラリ

HTTPリクエストには requests、表形式の整形には pandas を使います。

pip install -U requests pandas

まず取得できるか確認する

SteamSpyは環境によって取得できない場合があります。まず、ブラウザで以下のURLを開き、JSONが表示されるか確認します。

https://steamspy.com/api.php?request=appdetails&appid=730

JSONが表示されれば取得できる可能性があります。一方で、HTMLページやエラー画面が表示される場合は、Pythonからも取得できない可能性があります。

取得できない場合でも、Steamレビュー分析自体は可能です。価格・ジャンルは appdetails、レビュー件数や好評率は appreviews、現在の同時接続数は公式Web APIで代替できます。

SteamSpyから1タイトルの情報を取得する

まず、AppIDを1つ指定してSteamSpyの appdetails を取得します。取得できない場合は空の辞書を返すようにします。

import time
import requests


def fetch_steamspy_appdetails(
    appid: int,
    sleep_sec: float = 1.0,
) -> dict:
    """
    SteamSpy APIから1タイトルの詳細情報を取得する。
    取得できない場合は空dictを返す。
    """
    url = "https://steamspy.com/api.php"

    headers = {
        "User-Agent": "Mozilla/5.0",
        "Referer": "https://steamspy.com/",
    }

    params = {
        "request": "appdetails",
        "appid": appid,
    }

    try:
        response = requests.get(
            url,
            params=params,
            headers=headers,
            timeout=30,
        )

        content_type = response.headers.get("Content-Type", "").lower()

        if not response.ok:
            return {}

        if "json" not in content_type:
            return {}

        data = response.json()

        if not data or not isinstance(data, dict):
            return {}

        return data

    except Exception:
        return {}

    finally:
        time.sleep(sleep_sec)


appid = 730

data = fetch_steamspy_appdetails(appid)

print(data.keys() if data else "SteamSpyから取得できませんでした。")

このコードでは、取得に失敗してもエラーで停止しないようにしています。SteamSpyは任意のデータ源として扱うため、取得できない場合はスキップする方針が安全です。

推定オーナー数を扱いやすく変換する

SteamSpyの owners は、200000 .. 500000 のような範囲文字列で返る場合があります。分析では、下限・上限・中央値に分解しておくと扱いやすくなります。

def parse_owners_range(owners: str) -> dict:
    """
    SteamSpyのowners範囲文字列を下限・上限・中央値に変換する。
    例: "200000 .. 500000"
    """
    if not owners or not isinstance(owners, str):
        return {
            "owners_min": None,
            "owners_max": None,
            "owners_mid": None,
        }

    text = owners.replace(",", "").replace(" ", "")

    try:
        low, high = text.split("..")
        low = int(low)
        high = int(high)
        mid = (low + high) // 2
    except Exception:
        return {
            "owners_min": None,
            "owners_max": None,
            "owners_mid": None,
        }

    return {
        "owners_min": low,
        "owners_max": high,
        "owners_mid": mid,
    }


owners_info = parse_owners_range(data.get("owners") if data else None)

print(owners_info)

owners_mid はあくまで範囲の中央値です。実際の所有者数そのものではないため、厳密な数値としてではなく、規模感の比較に使います。

必要な項目だけを取り出す

SteamSpyのレスポンスから、分析で使いやすい項目だけを取り出します。

def extract_steamspy_fields(
    appid: int,
    data: dict,
) -> dict:
    """
    SteamSpyのレスポンスから主要項目を取り出す。
    """
    if not data:
        return {
            "appid": appid,
            "steamspy_available": False,
        }

    owners_info = parse_owners_range(data.get("owners"))

    tags = data.get("tags") or {}

    sorted_tags = sorted(
        tags.items(),
        key=lambda item: item[1],
        reverse=True,
    )

    return {
        "appid": appid,
        "steamspy_available": True,
        "name": data.get("name"),
        "developer": data.get("developer"),
        "publisher": data.get("publisher"),
        "owners_raw": data.get("owners"),
        "owners_min": owners_info["owners_min"],
        "owners_max": owners_info["owners_max"],
        "owners_mid": owners_info["owners_mid"],
        "ccu": data.get("ccu"),
        "positive": data.get("positive"),
        "negative": data.get("negative"),
        "userscore": data.get("userscore"),
        "average_forever": data.get("average_forever"),
        "median_forever": data.get("median_forever"),
        "price": data.get("price"),
        "initialprice": data.get("initialprice"),
        "discount": data.get("discount"),
        "tag_top1": sorted_tags[0][0] if len(sorted_tags) > 0 else None,
        "tag_top2": sorted_tags[1][0] if len(sorted_tags) > 1 else None,
        "tag_top3": sorted_tags[2][0] if len(sorted_tags) > 2 else None,
        "tags": tags,
    }


appid = 730

data = fetch_steamspy_appdetails(appid)
row = extract_steamspy_fields(appid, data)

print(row)

上位タグや推定オーナー数を取り出しておくと、複数タイトルを比較するときに使いやすくなります。

複数タイトルを一括取得する

複数のAppIDについて、SteamSpyから取得できる分だけ取得します。取得できないタイトルは steamspy_available=False として残します。

import pandas as pd


def fetch_steamspy_many(
    appids: list[int],
    sleep_sec: float = 1.0,
) -> pd.DataFrame:
    """
    複数AppIDのSteamSpy情報を取得する。
    取得失敗しても処理を止めない。
    """
    rows = []

    for appid in appids:
        data = fetch_steamspy_appdetails(
            appid=appid,
            sleep_sec=sleep_sec,
        )

        row = extract_steamspy_fields(
            appid=appid,
            data=data,
        )

        rows.append(row)

    return pd.DataFrame(rows)


appids = [
    570,
    730,
    1172470,
    2246340,
]

df_steamspy = fetch_steamspy_many(appids)

print(df_steamspy[[
    "appid",
    "steamspy_available",
    "name",
    "owners_raw",
    "owners_mid",
    "ccu",
    "tag_top1",
    "tag_top2",
    "tag_top3",
]])

取得結果はCSVとして保存できます。

df_steamspy.to_csv(
    "steamspy_appdetails.csv",
    index=False,
    encoding="utf-8-sig",
)

SteamSpyは取得できない場合があるため、CSVには取得可否を示す steamspy_available を残しておくと後で確認しやすくなります。

タグ情報を縦持ちテーブルに変換する

SteamSpyの tags は辞書形式で返ります。タグ分析を行う場合は、縦持ちのテーブルに変換すると扱いやすくなります。

def steamspy_tags_to_dataframe(
    appid: int,
    data: dict,
) -> pd.DataFrame:
    """
    SteamSpyのtags辞書を縦持ちDataFrameに変換する。
    """
    if not data:
        return pd.DataFrame()

    tags = data.get("tags") or {}

    rows = []

    for tag, votes in tags.items():
        rows.append({
            "appid": appid,
            "tag": tag,
            "votes": votes,
        })

    return pd.DataFrame(rows)


tag_frames = []

for appid in appids:
    data = fetch_steamspy_appdetails(appid)
    df_tags = steamspy_tags_to_dataframe(appid, data)

    if not df_tags.empty:
        tag_frames.append(df_tags)

df_tags_all = (
    pd.concat(tag_frames, ignore_index=True)
    if tag_frames
    else pd.DataFrame()
)

print(df_tags_all.head())

縦持ちにしておくと、タグ別の投票数ランキングや、タイトル間のタグ比較がしやすくなります。

タグ投票数を集計する

複数タイトルのタグをまとめて、投票数の多いタグを確認します。

if not df_tags_all.empty:
    tag_summary = (
        df_tags_all
        .groupby("tag", as_index=False)
        .agg(
            title_count=("appid", "nunique"),
            votes_sum=("votes", "sum"),
            votes_mean=("votes", "mean"),
        )
        .sort_values("votes_sum", ascending=False)
    )

    print(tag_summary.head(20))

votes_sum が大きいタグは、対象タイトル群で多く付与されているタグです。ただし、対象にしたタイトル数やタイトルの知名度に大きく影響されるため、結果は比較対象の範囲内で解釈します。

取得できない場合のフォールバック

SteamSpyが取得できない場合でも、Steamレビュー分析に必要な情報の多くは他の方法で補えます。

見たい情報SteamSpy代替手段
タイトル名・発売日・価格namepriceappdetails
ジャンル・カテゴリtagsappdetailsgenres / categories
レビュー件数・好評率positivenegativeappreviewsquery_summary
現在の同時接続数ccu公式Web APIの GetNumberOfCurrentPlayers
同時接続数の時系列取得できる場合でも限定的Steam同時接続数を自前収集して可視化する方法

特に、レビュー分析に必要な最低限の情報は appdetailsappreviews、公式Web APIでかなり補えます。SteamSpyは「取得できたら使う補助情報」として扱うのがおすすめです。

フォールバック付きで基本情報を取得する

ここでは、SteamSpyが取得できた場合はSteamSpyの推定オーナー数やタグを使い、取得できない場合は appdetailsappreviews の情報で最低限の表を作る例を紹介します。

def fetch_store_appdetails(
    appid: int,
    cc: str = "jp",
    lang: str = "japanese",
) -> dict:
    """
    Steam Storefront appdetails から基本情報を取得する。
    """
    url = "https://store.steampowered.com/api/appdetails"

    params = {
        "appids": appid,
        "cc": cc,
        "l": lang,
    }

    try:
        response = requests.get(
            url,
            params=params,
            timeout=30,
        )
        response.raise_for_status()

        data = response.json()
        node = data.get(str(appid), {})

        if not node.get("success"):
            return {}

        return node.get("data", {})

    except Exception:
        return {}


def fetch_appreviews_summary(
    appid: int,
) -> dict:
    """
    Steam appreviews からレビューサマリーを取得する。
    """
    url = f"https://store.steampowered.com/appreviews/{appid}"

    params = {
        "json": 1,
        "language": "all",
        "purchase_type": "all",
        "num_per_page": 0,
    }

    try:
        response = requests.get(
            url,
            params=params,
            timeout=30,
        )
        response.raise_for_status()

        data = response.json()

        return data.get("query_summary", {})

    except Exception:
        return {}

次に、SteamSpy、appdetailsappreviews をまとめて取得します。

def fetch_game_overview_with_fallback(
    appid: int,
) -> dict:
    """
    SteamSpyを優先し、取得できない場合もappdetails/appreviewsで基本情報を補う。
    """
    spy = fetch_steamspy_appdetails(appid)
    store = fetch_store_appdetails(appid)
    reviews = fetch_appreviews_summary(appid)

    spy_row = extract_steamspy_fields(appid, spy)

    genres = [
        item.get("description")
        for item in store.get("genres", [])
        if item.get("description")
    ]

    categories = [
        item.get("description")
        for item in store.get("categories", [])
        if item.get("description")
    ]

    return {
        "appid": appid,
        "name": spy_row.get("name") or store.get("name"),
        "steamspy_available": spy_row.get("steamspy_available", False),
        "owners_raw": spy_row.get("owners_raw"),
        "owners_mid": spy_row.get("owners_mid"),
        "steamspy_ccu": spy_row.get("ccu"),
        "tag_top1": spy_row.get("tag_top1"),
        "tag_top2": spy_row.get("tag_top2"),
        "tag_top3": spy_row.get("tag_top3"),
        "type": store.get("type"),
        "release_date": (store.get("release_date") or {}).get("date"),
        "is_free": store.get("is_free"),
        "genres": " / ".join(genres[:3]),
        "categories": " / ".join(categories[:3]),
        "total_reviews": reviews.get("total_reviews"),
        "total_positive": reviews.get("total_positive"),
        "total_negative": reviews.get("total_negative"),
        "review_score_desc": reviews.get("review_score_desc"),
    }


overview = fetch_game_overview_with_fallback(730)

print(overview)

このようにしておくと、SteamSpyが取れなかった場合でも、記事や比較表に必要な最低限の情報を作れます。

複数タイトルをフォールバック付きで取得する

複数タイトルの比較表を作る場合は、以下のようにループします。

def fetch_many_game_overviews(
    appids: list[int],
    sleep_sec: float = 1.0,
) -> pd.DataFrame:
    """
    複数タイトルの概要情報をフォールバック付きで取得する。
    """
    rows = []

    for appid in appids:
        row = fetch_game_overview_with_fallback(appid)
        rows.append(row)

        time.sleep(sleep_sec)

    return pd.DataFrame(rows)


df_overview = fetch_many_game_overviews(appids)

print(df_overview.head())

df_overview.to_csv(
    "steam_game_overview_with_fallback.csv",
    index=False,
    encoding="utf-8-sig",
)

steamspy_available を確認すれば、SteamSpy由来の情報が入っているタイトルと、フォールバック情報だけで構成されたタイトルを区別できます。

SteamSpy情報を見るときの注意点

SteamSpyの情報を使うときは、以下の点に注意してください。

  • Steam公式データではありません
    第三者サービスの推定値として扱います。
  • 取得できない場合があります
    アクセス制限、Cloudflare、未収録、仕様変更などにより取得できないことがあります。
  • 推定オーナー数は範囲で返る場合があります
    中央値は便宜的な近似であり、実数ではありません。
  • タグ投票数は対象タイトルの知名度に影響されます
    タグの強さを比較するときは、タイトル規模の違いを考慮します。
  • CCU指標は公式APIの現在値とは意味が異なる可能性があります
    現在の同時接続数を見たい場合は、公式Web APIの GetNumberOfCurrentPlayers を使います。
  • 短時間に大量アクセスしない
    取得できる場合でも、間隔を空けて必要な範囲だけ取得します。
  • 分析の主軸にしすぎない
    レビュー本文、ストア情報、公式APIの同時接続数などと組み合わせて補助的に使います。

レビュー分析での活用例

SteamSpyの情報は、レビュー分析の補助指標として使えます。

SteamSpy情報活用例
推定オーナー数レビュー分析対象の規模感を把握する
タグゲーム特徴やジャンル傾向を補助的に確認する
positive / negativeレビュー規模や評価傾向の参考にする
average_forever / median_foreverプレイ時間の長さから、やり込み度を推測する
ccu人気の補助指標として見る。ただし公式APIの現在値とは分けて扱う

たとえば、レビュー数が少なくても推定オーナー数が多いタイトルでは、レビュー投稿率が低い可能性があります。逆に、タグ投票数が特定ジャンルに偏っているタイトルでは、そのジャンル要素がレビュー本文にも多く出ているか確認できます。

SteamSpyを使わない場合の代替方針

SteamSpyが取得できない場合でも、Steamレビュー分析は問題なく進められます。代替方針は以下です。

取得できない外部サービスに依存しすぎないように、SteamSpyが取得できなくても分析が成立する構成にしておくことが重要です。SteamSpyは便利ですが、レビュー本文やストア情報、公式Web APIの情報を主軸にし、補助データとして使うのがおすすめです。

次に読む記事

まとめ

この記事では、SteamSpy APIを使って、推定オーナー数、タグ、CCU指標などをPythonで取得する方法を紹介しました。

SteamSpyは、ゲームの規模感やタグ傾向を補助的に見るうえで便利ですが、Steam公式データではなく、取得できない場合もあります。そのため、取得失敗を前提にした実装にし、appdetailsappreviews、公式Web APIへフォールバックできるようにしておくことが重要です。

Steamレビュー分析では、SteamSpyを主軸にするのではなく、レビュー本文、ストア情報、公式APIの同時接続数と組み合わせて、補助指標として活用するのがおすすめです。

-Steam, データ活用