DjangoでMOBAのランキングを作る③

## はじめに
前回の記事に引き続き、Django✕Herokuでモバイルゲーム「Mobile Legends: Bang Bang」のヒーローランキングを作成していきます。今回は特にスクレイピングとランキング表示の実装に注目します。

前回の記事はこちらです：

https://qiita.com/Kazukiya/items/901b78b931d03e81f8af

## スクレイピングでランキング作成
### モデルを作成
スクレイピングで取得したデータを保存するために、`main/models.py`にHeroMetaDataモデルを定義します。このモデルには、各ヒーローの名前、勝率、ピック率、BAN率、参照日、ランクレベル、作成日時を保存します。

```python:main/models.py
class HeroMetaData(models.Model):
    name = models.TextField()
    win_rate = models.FloatField()
    pick_rate = models.FloatField() 
    ban_rate = models.FloatField()
    reference_date = models.DateField()
    rank_level = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = (('name', 'rank_level', 'reference_date'),)
```

モデルを定義した後、マイグレーションを実行してデータベースにテーブルを作成します。ローカル環境だけでなく、Heroku環境でもマイグレーションを実行することを忘れないでください。

```bash
python manage.py makemigrations
python manage.py migrate
heroku run python manage.py migrate
```

### スクレイピングするファイルを作る
次に、スクレイピングを行うためのファイルを作成します。今回は、下記サイトからデータを取得します。必要なデータは各ヒーローの勝率、BAN率、ピック率です。

https://m.mobilelegends.com/en/rank

まず、必要なライブラリをインストールします。

```bash
pip install pandas selenium webdriver_manager beautifulsoup4
```

続いて、`main/scraper.py`にスクレイピングのロジックを記述します。スクレイピングしたデータはHeroMetaDataモデルを使ってデータベースに保存するようにしてください。

```scraper.py
import time

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from datetime import datetime
from .models import HeroMetaData


def scrape_mlbb_meta_data():
    # スクレイピングロジックを記述
    # ...
```

### 管理コマンドの作成
`main/management/commands/run_scraper.py`に、`scrape_mlbb_meta_data`関数を実行するための管理コマンドを作成します。

```python:run_scraper.py
from django.core.management.base import BaseCommand
from main.scraper import scrape_mlbb_meta_data

class Command(BaseCommand):
    def handle(self, *args, **options):
        scrape_mlbb_meta_data()
```

管理コマンドを実行し、データが正しくデータベースに保存されているか確認します。

```bash
python manage.py run_scraper
```

![スクリーンショット 2024-03-29 1.49.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/849608/e1a950cd-e923-26f2-07bf-9d7667fa9d79.png)

データが保存されていることが確認できました。

## ランキングの表示
### ビューの作成
`main/views.py`に、スクレイピングしたデータを使ってランキングを作成するロジックを記述します。

1. ヒーローのデータをz-scoreで評価し、各ヒーローの相対的な強さを計算
2. z-scoreに基づいてヒーローにランクを割り当て
3. ランクごとにヒーローの情報をグループ化
4. ビューにデータを渡す

ランキング表示には、各ヒーローの画像を使用します。画像はWordPress上で保存されており、画像URLがヒーローのデータと一緒にデータベースに保存されています。

```python:views.py
from django.shortcuts import render
from .models import Hero
from django.db.models import Max
from .models import HeroMetaData  
import pandas as pd

# 定数の定義
RANK_THRESHOLDS = [1.0, 0.5, 0, -0.5, -1.0]  # ランクの閾値
RANK_LABELS = ['S+', 'S', 'A+', 'A', 'B', 'C']  # ランクのラベル

def get_rank_from_z_score(score):
    """
    z-scoreからランクを取得する関数 
    """
    for threshold, label in zip(RANK_THRESHOLDS, RANK_LABELS):
        if score >= threshold:
            return label
    return 'C'

def hero_ranking(request):
    # Heroモデルから必要なデータを取得
    heroes = Hero.objects.all().values_list('name_jp', 'name_en', 'image_url')
    # ロジックを記述
    # ... 
```

### テンプレートの作成
ビューから渡されたデータを`main/templates/main/hero_ranking.html`で表示します。各ランクごとにヒーローの画像を表示するようにします。

```html:hero_ranking.html
<!DOCTYPE html>
<html>
    <head>
        <title>Hero List</title>
    </head>
    <body>
        <h1>ヒーローTier</h1>
        <p>Latest Update: {{ latest_date }}</p>
        <h2>S+ Heroes</h2>
        {% for hero in s_plus_heroes %}
        <img src="{{ hero.2 }}" alt="{{ hero.0 }}" width="100">
        {% endfor %}
        
        <h2>S Heroes</h2> 
        {% for hero in s_heroes %}
        <img src="{{ hero.2 }}" alt="{{ hero.0 }}" width="100">
        {% endfor %}
        
        ...
</html>
```

