TDR待ち時間自動収集システムの構築手順|GitHubとGASで資産化する方法

※本記事はアフィリエイト広告を含みます。

TDR待ち時間自動収集システムの構築手順|GitHubとGASで資産化する方法
  • ディズニーパークの混雑傾向を自分で分析したいが、手動での記録には限界がある
  • 既存の待ち時間サイトが閉鎖されるリスクを考え、自前のデータベースを持ちたい
  • サーバー維持費を一切かけずに、24時間365日の自動収集を完結させたい

パークの混雑状況を確認する際、他者が提供するデータの二次利用だけでは独自の分析は不可能に近いです。

信頼性の高い記事を書くためには、自ら取得した「一次情報」を蓄積し、それを根拠とした論理的な主張が求められますがいかがでしょうか。

私は20社以上の実務経験を持つ独立エンジニアとして、WPやGAS、インフラ構築に精通しており、今回はGitHub ActionsとGoogle Apps Script(GAS)を組み合わせた堅牢なシステムを構築してみました。

本記事では、Node.jsを用いたデータ取得プログラムの実装から、深夜の無駄な稼働を排除する営業時間に合わせた自動化設定までを詳しく解説いたします。

この記事を読めば、PCを起動することなく、正確な待ち時間ログがスプレッドシートに積み上がり続ける環境が手に入ります。

先に結論をお伝えすると、クラウドサービスを論理的に組み合わせることで、完全無料で保守不要のTDR待ち時間取得データ収集エンジンを構築することは十分に可能です。

収集システムの全体設計と技術選定

Themeparks Wiki API

大前提として本システムは、情報の取得・加工・保存を3つのレイヤーで分離して設計しています。

  • Themeparks Wiki API:データソースの選定
  • GitHub Actions:サーバーレスでの定期実行
  • Google Apps Script(GAS):スプレッドシートへの永続化

まず、世界中のパーク情報を配信しているAPIからデータを抜き出すプログラムをNode.jsで作成します。

次に、このプログラムを24時間稼働させるための環境として、GitHub Actionsを採用しました。GitHub Actionsを利用することで、自宅のPCを稼働させるコストやリスクを排除しています。

最後に、取得したデータを永続的に記録するためにGoogle Apps Script(GAS)を用い、スプレッドシートをデータベースとして活用します。

「Themeparks Wiki API」「GitHub Actions」「GAS」、この3段階の構成により、特定のサービスが停止しても他の部分でカバーできる柔軟なシステムを実現しています。

データ取得用「index.js」の実装手順

プログラムの実装において最も重要なのは、API側の仕様変更に耐えうる柔軟な検索ロジックです。

  • API検索ロジックの柔軟性確保
  • ランド(TDL)とシー(TDS)のデータ並列処理
  • 日本標準時(JST)へのフォーマット統一

当初は特定のID(slug)を直接指定していましたが、IDの変更によりデータが取得できなくなるリスクを考慮し、名称に「Tokyo」が含まれるものを動的に検索する方式に変更しました。

また、東京ディズニーランドとシーの両方のデータを1回の実行で取得し、配列で結合した後に一つのJSONファイルとして書き出します。

取得時刻は後の分析を容易にするため、サーバー時刻(UTC)ではなく日本標準時(JST)に固定して記録します。

Node.js(JavaScript)

const fs = require('fs');

(async () => {
  try {
    console.log("1. Fetching destinations...");
    const destRes = await fetch('https://api.themeparks.wiki/v1/destinations');
    const destData = await destRes.json();

    // 名称に「Tokyo」を含むリゾートを動的に特定
    const resort = destData.destinations.find(d => d.name.toLowerCase().includes('tokyo'));
    if (!resort) throw new Error("Tokyo Resort not found.");

    console.log("2. Fetching parks...");
    const childrenRes = await fetch(`https://api.themeparks.wiki/v1/entity/${resort.id}/children`);
    const childrenData = await childrenRes.json();

    const targetParks = childrenData.children.filter(c => 
      c.name.toLowerCase().includes('disneyland') || c.name.toLowerCase().includes('disneysea')
    );

    let allAttractions = [];
    for (const park of targetParks) {
      const liveRes = await fetch(`https://api.themeparks.wiki/v1/entity/${park.id}/live`);
      const liveData = await liveRes.json();
      const parkType = park.name.toLowerCase().includes('disneyland') ? 'TDL' : 'TDS';

      const attractions = liveData.liveData.map(ride => ({
        park: parkType,
        name: ride.name,
        waitTime: (ride.queue && ride.queue.STANDBY) ? (ride.queue.STANDBY.waitTime || 0) : -1,
        status: ride.status,
        updateTime: new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })
      }));
      allAttractions = allAttractions.concat(attractions);
    }

    // TDL/TDS統合データとして保存
    fs.writeFileSync("tdr_status.json", JSON.stringify(allAttractions, null, 2));
    console.log("Success: Saved to tdr_status.json");

  } catch (e) {
    console.error("Critical Error:", e.message);
    process.exit(1);
  }
})();

