技術ブログ

【GAS】ChatGPT/Claude連携でブログ記事を自動生成!

/ 初心者向け

【GAS】ChatGPT/Claude連携でブログ記事を自動生成!

ChatGPT/Claude APIとGASを連携させてブログ記事を自動生成する方法

「ブログ記事のネタ探しに時間がかかる」「執筆に追われてしまう」「もっと効率的にコンテンツを作成したい」

あなたはそんな悩みを抱えていませんか?

この記事では、Google Apps Script(GAS)とChatGPTやClaudeなどの大規模言語モデル(LLM)APIを連携させることで、ブログ記事のアイデア出しから記事の本文生成までを自動化する方法を、初心者の方でもすぐに実践できるように、具体的なコードと設定手順を交えて解説します。

この記事で解決できること

  • ブログ記事のアイデア出しの自動化: キーワードに基づいて、AIが記事のテーマや構成案を提案します。
  • ブログ記事本文の自動生成: 提案された構成案に基づき、AIが記事の本文を執筆します。
  • 執筆作業の効率化: 手作業での記事作成にかかる時間を大幅に削減できます。
  • GASとLLM API連携の基本: 初めてGASやAPI連携に触れる方でも理解できるように、丁寧に解説します。

必要なもの

1. Googleアカウント: Google Apps Scriptを使用するために必要です。

2. ChatGPTまたはClaudeのアカウントとAPIキー: OpenAIまたはAnthropicの公式サイトで取得します。

3. GASの基本的な知識(あれば尚可): なくても大丈夫ですが、あると理解が深まります。

設定手順

Step 1: LLM APIキーの取得

  • OpenAI (ChatGPT):

1. OpenAIのウェブサイトにアクセスし、アカウントを作成またはログインします。

2. APIセクションに移動し、APIキーを生成します。

3. 生成されたAPIキーを安全な場所にコピーしておきます。

  • Anthropic (Claude):

1. Anthropicのウェブサイトにアクセスし、アカウントを作成またはログインします。

2. APIセクションに移動し、APIキーを生成します。

3. 生成されたAPIキーを安全な場所にコピーしておきます。

Step 2: Google Apps Scriptプロジェクトの作成

1. Google Driveを開き、「新規」→「その他」→「Google Apps Script」を選択して、新しいプロジェクトを作成します。

2. プロジェクトに分かりやすい名前(例: 「ブログ記事自動生成」)を付けます。

Step 3: GASコードの実装

以下のコードをGASエディタにコピー&ペーストしてください。

/**
 * ブログ記事のアイデアを生成し、本文を執筆するGASスクリプト
 */

// --- 設定項目 ---
const OPENAI_API_KEY = 'YOUR_OPENAI_API_KEY'; // ここにOpenAIのAPIキーを入力
// const ANTHROPIC_API_KEY = 'YOUR_ANTHROPIC_API_KEY'; // Claudeを使う場合はこちらを有効化し、APIキーを入力
const TARGET_KEYWORD = 'GAS 自動化'; // ブログ記事のテーマとなるキーワード
const ARTICLE_TITLE_PROMPT = `「${TARGET_KEYWORD}」に関するブログ記事のタイトル案を3つ提案してください。読者の検索意図を考慮した、魅力的でクリックされやすいタイトルにしてください。`;
const ARTICLE_STRUCTURE_PROMPT_TEMPLATE = (title) => `「${title}」というタイトルのブログ記事の構成案を、見出し(h2, h3)を用いて具体的に提案してください。各見出しでどのような内容を解説するか、簡潔に説明を加えてください。

構成案はMarkdown形式で出力してください。`;
const ARTICLE_BODY_PROMPT_TEMPLATE = (title, structure) => `「${title}」というタイトルのブログ記事を、以下の構成案に基づいて執筆してください。

構成案:
${structure}

記事は、読者が理解しやすいように、専門用語は避け、平易な言葉で解説してください。導入部分では読者の興味を引きつけ、結論部分では記事の要点をまとめ、読者に行動を促すような内容にしてください。

記事はMarkdown形式で出力してください。`;

