はじめに
こんにちは。
4 年間 続けたSEO マーケターから、エンジニアにジョブチェンジして約 2 ヶ月が経ちました。
転身後、実際の業務で Instagram API を使ったシステムを構築する機会がありました。特定のハッシュタグが付いた投稿を自動で取得し、スプレッドシートに保存するシステムです。
Instagram Graph API の仕様を調査し、Google Apps Script(以下、GAS)で実装する過程で、API の制約や認証の仕組みについて学ぶことができました。
本記事では、Instagram API を使った投稿データ取得システムの構築過程を、初期調査から実装まで順を追って紹介します。同様のシステムを構築したい方の参考になれば嬉しいです!
前提
今回はInstagramアカウントを運用しているメンバーからの依頼で、要求は以下の通りでした。
- 特定のハッシュタグをつけて投稿してくれるファンの方々をピックアップしたい(投稿 URL、アカウント名、アカウントのフォロワーなどが取得できたら嬉しい)
- 過去の投稿も全て抽出したい
まずは、APIの調査から開始しました。
1. Instagram API まわりの初期調査
まず、API を使ってどこまでできるかを把握する必要がありました。
具体的には以下の点を確認しました。
- 取得可能なデータは何か
- 特定ハッシュタグで特定期間(例えば過去 1 年分)の投稿を取得できるか
- パーマリンクから username を取得できるか
分かったこと
調査の結果、以下の制約があることが分かりました。
-
ハッシュタグ検索は
recent_media/top_mediaのみで、過去 1 年分の投稿取得は不可能-
recent_mediaは過去 24 時間以内の投稿のみ取得可能 -
top_mediaは人気の投稿を取得できるが、時系列での取得は難しい
-
- 取得できるデータ範囲は自分の IG ビジネスアカウントのみで、個人アカウントは不可
- API の制約に基づき、パーマリンクからのアカウント情報取得は不可能
これらの制約を踏まえ、要件を調整する必要がありました。
API 利用の前提条件
Instagram Graph API を利用するには、以下の情報が必要です。
- Instagram ビジネスアカウント ID(
INSTAGRAM_USER_ID) - ハッシュタグ ID(
INSTAGRAM_HASHTAG_ID) - Facebook ユーザーアクセストークン(
INSTAGRAM_ACCESS_TOKEN) - App ID と App Secret(トークンの長期化に使用、オプション)
2. システム要件・仕様
2.1 要件
API の制約を踏まえ、以下の要件でシステムを構築することにしました。
- GAS から Instagram Graph API を呼び出し、特定のハッシュタグが付いている投稿の「投稿パーマリンク」「投稿 ID」「投稿日時」を取得
-
recent_mediaであれば「#hoge」の投稿を過去 24 時間分であれば取得できるため、recent_mediaを採用 - 1 日 1 回、時間ベースのトリガーで実行する
- 投稿データを書き込んだ後にバックアップファイルを作成し、「backup」フォルダに格納
2.2 技術スタック
- 実行環境: Google Apps Script(V8)
-
API: Instagram Graph API(Hashtag
recent_media) - データストア: Google スプレッドシート
- 認証情報管理: Google Apps Script スクリプトプロパティ
- スケジューリング: Google Apps Script トリガー(時間ベース/毎日 23 時台)
2.3 主要機能
-
ハッシュタグ投稿の取得:
recent_mediaエンドポイントで特定のハッシュタグの投稿を取得 -
取得項目:
permalink(投稿 URL)、id、timestamp(投稿日時) - 重複排除: 投稿 ID を基準に既存データと照合して新規投稿のみを抽出し、既存投稿と結合して全投稿をソート
-
ソート: 投稿日時(
timestamp)で降順ソート(新しい投稿が上) - スプレッドシート保存: 既存データをクリアしてから全投稿を書き込み(常に最新の状態を保持)
-
バックアップ作成: 実行ごとにスプレッドシートのバックアップを作成
- 名前形式:
シート名_backup_yyyyMMdd_HHmm - 元スプレッドシートと同じ階層の
backupフォルダ内にコピー
- 名前形式:
2.4 システム構成図
┌─────────────────────────────────────────────────────────┐
│ Google Apps Script │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │fetchInstagram│ │ Config.gs │ │
│ │ Posts.gs │─────▶│ (定数定義) │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ├──▶ ApiClient.gs ────┐ │
│ │ (API呼び出し) │ │
│ │ │ │
│ ├──▶ TokenManager.gs │ │
│ │ (認証管理) │ │
│ │ │ │
│ ├──▶ DataProcessor.gs │ │
│ │ (データ処理) │ │
│ │ │ │
│ ├──▶ SpreadsheetHandler.gs │
│ │ (スプレッドシート操作) │
│ │ │ │
│ ├──▶ Backup.gs │ │
│ │ (バックアップ) │ │
│ │ │ │
│ └──▶ Logger.gs │ │
│ (エラーログ) │ │
│ │ │
└────────────────────────────────┼─────────────────────────┘
│
┌────────────┴────────────┐
│ │
┌──────────▼──────────┐ ┌──────────▼──────────┐
│ Instagram Graph API │ │ Google Spreadsheet │
│ (v24.0) │ │ (データ保存) │
└─────────────────────┘ └─────────────────────┘
2.5 データフロー
1. トリガー実行 / 手動実行
↓
2. fetchInstagramPosts() 実行
↓
3. TokenManager.getAccessToken() - アクセストークン取得
↓
4. ApiClient.fetchRecentMediaAll() - API呼び出し(全件取得)
↓
5. SpreadsheetHandler.getExistingPostsFromSpreadsheet() - 既存投稿取得
↓
6. DataProcessor.filterNewPosts() - 新規投稿抽出
↓
7. 既存投稿と新規投稿を結合
↓
8. DataProcessor.sortPostsByTimestamp() - 日時降順ソート(全投稿)
↓
9. SpreadsheetHandler.writeAllPostsToSpreadsheet() - スプレッドシート保存(全投稿)
↓
10. Backup.createSpreadsheetBackup() - バックアップ作成
↓
11. 完了
2.6 ファイル構成
gas/
├── fetchInstagramPosts.gs # メインエントリポイント
├── Config.gs # 設定値・定数定義
├── TokenManager.gs # アクセストークン管理
├── ApiClient.gs # Instagram API クライアント
├── DataProcessor.gs # データ処理(ソート・フィルタ)
├── SpreadsheetHandler.gs # スプレッドシート操作
├── Logger.gs # エラーログ記録
├── Backup.gs # バックアップ作成
└── appsscript.json # プロジェクト設定(V8 ランタイム設定など)
3. 実装手順
3.1 Instagram ビジネスアカウント ID とユーザーアクセストークンの取得
手順
-
Meta for Developers で「アプリを作成」をクリックし、アプリ名とメールアドレスを入れる

