📸

Instagramの簡易的なレポートをスプレッドシートで管理する方法

に公開

はじめに

Instagramアカウント運用におけるレポート作成、特に日々の数値追跡や広告効果測定は、多くの時間と労力を要する作業です。手作業でのデータ収集や集計は非効率であり、分析に十分な時間を確保することが難しい場合があります。

この記事では、Google Apps Script (GAS) と Google スプレッドシートを用いて、Instagramのレポート作成プロセスを自動化する方法を解説します。構築するシステムは、以下の情報を自動取得し、スプレッドシートに記録します。

  1. アカウント指標: 日々のフォロワー数、フォロー数、投稿数の推移と前日比。
  2. 広告成果: 指定した広告キャンペーンの日別パフォーマンスデータ(インプレッション、クリック、費用、リンククリックなど)。
  3. 投稿別インサイト: 各投稿のエンゲージメント指標(いいね、コメント、リーチ、保存など)。

この仕組みを導入することで、レポート作成にかかる時間を削減し、データに基づいた迅速な意思決定や施策改善に注力できるようになります。プログラミングやGASの初心者、およびInstagram運用担当者を対象に、設定手順からスクリプトの解説、開発過程で遭遇した問題とその解決策まで、段階的に説明します。

準備するもの

自動化環境を構築するために、以下の準備が必要です。

  1. Googleアカウント: Google スプレッドシートおよび Google Apps Script の利用に必須です。
  2. Instagramビジネスアカウント: Meta Graph API を利用するには、Instagramアカウントがビジネスアカウントまたはクリエイターアカウントである必要があります。
  3. Facebook開発者アカウントとFacebookアプリ: Meta Graph API へのアクセスには、開発者アカウントと専用のFacebookアプリが必要です。
    • 未登録の場合は、Meta for Developers にてアカウント登録とアプリの新規作成を行ってください。アプリ作成時には「ビジネス」タイプを選択するのが一般的です。
  4. Meta Graph API アクセストークン: APIリクエスト時に必要となる認証キーです。ここでは比較的長期間(約60日間)有効な長期アクセストークンを取得する方法を説明します。
    • 必要なアクセス許可 (Permission): Facebookアプリのダッシュボードで、以下のアクセス許可を追加し、アプリレビューを申請・承認される必要があります(開発モード中は承認なしでもテスト可能な場合があります)。
      • instagram_basic
      • instagram_manage_insights
      • pages_show_list
      • pages_read_engagement
      • ads_read (広告データを取得する場合)
      • read_insights (広告データを取得する場合)
      • (その他、利用したい機能に応じて追加)
    • 長期アクセストークン取得手順:
      1. Meta for Developers にログインし、「ツール」メニューから「グラフAPIエクスプローラ」を開きます。
      2. 右上の「Facebookアプリ」ドロップダウンで、作成したアプリを選択します。
      3. 「ユーザーまたはページ」ドロップダウンで「ページアクセストークンを取得」を選択し、連携したいInstagramアカウントが紐づいているFacebookページを選択します。(ここでログインとアクセス許可の承認が求められます)
      4. 「アクセス許可を追加」ドロップダウンで、上記リストアップした必要なアクセス許可を選択します。
      5. アクセストークンを生成」ボタンをクリックします。短い有効期間(通常1〜2時間)のアクセストークンが生成され、「アクセストークン」フィールドに表示されます。
      6. 生成された短期アクセストークンをコピーします。
      7. 次に、「ツール」メニューから「アクセストークンデバッガー」を開きます。(または、グラフAPIエクスプローラで生成したトークン横の「i」アイコン > 「アクセストークンツールで開く」でも可能です)
      8. デバッグしたいアクセストークンの入力欄に、コピーした短期アクセストークンを貼り付け、「デバッグ」ボタンをクリックします。
      9. トークンの情報が表示されます。その下部にある「アクセストークン期間を延長」ボタンをクリックします。
      10. これにより、有効期間が約60日間の長期アクセストークンが生成されます。このトークンをコピーして安全な場所に保管し、後のスクリプト設定で使用します。
  5. 各種ID: スクリプト内で使用する以下のIDを事前に確認しておきます。
    • InstagramビジネスアカウントID: グラフAPIエクスプローラやアプリ設定画面などで確認できます。生成したアクセストークンをアクセストークンツールでデバッグをかけた時に「詳細なスコープ」カラムのinstagram_basicから出力されている17文字の数字がInstagramビジネスアカウントIDです。
    • 広告アカウントID: Meta広告マネージャーのURLや設定画面で確認できる act_ から始まるIDです。広告アカウントIDだけでなく、act_も含めてください。
    • Instagramユーザー名: @ を含まないアカウントのユーザー名です。

GASプロジェクトの作成と設定

  1. Google スプレッドシートの作成: Googleドライブで新規にスプレッドシートを作成します(例: "Instagramレポート管理")。
  2. GASエディタを開く: スプレッドシートのメニュー「拡張機能」>「Apps Script」を選択します。
  3. プロジェクト名の設定: エディタ左上の「無題のプロジェクト」をクリックし、「Instagram Report Automation」などに変更します。
  4. スクリプトファイルの作成: 左側のファイル一覧「ファイル」の横にある「+」ボタン > 「スクリプト」を選択し、以下の4つのファイルを作成します。
    • common.gs
    • ads.gs
    • follow.gs
    • post.gs

共通処理 (common.gs)

common.gs ファイルには、すべてのスクリプトで共通して利用する定数や汎用関数を記述します。

common.gs
// common.gs - 共通処理と定数の定義