GitHub Actionsによる自動実行スケジュール設定

GitHub Actionsを稼働させるための設定ファイル(YAML)では、パークの営業時間に合わせた実行スケジュールを定義します。

  • 営業時間(8:00〜22:00)への最適化
  • UTCとJSTの時差計算(9時間の乖離)
  • 混雑を避けるためのキリの悪い時刻指定

ハッピーエントリーの開始を見据えて日本時間の朝8時から、閉園後の余韻を含めた夜22時までを稼働時間とします。

ここで注意が必要なのは、GitHubのcron設定はUTC(世界標準時)で行う必要がある点です。日本時間の朝8時はUTCでは前日の23時に該当するため、これを正確に記述しなければなりません。

また、世界中の実行が集中する00分や30分を避け、あえて31分などのキリの悪い数字を指定することで、実行待ちの遅延を緩和させる工夫を施しています。

YAML

name: TDR Wait Time Auto-Fetcher

on:
  schedule:
    # JST 08:00 - 21:31 (UTC 23:00 - 12:31) 31分おき
    - cron: '*/31 23,0-12 * * *'
    # JST 22:00 (UTC 13:00) 最終取得
    - cron: '0 13 * * *'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  update-data:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Run Fetch Script
        run: node index.js

      - name: Commit and Push changes
        run: |
          git config --global user.name 'GitHub Action-bot'
          git config --global user.email 'action@github.com'
          git add tdr_status.json
          git diff --quiet && git diff --staged --quiet || (git commit -m "Update wait times" && git push)

スプレッドシートへの蓄積とGASのガードロジック

取得したデータをスプレッドシートへ蓄積する際、深夜帯などのデータが更新されない時間に同じ内容を何度も書き込むのは非効率的です。

以下のようなルールで作成しました。

  • 重複データ書き込みの論理的排除
  • 最新データと蓄積ログのシート分離
  • エラー通知設定による監視体制

取得したデータをスプレッドシートへ蓄積する際、深夜帯などのデータが更新されない時間に同じ内容を何度も書き込むのは非効率的です。

この課題を解決するため、GASには「前回の最終ログの時刻」と「取得したJSON内の時刻」を照合し、一致した場合は書き込みをスキップするガードロジックを実装しました。これにより、データベースとしての純度を高めています。

また、スプレッドシートを「最新データ」と「蓄積ログ」の2枚に分けることで、現在の状況確認と長期的な分析を並列して行えるようにしています。

GoogleAppScript(GAS)

/**
 * TDRデータ収集システム:最新表示 & 履歴蓄積 統合版
 */
function updateAndLogTDRData() {
  const url = 'https://raw.githubusercontent.com/あなたのユーザー名/リポジトリ名/main/tdr_status.json';
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  
  let currentSheet = ss.getSheetByName("最新データ") || ss.insertSheet("最新データ");
  let logSheet = ss.getSheetByName("蓄積ログ") || ss.insertSheet("蓄積ログ");

  try {
    const response = UrlFetchApp.fetch(url);
    const data = JSON.parse(response.getContentText());
    
    // データが空、または予期せぬ形式の場合は中断
    if (!data || data.length === 0 || !data[0].updateTime) return;

    // --- 論理デバッグ:二重書き込み防止の完全版 ---
    const lastRow = logSheet.getLastRow();
    if (lastRow > 1) {
      // getValue()ではなくgetDisplayValue()で「見たままの文字列」を取得
      const lastLoggedTime = logSheet.getRange(lastRow, 6).getDisplayValue();
      
      // 文字列として厳密に比較
      if (String(lastLoggedTime).trim() === String(data[0].updateTime).trim()) {
        console.log("重複データのため蓄積をスキップしました: " + lastLoggedTime);
        // 表示シートのみ更新して終了
        updateCurrentSheet(currentSheet, data);
        return;
      }
    }

    // --- 処理1:最新データシートの更新(表示用) ---
    updateCurrentSheet(currentSheet, data);

    // --- 処理2:蓄積ログシートへの追記 ---
    const now = new Date();
    const logRows = data.map(item => [
      Utilities.formatDate(now, "Asia/Tokyo", "yyyy-MM-dd"), // 1列目:日付
      Utilities.formatDate(now, "Asia/Tokyo", "HH:mm"),       // 2列目:取得実行時間
      item.park,                                            // 3列目:パーク種別
      item.name,                                            // 4列目:施設名
      item.waitTime,                                        // 5列目:待ち時間
      item.updateTime                                       // 6列目:判定用のキー
    ]);
    
    if (logRows.length > 0) {
      if (logSheet.getLastRow() === 0) {
        logSheet.appendRow(["日付", "時間", "パーク", "施設名", "待ち時間", "元データ更新時刻"]);
      }
      // まとめて最終行に追記
      logSheet.getRange(logSheet.getLastRow() + 1, 1, logRows.length, 6).setValues(logRows);
    }

  } catch (e) {
    console.error("実行エラー:" + e.toString());
  }
}