これでローカル環境ではランキングが表示できるようになりました。

![127.0.0.1_8000_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/849608/a668efb9-3fc6-08e3-dd73-86397889e94f.png)

## Herokuでの実行
次に、Heroku上でもスクレイピングとランキング表示ができるように設定します。

### ビルドパックの設定
`requirements.txt`に必要なライブラリを追加します。

```
...
pandas==2.2.1
selenium==4.19.0
webdriver_manager==4.0.1
beautifulsoup4==4.12.3
```

当初、以下の記事を参考にビルドパックを設定していましたが、この方法は非推奨になっていました。

https://codelabsjp.net/heroku-selenium/

https://zenn.dev/eito_blog/articles/7b9c489ebe75c8#%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0%EF%BC%88selenium%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%EF%BC%89%E7%94%A8%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4

代わりに、以下のビルドパックが推奨されていました。

https://github.com/heroku/heroku-buildpack-chrome-for-testing

このビルドパックは、ChromeとChromeDriverのバージョンを自動で合わせてくれるため、バージョン違いによるエラーを防ぐことができます。

以下のコマンドでビルドパックを追加します。

```
heroku create --buildpack https://github.com/heroku/heroku-buildpack-google-chrome.git
```

その後、アプリを再デプロイします。

```bash
git commit --allow-empty -m "Update buildpacks" 
git push heroku main
```

### 上手くいかなかった

これでスクレイピングができるようになったので、まずは下記コマンドで取ってみることにしました。

```
heroku run python manage.py run_scraper
```

ただ、残念ながら、Heroku上でスクレイピングを実行することができませんでした。スクレイピング自体はできたのですが、対象のサイトを表示した際に表示されるプライバシーポリシーを上手く閉じることができず、データを取得できませんでした。

![m.mobilelegends.com_en_rank.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/849608/94fe9b8f-ca73-679c-6970-3ffa5472ab3e.png)

ローカル環境では問題なくプライバシーポリシーを閉じられるのですが、Heroku上では何故か閉じることができません。様々な方法を試しましたが、上手くいかなかったため、Heroku上でのスクレイピングは諦めることにしました。

### データ同期の自動化
なので、ローカルでスクレイピングしたデータをJSON形式で保存し、そのデータをHerokuに同期するようにします。

`scraper.py`にJSONファイルを作成するロジックを追加します。

```python
def create_mlbb_meta_data():
    # ロジックを記載
```

`main/management/commands/run_create_hero_meta.py`を作成し、データ作成コマンドを実装します。

```python:run_create_hero_meta.py
from django.core.management.base import BaseCommand
from main.scraper import create_mlbb_meta_data

class Command(BaseCommand):
    def handle(self, *args, **options):
        create_mlbb_meta_data()
```

下記コマンドを実行するとJSONファイルを作成できます。

```
 python manage.py run_create_hero_meta
```

作成したJSONをデータベースに保存するロジックを`scraper.py`に追加します。

```python
def save_latest_meta_data_to_db():
    # ロジックを記載  
```

実行すると下記のようになります。作成しているか更新しているかを確認できるようにしました。

 ```
% python manage.py run_save_latest_meta_data_to_db
Updated: 124 records
Created: 0 records
 ```

このコマンドはHeroku側で問題なく実行できました。

```
% heroku run python manage.py run_save_latest_meta_data_to_db
Updated: 0 records
Created: 124 records
```

### GitHubアクションによる自動化
データを最新の状態に保つ工程をGitHubアクションで自動化します。

`.github/workflows/update_meta_data.yml`を作成し、自動化の手順を記述します。

```yaml:update_meta_data.yml
name: Update Meta Data  # ワークフローの名前
# ロジックを記載
...
```

GitHubのリポジトリ設定ページで、`HEROKU_API_KEY`と`HEROKU_APP_NAME`のシークレットを設定します。

また、テストとして手動でワークフローを実行できるようにするため、`workflow_dispatch`トリガーを追加します。

https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch

しかし、GitHub上でワークフローを実行したところ、Heroku上で実行した場合と同様に、プライバシーポリシーが上手く閉じられずデータを取得できませんでした。

そのため、現時点ではコマンドの実行などは手動で行うことにしました。完全自動化については、今後も検討を続けていきます。

## まとめ
とりあえず最低限としてヒーローをランキングで表示できるようになりました。
次はBootstrapを入れて見た目を整えていきます。