// ----- ★★★ 設定値 (ご自身の情報に書き換えてください) ★★★ -----
const INSTAGRAM_ID = "123456789123456";       // ★ ご自身のInstagramビジネスアカウントID
const ACCESS_TOKEN = "YOUR_LONG_LIVED_ACCESS_TOKEN"; // ★ ご自身で取得した長期アクセストークン
const SSId = "YOUR_SPREADSHEET_ID";               // ★ このスプレッドシートのID (URLから取得)
const USER_NAME = "your_instagram_username";       // ★ ご自身のInstagramユーザー名 (@なし, follow.gsで使用)
// ----- ★★★ 設定値ここまで ★★★ -----


/**
 * 指定された名前のシートオブジェクトを取得またはnullを返す
 * @param {string} sheetName 取得したいシート名
 * @return {GoogleAppsScript.Spreadsheet.Sheet | null} シートオブジェクト、またはnull
 */
function loadSheet(sheetName) {
  try {
      const ss = SpreadsheetApp.openById(SSId); // 指定IDのスプレッドシートを開く
      const sheet = ss.getSheetByName(sheetName); // 名前でシートを取得
      if (!sheet) {
        Logger.log(`シート "${sheetName}" が見つかりませんでした。`);
        return null; // シートがない場合はnullを返す
      }
      return sheet;
  } catch (e) {
      // スプレッドシートが開けない、権限がない等のエラー
      Logger.log(`スプレッドシート(${SSId})またはシート(${sheetName})の読み込み中にエラー: ${e}`);
      return null;
  }
}


/**
 * 指定されたURLにAPIリクエストを送信し、結果をJSONオブジェクトとして返す
 * Meta Graph API 呼び出し用の共通関数
 * @param {string} url リクエスト先のURL (パラメータ、アクセストークン含む)
 * @return {object} APIからの正常なレスポンス(JSONパース後)、またはエラー情報を含むオブジェクト { error: {...} }
 */
function fetchData(url) {
  try {
    const options = {
      muteHttpExceptions: true, // APIエラー(4xx, 5xx)でも例外を発生させない
      headers: {
          'Cache-Control': 'no-cache' // キャッシュを利用しない設定
       }
    };
    const response = UrlFetchApp.fetch(url, options); // URLにリクエスト
    const responseCode = response.getResponseCode(); // HTTPステータスコード取得
    const responseBody = response.getContentText(); // レスポンスボディ取得

    // ステータスコード 2xx は成功とみなす
    if (responseCode >= 200 && responseCode < 300) {
      try {
        return JSON.parse(responseBody); // JSON文字列をオブジェクトに変換
      } catch (e) {
         Logger.log(`JSONパースエラー: ${e}, Response: ${responseBody}`);
         return { error: { code: 'PARSE_ERROR', message: `JSON parse error: ${e}` } }; // パース失敗エラー
      }
    } else {
      // APIからのエラーレスポンス処理
      Logger.log(`APIリクエスト失敗: URL=${url}, Status=${responseCode}, Response=${responseBody}`);
      try {
        const errorData = JSON.parse(responseBody); // エラー内容もJSON形式を試みる
        // Meta API標準のエラー形式 { "error": { ... } } か確認
        if (errorData && errorData.error) {
             return errorData;
        } else {
             // JSONだが期待する形式でない場合
             return { error: { code: responseCode, message: `API Error (Unknown Format): ${responseBody}` } };
        }
      } catch (e) {
        // エラーレスポンスがJSON形式でない場合 (HTMLページなど)
        return { error: { code: responseCode, message: `API request failed. Response: ${responseBody}` } };
      }
    }
  } catch (e) {
    // UrlFetchApp.fetch自体が失敗した場合 (ネットワークエラー、タイムアウトなど)
    Logger.log(`UrlFetchApp.fetch 例外: ${e}`);
    Logger.log(`URL: ${url}`);
    return { error: { code: 'FETCH_EXCEPTION', message: `GAS Exception: ${e}` } };
  }
}

/**
 * 指定されたシート内で、特定の日付(年月日が一致)の行番号を検索する関数 (Dateオブジェクト比較版)
 * A列の日付形式が Dateオブジェクト, 文字列(YYYY/MM/DD or YYYY-MM-DD), シリアル値 に対応
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 検索対象のシートオブジェクト
 * @param {Date} targetDate 検索する日付 (Dateオブジェクト)
 * @param {number} [dateColumn=1] 日付が格納されている列番号 (1始まり、デフォルトはA列)
 * @return {number | false} 見つかった行番号(1始まり)、見つからなければ false
 */
function findRow(sheet, targetDate, dateColumn = 1) {
  // 引数の基本的なチェック
  if (!sheet || typeof sheet.getLastRow !== 'function' || !targetDate || !(targetDate instanceof Date)) {
    Logger.log(`findRow: 無効な引数です。sheet: ${sheet}, targetDate: ${targetDate}`);
    return false;
  }

  // 検索対象の日付の年・月・日を取得
  const targetYear = targetDate.getFullYear();
  const targetMonth = targetDate.getMonth(); // 0 (1月) から 11 (12月)
  const targetDay = targetDate.getDate();
  // Logger.log(`findRow: 検索対象日付 Year:${targetYear}, Month:${targetMonth + 1}, Day:${targetDay}`); // デバッグ用

  try {
      const lastRow = sheet.getLastRow();
      if (lastRow <= 1) return false; // ヘッダーのみなら対象なし

      // 日付列のデータを一括取得 (2行目から最終行まで)
      const dateRange = sheet.getRange(2, dateColumn, lastRow - 1, 1);
      const dateValues = dateRange.getValues(); // 2次元配列 [[日付1], [日付2], ...]

      // 配列の末尾(シートの下の行)から検索
      for (let i = dateValues.length - 1; i >= 0; i--) {
        const cellValue = dateValues[i][0]; // セルの値
        let rowDate = null; // 比較用のDateオブジェクト

        // セルの値からDateオブジェクトへの変換を試みる
        try {
            if (cellValue instanceof Date) {
              rowDate = cellValue;
            } else if (typeof cellValue === 'string' && cellValue.length >= 8) {
              const parsedDate = new Date(cellValue.replace(/-/g, '/').substring(0, 10));
              if (!isNaN(parsedDate.getTime())) rowDate = parsedDate;
            } else if (typeof cellValue === 'number' && cellValue > 25569) {
              rowDate = new Date((cellValue - 25569) * 86400 * 1000);
            }
        } catch(e) { /* 変換エラーは無視 */ }

        // 有効なDateオブジェクトなら年月日を比較
        if (rowDate instanceof Date) {
          if (rowDate.getFullYear() === targetYear &&
              rowDate.getMonth() === targetMonth &&
              rowDate.getDate() === targetDay) {
            const foundRowIndex = i + 2; // 配列インデックスから実際の行番号へ
            Logger.log(`findRow: ${foundRowIndex} 行目で一致。`);
            return foundRowIndex;
          }
        }
      }
  } catch (e) {
      Logger.log(`findRow 実行エラー: ${e}`); Logger.log(e.stack); return false;
  }

  return false; // 見つからなかった場合
}