5.ツール→Facebook Graph API Explorer にアクセス

6.以下 3 つの権限(permission)を設定
instagram_basicinstagram_manage_insightspages_show_list
7.「Generate Access Token」クリックし、アクセストークンを取得

8.アクセストークンの有効期限を延長するために、アクセストークンデバッガーを開き、取得したアクセストークンを記入して「デバッグ」ボタンをクリック

9.画面下部の「アクセストークンを延長」をクリックし、有効期限を延長した新しいトークンを取得

- GAS のスクリプトプロパティに
INSTAGRAM_ACCESS_TOKENとして保存
これで、Instagram ビジネスアカウント ID とユーザーアクセストークンの取得ができました。
3.2 API エンドポイントの確認
ハッシュタグ ID を取得
まず、ハッシュタグ ID を取得する必要があります。
GET https://graph.facebook.com/v24.0/ig_hashtag_search?
user_id=【IG_USER_ID】
&q=travel
&access_token=【PAGE_ACCESS_TOKEN】
ハッシュタグの投稿(recent / top)を取得
「recent」と「top」の 2 種類がありますが、今回は recent を採用しました。
top は Facebook の内部アルゴリズムで人気フラグが付けられた投稿が返ってくるらしく要件に合いません。
日付を降順にソートするために recent を採用しました。
recent
GET https://graph.facebook.com/v24.0/【IG_HASHTAG_ID】/recent_media
?user_id=【IG_USER_ID】
&fields=id,permalink,timestamp
&access_token=【PAGE_ACCESS_TOKEN】
top
GET https://graph.facebook.com/v24.0/【IG_HASHTAG_ID】/top_media
?user_id=【IG_USER_ID】
&fields=id,permalink,timestamp
&access_token=【PAGE_ACCESS_TOKEN】
3.3 トークンのリフレッシュ(オプション)
Facebook ユーザーアクセストークンは約 2 ヶ月で失効しますが、refreshToLongLivedToken() 関数を使用することでトークンをリフレッシュ(更新)できます。
この関数は、現在保存されているトークン(短期トークンまたは長期トークン)を使って Facebook API に問い合わせ、新しい長期トークンを取得して保存します。
設定方法
- Meta for Developers で App ID と App Secret を取得
- 上部メニュー → "マイアプリ" → 対象のアプリを選択
- 左メニュー → 「設定」→「基本」(Settings → Basic)
- GAS のスクリプトプロパティに以下を追加
-
FACEBOOK_APP_ID: App ID -
FACEBOOK_APP_SECRET: App Secret
-
-
refreshToLongLivedToken()関数を手動実行して長期トークンに更新
3.4 GAS の実装
Config.gs(設定値・定数定義)
/**
* 設定値・定数定義ファイル
*/
// Instagram Graph API 関連
const INSTAGRAM_API_BASE_URL = "https://graph.facebook.com/v24.0";
// ハッシュタグ ID(例: #wedog)
const INSTAGRAM_HASHTAG_ID = "【ハッシュタグID】";
// Instagram ビジネスアカウント ID
const INSTAGRAM_USER_ID = "【InstagramビジネスアカウントID】";
// アクセストークンを保存しているプロパティ名
const PROP_INSTAGRAM_ACCESS_TOKEN = "INSTAGRAM_ACCESS_TOKEN";
// Facebook App 認証情報のプロパティ名
const PROP_FACEBOOK_APP_ID = "FACEBOOK_APP_ID";
const PROP_FACEBOOK_APP_SECRET = "FACEBOOK_APP_SECRET";
// スプレッドシート関連
const SPREADSHEET_ID = "【スプレッドシートID】";
// データを書き込むシート名
const SHEET_NAME_DATA = "投稿データ";
// エラーログを書き込むシート名
const SHEET_NAME_ERROR_LOG = "error_log";
TokenManager.gs(アクセストークン管理)
/**
* アクセストークンの取得・更新を行うユーティリティ
*/
/**
* プロパティサービスからアクセストークンを取得
*/
function getAccessToken() {
const props = PropertiesService.getScriptProperties();
const token = props.getProperty(PROP_INSTAGRAM_ACCESS_TOKEN);
if (!token) {
throw new Error(
"アクセストークンが設定されていません。プロパティサービスに " +
PROP_INSTAGRAM_ACCESS_TOKEN +
" を設定してください。"
);
}
return token;
}
/**
* アクセストークンを直接保存する(手動実行用)
* @param {string} newToken - 保存するアクセストークン
*/
function setAccessToken(newToken) {
if (!newToken) {
throw new Error("newToken が指定されていません。");
}
const props = PropertiesService.getScriptProperties();
props.setProperty(PROP_INSTAGRAM_ACCESS_TOKEN, newToken);
}
/**
* 現在保存されている EAA トークンを使って、Facebook の API に問い合わせ
* 新しい長期トークン(Long-lived token)を取得して Script Properties に保存する
*
* @returns {Object} 更新結果(新しいトークン、有効期限など)
*/
function refreshToLongLivedToken() {
try {
const props = PropertiesService.getScriptProperties();
// 現在保存されているトークンを取得
const currentToken = props.getProperty(PROP_INSTAGRAM_ACCESS_TOKEN);
if (!currentToken) {
throw new Error(
"現在のアクセストークンが設定されていません。プロパティサービスに " +
PROP_INSTAGRAM_ACCESS_TOKEN +
" を設定してください。"
);
}
// App ID と App Secret を取得
const appId = props.getProperty(PROP_FACEBOOK_APP_ID);
const appSecret = props.getProperty(PROP_FACEBOOK_APP_SECRET);
if (!appId || !appSecret) {
throw new Error(
"Facebook App ID または App Secret が設定されていません。\n" +
"プロパティサービスに以下を設定してください:\n" +
"- " +
PROP_FACEBOOK_APP_ID +
"\n" +
"- " +
PROP_FACEBOOK_APP_SECRET
);
}
// Facebook Graph API のトークン交換エンドポイント
const exchangeUrl = INSTAGRAM_API_BASE_URL + "/oauth/access_token";
const params = {
grant_type: "fb_exchange_token",
client_id: appId,
client_secret: appSecret,
fb_exchange_token: currentToken,
};
const url = buildUrlWithParams_(exchangeUrl, params);
// API に問い合わせ
const response = UrlFetchApp.fetch(url, {
method: "get",
muteHttpExceptions: true,
});
const code = response.getResponseCode();
const responseText = response.getContentText();
if (code !== 200) {
const errorDetail = "HTTP " + code + ": " + responseText;
logError_("Facebook トークン交換 API エラー", errorDetail);
throw new Error(
"Facebook トークン交換 API 呼び出しに失敗しました。ステータスコード: " +
code +
"\nレスポンス: " +
responseText
);
}
const json = JSON.parse(responseText);
// レスポンスに access_token が含まれているか確認
if (!json.access_token) {
const errorDetail =
"レスポンスに access_token が含まれていません: " + responseText;
logError_("Facebook トークン交換 API レスポンスエラー", errorDetail);
throw new Error(
"Facebook API からのレスポンスに access_token が含まれていません。"
);
}
const newToken = json.access_token;
const expiresIn = json.expires_in || "不明";
// 新しい長期トークンを保存
props.setProperty(PROP_INSTAGRAM_ACCESS_TOKEN, newToken);
console.log("アクセストークンを長期トークンに更新しました。");
console.log("更新日時: " + new Date().toISOString());
console.log("有効期限: " + expiresIn + " 秒");
return {
success: true,
access_token: newToken.substring(0, 10) + "...", // セキュリティのため先頭のみ
expires_in: expiresIn,
updated_at: new Date().toISOString(),
};
} catch (error) {
const errorMessage = "長期トークンへの更新に失敗しました: " + error.message;
console.error(errorMessage);
logError_(errorMessage, error.toString());
throw new Error(errorMessage);
}
}
/**
* クエリパラメータ付き URL を組み立てる内部関数
*/
function buildUrlWithParams_(baseUrl, params) {
const query = Object.keys(params)
.map(function (key) {
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
})
.join("&");
return baseUrl + "?" + query;
}
ApiClient.gs(Instagram API クライアント)
/**
* Instagram Graph API 呼び出しを行うクライアント
*/
/**
* recent_media エンドポイントを使用してハッシュタグ投稿を取得する
* ページネーションも含めて全件取得する
*
* @returns {Object[]} 投稿オブジェクト配列
*/
function fetchRecentMediaAll() {
const accessToken = getAccessToken();
const hashtagId = INSTAGRAM_HASHTAG_ID;
const userId = INSTAGRAM_USER_ID;
const fields = "id,permalink,timestamp";
const limit = 50; // 最大件数
const baseUrl = INSTAGRAM_API_BASE_URL + "/" + hashtagId + "/recent_media";
const params = {
user_id: userId,
fields: fields,
limit: limit,
access_token: accessToken,
};
const allPosts = [];
let nextUrl = buildUrlWithParams_(baseUrl, params);
while (nextUrl) {
const response = UrlFetchApp.fetch(nextUrl, {
method: "get",
muteHttpExceptions: true,
});
const code = response.getResponseCode();
const text = response.getContentText();
if (code !== 200) {
logError_("Instagram API エラー: " + code, text);
throw new Error(
"Instagram API 呼び出しに失敗しました。ステータスコード: " + code
);
}
const json = JSON.parse(text);
if (Array.isArray(json.data)) {
allPosts.push.apply(allPosts, json.data);
}
if (json.paging && json.paging.next) {
nextUrl = json.paging.next;
} else {
nextUrl = null;
}
}
return allPosts;
}
/**
* クエリパラメータ付き URL を組み立てる内部関数
*/
function buildUrlWithParams_(baseUrl, params) {
const query = Object.keys(params)
.map(function (key) {
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
})
.join("&");
return baseUrl + "?" + query;
}
DataProcessor.gs(データ処理)
/**
* 取得した投稿データのソート・重複排除などを行う
*/
/**
* timestamp の降順(新しい投稿が上)でソートする
*
* @param {Object[]} posts
* @returns {Object[]} ソート済み配列
*/
function sortPostsByTimestamp(posts) {
return posts.slice().sort(function (a, b) {
var dateA = new Date(a.timestamp);
var dateB = new Date(b.timestamp);
return dateB.getTime() - dateA.getTime();
});
}
/**
* 既存 ID と比較して新規投稿のみを抽出
*
* @param {Object[]} posts
* @param {string[]} existingIds
* @returns {Object[]} 新規投稿のみの配列
*/
function filterNewPosts(posts, existingIds) {
var existingSet = new Set(existingIds);
return posts.filter(function (post) {
return !existingSet.has(post.id);
});
}
SpreadsheetHandler.gs(スプレッドシート操作)
/**
* スプレッドシートの読み書きを担当するヘルパー
*/
/**
* データシートを取得
*/
function getDataSheet_() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_NAME_DATA);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME_DATA);
// ヘッダー行
sheet.getRange(1, 1, 1, 3).setValues([["投稿 URL", "ID", "投稿日時"]]);
}
return sheet;
}
/**
* 既存の ID を B 列から取得(2 行目以降)
*
* @returns {string[]} 既存 ID 配列
*/
function getExistingIdsFromSpreadsheet() {
var sheet = getDataSheet_();
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
return [];
}
var range = sheet.getRange(2, 2, lastRow - 1, 1); // B列
var values = range.getValues();
return values
.map(function (row) {
return row[0];
})
.filter(function (id) {
return id;
});
}
/**
* 既存投稿をスプレッドシートから取得(2行目以降)
*
* @returns {Object[]} 既存投稿配列 [{permalink, id, timestamp}, ...]
*/
function getExistingPostsFromSpreadsheet() {
var sheet = getDataSheet_();
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
return [];
}
var range = sheet.getRange(2, 1, lastRow - 1, 3); // A列からC列
var values = range.getValues();
return values
.map(function (row) {
return {
permalink: row[0],
id: row[1],
timestamp: row[2] instanceof Date ? row[2].toISOString() : row[2],
};
})
.filter(function (post) {
return post.id; // IDが存在するもののみ
});
}
/**
* 全投稿をシートに書き込む(既存データをクリアしてから書き込み)
*
* @param {Object[]} posts
*/
function writeAllPostsToSpreadsheet(posts) {
var sheet = getDataSheet_();
// 既存データをクリア(ヘッダー行は残す)
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
sheet.getRange(2, 1, lastRow - 1, 3).clear();
}
if (!posts || posts.length === 0) {
return;
}
var values = posts.map(function (post) {
return [
post.permalink,
post.id,
new Date(post.timestamp), // JST表示はシート側のタイムゾーン設定に依存
];
});
sheet.getRange(2, 1, values.length, 3).setValues(values);
}
Backup.gs(バックアップ作成)
/**
* スプレッドシートのバックアップを作成するユーティリティ
*
* フォーマット:
* シート名_backup_yyyyMMdd_HHmm
*
* 例:
* InstagramData_backup_20251202_2315
*/
function createSpreadsheetBackup() {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var originalName = ss.getName();
// 実行日時を JST(スクリプトのタイムゾーン)でフォーマット
var now = new Date();
var tz = Session.getScriptTimeZone();
var stamp = Utilities.formatDate(now, tz, "yyyyMMdd_HHmm");
var backupName = originalName + "_backup_" + stamp;
// 元ファイルの親フォルダを取得
var file = DriveApp.getFileById(SPREADSHEET_ID);
var parents = file.getParents();
if (!parents.hasNext()) {
// 親フォルダが無いケース(マイドライブ直下など)は、そのままマイドライブ直下に backup フォルダを作る
var backupRoot = getOrCreateBackupFolder_(DriveApp);
file.makeCopy(backupName, backupRoot);
return;
}
// 通常は親フォルダを 1 つだけ想定
var parent = parents.next();
var backupFolder = getOrCreateBackupFolder_(parent);
// backup フォルダ内にコピーを作成
file.makeCopy(backupName, backupFolder);
}
/**
* 指定フォルダ配下に backup フォルダを取得 or 作成
*
* @param {Folder} parentFolder
* @returns {Folder}
*/
function getOrCreateBackupFolder_(parentFolder) {
var folders = parentFolder.getFoldersByName("backup");
if (folders.hasNext()) {
return folders.next();
}
return parentFolder.createFolder("backup");
}
Logger.gs(エラーログ記録)
/**
* エラーログなどの記録を行うユーティリティ
*/
/**
* エラー内容をログシートに記録する
*
* @param {string} message
* @param {string=} detail
*/
function logError_(message, detail) {
try {
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_NAME_ERROR_LOG);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME_ERROR_LOG);
sheet.getRange(1, 1, 1, 3).setValues([["日時", "メッセージ", "詳細"]]);
}
sheet.appendRow([new Date(), message, detail || ""]);
} catch (e) {
// ログ書き込み自体が失敗した場合は、Stackdriver ログにだけ残す
console.error("logError_ でエラー: " + e.message);
}
}
fetchInstagramPosts.gs(メインエントリポイント)
/**
* メインエントリポイント
* - アクセストークン取得
* - Instagram Graph API 呼び出し
* - データの重複排除・ソート
* - スプレッドシートへの書き込み
*/
function fetchInstagramPosts() {
try {
// 1. Instagram から投稿データを取得(全件)
var posts = fetchRecentMediaAll();
// 2. 既存投稿をスプレッドシートから取得
var existingPosts = getExistingPostsFromSpreadsheet();
// 3. 既存投稿のIDセットを作成(重複排除用)
var existingIdsSet = new Set(
existingPosts.map(function (post) {
return post.id;
})
);
// 4. 新規投稿のみ抽出
var newPosts = posts.filter(function (post) {
return !existingIdsSet.has(post.id);
});
// 5. 全投稿を結合(既存投稿 + 新規投稿)
var allPosts = existingPosts.concat(newPosts);
// 6. 投稿日時で降順ソート(全投稿)
var sorted = sortPostsByTimestamp(allPosts);
// 7. スプレッドシートへ全投稿を書き込み
writeAllPostsToSpreadsheet(sorted);
// 8. バックアップ作成
// 同じ階層の「backup」フォルダに
// シート名_backup_yyyyMMdd_HHmm 形式でコピーを作成
createSpreadsheetBackup();
} catch (e) {
logError_(
"fetchInstagramPosts でエラー",
e && e.stack ? e.stack : e.message
);
throw e;
}
}
3.5 トリガーの設定
GAS のエディタから「トリガー」を設定し、毎日 23 時台に実行されるように設定します。
- 左メニューから「トリガー」を選択
- 「トリガーを追加」をクリック
- 以下の設定を行う
- 実行する関数:
fetchInstagramPosts - イベントのソース: 時間主導型
- 時間ベースのトリガー: 日次タイマー
- 時刻: 23 時台
- 実行する関数:
3.6 動作確認
実装が完了したら、手動で実行して動作を確認しました。
- GAS エディタで
fetchInstagramPosts関数を選択 - 「実行」ボタンをクリック
- スプレッドシートにデータが正しく保存されているか確認
-
backupフォルダにバックアップが作成されているか確認
まとめ
本記事では、Instagram Graph API を使って投稿データを取得するシステムを構築する過程を紹介しました!
Instagram API には制約が多く、最初は「過去 1 年分の投稿を取得したい」という要件でしたが、recent_media は過去 24 時間分のみ取得可能という制約があることが分かりました。そのため、1 日 1 回の定期実行でデータを蓄積していく方式に変更しました。
また、Instagram Graph API の認証は複雑で、Facebook ページとの連携や適切な権限設定が必要でした。権限が表示されない問題に直面した際は、アプリの設定や Facebook ページとのリンクを確認することで解決できました。
GAS で実装することで、スプレッドシートとの連携やバックアップの自動化が容易になり、運用面でもメリットがありました。特に、スクリプトプロパティを使った認証情報の管理や、トリガーを使った定期実行は、GAS の強みを活かせた部分だと思います。
同様のシステムを構築したい方の参考になれば嬉しいです!


