【コピペOK】GASでX(旧Twitter)の投稿を完全自動化する方法|元ITパパ

ブログ運営

本ページはプロモーションが含まれています

こんにちは、「元ITパパ」のシュウです。

ブログへの集客に、X(旧Twitter)の活用が欠かせないことは、誰もが分かっています。
でも、毎日決まった時間に投稿し続けるのは、本当に大変ですよね。

「Googleスプレッドシートで、全ての投稿を一度に、直感的に管理したい」
「SNS運用は自動化して、自分は記事執筆に集中したい」

そう考えたのが、このシステムを作ったきっかけです。

この記事では、元ITエンジニアの僕が、実際にこのブログで運用している「投稿自動化システム」の作り方を、誰でも絶対に挫折しないよう、3つのポイントを押さえて解説していきます。

  • ポイント①:プログラミング知識は一切不要です。 
    全てのコードは「コピペでOK」な形で用意します。
  • ポイント②:全ての操作を、スクリーンショット付きで解説します。
    あなたは、この記事の画像と同じ場所をクリックし続けるだけで大丈夫です。
  • ポイント③:設定済みの「スプレッドシート」を、テンプレートとして無料配布します。
    あなたは、そのシートをコピーして、文字を書き換えるだけで始められます。

特に、多くの人がつまずきがちな「XのAPIキー取得」については、別記事で、全ての画面の画像を付けて、世界一やさしく解説しています。 ですので、安心してください。

この記事は「難しい技術解説」ではありません。 面倒な作業からあなたを解放し、ブログで最も大切な「記事を書く」という本質的な作業に、もっと多くの時間を使えるようにするための、「穴埋め式の、親切なワークブック」です。

このシステムで、できること(活用例)

具体的な手順に入る前に、このシステムが完成すると、一体どんな応用ができるのか。
その「活用例」からお見せします。

このシステムの心臓部は、たった2つ。
「Googleスプレッドシート」と、その上で動く「GAS」だけです。
一度設定してしまえば、あとはスプレッドシートに投稿したい内容を書き込んでいくだけ。

例えば、僕のブログ『シュウの備忘録』のXアカウントでは、このシステムを使って、現在、実験的に以下のような時間で、自動投稿を運用しています。

  • 朝8:00: ひとことIT・豆知識ポスト
  • 昼12:00: 過去の良質な記事を再紹介するポスト
  • 夜21:00: その日に公開した新しい記事の告知ポスト

もちろん、これはあくまで一例です。
このシステムの本当に素晴らしいところは、スプレッドシートの中身や、GASのトリガー設定を少し変えるだけで、あなたのブログの戦略に合わせて、いくらでも自由に応用できる点にあります。

どうですか?ワクワクしてきませんか?
次の章から、この柔軟なシステムの具体的な作り方を、ステップバイステップで解説していきますね。

STEP1:投稿用のデータベースを、スプレッドシートで作成する

まずはじめに、自動投稿したいツイートの内容を溜めておくための、データベース(台帳)を作成します。 難しく考える必要はありません。ただの「Googleスプレッドシート」です。

ご自身のGoogleアカウントで、新しいスプレッドシートを作成し、1行目に、以下の7つの項目を、A列からG列にかけて入力してください。

  • A列:投稿内容
  • B列:種別
  • C列:投稿予定日 (※時間指定する場合のみ入力)
  • D列:投稿予定時刻 (※時間指定する場合のみ入力)
  • E列:投稿済み日時 (※GASが自動記録)
  • F列:文字数 (※カスタム関数で自動計算)
  • G列:判定 (※数式で自動判定)

それぞれの役割の詳細は、記事の後半で解説します。まずは、この見出しだけを作成してください。

とはいえ、自分で一から作るのは少し面倒かもしれませんね。
以下のリンクから、僕が実際に使っているものと全く同じ構成のスプレッドシートを閲覧できます。
ご自身のGoogleアカウントにログインした状態で、メニューの「ファイル」→「コピーを作成」を選択すれば、あなただけの管理シートとして、すぐに使い始めることができます。