// --- メイン処理 ---
function generateBlogPost() {
  try {
    // 1. 記事タイトルの生成
    Logger.log('記事タイトルの生成を開始します...');
    const titleResponse = callLLMAPI(OPENAI_API_KEY, TARGET_KEYWORD, ARTICLE_TITLE_PROMPT, 'openai'); // 'openai' または 'anthropic' を指定
    const articleTitle = parseTitleResponse(titleResponse);
    Logger.log(`生成されたタイトル案: ${articleTitle}`);

    if (!articleTitle) {
      throw new Error('記事タイトルが生成できませんでした。');
    }

    // 2. 記事構成案の生成
    Logger.log('記事構成案の生成を開始します...');
    const structureResponse = callLLMAPI(OPENAI_API_KEY, TARGET_KEYWORD, ARTICLE_STRUCTURE_PROMPT_TEMPLATE(articleTitle), 'openai');
    const articleStructure = parseStructureResponse(structureResponse);
    Logger.log(`生成された構成案:
${articleStructure}`);

    if (!articleStructure) {
      throw new Error('記事構成案が生成できませんでした。');
    }

    // 3. 記事本文の生成
    Logger.log('記事本文の生成を開始します...');
    const bodyResponse = callLLMAPI(OPENAI_API_KEY, TARGET_KEYWORD, ARTICLE_BODY_PROMPT_TEMPLATE(articleTitle, articleStructure), 'openai');
    const articleBody = parseBodyResponse(bodyResponse);
    Logger.log(`生成された記事本文(一部):
${articleBody.substring(0, 200)}...`);

    // 4. 生成された記事をスプレッドシートに記録
    recordBlogPost(articleTitle, articleStructure, articleBody);
    Logger.log('ブログ記事の生成が完了し、スプレッドシートに記録されました。');

  } catch (e) {
    Logger.log(`エラーが発生しました: ${e.message}`);
    SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Log').getRange('A1').setValue(`エラー: ${e.message}`);
  }
}

/**
 * LLM APIを呼び出す関数
 * @param {string} apiKey - APIキー
 * @param {string} keyword - ターゲットキーワード
 * @param {string} prompt - APIに渡すプロンプト
 * @param {string} modelType - 'openai' または 'anthropic'
 * @returns {object} APIからのレスポンスオブジェクト
 */
function callLLMAPI(apiKey, keyword, prompt, modelType) {
  let endpoint;
  let requestBody;

  if (modelType === 'openai') {
    endpoint = 'https://api.openai.com/v1/chat/completions';
    requestBody = {
      model: 'gpt-3.5-turbo', // または 'gpt-4'
      messages: [
        { role: 'system', content: 'あなたは経験豊富なブロガーです。' },
        { role: 'user', content: prompt }
      ],
      max_tokens: 1000, // 必要に応じて調整
      temperature: 0.7
    };
  } else if (modelType === 'anthropic') {
    // Claude APIの例(最新のAPI仕様に合わせて調整してください)
    endpoint = 'https://api.anthropic.com/v1/messages';
    requestBody = {
      model: 'claude-3-opus-20240229', // または 'claude-3-sonnet-20240229' など
      max_tokens: 1000, // 必要に応じて調整
      messages: [
        { role: 'user', content: prompt }
      ]
    };
    // ClaudeではHTTPヘッダーでAPIキーを指定します
    return UrlFetchApp.fetch(endpoint, {
      method: 'post',
      headers: {
        'x-api-key': apiKey,
        'anthropic-version': '2023-06-01' // APIバージョン
      },
      contentType: 'application/json',
      payload: JSON.stringify(requestBody)
    });

  } else {
    throw new Error('指定されたモデルタイプはサポートされていません。');
  }

  const options = {
    method: 'post',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify(requestBody)
  };

  return UrlFetchApp.fetch(endpoint, options);
}

/**
 * LLM APIレスポンスから記事タイトルを抽出する関数
 * @param {object} response - APIからのレスポンスオブジェクト
 * @returns {string|null} 抽出された記事タイトル、またはnull
 */
function parseTitleResponse(response) {
  const responseText = response.getContentText();
  const data = JSON.parse(responseText);
  if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
    // 複数提案されたタイトルから最初のものを採用
    const titles = data.choices[0].message.content.split('\n').map(t => t.trim()).filter(t => t !== '');
    return titles.length > 0 ? titles[0].replace(/^\d+\.\s*/, '') : null;
  } else if (data.content && data.content.length > 0 && data.content[0].text) { // Claude APIの場合
    const titles = data.content[0].text.split('\n').map(t => t.trim()).filter(t => t !== '');
    return titles.length > 0 ? titles[0].replace(/^\d+\.\s*/, '') : null;
  }
  return null;
}

/**
 * LLM APIレスポンスから記事構成案を抽出する関数
 * @param {object} response - APIからのレスポンスオブジェクト
 * @returns {string|null} 抽出された記事構成案、またはnull
 */
function parseStructureResponse(response) {
  const responseText = response.getContentText();
  const data = JSON.parse(responseText);
  if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
    return data.choices[0].message.content;
  } else if (data.content && data.content.length > 0 && data.content[0].text) { // Claude APIの場合
    return data.content[0].text;
  }
  return null;
}

