3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オプチャグラフ開発記① 25万件を毎時クロールする「オプチャグラフ」のデータパイプライン

Last updated at Posted at 2025-07-09

はじめに

オプチャグラフは、LINEオープンチャットの統計情報を収集・分析・可視化するWebサービスです。

2023年10月にLINEがオープンチャット公式サイトを公開して以来、約2年間にわたりデータを収集し続けています。本記事では、このサービスを支えるデータパイプラインの全体像を解説します。

収集データの規模(2025年12月現在)

データ 件数
収集済みオープンチャット 約25万件
メンバー数の日次統計 約9,100万レコード
ランキング履歴 約8,900万レコード
合計 約1.8億レコード

これらのデータを、月額1,000円程度のレンタルサーバーで運用しています。

システム全体像

[LINE公式サイト API]
    ↓ (1) クローリング(25カテゴリ × 2種類)
[APIレスポンス JSON]
    ↓ (2) バリデーション・DTO変換
[OpenChatDto]
    ↓ (3) 差分検出(メモリキャッシュと比較)
[OpenChatUpdaterDto]
    ↓ (4) 差分のみDB更新
[MySQL / SQLite]
    ↓ (5) 静的ファイル生成(タクソノミーごと)
[キャッシュファイル群]
    ↓
[ユーザー]

(1) クローリング:LINE公式APIからのデータ取得

LINEオープンチャット公式サイトは、内部的にJSONを返すAPIを使用しています。このAPIは公式ドキュメントが存在しないため、ブラウザの開発者ツールで解析しました。

APIの構造

https://openchat.line.me/api/category/{カテゴリID}?sort={RANKING|RISING}&limit=40&ct={継続トークン}
  • カテゴリ: 25種類(ゲーム、スポーツ、芸能人・有名人など)
  • ソート: RANKING(人気順)と RISING(急上昇)の2種類
  • ページネーション: continuation token方式(ctパラメータ)

1回のリクエストで最大40件を取得し、レスポンスに含まれる継続トークンを使って次のページを取得します。これを全カテゴリ×2種類で繰り返すと、約25万件のデータが取得できます。

関連コード:

レートリミットとの付き合い方

当初は並列処理で高速化を試みましたが、LINEのサーバーから429(Too Many Requests)が返されるようになったため、現在は順次処理で実行しています。全カテゴリの取得には約10〜15分かかりますが、安定運用を優先しています。

関連コード:

(2) バリデーションとDTO変換

APIから取得したJSONは、そのままでは使いにくい構造をしています。これを扱いやすい形に変換するのがDTOファクトリの役割です。

// APIレスポンスの構造
{
  "squaresByCategory": [{
    "category": { "id": 17 },
    "squares": [{
      "square": {
        "emid": "xxx",
        "name": "チャット名",
        "desc": "説明文",
        "profileImageObsHash": "画像ハッシュ",
        "emblems": [1],
        "joinMethodType": 0
      },
      "memberCount": 1234,
      "createdAt": 1234567890000
    }]
  }]
}

これをOpenChatDtoというシンプルなオブジェクトに変換します。この段階でバリデーションも行い、不正なデータは除外してログに記録します。

関連コード:

(3) 差分検出:25万件のメモリキャッシュ

25万件を毎時取得しても、実際に内容が変わっているのは数千件程度です。全件をUPDATEするのは無駄なので、変更があったデータだけを検出します。

アプローチ

  1. クローリング開始時に、DBの全データ(25万件)をメモリに読み込む
  2. APIから取得した各データについて、emid(LINE内部ID)でメモリ上のデータを検索
  3. 名前・説明・画像・カテゴリ・バッジ・参加方法を比較
  4. 1つでも違いがあれば「更新対象」としてマーク
// 差分検出の判定ロジック(OpenChatApiDbMergerProcess.php より)
if (
    $repoDto->name === $apiDto->name
    && $repoDto->desc === $apiDto->desc
    && $repoDto->profileImageObsHash === $apiDto->profileImageObsHash
    && $repoDto->category === $apiDto->category
    && $repoDto->emblem === $apiDto->emblem
    && $repoDto->joinMethodType === $apiDto->joinMethodType
) {
    return null; // 変更なし、何もしない
}

この設計により、DBへの書き込みを大幅に削減しています。

関連コード:

(4) MySQLとSQLiteの使い分け