▼【テンプレート】X(旧Twitter)自動投稿管理シート▼

STEP2:GAS(Google Apps Script)を記述する

スプレッドシートという「データベース」の準備ができました。
次はいよいよ、このシステムに「魂」を吹き込む、プログラムを記述していきます。
使うのは、GAS(Google Apps Script)です。

その前に、そもそも「GAS」とは?

GAS、と聞いても、ピンとこない方がほとんどだと思います。 ご安心ください。
一言でいうと、GASは「Google版の、Excelマクロ」のようなものです。

Excelのマクロが、ボタン一つで複雑な計算や作業を自動化してくれるように、GASは、スプレッドシートやGmailといった、Googleの様々なサービスを、プログラムで自由に操るための、公式の道具なんです。

「プログラム」と聞くと、とても難しそうに感じるかもしれません。 でも、大丈夫。
今回の記事の3つのポイントで述べた通り、あなたがその「使い方」を覚える必要は一切ありません。

僕が作った「完成品のプログラム」を、ただコピー&ペーストするだけです。

それでは、早速システムの頭脳となるプログラムを、作成していきましょう。

2-1. スクリプトエディタを開く

まず、先ほど作成したスプレッドシートを開いた状態で、上部のメニューから「拡張機能」をクリックし、「Apps Script」を選択してください。

2-2. コードを貼り付ける

すると、新しいタブで、コードを書き込むための「エディタ」画面が開きます。 最初から入力されているfunction myFunction() { ... }といった文字は、一度すべて削除して、画面を空っぽにしてください。

コードが長いので折りたたんでいます。コードをコピーしたい方は、「ソースコードを見る」をクリックして、その下にある「クリップボードにコピー」をクリックしてコードをコピーし、空っぽになったエディタ画面に、そのまま貼り付けてください。

/**
 * X(旧Twitter)投稿自動化スクリプト Ver. 1.0
 * * このスクリプトは、ブログ「シュウの備忘録」の読者のために提供されています。
 * * @author   シュウ(元ITパパ)
 * @blog     シュウの備忘録
 * @url      https://shu-note.com/
 * @license  改変、再配布はご自由ですが、その際は出典元として当ブログへのリンクを明記していただけると嬉しいです。
 */

// === スクリプトプロパティから認証情報を取得 ===
// セキュリティのため、APIキーやIDはここに直接書き込まず、
// GASの「プロジェクトの設定」>「スクリプト プロパティ」に保存してください。
const SCRIPT_PROPERTIES = PropertiesService.getScriptProperties();
const consumerKey = SCRIPT_PROPERTIES.getProperty("consumerKey");
const consumerSecret = SCRIPT_PROPERTIES.getProperty("consumerSecret");
const accessToken = SCRIPT_PROPERTIES.getProperty("accessToken");
const accessSecret = SCRIPT_PROPERTIES.getProperty("accessSecret");
const SPREADSHEET_ID = SCRIPT_PROPERTIES.getProperty("SPREADSHEET_ID");

// === 基本設定 ===
const SHEET_NAME = "PostSchedule"; // あなたが使用するシート名に合わせてください