広告レポートの自動化 (ads.gs)

ads.gs は、指定した広告キャンペーンの成果を日別・広告単位で取得し、「広告効果測定_日別」シートに記録します。

うまくいくと下記スクリーンショットのようなシートが生成されます。

ads.gs
// ads.gs - 広告レポート取得

// ----- ★★★ 設定値 (ご自身の情報に書き換えてください) ★★★ -----
const AD_ACCOUNT_ID = "act_YOUR_AD_ACCOUNT_ID"; // ★ ご自身の広告アカウントID (act_...)
const API_VERSION = 'v18.0';                  // 利用するAPIバージョン
const TARGET_CAMPAIGN_ID = 'YOUR_TARGET_CAMPAIGN_ID'; // ★ 分析対象のキャンペーンID
const SHEET_NAME = '広告効果測定_日別';      // ★ 出力先のシート名
const START_DATE = '2025-03-01';              // ★ データ取得開始日 (YYYY-MM-DD形式)
// ----- ★★★ 設定値ここまで ★★★ -----

/**
 * 前日の日付を YYYY-MM-DD 形式で取得する
 * @return {string} 前日の日付文字列
 */
function getYesterdayDate() {
  const today = new Date();
  today.setDate(today.getDate() - 1);
  const yyyy = today.getFullYear();
  const mm = ('0' + (today.getMonth() + 1)).slice(-2);
  const dd = ('0' + today.getDate()).slice(-2);
  return `${yyyy}-${mm}-${dd}`;
}

/**
 * 特定キャンペーン配下の広告インサイトを日別に取得する
 * @param {string} campaignId 対象キャンペーンID
 * @param {string} startDate 取得開始日 (YYYY-MM-DD)
 * @param {string} endDate 取得終了日 (YYYY-MM-DD)
 * @return {Array} 広告インサイトデータの配列、エラー時は空配列
 */
function getDailyAdInsights(campaignId, startDate, endDate) {
  // 取得する指標 (フィールド) を指定
  const fields = [
    'date_start', 'campaign_name', 'adset_name', 'ad_name',
    'impressions', 'clicks', 'spend', 'reach', 'frequency', 'ctr', 'cpm', 'cpc',
    'inline_link_clicks', 'cost_per_inline_link_click'
  ].join(',');

  const timeRange = { since: startDate, until: endDate };
  const encodedTimeRange = encodeURIComponent(JSON.stringify(timeRange));

  // APIエンドポイントURL構築
  const initialUrl = `https://graph.facebook.com/${API_VERSION}/${campaignId}/insights?level=ad&fields=${fields}&time_range=${encodedTimeRange}&time_increment=1&limit=500&access_token=${ACCESS_TOKEN}`;
  Logger.log(`インサイト取得URL (初): ${initialUrl}`);

  let allInsightsData = [];
  let nextUrl = initialUrl;

  try {
    let pageCount = 1;
    while (nextUrl) { // ページネーションループ
      Logger.log(`ページ ${pageCount} を取得中...`);
      const data = fetchData(nextUrl); // common.gs の関数でAPI呼び出し

      if (data.error) { Logger.log(`APIエラー (ページ ${pageCount}): ${JSON.stringify(data.error)}`); nextUrl = null; break; }
      if (data && data.data && data.data.length > 0) {
        allInsightsData = allInsightsData.concat(data.data);
        Logger.log(`${data.data.length} 件取得。合計: ${allInsightsData.length}`);
      } else if (pageCount === 1) { Logger.log(`データなし: ${startDate} - ${endDate}`); nextUrl = null; break; }
      if (data.paging && data.paging.next) { // 次ページ処理
        nextUrl = data.paging.next; pageCount++; Utilities.sleep(1500);
      } else { nextUrl = null; }
    }
  } catch (e) { Logger.log(`インサイト取得ループ中エラー: ${e}`); Logger.log(e.stack); return []; }
  Logger.log(`最終的に ${allInsightsData.length} 件取得。`);
  return allInsightsData;
}

/**
 * 取得した広告インサイトデータを指定シートに書き込む
 * @param {Array} data 書き込むインサイトデータの配列
 */