/**
 * LLM APIレスポンスから記事本文を抽出する関数
 * @param {object} response - APIからのレスポンスオブジェクト
 * @returns {string|null} 抽出された記事本文、またはnull
 */
function parseBodyResponse(response) {
  const responseText = response.getContentText();
  const data = JSON.parse(responseText);
  if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) {
    return data.choices[0].message.content;
  } else if (data.content && data.content.length > 0 && data.content[0].text) { // Claude APIの場合
    return data.content[0].text;
  }
  return null;
}

/**
 * 生成されたブログ記事をスプレッドシートに記録する関数
 * @param {string} title - 記事タイトル
 * @param {string} structure - 記事構成案
 * @param {string} body - 記事本文
 */
function recordBlogPost(title, structure, body) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('GeneratedBlogPosts');

  if (!sheet) {
    ss.insertSheet('GeneratedBlogPosts');
  }

  const logSheet = ss.getSheetByName('Log');
  if (!logSheet) {
    ss.insertSheet('Log');
    logSheet.getRange('A1').setValue('Log messages will appear here.');
  }

  const sheetToRecord = ss.getSheetByName('GeneratedBlogPosts');
  if (!sheetToRecord.getRange('A1').getValue()) {
    // ヘッダー行の書き込み
    sheetToRecord.appendRow(['作成日時', 'タイトル', '構成案', '本文']);
  }

  sheetToRecord.appendRow([new Date(), title, structure, body]);
}

// スプレッドシートとシートの初期設定を行う関数(初回実行時やシートがない場合に呼び出す)
function setupSheets() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss.getSheetByName('GeneratedBlogPosts')) {
    ss.insertSheet('GeneratedBlogPosts');
    ss.getSheetByName('GeneratedBlogPosts').appendRow(['作成日時', 'タイトル', '構成案', '本文']);
    Logger.log('GeneratedBlogPostsシートを作成しました。');
  }
  if (!ss.getSheetByName('Log')) {
    ss.insertSheet('Log');
    ss.getSheetByName('Log').getRange('A1').setValue('Log messages will appear here.');
    Logger.log('Logシートを作成しました。');
  }
}

// スクリプト実行前にsetupSheets()を一度実行してシートを作成しておくと安心です。
// SpreadsheetApp.getUi().alert('スプレッドシートの初期設定が完了しました。');

コードの解説

  • OPENAI_API_KEY / ANTHROPIC_API_KEY: 取得したAPIキーを設定します。
  • TARGET_KEYWORD: 生成したいブログ記事のメインキーワードを設定します。
  • ***_PROMPT:** LLMにどのような記事を生成してほしいかを指示する「プロンプト」です。タイトル、構成案、本文それぞれのプロンプトを用意しています。
  • generateBlogPost(): メインの処理関数です。タイトル→構成案→本文の順に生成し、スプレッドシートに記録します。
  • callLLMAPI(): 指定されたAPIキーとプロンプトを使って、ChatGPTまたはClaudeのAPIを呼び出します。
  • **parse*Response():** APIからのレスポンスを解析し、必要な情報(タイトル、構成案、本文)を抽出します。
  • recordBlogPost(): 生成された記事のタイトル、構成案、本文をGoogleスプレッドシートの「GeneratedBlogPosts」シートに記録します。

Step 4: スプレッドシートの準備

1. GASプロジェクトのメニューから「実行」→「setupSheets」を選択して実行します。

2. これにより、「GeneratedBlogPosts」と「Log」という名前のシートが作成されます。(すでに存在する場合はスキップされます)

3. 「GeneratedBlogPosts」シートには、記事の記録に必要なヘッダー行(作成日時、タイトル、構成案、本文)が自動で追加されます。

Step 5: スクリプトの実行

1. GASエディタのメニューから「実行」→「generateBlogPost」を選択して実行します。

2. 初回実行時には、Googleアカウントへの認可を求められますので、指示に従って許可してください。

3. 実行が完了すると、「GeneratedBlogPosts」シートに新しく生成されたブログ記事の情報が追加されます。

よくあるエラーと対処法

エラー1: APIキーが無効、または正しく設定されていない

  • エラーメッセージ例: API key invalid または Authentication error
  • 原因: APIキーの入力ミス、またはAPIキーが期限切れになっている。
  • 対処法:
  • YOUR_OPENAI_API_KEY または YOUR_ANTHROPIC_API_KEY の部分を、取得したAPIキーに正確に置き換えているか確認してください。
  • APIキーはコードに直接埋め込むため、他人に知られないように注意してください。
  • OpenAIやAnthropicの管理画面でAPIキーが有効か確認し、必要であれば再生成してください。