// ===============================================================
// ■ メインの実行関数
// この「main」関数を、10分または15分おきのトリガーで実行してください。
// ===============================================================
function main() {
  const now = new Date();
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
  if (!sheet) {
    Logger.log("エラー: シートが見つかりません。シート名を確認してください。");
    return;
  }
  const data = sheet.getDataRange().getValues();

  // --- 1. 時間指定投稿を最優先でチェック ---
  for (let i = 1; i < data.length; i++) {
    const postDate = data[i][2]; // C列: 投稿予定日
    const postTime = data[i][3]; // D列: 投稿予定時刻
    const postedTime = data[i][4]; // E列: 投稿済み日時

    // 投稿予定日時があり、まだ投稿されていないものを探す
    if (postDate instanceof Date && postTime instanceof Date && !postedTime) {
      // 日付と時刻を組み合わせて、一つのDateオブジェクトを生成
      const scheduledTime = new Date(
        postDate.getFullYear(),
        postDate.getMonth(),
        postDate.getDate(),
        postTime.getHours(),
        postTime.getMinutes()
      );

      if (scheduledTime <= now) {
        const textToPost = data[i][0]; // A列: 投稿内容
        Logger.log(`時間指定投稿を実行します: ${textToPost}`);
        const result = postToTwitter(textToPost);
        if (result) {
          sheet.getRange(i + 1, 5).setValue(new Date()); // E列に投稿日時を記録
        }
        return; // 1回の実行で1投稿のみなので、ここで処理を終了
      }
    }
  }

  // --- 2. 時間指定投稿がなければ、定時ランダム投稿をチェック ---
  const currentHour = now.getHours();

  if (currentHour === 8) { // 朝8時台なら「豆知識」を投稿
    Logger.log("朝の「豆知識」投稿を試みます。");
    postRandomTweetByType("豆知識", sheet, data);
  } else if (currentHour === 12) { // 昼12時台なら「過去記事」を投稿
    Logger.log("昼の「過去記事」投稿を試みます。");
    postRandomTweetByType("過去記事", sheet, data);
  }
}

/**
 * [内部関数] 指定された「種別」の中から、未投稿のツイートを1つランダムに選び投稿
 * @param {string} type - 投稿したいツイートの種別 (B列の値)
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet - 対象シート
 * @param {Array<Array<any>>} data - シートの全データ
 */
function postRandomTweetByType(type, sheet, data) {
  // 本日、同じ種別の投稿が既に完了しているかチェック
  const today = new Date();
  today.setHours(0, 0, 0, 0); // 今日の日付の0時0分

  for (let i = 1; i < data.length; i++) {
    const postType = data[i][1];     // B列: 種別
    const postedTime = data[i][3]; // D列: 投稿済み日時

    if (postedTime instanceof Date && postType === type) {
      const postedDate = new Date(postedTime);
      postedDate.setHours(0, 0, 0, 0); // 投稿日の0時0分
      
      if (today.getTime() === postedDate.getTime()) {
        Logger.log(`本日、種別'${type}'の投稿は既に完了しています。`);
        return; // 今日のこの種別の投稿は終わり
      }
    }
  }

  // 投稿候補をリストアップ
  const candidates = [];
  for (let i = 1; i < data.length; i++) {
    const text = data[i][0]; // A列: 投稿内容
    const postType = data[i][1]; // B列: 種別
    const postedTime = data[i][3]; // D列: 投稿済み日時
    
    // 投稿内容があり、種別が一致し、まだ投稿されていないものを候補とする
    if (text && postType === type && !postedTime) {
      candidates.push({
        row: i + 1, // 行番号 (1始まり)
        text: text,
      });
    }
  }

  // 投稿できる候補がなければ終了
  if (candidates.length === 0) {
    Logger.log(`'${type}'で投稿できるツイートがありませんでした。`);
    return;
  }
  
  // 候補の中からランダムで1つ選ぶ
  const selectedTweet = candidates[Math.floor(Math.random() * candidates.length)];
  
  // Xに投稿
  const result = postToTwitter(selectedTweet.text);
  
  // 成功したらD列に投稿日時を記録
  if (result) {
    sheet.getRange(selectedTweet.row, 4).setValue(new Date());
  }
}


/**
 * [X投稿実行関数] X(旧Twitter)にツイートを投稿する
 * @param {string} tweetText 投稿するテキスト
 */
