Steam Web APIでデータ収集


PCゲームのダウンロード兼販売プラットフォームであるSteamでは
Web APIが公開されており、ユーザー情報やサーバー情報など様々なデータを取得することができる。

https://partner.steamgames.com/doc/webapi_overview

Web APIの使用方法を学ぶついでに、データ分析の練習に使用するデータを集めることにした。
Steam Web APIの機能は、PyPIにてpython用のラッパーがいくつか公開されているが、今回は自分で機能を実装してみる。

## 準備するもの

- steamアカウント（必須）

[https://store.steampowered.com/login/](https://store.steampowered.com/login/)

- apiキー（必須）

steamアカウントを使って発行。Web API利用時に必要。

[http://steamcommunity.com/dev/apikey](http://steamcommunity.com/dev/apikey)

```python
api_key = "XXXXX"
```

- steamID（任意）

自分のでも良いし他人のでも構わない。
ただしプロフィール公開度が「公開」になっているアカウントでないとデータが取得できない。

https://support.steampowered.com/kb_article.php?ref=4113-YUDH-6401

[steamidfinder](https://steamidfinder.com/)で適当にアカウントを検索するのも手。

```python
uid = "XXXXX"
```

## ユーザー情報取得関数の作成

IPlayerService インターフェースを実装する。

https://partner.steamgames.com/doc/webapi/IPlayerService

ドキュメントを参考に、型となるurlを作成し、json形式で受け取るよう指定。
受け取ったデータをデコードして、"response"キーに対応した値を取得。

```python
import json
import requests

def GetOwnedGames(api_key, uid):
    url = "http://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key={}&steamid={}&format=json".format(
        api_key, uid
    )
    r = requests.get(url)
    data = json.loads(r.text)
    return data["response"]
```

```python
ownd_games = GetOwnedGames(api_key, uid)
print(ownd_games["game_count"]) #所持ゲーム数
print(ownd_games["games"]) # 所持ゲームの詳細（appid, プレイ時間等）
```

その他の機能もurlと引数を変えるだけで関数化できる。

数が多いので他の関数の実装方法は省略（[jupyter notebook](https://github.com/Chronona/FetchSteamUserInfo/blob/main/fetch_user_info.ipynb)）。


## 使用例

steamレベルが非常に高いことで有名な、**St4ck**氏のデータを取得してみた(2021/03/21)

[Steamにて、大金をかけアカウントを「レベル4000」に到達させたユーザー現る。カタールの王族であるとの噂](https://automaton-media.com/articles/newsjp/20190118-83483/)
（補足）Steamレベルについて
> Steamレベルとは、Steamプロフィールの右上に表示されている数字である。レベルを上げる手順を、まずざっくりと説明しよう。Steamにてトレーディングカード対応のゲームを一定時間プレイすることで、カードをドロップ。ドロップだけではカードはコンプリートできないので、カードを購入してタイトルごとのカードを揃え、バッジが完成する。バッジを完成させれば経験値がたまっていき、指定の経験値をためるとレベルがアップするという流れだ。（https://automaton-media.com/articles/newsjp/20190118-83483/）





```python
uid = 76561198023414915
```

### 所有しているゲーム

```python
ownd_games = GetOwnedGames(api_key, uid)
ownd_games["game_count"]

4728
```
所有ゲーム数　4728


```python
ownd_games["games"][:5]

[{'appid': 10,
  'playtime_forever': 0,
  'playtime_windows_forever': 0,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0},
 {'appid': 80,
  'playtime_forever': 0,
  'playtime_windows_forever': 0,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0},
 {'appid': 100,
  'playtime_forever': 0,
  'playtime_windows_forever': 0,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0},
 {'appid': 300,
  'playtime_forever': 0,
  'playtime_windows_forever': 0,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0},
 {'appid': 20,
  'playtime_forever': 0,
  'playtime_windows_forever': 0,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0}]
```

所有しているゲームのIDとプレイ時間を取得

```python
# 最近遊んだゲーム数を表示
recently_played_games["total_count"]
3
```

### 最近遊んだゲーム

```python
# 最近遊んだゲーム
recently_played_games["games"][:3]

[{'appid': 433850,
  'name': 'Z1 Battle Royale',
  'playtime_2weeks': 4871,
  'playtime_forever': 131291,
  'img_icon_url': 'aee7491abfd812e2fbb4ec3326ad5f4b85c8137a',
  'img_logo_url': 'b83cffe1839f3ecaec18754844b21bda9b397659',
  'playtime_windows_forever': 4965,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0},
 {'appid': 730,
  'name': 'Counter-Strike: Global Offensive',
  'playtime_2weeks': 49,
  'playtime_forever': 414870,
  'img_icon_url': '69f7ebe2735c366c65c0b33dae00e12dc40edbe4',
  'img_logo_url': 'd0595ff02f5c79fd19b06f4d6165c3fda2372820',
  'playtime_windows_forever': 1200,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0},
 {'appid': 1238860,
  'name': 'Battlefield 4™ ',
  'playtime_2weeks': 31,
  'playtime_forever': 13969,
  'img_icon_url': 'fe0bd4bd2adde978ca411a4b2725c1b6d72717ef',
  'img_logo_url': '6f4f3bf49342fcf38f9e7c63b9d9148b42c53b0b',
  'playtime_windows_forever': 13969,
  'playtime_mac_forever': 0,
  'playtime_linux_forever': 0}]
```
最近（おそらく2週間以内）遊んだゲームが表示される。こちらはゲーム名も明記されるらしい

### Steamレベル

```python
# レベルの表示
steam_level = GetSteamLevel(api_key, uid)
steam_level
5000
```

ニュース記事では4000超だったのが5000に増えていた。

### バッジ

```python
# バッジ情報
badges["badges"][:3]

[{'badgeid': 48,
  'level': 1070,
  'completion_time': 1607333397,
  'xp': 2070,
  'scarcity': 4795},
 {'badgeid': 49,
  'level': 1,
  'completion_time': 1616276919,
  'xp': 10,
  'scarcity': 3201589},
 {'badgeid': 13,
  'level': 4608,
  'completion_time': 1615953180,
  'xp': 4858,
  'scarcity': 17877}]
```

```python
# 総経験値
badges["player_xp"]
125265813
```

基本的にバッジ１つ作成で100 xpであることを考えると恐ろしい経験値。

### コミュニティバッジ進捗

```python
# コミュニティバッジ進捗
community_Badge_Progress
[{'questid': 115, 'completed': True},
 {'questid': 128, 'completed': True},
 {'questid': 134, 'completed': True},
 {'questid': 133, 'completed': True},
 {'questid': 132, 'completed': True},
 {'questid': 108, 'completed': True},
 {'questid': 113, 'completed': True},
 {'questid': 112, 'completed': True},
 {'questid': 104, 'completed': True},
 {'questid': 105, 'completed': True},
 {'questid': 106, 'completed': False},
 {'questid': 114, 'completed': True},
 {'questid': 119, 'completed': True},
 {'questid': 121, 'completed': True},
 {'questid': 110, 'completed': True},
 {'questid': 111, 'completed': True},
 {'questid': 126, 'completed': True},
 {'questid': 101, 'completed': True},
 {'questid': 103, 'completed': True},
 {'questid': 118, 'completed': True},
 {'questid': 117, 'completed': True},
 {'questid': 109, 'completed': True},
 {'questid': 124, 'completed': True},
 {'questid': 127, 'completed': True},
 {'questid': 125, 'completed': True},
 {'questid': 123, 'completed': True},
 {'questid': 120, 'completed': True},
 {'questid': 122, 'completed': True}]
```

コミュニティバッジ入手のために課されたクエストの達成状況

上から順に

```
    "Steamガードを有効化",
    "携帯番号の追加",
    "Steamモバイルアプリを利用",
    "ブロードキャストを表示",
    "ストアディスカバリーキューを使用",
    "ウィッシュリストにゲームを追加",
    "フレンドリストにフレンドを追加",
    "ゲームをプレイ",
    "ゲームのレビューを書く",
    "スクリーンショットを投稿",
    "ムービーを投稿",
    "ワークショップのアイテムを評価",
    "ワークショップのアイテムをサブスクライブ",
    "Steamオーバーレイからガイドを表示",
    "コミュニティプロフィールにアバターを設定",
    "コミュニティプロフィールに本名を設定",
    "プロフィールの背景を設定",
    "グループに参加",
    "フレンドのプロフィールにコメント",
    "アクティビティフィード内のコンテンツを評価",
    "フレンドに向けてコメントを投稿",
    "フレンドのスクリーンショットにコメント",
    "ゲームバッジを作成",
    "プロフィール上にバッジを張り付け",
    "チャット内で絵文字を使用",
    "掲示板を検索",
    "コミュニテイマーケットを利用",
    "トレードを利用",
```

11番目のクエストである動画投稿は、まだやったことがないらしい。

上記のユーザー情報は、基本的にSteamのプロフィールページからも閲覧可能なものばかりだが、Web APIを使用すれば高速かつ大量のデータを得ることができる。
（細かな違いとしては、OS別でのゲームプレイ時間の閲覧や、携帯番号を追加しているかの確認はプロフィールページ上では不可能だった。APIを利用した方がより多くの情報を得られる様子）

## データ収集

steamidは17桁。いくつかのsteamアカウントをhttps://steamidfinder.com/ で適当に検索すると、
初めの"76561198"は全員同じだった。

```python
for i in tqdm(range(5)):
    # 76561198000000000 ~ 76561198999999999が格納されたジェネレータから候補ID抽出
    random_numbers = (i for i in range(76561198000000000, 76561199000000000))
    ids = random.sample(list(random_numbers), 100)
    result = GetPlayerSummaries(api_key, ids)
    print(len(result))
# 54, 65, 62, 71, 59。期待値62.2

# IDの先頭8桁目を変えてみる
for i in tqdm(range(5)):
    # 76561197000000000 ~ 76561197999999999が格納されたジェネレータから候補ID抽出
    random_numbers = (i for i in range(76561197000000000, 76561198000000000))
    ids = random.sample(list(random_numbers), 100)
    result = GetPlayerSummaries(api_key, ids)
    print(len(result))
# 3,2,1,2,2 ほとんどヒットしない

for i in tqdm(range(5)):
    # 76561199000000000 ~ 76561199999999999が格納されたジェネレータから候補ID抽出
    random_numbers = (i for i in range(76561199000000000, 76561200000000000))
    ids = random.sample(list(random_numbers), 100)
    result = GetPlayerSummaries(api_key, ids)
    print(len(result))
# 19, 14, 12, 18, 28 微妙。
```

この部分をsteamidの識別子として利用していると仮定して、残り9桁を乱数で生成。
デタラメなidから実在するアカウントを1万件探して情報を取得。

標本は10000件程度欲しいので、多めに見積もって20000個乱数を生成する。想定通りなら12000件ぐらい取れるはず。

```python

# ID候補が入ったジェネレータ作成
random_numbers = (i for i in range(76561198000000000, 76561199000000000))
# 2万件抽出
ids = random.sample(list(random_numbers), 20000)
# 100件ずつしかリクエストできないので分割
ids_list = list(np.array_split(ids, 200))
# 出力ファイル初期化
pd.DataFrame(
    {
        "steamid": [],
        "personaname": [],
        "timecreated": [],
    }
).to_csv("user_info.csv")

for i in tqdm(ids_list):
    # リクエスト用のID文字列作成
    ids_str = ",".join([str(j) for j in i])
    # ユーザー情報取得
    result = GetPlayerSummaries(api_key, ids_str)
    # ID, name, createtimeを取得して辞書化しリストに格納
    user_info_list = []
    for i in result:
        user_info = {}
        user_info["steamid"] = i["steamid"]
        user_info["personaname"] = i["personaname"]
        # 作成日がないユーザーに対しては例外処理
        try:
            user_info["timecreated"] = i["timecreated"]
        except KeyError:
            user_info["timecreated"] = ""
        user_info_list.append(user_info)

    # 出力
    # 大体60件ずつ追加されていることを確認したいのでindexは残す
    pd.DataFrame(user_info_list).to_csv("user_id.csv", mode="a", header=False)
```

確認すると11508件入手できた。
入手したIDを使ってユーザー情報を取得。1件あたり1~2秒程度かかる。

```python
# ユーザー情報を取得
user_info_list = []
steam_ids = df["steamid"]
for id in tqdm(steam_ids):
    user_info = {}
    user_info["steamid"] = id

    # 情報が取得できないユーザーがいたので例外処理
    # プロフィールを非表示設定にしている場合は取得できない？

    # 所持ゲーム
    ownd_games = GetOwnedGames(api_key, id)
    try:
        user_info["game_count"] = ownd_games["game_count"]
        user_info["games"] = ownd_games["games"]
    except KeyError:
        user_info["game_count"] = None
        user_info["games"] = None

    # バッジ情報
    badges = GetBadges(api_key, id)
    try:
        user_info["badges"] = badges["badges"]
        user_info["player_xp"] = badges["player_xp"]
        user_info["player_level"] = badges["player_level"]
        # コミュニティバッジ進捗
        community_Badge_Progress = GetCommunityBadgeProgress(api_key, id, 2)
        user_info["cleard_quests"] = len(
            [i for i in community_Badge_Progress if i["completed"] == True]
        )
        user_info["quests"] = community_Badge_Progress
    except KeyError:
        user_info["badges"] = None
        user_info["player_xp"] = None
        user_info["player_level"] = None
        user_info["cleard_quests"] = None
        user_info["quests"] = None

    user_info_list.append(user_info)
    # ウェイト時間を設けておく
    time.sleep(0.5)

# DF化→jsonで出力
badge_df = pd.DataFrame(user_info_list)
badge_df.to_json("user_info.json")
```

次回は、作成したjsonファイルを使ってデータを分析してみる予定。

作成したスクリプトは以下のページで閲覧できます。

[https://github.com/Chronona/FetchSteamUserInfo/blob/main/fetch_user_info.ipynb](https://github.com/Chronona/FetchSteamUserInfo/blob/main/fetch_user_info.ipynb)