function writeToSheet(data) {
  if (!Array.isArray(data) || data.length === 0) { Logger.log('書き込む広告データなし'); return; }
  try {
    const spreadsheet = SpreadsheetApp.openById(SSId);
    let sheet = spreadsheet.getSheetByName(SHEET_NAME);
    if (!sheet) { sheet = spreadsheet.insertSheet(SHEET_NAME); Logger.log(`シート "${SHEET_NAME}" 作成`); }
    sheet.clearContents(); Logger.log(`シート "${SHEET_NAME}" クリア`);

    const headers = [
      '日付', 'キャンペーン名', '広告セット名', '広告名',
      'インプレッション', 'クリック数', '消化金額', 'リーチ', 'フリークエンシー',
      'CTR (%)', 'CPM', 'CPC', 'リンククリック数', 'リンククリック単価'
    ];
    const headerRange = sheet.getRange(1, 1, 1, headers.length);
    headerRange.setValues([headers]); headerRange.setFontWeight('bold');
    sheet.setFrozenRows(1); Logger.log('ヘッダー行書き込み');

    const dataToWrite = data.map(row => [ // データ整形
      row.date_start || '', row.campaign_name || '', row.adset_name || '', row.ad_name || '',
      Number(row.impressions || 0), Number(row.clicks || 0), Number(row.spend || 0),
      Number(row.reach || 0), Number(row.frequency || 0), Number(row.ctr || 0),
      Number(row.cpm || 0), Number(row.cpc || 0), Number(row.inline_link_clicks || 0),
      Number(row.cost_per_inline_link_click || 0)
    ]);

    if (dataToWrite.length > 0) { // データ書き込み
       sheet.getRange(2, 1, dataToWrite.length, headers.length).setValues(dataToWrite);
       Logger.log(`${dataToWrite.length} 行の広告データを書き込み`);
       // 列幅調整
       sheet.setColumnWidth(1, 100); sheet.setColumnWidth(2, 150);
       sheet.setColumnWidth(3, 150); sheet.setColumnWidth(4, 200);
       sheet.setColumnWidth(13, 100); sheet.setColumnWidth(14, 100);
    }
  } catch (e) { Logger.log(`シート書き込みエラー: ${e}`); Logger.log(e.stack); }
}

/**
 * メイン関数: 広告インサイト取得とシート書き込みを実行
 */
function fetchAndWriteDailyAdInsights() {
  const endDate = getYesterdayDate(); const startDate = START_DATE;
  if (!startDate || !endDate || new Date(startDate) > new Date(endDate)) {
      Logger.log(`日付指定が無効: ${startDate} - ${endDate}`); return;
  }
  Logger.log(`広告データ取得: ${startDate}${endDate}, Campaign: ${TARGET_CAMPAIGN_ID}`);
  const insightsData = getDailyAdInsights(TARGET_CAMPAIGN_ID, startDate, endDate);
  if (insightsData && insightsData.length > 0) { writeToSheet(insightsData); }
  else { Logger.log('広告データなし、書き込みスキップ。'); }
  Logger.log('広告データ取得処理完了。');
}

投稿別インサイトレポートの自動化 (post.gs)

post.gs は、Instagramアカウントの最近の投稿(最大100件)を取得し、投稿ごとの基本情報とインサイト指標(リーチ、保存など)を「投稿別インサイト」シートに記録します。

post.gs
// post.gs

/**
 * Instagramの投稿別インサイト(いいね、コメント、リーチ、保存など)を取得し、
 * 指定されたシートに書き込む関数。
 * サムネイル画像列には =IMAGE(URL, 1) の数式を設定する。
 * 【注意】数式に設定されるURLは一時的なもので、後で表示されなくなる可能性があります。
 */