function postToTwitter(tweetText) {
  const apiUrl = "https://api.twitter.com/2/tweets";
  const method = "POST";

  const nonce = Utilities.getUuid().replace(/-/g, "");
  const timestamp = Math.floor(Date.now() / 1000).toString();

  const oauthParams = {
    oauth_consumer_key: consumerKey,
    oauth_nonce: nonce,
    oauth_signature_method: "HMAC-SHA1",
    oauth_timestamp: timestamp,
    oauth_token: accessToken,
    oauth_version: "1.0",
  };

  const baseParams = Object.keys(oauthParams).sort().map(key => 
    `${encodeURIComponent(key)}=${encodeURIComponent(oauthParams[key])}`
  ).join("&");

  const baseString = [method, encodeURIComponent(apiUrl), encodeURIComponent(baseParams)].join("&");
  const signingKey = `${encodeURIComponent(consumerSecret)}&${encodeURIComponent(accessSecret)}`;

  // GASの標準機能で署名を生成
  const signatureBytes = Utilities.computeHmacSignature(
    Utilities.MacAlgorithm.HMAC_SHA_1,
    baseString,
    signingKey
  );
  
  // 署名をBase64形式にエンコード
  const signature = Utilities.base64Encode(signatureBytes);

  oauthParams.oauth_signature = signature;

  const authHeader = "OAuth " + Object.keys(oauthParams).sort().map(key => 
    `${encodeURIComponent(key)}="${encodeURIComponent(oauthParams[key])}"`
  ).join(", ");

  const payload = JSON.stringify({ text: tweetText });

  try {
    const res = UrlFetchApp.fetch(apiUrl, {
      method: "POST",
      contentType: "application/json",
      payload: payload,
      muteHttpExceptions: true,
      headers: {
        Authorization: authHeader,
      },
    });

    const code = res.getResponseCode();
    const body = res.getContentText();

    if (code === 201 || code === 200) {
      Logger.log(`✅ 投稿成功: ${tweetText}`);
      return true;
    } else {
      Logger.log(`❌ 投稿失敗 (${code}): ${body}`);
      return false;
    }
  } catch (e) {
    Logger.log(`致命的なエラー: ${e}`);
    return false;
  }
}

/**
 * X(旧Twitter)の特殊な文字数カウントを、より正確に計算する新バージョンの関数
 * @param {string} text 文字数を計算したいテキスト
 * @return {number} Twitterルールで計算した文字数
 * @customfunction
 */
