この記事では、Steam Store APIの appreviews を使い、Steamレビューの評価サマリーやレビュー本文をPythonで取得する方法をまとめます。
自分のデータに合う実行経路を選ぶ
以下はWindows PowerShellでの手順です。Python 3.10以上を用意し、必要なファイルを保存したフォルダーをエクスプローラーで開き、アドレス欄に powershell と入力してEnterを押します。Get-Location で作業フォルダーを確認してください。コード全文は後ろに掲載しています。選んだ経路だけを実行し、別の経路のコードを同じファイルへつなげないでください。
以下はSteamへの通信が必要ですがAPIキーは不要です。CSVが既にある方は 取得なしの可視化、取得せず練習する方は 架空サンプルへ進んでください。
分析へ渡すなら条件記録付き実用版を選ぶ
実用版コード1個を collect_steam_reviews.py に保存します。AppIDと条件をそのファイルで確認し、下を実行します。基本例の定義コードを足す必要はありません。
python -m pip install requests
python collect_steam_reviews.py動きを段階ごとに学ぶなら基本例を選ぶ
「評価サマリーを取得する」→「レビュー本文を取得する」→「カーソルでページングする」→「CSVに保存する」の4コードだけを順に steam_reviews.py に保存します。実用版とは代替で、両方を実行しません。基本例には説明用のサマリー取得・本文取得も含まれます。
python -m pip install requests
python steam_reviews.py両経路とも保存CSVは作業フォルダー内の outputs/steam_reviews.csv。基本例は同名CSVを上書きするので過去分を退避してください。実用版は既存ファイルがあれば停止し、outputs/steam_reviews.metadata.json も作ります。可視化へはCSVとこのmetadataをセットで渡します。基本例にはmetadataがなく、後で過去の取得条件を推定して補いません。
最大5ページであり全件取得ではありません。実用版のmetadataにある termination_reason と saved_rows、送信条件を先に確認します。page_limitは上限、empty_responseは空応答です。初回の first_query_summary は保存CSVの件数・好評率とは別です。0件なら列だけのCSVになります。通信失敗を「レビューなし」と扱いません。
掲載コードと詳しい条件
SteamworksのUser Reviewsドキュメントでは、ストアページに表示されるレビュー情報を取得するAPIとして説明されています。レビュー本文はユーザー投稿に近い情報なので、公開記事では全文転載ではなく、集計・要約・傾向分析として扱います。
Python 3.10以上で、ターミナルから python -m pip install requests を実行してください。以下4つのPythonコードを上から順に同じ steam_reviews.py へ保存し、作業フォルダーで python steam_reviews.py を実行します。2・3番目は最初の requests と url、CSV保存は3番目の reviews に依存します。Notebookでは同じカーネルで上から順に実行します。
評価サマリーを取得する
import requests
appid = 2246340
url = f"https://store.steampowered.com/appreviews/{appid}"
params = {
"json": 1,
"language": "japanese",
"purchase_type": "all",
"filter": "recent",
"review_type": "all",
"cursor": "*",
"num_per_page": 1,
}
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
if data.get("success") != 1:
raise RuntimeError("Steam APIが成功を返しませんでした。")
print(data["query_summary"])公式仕様の filter は recent・updated・all です。summary は公式の選択肢ではありません。query_summary は初回レスポンスに含まれる情報で、この例も「サマリー専用API」ではなく、レビューを最大1件受け取ってサマリーを表示します。num_reviews は今回返った件数、total_reviews は指定条件に一致する総件数で、後で保存するCSVの行数とは区別してください。
レビュー本文を取得する
params = {
"json": 1,
"language": "japanese",
"purchase_type": "all",
"filter": "recent",
"num_per_page": 20,
"review_type": "all",
"cursor": "*",
}
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
if data.get("success") != 1:
raise RuntimeError("Steam APIが成功を返しませんでした。")
for review in data["reviews"][:3]:
print(review["voted_up"], review["review"][:100])カーソルでページングする
cursor は初回に *、次回に返却値を渡します。requests.get(..., params=params) がURLエンコードを行うため、手動の quote() は不要です。この例は最大5ページ・1ページ最大100件であり、全件取得ではありません。空の一覧、次カーソルの欠落・同値でも終了します。HTTPエラーやAPI失敗は0件扱いにせず例外で停止します。
import time
reviews = []
cursor = "*"
for _ in range(5):
params = {
"json": 1,
"language": "japanese",
"purchase_type": "all",
"filter": "recent",
"num_per_page": 100,
"review_type": "all",
"cursor": cursor,
}
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
if data.get("success") != 1:
raise RuntimeError("Steam APIが成功を返しませんでした。")
batch = data["reviews"]
reviews.extend(batch)
if not batch:
break
next_cursor = data.get("cursor")
if not next_cursor or next_cursor == cursor:
break
cursor = next_cursor
time.sleep(1)
print(len(reviews))CSVに保存する
import csv
from pathlib import Path
def normalize_review(review: dict) -> dict:
author = review.get("author", {})
return {
"recommendationid": review.get("recommendationid"),
"voted_up": review.get("voted_up"),
"review": review.get("review"),
"timestamp_created": review.get("timestamp_created"),
"timestamp_updated": review.get("timestamp_updated"),
"playtime_forever": author.get("playtime_forever"),
"playtime_at_review": author.get("playtime_at_review"),
"language": review.get("language"),
}
fieldnames = [
"recommendationid", "voted_up", "review",
"timestamp_created", "timestamp_updated",
"playtime_forever", "playtime_at_review", "language",
]
rows = [normalize_review(r) for r in reviews]
csv_path = Path("outputs/steam_reviews.csv")
csv_path.parent.mkdir(parents=True, exist_ok=True)
with csv_path.open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print("保存先:", csv_path.resolve())
print("保存件数:", len(rows))
if not rows:
print("0件のため、列名だけのCSVを保存しました。対象App ID・言語・取得条件を確認してください。")
with csv_path.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
assert reader.fieldnames == fieldnames
saved_count = sum(1 for _ in reader)
assert saved_count == len(rows)
print("再読込で確認した件数:", saved_count)CSVは実行時の作業フォルダーを基準に outputs/steam_reviews.csv へ保存されます。同名ファイルは上書きするため、過去分は先に退避してください。表示された絶対パスと再読込件数を確認します。0件でも列名は残り、再読込件数0で正常終了します。レビューが存在しないと断定せず、対象と条件を確認してください。
公開記事に使うときの注意
- レビュー本文を長文転載しない
- 特定ユーザーを追跡・特定しない
- 取得条件、言語、取得日、件数を明記する
- レビューは投稿者の主観であり、ゲーム全体の評価と同一視しない
- 機械翻訳や要約を使う場合は、誤読の可能性を残す
取得条件も残す実用版:CSVとmetadataをセットで保存
ここからは上の最小手順に代わる実用版です。両方を続けて実行する必要はありません。作業フォルダーに以下を collect_steam_reviews.py として保存し、同じフォルダーで実行します。前のPython実行の変数には依存しません。
import csv
import hashlib
import io
import json
import time
from datetime import datetime, timezone
from pathlib import Path
import requests
FIELDS = ['recommendationid', 'voted_up', 'review', 'timestamp_created',
'timestamp_updated', 'playtime_forever', 'playtime_at_review', 'language']
def collect(appid, max_pages=5, get=requests.get, pause=time.sleep):
if max_pages < 1:
raise ValueError('max_pagesは1以上にしてください')
started = datetime.now(timezone.utc).isoformat()
sent = dict(json=1, language='japanese', purchase_type='all', filter='recent',
num_per_page=100, review_type='all')
cursor, unique, raw_count, conflicts, duplicates = '*', {}, 0, 0, 0
summary, reason = None, 'page_limit'
for page in range(1, max_pages + 1):
response = get(f'https://store.steampowered.com/appreviews/{appid}',
params={**sent, 'cursor': cursor}, timeout=30)
response.raise_for_status()
data = response.json()
if data.get('success') != 1 or not isinstance(data.get('reviews'), list):
raise RuntimeError('API失敗または応答形式不正。CSVは保存しません')
if page == 1:
summary = data.get('query_summary')
if not isinstance(summary, dict):
raise RuntimeError('初回query_summaryがありません')
for source in data['reviews']:
rid = str(source.get('recommendationid', '')).strip()
if not rid.isdigit():
raise ValueError('レビューID不正。保存せず停止します')
row = {k: source.get(k) for k in FIELDS}
row['recommendationid'] = rid
for k in ['playtime_forever', 'playtime_at_review']:
row[k] = source.get('author', {}).get(k)
raw_count += 1
if rid in unique:
duplicates += 1
conflicts += row != unique[rid]
else:
unique[rid] = row # 同一IDは最初に受け取った内容を維持
if not data['reviews']:
reason = 'empty_response'; break
following = data.get('cursor')
if not following:
reason = 'missing_cursor'; break
if following == cursor:
reason = 'same_cursor'; break
cursor = following
if page < max_pages:
pause(1)
metadata = dict(appid=str(appid), started_at=started,
finished_at=datetime.now(timezone.utc).isoformat(),
sent_parameters=sent, first_cursor='*', max_pages=max_pages,
fetched_pages=page, termination_reason=reason,
raw_rows=raw_count, saved_rows=len(unique), duplicate_rows=duplicates,
conflicting_duplicate_rows=conflicts, duplicate_policy='first_seen',
first_query_summary=summary,
omitted_parameters={'filter_offtopic_activity': 'default: off-topic reviews excluded',
'day_range': 'not sent; not applicable to recent'},
scope='bounded API result, not all reviews or a random sample')
return list(unique.values()), metadata
def save(rows, metadata, path=Path('outputs/steam_reviews.csv')):
path = Path(path)
sidecar = path.with_suffix('.metadata.json')
# 過去の成果物を守るため、既存ファイルは上書きしない。
if path.exists() or sidecar.exists():
raise FileExistsError('以前のCSVとmetadataをセットで退避してください')
buf = io.StringIO(newline='')
writer = csv.DictWriter(buf, fieldnames=FIELDS)
writer.writeheader(); writer.writerows(rows)
raw = buf.getvalue().encode('utf-8-sig')
metadata = {**metadata, 'csv_file': path.name, 'csv_sha256': hashlib.sha256(raw).hexdigest()}
path.parent.mkdir(parents=True, exist_ok=True)
with path.open('xb') as f:
f.write(raw)
sidecar.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding='utf-8')
with path.open(encoding='utf-8-sig', newline='') as f:
assert len(list(csv.DictReader(f))) == len(rows)
print(path.resolve(), len(rows), '件', metadata['termination_reason'])
if __name__ == '__main__':
rows, metadata = collect(2246340, max_pages=5)
save(rows, metadata)
保存先は outputs/steam_reviews.csv と outputs/steam_reviews.metadata.json。既存ファイルがあれば停止するので、過去分は2ファイルをセットで退避してください。正常な0件は列名だけのCSVを保存します。HTTPエラー・API失敗・不正な応答は保存前に停止し、以前の正常なCSVを空データで上書きしません。保存途中のディスク障害等で片方しかない場合は、2ファイルの対応を確認してから再実行します。
同じ recommendationid は文字列IDで重複を除き、最初に受け取った内容を残します。後のページで内容が異なる場合も更新せず、metadataの conflicting_duplicate_rows に記録します。これは取得中の変化を記録する方針であり、常に最新の内容を選ぶものではありません。
送信する条件は sent_parameters、省略項目は omitted_parameters に分けます。今回も filter_offtopic_activity は送信しません。Steam公式仕様では、省略時はオフトピックなレビューが除外され、0を渡すと含まれます。既存の取得対象を変えないため、ここでは0を追加しません。day_range はrecentでは使いません。
終了理由は page_limit(上限)、empty_response(空応答)、same_cursor(同じカーソル)、missing_cursor(欠落)を区別します。最大5ページはSteam全レビューでも無作為標本でもありません。空応答も、この条件・取得時点における終了であり、将来を含む完全性の保証ではありません。初回の first_query_summary とCSVの保存件数は別に確認します。
保存したら、API通信なしで 投稿月別件数・好評率・投稿時プレイ時間を集計・可視化できます。レビュー本文の転載や個人の特定を行わず、取得範囲に限って読み取ります。
まとめ
appreviews を使うと、Steamレビューの評価サマリーやレビュー本文を取得できます。分析記事では、本文をそのまま載せるより、評価傾向、投稿時期、プレイ時間、話題の変化として集計する使い方が向いています。