function postReporting() {
  const sheetName = '投稿別インサイト'; // 出力シート名
  const num = 100; // 取得する投稿数

  let sheet = null;
  try {
      // common.gs の loadSheet を使用してシートを取得、なければ作成
      sheet = loadSheet(sheetName);
      Logger.log(`シート "${sheetName}" 取得試行: ${sheet ? '成功' : '失敗(null)'}`);
      if (!sheet) {
          Logger.log(`シート "${sheetName}" 新規作成します。`);
          const ss = SpreadsheetApp.openById(SSId); // common.gs の SSId
          sheet = ss.insertSheet(sheetName);
          if (!sheet) { Logger.log(`シート "${sheetName}" 作成失敗。`); return; }
          Logger.log(`シート "${sheetName}" を作成しました。`);
      }
  } catch (e) {
      Logger.log(`シート取得/作成エラー: ${e}`); return;
  }

  // --- 投稿リストと基本情報の取得 ---
  const fieldsValue = `media.limit(${num}){id,timestamp,like_count,comments_count,permalink,media_type,caption,thumbnail_url,media_url}`;
  const encodedFieldsValue = encodeURIComponent(fieldsValue);
  const facebookUrl = `https://graph.facebook.com/v18.0/${INSTAGRAM_ID}?fields=${encodedFieldsValue}&access_token=${ACCESS_TOKEN}`;
  Logger.log("Request URL (Post List): " + facebookUrl);

  let igMediaData; // APIレスポンス用
  try {
    igMediaData = fetchData(facebookUrl); // common.gs の fetchData
    if (igMediaData.error) {
      Logger.log(`投稿一覧取得APIエラー: ${JSON.stringify(igMediaData.error)}`);
      initializeSheet(sheet); // エラーでもヘッダーは表示
      return;
    }
  } catch (e) {
      Logger.log(`投稿一覧取得処理エラー: ${e}`);
      initializeSheet(sheet); // エラーでもヘッダーは表示
      return;
  }

  // メディアデータ配列取得、なければ終了
  const mediaList = igMediaData?.media?.data || [];
  if (mediaList.length === 0) {
    Logger.log('取得できる投稿データがありません。');
    initializeSheet(sheet); // ヘッダーだけ表示
    return;
  }
  Logger.log(`${mediaList.length} 件のメディアデータを取得。`);

  // --- 各投稿のインサイト取得とデータ整形 ---
  const dataToWrite = []; // スプレッドシート書き込み用配列

  mediaList.forEach(function(mediaData, index) {
    const mediaId = mediaData?.id;
    const mediaType = mediaData?.media_type;
    const likeCount = mediaData?.like_count ?? 0;
    const commentsCount = mediaData?.comments_count ?? 0;

    if (!mediaId || !mediaType) {
      Logger.log(`メディア ${index + 1}: ID or Typeなし、スキップ。`); return;
    }

    // --- 投稿インサイト取得 ---
    let metricsToFetch = ['reach', 'saved'];
    // if (mediaType === 'VIDEO') { metricsToFetch.push('video_views'); }

    let postInsightData = { data: [] };
    if (metricsToFetch.length > 0) {
        const postInsightUrl = `https://graph.facebook.com/v18.0/${mediaId}/insights?metric=${metricsToFetch.join(',')}&access_token=${ACCESS_TOKEN}`;
        // Logger.log(`Request URL (Insight ${mediaId}): ${postInsightUrl}`);

        try {
          const fetchedInsight = fetchData(postInsightUrl);
          if (fetchedInsight.error) {
            Logger.log(`ID:${mediaId} インサイト取得APIエラー: ${JSON.stringify(fetchedInsight.error)}`);
          } else {
            postInsightData = fetchedInsight;
          }
        } catch (e) { Logger.log(`ID:${mediaId} インサイト取得処理エラー: ${e}`); }
    }
    // --- インサイト取得ここまで ---

    const timestamp = mediaData.timestamp ? new Date(mediaData.timestamp) : null;
    const formattedDate = timestamp ? Utilities.formatDate(timestamp, Session.getScriptTimeZone(), 'yyyy/MM/dd HH:mm:ss') : '';
    const caption = mediaData.caption ?? '';
    const permalink = mediaData.permalink ?? '';
    let thumbnailUrl = mediaData.thumbnail_url ?? '';

    let reach = 0, saved = 0;
    (postInsightData?.data || []).forEach(function(insight) {
      const value = insight?.values?.[0]?.value ?? 0;
      if (insight.name === 'reach') reach = value;
      if (insight.name === 'saved') saved = value;
    });

    const engagement = likeCount + commentsCount + saved;

    // ★★★ サムネイル画像を表示する数式を作成 ★★★
    let imageFormula = '';
    if (thumbnailUrl) {
      // URL内のダブルクォーテーションをエスケープ (念のため)
      const escapedThumbnailUrl = thumbnailUrl.replace(/"/g, '""');
      // ご指定の形式で数式文字列を生成
      imageFormula = `=IMAGE("${escapedThumbnailUrl}", 1)`;
    }
    // ★★★ 数式作成ここまで ★★★

    // シートに書き込む1行分のデータを作成 (imageFormula を使用)
    dataToWrite.push([
        formattedDate, likeCount, commentsCount, caption, imageFormula, // ← ここで imageFormula を設定
        permalink, reach, saved, engagement
    ]);
  }); // mediaList.forEach 終了

  // --- シートへの書き込み ---
  initializeSheet(sheet); // シート初期化(クリア&ヘッダー設定)

  if (dataToWrite.length > 0) { // 書き込むデータがあるか確認
    try {
      const startRow = 2;
      const numRows = dataToWrite.length;
      // ヘッダーの列数を取得して書き込み範囲を決定
      const numColumns = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getWidth();

      // 列数チェック (念のため)
      if (dataToWrite[0].length !== numColumns) {
          Logger.log(`警告: データ列数(${dataToWrite[0].length})とヘッダー列数(${numColumns})が不一致`);
          // データ配列の列数に合わせて書き込み
          sheet.getRange(startRow, 1, numRows, dataToWrite[0].length).setValues(dataToWrite);
      } else {
           sheet.getRange(startRow, 1, numRows, numColumns).setValues(dataToWrite);
      }
      Logger.log(`${numRows} 件の投稿データをシート '${sheetName}' に書き込みました。`);

      // 行高さ調整 (任意)
      // try { sheet.setRowHeightsForced(startRow, numRows, 80); } catch(e){ /* ignore */}
    } catch (e) {
      Logger.log(`シート書き込みエラー: ${e}`); Logger.log(e.stack);
    }
  } else {
     Logger.log('書き込む有効なデータがありませんでした。');
  }
}

/**
 * 「投稿別インサイト」シートを初期化(クリアしてヘッダーを設定)する関数
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 対象のシートオブジェクト
 */
function initializeSheet(sheet) {
  // シートオブジェクトの有効性チェック
  if (!sheet || typeof sheet.clearContents !== 'function') {
      Logger.log("initializeSheet: 無効なシートオブジェクトです。");
      return;
  }
  try {
    sheet.clearContents(); // 内容クリア
    // ヘッダー定義
    const headers = [
        '日時', 'いいね数', 'コメント数', 'キャプション', 'サムネイル画像', // サムネイル列を含む
        'パーマリンク', 'リーチ数', '保存数', 'エンゲージメント数'
    ];
    // ヘッダー書き込みと書式設定
    const headerRange = sheet.getRange(1, 1, 1, headers.length);
    headerRange.setValues([headers]);
    headerRange.setFontWeight('bold');
    // 列幅調整 (任意)
    sheet.setColumnWidth(1, 150); // 日時
    sheet.setColumnWidth(4, 300); // キャプション
    sheet.setColumnWidth(5, 100); // サムネイル画像
    sheet.setColumnWidth(6, 150); // パーマリンク
    Logger.log(`シート "${sheet.getName()}" を初期化しました。`);
  } catch (e) {
      Logger.log(`シート初期化エラー: ${e}`);
      Logger.log(e.stack);
  }
}

サムネイル画像が表示されない場合の注意点

上記の post.gs では、取得したサムネイル画像のURLをスプレッドシートの =IMAGE() 関数で表示しようとしています。しかし、この方法では画像が表示されない場合があります。

  • 原因: APIから取得できるサムネイル画像のURL ( thumbnail_url ) は、多くの場合、有効期限付きの一時的なURLです。スクリプト実行時にはアクセスできても、後でスプレッドシートを開いた時には期限が切れて画像が表示されない、ということが頻繁に起こります。特に動画のサムネイルなどで顕著です。また、URLが非常に長い場合や、Google Sheets側の仕様変更・不具合なども原因となりえます。
  • 対策:
    • パーマリンクの活用: 最も簡単な代替策は、サムネイル表示を諦め、代わりに「パーマリンク」列のURLをクリックして実際の投稿を確認する運用にすることです。
    • Google Drive連携(高度): より確実に表示させたい場合は、GASで画像データを取得してGoogle Driveに保存し、そのDriveファイルの共有リンク(プレビュー可能な形式)を =IMAGE() 関数に渡す方法があります。ただし、スクリプトの実装が複雑になり、Driveの容量管理も必要になります。

現状のコードは =IMAGE() 関数を設定する実装になっていますが、上記理由により表示されない可能性がある点にご留意ください。

フォロワー/フォロー数レポートの自動化 (follow.gs)

follow.gs は、日々のフォロワー数、フォロー数、投稿数を取得し、「フォロ・フォロワー」シートに記録します。前日比増減の計算に加え、「広告効果測定_日別」シートを参照して広告費用合計やCPAなどを計算するスプレッドシート関数を自動設定します。

follow.gs
/**
 * フォロワー数・フォロー数・投稿数を取得し、日々の増減を記録し、
 * 広告費用・CPA・リンククリック数・リンククリック単価を計算する【数式】を
 * 「フォロ・フォロワー」シートに設定する。(既存日付は更新)
 */
function logFollowDataWithFormulas() {
  const sheetName = "フォロ・フォロワー"; // このスクリプトが出力するシート名
  // ★ 数式内で参照する、ads.gsが出力するシート名を正確に指定 ★
  const adSheetNameForFormula = "広告効果測定_日別";

  let sheet = null;
  try {
      sheet = loadSheet(sheetName); // common.gs の関数でシート取得
      Logger.log(`シート "${sheetName}" 取得試行: ${sheet ? '成功' : '失敗(null)'}`);
      if (!sheet) { // シートがなければ新規作成
          Logger.log(`シート "${sheetName}" 新規作成します。`);
          const ss = SpreadsheetApp.openById(SSId); // common.gs の SSId
          sheet = ss.insertSheet(sheetName);
          if (!sheet) { Logger.log(`シート "${sheetName}" 作成失敗。`); return; }
          Logger.log(`シート "${sheetName}" を作成しました。`);
      }
  } catch (e) { Logger.log(`シート取得/作成エラー: ${e}`); return; }

  // ヘッダー行を確認・設定
  try {
      setupFollowSheetHeaderWithFormulas(sheet);
  } catch (e) { Logger.log(`ヘッダー設定エラー: ${e}`); return; }

  // --- Instagramアカウント情報の取得 ---
  const fieldsValue = `business_discovery.username(${USER_NAME}){followers_count,follows_count,media_count}`;
  const encodedFieldsValue = encodeURIComponent(fieldsValue);
  const url = `https://graph.facebook.com/v18.0/${INSTAGRAM_ID}?fields=${encodedFieldsValue}&access_token=${ACCESS_TOKEN}`;
  Logger.log("Request URL (Follower Count): " + url);

  let igData;
  try {
    igData = fetchData(url); // common.gs の fetchData
    if (igData.error) { Logger.log(`APIエラー: ${JSON.stringify(igData.error)}`); return; }
    if (!igData.business_discovery) { Logger.log(`business_discovery データ取得不可`); return; }
     Logger.log("Business Discovery データ取得成功。");
  } catch (e) { Logger.log(`WorkspaceDataエラー: ${e}`); return; }

  const now = new Date();
  const todayStr = Utilities.formatDate(now, Session.getScriptTimeZone(), "yyyy/MM/dd");
  const followers = Number(igData.business_discovery.followers_count || 0);
  const follows = Number(igData.business_discovery.follows_count || 0);
  const posts = Number(igData.business_discovery.media_count || 0);

  // --- データ書き込み/更新処理 ---
  try {
    // common.gs の findRow で今日の日付の行を検索
    const existingRowIndex = findRow(sheet, now, 1);

    let diffFollowers = ""; let diffPosts = ""; let diffFollows = "";
    let targetRow = 0;

    if (existingRowIndex) {
      // --- 既存行更新 ---
      targetRow = existingRowIndex;
      Logger.log(`今日(${todayStr})データが ${targetRow} 行目に存在、更新します。`);
      if (targetRow > 2) { // 前日比較
        const prevRow = targetRow - 1;
        try {
            const prevData = sheet.getRange(prevRow, 2, 1, 5).getValues()[0];
            const [prevFollowers, , prevPosts, , prevFollows] = prevData;
            if (typeof prevFollowers === 'number' && typeof prevPosts === 'number' && typeof prevFollows === 'number') {
              diffFollowers = followers - prevFollowers;
              diffPosts     = posts - prevPosts;
              diffFollows   = follows - prevFollows;
              Logger.log(`前日(${prevRow})比較: Follower Diff=${diffFollowers}`);
            } else { Logger.log(`前日(${prevRow})データ不正、増減計算スキップ。`); }
        } catch (e) { Logger.log(`前日(${prevRow})データ処理エラー: ${e}`); }
      } else { Logger.log("前日データなし、増減計算スキップ。"); }

      // データ部分(A-G列)上書き
      const rowDataToUpdate = [now, followers, diffFollowers, posts, diffPosts, follows, diffFollows];
      sheet.getRange(targetRow, 1, 1, rowDataToUpdate.length).setValues([rowDataToUpdate]);
      Logger.log(`${targetRow} 行目データ(A-G列)更新完了。`);
      // 数式設定 (H-L列)
      setFormulasForRow(sheet, targetRow, adSheetNameForFormula);

    } else {
      // --- 新規行追加 ---
      targetRow = sheet.getLastRow() + 1;
      Logger.log(`今日(${todayStr})データが存在しないため、${targetRow} 行目に新規追加します。`);
      const lastRowBeforeAppend = targetRow - 1;
      if (lastRowBeforeAppend >= 2) { // 前日比較
          const prevRow = lastRowBeforeAppend;
          try {
              const prevData = sheet.getRange(prevRow, 2, 1, 5).getValues()[0];
              const [prevFollowers, , prevPosts, , prevFollows] = prevData;
              if (typeof prevFollowers === 'number' && typeof prevPosts === 'number' && typeof prevFollows === 'number') {
                  diffFollowers = followers - prevFollowers;
                  diffPosts     = posts - prevPosts;
                  diffFollows   = follows - prevFollows;
                   Logger.log(`前日(${prevRow})比較: Follower Diff=${diffFollowers}`);
              } else { Logger.log(`前日(${prevRow})データ不正、増減計算スキップ。`); }
          } catch (e) { Logger.log(`前日(${prevRow})データ処理エラー: ${e}`); }
      } else { Logger.log("前日データなし、増減計算スキップ。"); }

      // 新規行データ (H-L列は空)
      const newDataRow = [now, followers, diffFollowers, posts, diffPosts, follows, diffFollows, "", "", "", "", ""];
      sheet.appendRow(newDataRow);
      Logger.log(`${targetRow} 行目に基本データを追加しました。`);
      // 追加した行に数式設定 (H-L列)
      setFormulasForRow(sheet, targetRow, adSheetNameForFormula);
    }
  } catch (e) {
      Logger.log(`シート書き込み/計算エラー: ${e}`); Logger.log(e.stack);
  }
}


/**
 * 「フォロ・フォロワー」シートのヘッダーを確認・設定する
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 対象シート
 */
function setupFollowSheetHeaderWithFormulas(sheet) {
    if (!sheet || typeof sheet.getLastRow !== 'function') {
        Logger.log("setupFollowSheetHeaderWithFormulas: 無効なシート"); throw new Error("Invalid sheet");
    }
    const expectedHeaders = [
      "取得日", "フォロワー数", "フォロワー増加数", "投稿数", "投稿増加数",
      "フォロー数", "フォロー増加数", "広告費用合計(数式)", "CPA(数式)",
      "日付キー(数式)", "リンククリック数合計(数式)", "リンククリック単価(数式)"
    ];
    const expectedNumCols = expectedHeaders.length;

    // ヘッダー行がない or 列数が足りない場合に再設定
    if (sheet.getLastRow() === 0 || sheet.getLastColumn() < expectedNumCols) {
        if (sheet.getLastRow() > 0) {
            Logger.log("ヘッダー再設定のためシートクリア"); sheet.clearContents();
        }
        try {
          sheet.appendRow(expectedHeaders);
          const headerRange = sheet.getRange(1, 1, 1, expectedNumCols);
          headerRange.setFontWeight('bold');
          // 列幅設定
          sheet.setColumnWidth(1, 120); sheet.setColumnWidth(8, 120); sheet.setColumnWidth(9, 100);
          sheet.setColumnWidth(10, 100); sheet.setColumnWidth(11, 150); sheet.setColumnWidth(12, 150);
          Logger.log("ヘッダー行を追加/再設定しました。");
        } catch (e) { Logger.log(`ヘッダー設定エラー: ${e}`); throw e; }
    } else { /* Logger.log("既存ヘッダー確認済み。"); */ }
}

/**
 * 指定行の H-L 列にスプレッドシート関数を設定する (J列参照SUMIF版)
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 対象シート
 * @param {number} rowNumber 対象行番号 (1始まり)
 * @param {string} adSheetName 参照する広告データシート名
 */
function setFormulasForRow(sheet, rowNumber, adSheetName) {
    if (!sheet || typeof sheet.getRange !== 'function' || !rowNumber || rowNumber < 2 || !adSheetName) {
        Logger.log(`setFormulasForRow: 無効な引数。row:${rowNumber}, adSheet:${adSheetName}`); return;
    }
    try {
        const quotedAdSheetName = `'${adSheetName.replace(/'/g, "''")}'`; // シート名エスケープ
        const indirectRangeA = `${quotedAdSheetName}&"!A:A"`; // 条件範囲 (日付)
        const indirectRangeG = `${quotedAdSheetName}&"!G:G"`; // 合計範囲 (費用)
        const indirectRangeM = `${quotedAdSheetName}&"!M:M"`; // 合計範囲 (LinkClick) ※要確認

        // H列(8): 広告費用合計 - SUMIF条件は J列参照
        const formulaH = `=IFERROR(SUMIF(INDIRECT(${indirectRangeA}), J${rowNumber}, INDIRECT(${indirectRangeG})), "")`;
        sheet.getRange(rowNumber, 8).setFormula(formulaH);

        // I列(9): CPA (費用/フォロワー増)
        const formulaI = `=IFERROR(H${rowNumber}/C${rowNumber}, "")`;
        sheet.getRange(rowNumber, 9).setFormula(formulaI);

        // J列(10): 日付キー (A列をテキスト化)
        const formulaJ = `=IFERROR(TEXT(A${rowNumber}, "yyyy-mm-dd"), "")`;
        sheet.getRange(rowNumber, 10).setFormula(formulaJ);

        // K列(11): リンククリック数合計 (広告シートM列参照)
        const formulaK = `=IFERROR(SUMIF(INDIRECT(${indirectRangeA}), J${rowNumber}, INDIRECT(${indirectRangeM})), "")`;
        sheet.getRange(rowNumber, 11).setFormula(formulaK);

        // L列(12): リンククリック単価 (費用/LinkClick)
        const formulaL = `=IFERROR(H${rowNumber}/K${rowNumber}, "")`;
        sheet.getRange(rowNumber, 12).setFormula(formulaL);

        Logger.log(`${rowNumber} 行目の H-L 列に数式を設定しました。(J列参照SUMIF)`);
    } catch (e) {
        Logger.log(`${rowNumber} 行目の数式設定エラー: ${e}`); Logger.log(e.stack);
    }
}

発生したエラーと解決策(要約)

  • 過去データ取得不可: API仕様上、過去のフォロワー数等は取得できません。
  • シート/ヘッダーエラー (TypeError): 対象シートが見つからない、またはヘッダー設定時に問題が発生。シート存在チェックとエラーハンドリングを強化しました (common.gs, follow.gs)。
  • 日付行重複: common.gsfindRow 関数が日付形式の違いで既存行を見つけられない問題。Dateオブジェクトの年月日比較に修正しました。
  • 数式エラー (#ERROR!): follow.gs で設定した SUMIF 関数がエラーになる場合。INDIRECT のシート名確認、SUMIF の条件(J列)と範囲(広告シートA列)のデータ型不一致が主な原因。エラー行のA列とJ列の値を確認することが重要です。

スクリプトの実行と自動化

作成したスクリプトを実行し、日々の実行を自動化します。

  1. 手動実行と承認:
    • GASエディタ上部メニューの「実行する関数を選択」で、まず WorkspaceAndWriteDailyAdInsights (ads.gs) を選択し、「実行」ボタン(▷)をクリックします。
    • 初回は「承認が必要です」と表示されるので、「権限を確認」をクリックし、画面の指示に従ってスクリプトに必要なアクセス許可を与えます。
    • 次に、実行関数を postReporting (post.gs) に切り替え、同様に実行・承認します。
    • 最後に、実行関数を logFollowDataWithFormulas (follow.gs) に切り替え、同様に実行・承認します。
  2. 実行ログの確認: 各関数の実行後、エディタ下部の実行ログや左メニュー「実行数」で結果を確認し、エラーが発生していないか確認します。
  3. トリガー設定 (自動実行):
    • GASエディタ左側のメニュー「トリガー」(時計アイコン)を開き、「トリガーを追加」をクリックします。
    • 広告データ取得用トリガー:
      • 実行する関数: WorkspaceAndWriteDailyAdInsights
      • イベントのソース: 時間主導型
      • トリガーのタイプ: 日付ベースのタイマー
      • 時刻: 例: 午前1時~2時
      • エラー通知設定: 毎日通知を受け取る
      • 「保存」をクリックします。
    • 投稿インサイト取得用トリガー:
      • 再度「トリガーを追加」します。
      • 実行する関数: postReporting
      • 時刻: 例: 午前1時~2時 または 午前2時~3時
      • 他は同様に設定し、「保存」します。
    • フォロワーデータ取得用トリガー:
      • 再度「トリガーを追加」します。
      • 実行する関数: logFollowDataWithFormulas
      • 時刻: 必ず WorkspaceAndWriteDailyAdInsights が完了した後になるように設定します(例: 午前3時~4時)。
      • 他は同様に設定し、「保存」します。

これで設定は完了です。毎日指定時刻に各スクリプトが自動実行され、データがスプレッドシートに蓄積・更新されていきます。

まとめ

Google Apps Script とスプレッドシートを利用することで、Instagramの日々のレポート(アカウント指標、広告成果、投稿別インサイト)作成を自動化する手順を解説しました。

この自動化により、手作業によるデータ収集・集計の負担を軽減し、より効率的なアカウント分析や戦略立案に時間を活用できるようになるでしょう。

提供したスクリプトは基本的な指標を取得するものですが、Meta Graph API のドキュメントを参照し、必要に応じて取得する指標を追加・変更するなど、ご自身の運用に合わせてカスタマイズが可能です。

注意点

  • アクセストークンの有効期限: 長期アクセストークンも約60日で失効します。失効前に再生成し、common.gsACCESS_TOKEN を更新する必要があります。
  • APIの仕様変更: Meta Graph API の仕様は変更される可能性があります。仕様変更によってスクリプトが動作しなくなった場合は、Meta for Developers のドキュメントを確認し、スクリプトを修正する必要があります。
  • エラーログの監視: 自動実行設定後も、定期的にGASの「実行数」メニューからログを確認し、エラーが発生していないか監視することを推奨します。エラー通知設定も活用してください。
  • シート名の整合性: スクリプト内で参照・指定しているシート名(広告効果測定_日別, 投稿別インサイト, フォロ・フォロワー)を変更する場合は、関連するスクリプト内の定数 (SHEET_NAME, adSheetNameForFormula) も必ず修正してください。
  • 数式の参照列: follow.gs の数式は、「広告効果測定_日別」シートの特定の列(G列: 費用, M列: リンククリック)を参照しています。ads.gs の出力列を変更した場合は、follow.gssetFormulasForRow 関数内の数式も修正が必要です。

以上です。この記事が、Instagram運用の効率化に貢献できれば幸いです。

Discussion