エラー2: quota exceeded (クォータ超過)

  • エラーメッセージ例: Quota exceeded または Rate limit exceeded
  • 原因: APIの利用回数やトークン数が、設定されている上限を超えた。
  • 対処法:
  • API提供元(OpenAI, Anthropic)の利用規約や料金プランを確認し、クォータを増やしたり、利用を一時停止して翌月まで待ったりしてください。
  • スクリプトの実行間隔を空ける、max_tokens の値を調整するなどの対策も有効です。

エラー3: ネットワーク接続の問題

  • エラーメッセージ例: Exception: Network error
  • 原因: 一時的なネットワークの問題や、GASのサーバー側の問題。
  • 対処法:
  • しばらく時間をおいてから再度実行してみてください。
  • インターネット接続が安定しているか確認してください。

エラー4: レスポンスの解析に失敗

  • エラーメッセージ例: TypeError: Cannot read properties of undefined (reading 'content')
  • 原因: LLMからのレスポンスの形式が予期しないものだった(AIがエラーを返した、またはプロンプトへの理解に問題があった)。
  • 対処法:
  • プロンプトをより具体的に、分かりやすく修正してみてください。
  • TARGET_KEYWORD を変更して、異なるテーマで試してみてください。
  • APIのモデル (gpt-3.5-turboclaude-3-opus-20240229 など) を変更してみるのも有効です。

FAQ

Q1: Claude APIはどのように使えますか?

A1: コード内の callLLMAPI 関数に、Claude API用の処理を追加しています。modelType パラメータを 'anthropic' に設定し、ANTHROPIC_API_KEY にClaudeのAPIキーを入力して使用します。AnthropicのAPI仕様は変更される可能性があるため、最新の情報は公式ドキュメントをご確認ください。

Q2: 生成される記事の質はどのように向上させられますか?

A2: プロンプト(指示文)の質が最も重要です。より詳細な指示、参考情報、出力形式の指定などを加えることで、AIの生成する記事の質を向上させることができます。また、temperature パラメータ(ChatGPTの場合)を調整することで、出力のランダム性を制御できます。

Q3: 記事の著作権はどうなりますか?

A3: AIが生成したコンテンツの著作権の扱いは、利用規約や各国・各サービスの方針によって異なります。一般的には、AI生成コンテンツの著作権は人間が創作したものとは異なり、保護されない場合が多いです。商用利用の際は、各API提供元の利用規約を必ずご確認ください。

Q4: 大量に記事を生成したいのですが、注意点はありますか?

A4: クォータ(利用上限)に注意が必要です。また、機械的に生成された記事ばかりを公開すると、検索エンジンからの評価が下がる可能性もあります。生成された記事は必ず人間が校正・加筆し、オリジナリティや独自性を加えるようにしましょう。

まとめ

この記事では、GASとChatGPT/Claude APIを連携させて、ブログ記事のアイデア出しから本文生成までを自動化する方法を解説しました。コピペで試せるコードと詳細な設定手順、よくあるエラーとその対処法まで含めているので、ぜひこの機会にブログ執筆の効率化に挑戦してみてください。

AIを活用することで、コンテンツ作成にかかる時間を大幅に削減し、より戦略的なブログ運営に集中できるようになります。例えば、集客の要となるキーワード選定やSEO対策に時間を割くことができ、ブログ全体のパフォーマンス向上に繋がるでしょう。

このような業務自動化のツールを導入・活用することで、日々のルーチンワークから解放され、より創造的で価値の高い業務に時間を費やすことが可能になります。もし、ご自身の業務に合わせた自動化ツールの導入や、GASでの開発に興味がある場合は、お気軽にご相談ください。専門家が貴社の状況に合わせて最適なソリューションをご提案いたします。

関連する業務自動化ツール

  • Google Workspace連携ツール:GASはGoogle Workspace(Gmail, Drive, Calendarなど)との連携に非常に強力です。例えば、Gmailに届いたメールの内容を分析してスプレッドシートに記録する、といった自動化も可能です。
  • Zapier / Make (Integromat): GASよりもさらに多様なWebサービスと連携させたい場合は、これらのノーコード・ローコード自動化ツールが便利です。LLM APIとも連携できるため、より高度な自動化フローを構築できます。

GAS自動化の導入相談

請求書PDF作成、Gmail自動送信、Slack通知、スプレッドシート連携などを業務に合わせて実装できます。

請求書自動生成ツールを見る / SNS自動投稿ツールを見る