1.8億レコードをすべてMySQLに入れると、レンタルサーバーの容量制限(数GB)を超えてしまいます。そこで、データの特性に応じてDBを使い分けています。

特性 MySQL SQLite
用途 頻繁に更新されるデータ 追記のみの時系列データ
オープンチャットマスタ(25万件) メンバー数統計(9,100万件)
操作 INSERT / UPDATE / DELETE INSERT のみ
容量 制限あり ファイルベースで制限なし

SQLiteはファイルベースなので、レンタルサーバーの容量制限を気にせず使えます。また、追記のみのデータなら書き込み競合も発生しません。

関連コード:

(5) 静的ファイル生成:タクソノミーごとのキャッシュ戦略

ランキングページは、全ユーザーが同じデータを見ます。アクセスのたびにDBクエリを実行するのは無駄なので、あらかじめ静的ファイルとして生成しておきます。

キャッシュの構造

オプチャグラフでは、以下のタクソノミー(分類軸)ごとにキャッシュファイルを生成しています。

/storage/ja/
├── static_data_top/                    # トップページ用
│   ├── ranking_list.dat                # 成長ランキング(1時間/24時間/週間/人気)
│   ├── ranking_arg_dto.dat             # ランキング表示用メタデータ
│   ├── recommend_page_dto.dat          # おすすめページ用メタデータ
│   └── tag_list.dat                    # 全タグ一覧
│
└── static_data_recommend/              # 各ページ埋め込み用
    ├── tag/                            # タグ別ランキング(634ファイル)
    │   ├── {hash}.dat                  # 例: "ゲーム実況" → crc32ハッシュ
    │   └── ...
    ├── category/                       # カテゴリ別ランキング(25ファイル)
    │   ├── 17.dat                      # ゲーム
    │   ├── 16.dat                      # スポーツ
    │   └── ...
    └── official/                       # 公式認証バッジ別(2ファイル)
        ├── 1.dat                       # スペシャル
        └── 2.dat                       # 公式認証

トップページのキャッシュ内容

StaticTopPageDto には以下のデータが含まれます:

class StaticTopPageDto
{
    public array $hourlyList;      // 1時間成長ランキング
    public array $dailyList;       // 24時間成長ランキング
    public array $weeklyList;      // 週間成長ランキング
    public array $popularList;     // メンバー数ランキング
    public array $recommendList;   // おすすめリスト
    public int $tagCount;          // 総タグ数
}

タクソノミー別キャッシュの生成

各タクソノミーごとに、そのランキングデータをシリアライズしてファイルに保存します。

// RecommendStaticDataGenerator.php より
private function updateRecommendStaticData()
{
    // 634個のタグそれぞれについてキャッシュ生成
    foreach ($this->getAllTagNames() as $tag) {
        $fileName = hash('crc32', $tag);  // タグ名をハッシュ化してファイル名に
        saveSerializedFile(
            AppConfig::getStorageFilePath('recommendStaticDataDir') . "/{$fileName}.dat",
            $this->getRecomendRanking($tag)
        );
    }
}

private function updateCategoryStaticData()
{
    // 25カテゴリそれぞれについてキャッシュ生成
    foreach (AppConfig::OPEN_CHAT_CATEGORY as $category) {
        saveSerializedFile(
            AppConfig::getStorageFilePath('categoryStaticDataDir') . "/{$category}.dat",
            $this->getCategoryRanking($category)
        );
    }
}

ページ表示時の読み込み

ページ表示時は unserialize() でDTOを復元するだけ。DBアクセスは一切発生しません。

// キャッシュから読み込み
$dto = getUnserializedFile(AppConfig::getStorageFilePath('topPageRankingData'));

関連コード:

キャッシュの更新タイミング

クローリング完了後、以下の順序でキャッシュを更新します:

// SyncOpenChat.php より
$this->hourlyMemberRanking->update();  // ランキング計算
$this->staticDataGenerator->updateStaticData();  // キャッシュ再生成

関連コード:

まとめ

オプチャグラフのデータパイプラインは、以下の工夫で低コスト運用を実現しています。

  1. 差分検出: 変更があったデータだけをDBに書き込む
  2. DB使い分け: 更新頻度でMySQL/SQLiteを選択
  3. タクソノミー別キャッシュ: トップページ、タグ(634個)、カテゴリ(25個)、公式認証(2個)それぞれに静的ファイルを生成

次回の記事では、差分検出を支えるDTO設計について詳しく解説します。

リンク

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?