/**
 * 最新データシートの描画処理(共通)
 */
function updateCurrentSheet(sheet, data) {
  sheet.clearContents();
  sheet.appendRow(["パーク", "名前", "待ち時間", "ステータス", "最終更新"]);
  
  const rows = data.map(item => [
    item.park, 
    item.name, 
    item.waitTime === -1 ? "終了/休止" : item.waitTime, 
    item.status, 
    item.updateTime
  ]);
  
  if (rows.length > 0) {
    sheet.getRange(2, 1, rows.length, 5).setValues(rows);
    // ヘッダーの装飾(任意)
    sheet.getRange(1, 1, 1, 5).setFontWeight("bold").setBackground("#f3f3f3");
  }
}

なお、一つだけ注意点があります。

「const url = ‘https://raw.githubusercontent.com/あなたのユーザー名/リポジトリ名/main/tdr_status.json’;」

この部分は自身のGithubのURLを指定する必要があります。

開発過程における技術的課題と解決策

構築の過程で直面した最大の障壁は、GitHub Actionsのbotが自動更新を行った直後に、ローカル環境から修正コードをプッシュしようとして発生する「Rejected」エラーでした。

  • Git Push 拒否エラー:rebaseによる歴史の統合
  • cron 実行遅延:GitHub無料枠の仕様理解
  • 命名規則の統一:TDLからTDRへの一貫した変更

これはリモート側の歴史が先行しているために起こります。解決策として、必ず git pull origin main --rebase を実行し、リモートの歴史をローカルの背後に再配置してからプッシュする手順を徹底しました。

また、ファイル名を当初は tdl_status.json にしており、それだとTDLだけの取得ではなくTDSも合わせたTDRとして取得するために、tdl_status.json から tdr_status.json に変更しました。

その際には、プログラム側だけでなくGitHub ActionsのYAMLファイル内の指定も一貫して修正しなければ動作が停止します。こうした細部の不整合を一つずつ潰していくことが、安定したシステムの鍵となります。

まとめ:自律型データ収集システムの構築がもたらす価値

本記事では、GitHub ActionsとGoogle Apps Scriptを組み合わせ、東京ディズニーリゾートの待ち時間を自動収集するシステムの構築手順を解説しました。

PCを起動することなく、クラウド上でプログラムが自律的に稼働し、スプレッドシートへログを刻み続ける環境は、情報発信において圧倒的な優位性をもたらします。

  • 完全自動・ゼロコストによる継続的なデータ蓄積の実現
  • 他者に依存しない「一次情報」の所有によるメディアの差別化
  • GitHub ActionsとGASの連携による堅牢なインフラ構成

既存のサービスから情報を得る「消費者」の立場から、自らデータを生成・管理する「所有者」へと転換することは、技術ブログとしての信頼性を底上げするだけでなく、将来的な分析の幅を無限に広げることにつながります。

今回構築したシステムは、単なるツールの作成に留まらず、自身のメディアに強固な「事実の裏付け」を与えるための重要なインフラ基盤となります。

今後の展望:蓄積されたデータが切り拓く「次世代の活用アイデア」

最後に、今後の展望としていくつか箇条書きで記しておきます。

  • 独自データに基づく傾向分析グラフの作成
  • WordPressへのスプレッドシート埋め込み(データの取得がきちんとできていたら)
  • AI・機械学習を用いた「未来の待ち時間予測」エンジンの開発
  • リアルタイム空き状況と現在地を組み合わせた「最短ルート最適化」ツール
  • SNSの感情分析と連動させた「パークの盛り上がり可視化」ダッシュボード

今回構築したシステムによって、手元には膨大な「生データ」が蓄積され始めます。このデータを単に眺めるだけでなく、さらに一歩進んだ「何か」を作り上げることを目的としておりますので、「これは凄い!」と思わせる画期的なコンテンツを提供できる可能性がありますので是非とも楽しみに。

もう少し突っ込んで話をすると、例えば、「AI・機械学習を用いた「未来の待ち時間予測」エンジンの開発」は、過去数年分のログをAIに学習させ、当日の天気や気温、近隣のイベント情報を掛け合わせることで、驚くほど高精度な「未来の待ち時間予測」を実現できるかもしれません。

また、蓄積された「30分刻みの変動」をヒートマップとして可視化して、パーク内の混雑がどのように波及していくのかをアニメーションで見せるたり、さらに高度な応用として、蓄積データから導き出した「アトラクションの混雑パターン」を基に、滞在時間を最大化するための「パーソナル・ツアー・プランナー」を構築するのも面白い試みだと思ってます。

蓄積データの活用と将来的な拡張性を兼ね備えた大きなプロジェクトなので暖かく見守っていただければと存じます。

…というか、そもそもこういうの誰か作ってたら教えてください笑

今まで見たこともないんですよね…APIで用意されているのに、誰もそれを有効活用してないってのがなんでかなーって思ってます…

とにかく、最後までご覧いただきありがとうございました!