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による自動化は単純な作業の繰り返しを排除し、チームの情報収集効率を大幅に改善します。
今回のシステムは基本的な構成ですが、記事のカテゴリ分類や感情分析、トレンド予測など、さらなる機能拡張も可能です。ぜひご自身の環境に合わせてカスタマイズし、より価値の高い情報配信システムに発展させてください。