1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GAS × Claude APIで毎朝AIニュース要約をSlackに自動投稿する実装ガイド

1
Posted at

AIによる情報処理の自動化が当たり前になった今、毎朝のニュースチェックも効率化したいものです。GitHub CopilotやClaude、ChatGPTなどのAIツールが開発現場に浸透する中、ビジネス情報の収集・要約も同様にAI化できます。

この記事では、Google Apps Script(GAS)とClaude APIを組み合わせて、指定したニュースサイトから記事を取得し、AIが要約してSlackに投稿するシステムを実装します。毎朝決まった時間に自動実行されるため、チームの情報共有が格段に効率化されます。

環境構築と前提条件

必要なもの

  • Googleアカウント(GASを利用)
  • Anthropic APIキー(Claude API用)
  • Slack APIトークン
  • 基本的なJavaScriptの知識

APIキーの取得手順

まず、Anthropic Console(https://console.anthropic.com/)でAPIキーを取得します。使用量に応じて課金されますが、毎日の要約程度であれば月数百円程度で収まります。

SlackについてはSlack API(https://api.slack.com/)から新しいアプリを作成し、`chat:write`権限を付与したBot User OAuth Tokenを取得してください。

GASプロジェクトのセットアップ

プロパティの設定

Google Apps Scriptの新規プロジェクトを作成し、まずは機密情報をプロパティストアに保存します。

// スクリプトプロパティに設定する項目
// ANTHROPIC_API_KEY: Claude APIキー
// SLACK_BOT_TOKEN: SlackのBot User OAuth Token
// SLACK_CHANNEL: 投稿先チャンネル(例: #general)

function setupProperties() {
  const scriptProperties = PropertiesService.getScriptProperties();
  scriptProperties.setProperties({
    'ANTHROPIC_API_KEY': 'your-anthropic-api-key',
    'SLACK_BOT_TOKEN': 'xoxb-your-slack-bot-token',
    'SLACK_CHANNEL': '#tech-news'
  });
}

メイン処理の実装

次に、ニュース取得から要約、Slack投稿までの一連の流れを実装します。

function main() {
  try {
    console.log('ニュース要約処理を開始します');
    
    // ニュース記事を取得
    const articles = fetchNews();
    
    if (articles.length === 0) {
      console.log('取得できた記事がありません');
      return;
    }
    
    // Claude APIで要約生成
    const summary = generateSummary(articles);
    
    // Slackに投稿
    postToSlack(summary);
    
    console.log('処理が完了しました');
  } catch (error) {
    console.error('エラーが発生しました:', error);
    // エラー時はSlackに通知
    postToSlack(`❌ ニュース要約処理でエラーが発生しました: ${error.message}`);
  }
}

ニュース取得機能の実装

今回はRSSフィードを利用してニュースを取得します。複数のソースから記事を収集できるよう設計します。

function fetchNews() {
  const feedUrls = [
    'https://feeds.techcrunch.com/TechCrunch/',
    'https://www.publickey1.jp/atom.xml',
    'https://zenn.dev/topics/ai/feed'
  ];
  
  const articles = [];
  const today = new Date();
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
  
  feedUrls.forEach(url => {
    try {
      const response = UrlFetchApp.fetch(url, {
        muteHttpExceptions: true,
        headers: {
          'User-Agent': 'Mozilla/5.0 (compatible; NewsBot/1.0)'
        }
      });
      
      if (response.getResponseCode() !== 200) {
        console.warn(`フィード取得失敗: ${url}`);
        return;
      }
      
      const xml = XmlService.parse(response.getContentText());
      const items = extractItemsFromFeed(xml, yesterday);
      articles.push(...items);
      
    } catch (error) {
      console.error(`フィード処理エラー (${url}):`, error);
    }
  });
  
  // 重複除去と記事数制限
  const uniqueArticles = removeDuplicates(articles);
  return uniqueArticles.slice(0, 10); // 最大10記事
}

function extractItemsFromFeed(xml, sinceDate) {
  const root = xml.getRootElement();
  const articles = [];
  
  // RSS 2.0とAtomの両方に対応
  const items = root.getName() === 'rss' 
    ? root.getChild('channel').getChildren('item')
    : root.getChildren('entry');
  
  items.forEach(item => {
    try {
      const title = getElementText(item, root.getName() === 'rss' ? 'title' : 'title');
      const link = root.getName() === 'rss' 
        ? getElementText(item, 'link')
        : item.getChild('link').getAttribute('href').getValue();
      const description = getElementText(item, root.getName() === 'rss' ? 'description' : 'summary');
      const pubDate = parseDate(item, root.getName());
      
      if (pubDate >= sinceDate && title && link) {
        articles.push({
          title: title,
          link: link,
          description: description || '',
          publishedAt: pubDate
        });
      }
    } catch (error) {
      console.warn('記事パース中にエラー:', error);
    }
  });
  
  return articles;
}

function getElementText(element, tagName) {
  const child = element.getChild(tagName);
  return child ? child.getText() : '';
}

function parseDate(item, feedType) {
  const dateStr = feedType === 'rss' 
    ? getElementText(item, 'pubDate')
    : getElementText(item, 'published') || getElementText(item, 'updated');
  
  return dateStr ? new Date(dateStr) : new Date();
}

function removeDuplicates(articles) {
  const seen = new Set();
  return articles.filter(article => {
    const key = article.title.toLowerCase();
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

Claude API連携の実装

取得した記事をClaude APIに送信して要約を生成します。APIレスポンスのパースが重要なポイントです。

function generateSummary(articles) {
  const properties = PropertiesService.getScriptProperties();
  const apiKey = properties.getProperty('ANTHROPIC_API_KEY');
  
  if (!apiKey) {
    throw new Error('Anthropic APIキーが設定されていません');
  }
  
  const prompt = createPrompt(articles);
  const payload = {
    model: 'claude-3-haiku-20240307',
    max_tokens: 1000,
    messages: [
      {
        role: 'user',
        content: prompt
      }
    ]
  };
  
  const response = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01'
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
  
  const responseCode = response.getResponseCode();
  const responseText = response.getContentText();
  
  if (responseCode !== 200) {
    console.error('Claude API Error:', responseText);
    throw new Error(`Claude API呼び出しに失敗しました: ${responseCode}`);
  }
  
  try {
    const data = JSON.parse(responseText);
    return data.content[0].text;
  } catch (error) {
    console.error('レスポンスパースエラー:', responseText);
    throw new Error('Claude APIのレスポンスが不正です');
  }
}

function createPrompt(articles) {
  const today = new Date().toLocaleDateString('ja-JP');
  
  let articlesText = '';
  articles.forEach((article, index) => {
    articlesText += `${index + 1}. ${article.title}\n`;
    articlesText += `   URL: ${article.link}\n`;
    if (article.description) {
      articlesText += `   概要: ${article.description.substring(0, 200)}...\n`;
    }
    articlesText += '\n';
  });
  
  return `
以下の技術・ビジネス系ニュース記事を日本語で要約してください。
日付: ${today}

記事一覧:
${articlesText}

要約の要件:
- 各記事を1-2行で簡潔に要約
- 重要度の高い記事を優先的に上位に配置
- 技術トレンドや業界への影響を重視
- 読みやすいMarkdown形式で出力
- 絵文字を適度に使用して親しみやすく

出力形式:
## 🗞️ 今日のテックニュース (${today})

### 📈 注目のニュース
(重要な記事3-4件)

### 🔧 その他のニュース
(その他の記事)
`;
}

Slack投稿機能の実装

生成された要約をSlackに投稿します。エラーハンドリングも含めて実装します。

function postToSlack(message) {
  const properties = PropertiesService.getScriptProperties();
  const token = properties.getProperty('SLACK_BOT_TOKEN');
  const channel = properties.getProperty('SLACK_CHANNEL');
  
  if (!token || !channel) {
    throw new Error('Slack設定が不完全です');
  }
  
  const payload = {
    channel: channel,
    text: message,
    username: 'ニュースBot',
    icon_emoji: ':robot_face:'
  };
  
  const response = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
  
  const responseData = JSON.parse(response.getContentText());
  
  if (!responseData.ok) {
    throw new Error(`Slack投稿失敗: ${responseData.error}`);
  }
  
  console.log('Slackへの投稿が完了しました');
}

自動実行の設定

毎朝8時に自動実行されるようトリガーを設定します。

function createDailyTrigger() {
  // 既存のトリガーを削除
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'main') {
      ScriptApp.deleteTrigger(trigger);
    }
  });
  
  // 毎日8時に実行するトリガーを作成
  ScriptApp.newTrigger('main')
    .timeBased()
    .everyDays(1)
    .atHour(8)
    .create();
  
  console.log('日次実行トリガーを作成しました');
}

ハマりポイントと対策

XMLパースの注意点

RSSとAtomで構造が異なるため、両方に対応する必要があります。また、フィード提供元によって要素名が微妙に異なる場合があるので、try-catch文での例外処理が重要です。

API制限への対応

Claude APIには使用量制限があります。記事数を制限し、不要な呼び出しを避けることでコストを抑えられます。

GASの実行時間制限

GASは最大6分の実行時間制限があります。処理が重い場合は記事数を調整するか、複数のトリガーに分割することを検討してください。

運用時の改善ポイント

実際に運用してみると、以下のような改善点が見えてきます。

  • 週末や祝日は投稿を停止する
  • 特定のキーワードを含む記事を優先的に要約する
  • 投稿結果をスプレッドシートにログとして保存する
  • エラー通知を別チャンネルに送信する

これらの機能追加により、より実用的なニュースボットに育てることができます。

まとめ

GAS、Claude API、Slackを組み合わせることで、毎朝のニュース要約システムを簡単に構築できました。AIによる自動化は単純な作業の繰り返しを排除し、チームの情報収集効率を大幅に改善します。

今回のシステムは基本的な構成ですが、記事のカテゴリ分類や感情分析、トレンド予測など、さらなる機能拡張も可能です。ぜひご自身の環境に合わせてカスタマイズし、より価値の高い情報配信システムに発展させてください。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?