function TWITTER_LEN(text) {
  if (typeof text !== "string" || text === "") {
    return 0;
  }

  // Twitterの文字数制限は280
  const TWEET_MAX_LENGTH = 280;
  // URLはt.coで短縮され、23文字としてカウントされる
  const URL_WEIGHT = 23;

  const urlRegex = /https?:\/\/[\w!?/+\-_~=;.,*&@#$%\(\)\'\[\]]+/g;
  
  // URLを特殊な文字に置き換えて、後でカウントできるようにする
  const textForCounting = text.replace(urlRegex, " ");

  let weightedLength = 0;
  // テキストの各文字をループ
  for (const char of textForCounting) {
    // Unicodeの正規化(特定の絵文字などを正しくカウントするため)
    const normalizedChar = char.normalize("NFC");
    const charCode = normalizedChar.charCodeAt(0);

    // 特定の範囲の文字(日本語、全角記号など)を2文字としてカウント
    if (
      (charCode >= 0x2000 && charCode <= 0x2e7f) || // 一般的な句読点、記号
      (charCode >= 0x3000 && charCode <= 0x30ff) || // CJKの記号、句読点、ひらがな、カタカナ
      (charCode >= 0x4e00 && charCode <= 0x9fff) || // CJK統合漢字
      (charCode >= 0xac00 && charCode <= 0xd7af) || // ハングル音節
      (charCode >= 0xff00 && charCode <= 0xffef)   // 半角・全角形
    ) {
      weightedLength += 2;
    } else {
      weightedLength += 1;
    }
  }

  // URLの文字数を加算
  const urls = text.match(urlRegex);
  if (urls) {
    weightedLength += urls.length * URL_WEIGHT;
  }
  
  return weightedLength;
}

2-3. プロジェクトを保存する

貼り付けが完了したら、エディタの上部にあるフロッピーディスクの形をした「プロジェクトを保存」アイコンをクリックして、スクリプトを保存します。

信じられないかもしれませんが、これだけで、システムのプログラム部分は、もう完成です。簡単ですよね?

STEP3:XのAPI設定を行う

さて、ここから、このシステムの心臓部である「APIキー」と「トークン」を取得していきますが、この手順が、初心者にとっては一番の難関です。

そのため、APIキーの詳しい取得手順については、下の記事で、全てのステップを画像付きで丁寧に解説しています。

まずは、こちらの記事を参考にして、4種類のキーの取得してください。

GASに「キー」と「トークン」を設定する

取得した4つのキーを、GASに設定します。

GASのエディタ画面に戻り、左メニューの「プロジェクトの設定」(歯車マーク)をクリックします。

「スクリプト プロパティを追加」を選択してください。

4つのプロパティを追加し、それぞれに、先ほど控えた「キー」と「トークン」の文字列を貼り付けていきます。

  • consumerKey (← API Keyを貼り付け)
  • consumerSecret (← API Key Secretを貼り付け)
  • accessToken (← Access Tokenを貼り付け)
  • accessSecret (← Access Token Secretを貼り付け)

次にスプレッドシートの固有IDを登録します。
固有のIDはスプレッドシートを開いたときのURLから取得することが出来ます。

https://docs.google.com/spreadsheets/d/XXXXXXX/edit#gid=YYY
「XXXXXXX」の部分が特定のスプレッドシートに割り当てられた固有のIDになります。

スクリプトプロパティを追加し、プロパティ名に「SPREADSHEET_ID」と入力し、「XXXXXXX」の部分を値に入力してください。

最後に「スクリプトプロパティを保存」を選択します。

STEP4:トリガーを設定し、自動化を完了させる

お疲れ様でした!
これで、スプレッドシート(手足)、GAS(頭脳)、APIキー(許可証)の全てが揃いました。

最後に、「10分に一回、投稿すべきツイートがないか、見回りに行ってください」と、Googleに命令を出す「トリガー」というものを設定します。これが、本当に最後の作業です。

4-1. トリガー設定画面を開く

GASのエディタ画面に戻り、左のメニューから「トリガー」(目覚まし時計のマーク)をクリックしてください。

4-2. 新しいトリガーを追加する

画面右下にある「トリガーを追加」ボタンをクリックします。

すると、トリガーの詳細を設定する画面が表示されますので、以下の通りに設定してください。

  • 実行する関数を選択: main
  • 実行するデプロイを選択: Head
  • イベントのソースを選択: 時間主導型
  • 時間ベースのトリガーのタイプを選択: 分ベースのタイマー
  • 時間の間隔を選択(分): 10分おき

全て選択できたら、最後に「保存」ボタンをクリックします。

Googleアカウントの承認を求める画面が表示されたら、画面の指示に従い、承認してください。

4.3. 完成!

おめでとうございます! これにて、全ての作業は完了です。

これ以降、あなたが寝ている間も、仕事をしている間も、Googleのサーバーが、10分に一度、あなたのスプレッドシートを健気に見回り、投稿すべきツイートがあれば、自動でXに投稿してくれます。

まとめ:面倒な作業は、すべて自動化しよう

お疲れ様でした!
これにて、あなただけの「X(旧Twitter)自動投稿システム」の、完成です。

最初は専門用語が多く、難しく感じたかもしれません。
しかし、一つ一つのステップを乗り越えた今、あなたはもう、面倒な投稿作業に、毎日頭を悩ませる必要はありません。

このシステムが、あなたの代わりに24時間365日働き続けてくれます。
空いた時間を、ぜひ、新しい記事の執筆や、ご家族と過ごす、もっと価値のある時間に使ってください。
ちなみに、その大事な記事を安心して公開できる、信頼性の高いサーバーの選び方については、こちらの記事で詳しく解説しています。

この記事が、あなたのブログ運営の一助となれば、僕にとって、それ以上に嬉しいことはありません。

コメント

タイトルとURLをコピーしました