GASでRSSの新着記事をAIで
要約しSlackに毎朝配信する方法
複数サイトのRSSフィードをXmlServiceで取得し、新着記事だけをClaude APIで1〜2行に要約して、毎朝Slackにまとめて配信する仕組みを、動くコードで解説します。 情報収集の「見に行く」を「届く」に変える、実務向けの自動化です。
Table of Contents
情報収集を「毎朝届く」に変える
業界のニュースや競合のブログを毎日チェックするのは、意外と続きません。 サイトを1つずつ開いて回るうちに、見落としたり、後回しにして溜め込んだりします。
そこで、各サイトが配信するRSS(更新情報を機械が読める形で配るフィード)を使います。 GASで複数フィードの新着だけを集め、Claude APIで1〜2行に要約し、毎朝Slackにまとめて届ける仕組みを作ります。 「見に行く」から「届く」に変わり、タイトルと要約を見て開くべき記事だけを選べるようになります。
巡回の手間ゼロ
複数サイトのRSSをGASが自動で回るので、1つずつ開かなくてよい
要約で取捨選択
タイトル+1行要約で、開く価値がある記事だけを素早く判断できる
チームで共有
Slackに流すので、担当者みんなが同じ情報を朝に受け取れる
重複なし
配信済みの記事を記録し、同じニュースを二度流さない
APIキーとWebhookを安全に保管する
使うのはClaude APIキーと、SlackのIncoming Webhook URL(指定チャンネルに投稿するための送信先URL)の2つです。 どちらもコードに直接書くと共有時に漏れる恐れがあるため、PropertiesService(スクリプトごとの設定値を安全に保管する仕組み)のスクリプトプロパティに保存します。
// スクリプトプロパティにAPIキーとSlack Webhookを保存(初回1回だけ実行)
function saveSecrets() {
PropertiesService.getScriptProperties().setProperties({
CLAUDE_API_KEY: "sk-ant-xxxxxxxx",
SLACK_WEBHOOK_URL: "https://hooks.slack.com/services/xxxx/xxxx/xxxx",
});
}
function getProp(name) {
const v = PropertiesService.getScriptProperties().getProperty(name);
if (!v) throw new Error(name + " が未設定です。saveSecrets を実行してください。");
return v;
}SlackのWebhook URLの取得方法は、GASからSlackに自動通知する方法で詳しく解説しています。
購読するRSSフィードを登録する
チェックしたいサイトのRSS/AtomフィードのURLを配列に並べます。 多くのサイトは/feedや/rss、/atom.xmlをURL末尾に付けるとフィードにアクセスできます。
// 購読したいRSS/AtomフィードのURLを並べる
const FEEDS = [
"https://example.com/blog/feed", // RSS形式でもAtom形式でもOK
"https://another-site.example/atom.xml",
"https://news.example/rss",
];XmlServiceでRSS・Atomを解析する
フィードはUrlFetchAppで取得し、XmlService(GAS標準のXML解析ライブラリ)で読み解きます。 フィードにはRSS形式(rssタグ)とAtom形式(feedタグ)の2種類があり、記事の入れ物や要素名が違います。 ルート要素の名前で判別し、どちらでも同じ形の記事オブジェクトに揃えます。
// 1つのフィードを取得し、記事の配列にして返す(RSS・Atom両対応)
function fetchFeed(url) {
const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
if (res.getResponseCode() !== 200) {
Logger.log("取得失敗: " + url);
return [];
}
// XmlServiceでXMLを解析する(GAS標準のXMLパーサー)
const root = XmlService.parse(res.getContentText()).getRootElement();
const atom = XmlService.getNamespace("http://www.w3.org/2005/Atom");
// <rss> なら channel > item、<feed>(Atom)なら entry を取り出す
if (root.getName() === "rss") {
const channel = root.getChild("channel");
return channel.getChildren("item").map((item) => ({
id: text(item, "guid") || text(item, "link"),
title: text(item, "title"),
link: text(item, "link"),
body: stripTags(text(item, "description")),
}));
} else {
// Atom形式
return root.getChildren("entry", atom).map((entry) => ({
id: text(entry, "id", atom) || getAtomLink(entry, atom),
title: text(entry, "title", atom),
link: getAtomLink(entry, atom),
body: stripTags(text(entry, "summary", atom) || text(entry, "content", atom)),
}));
}
}
// 子要素のテキストを安全に取り出す(無ければ空文字)
function text(parent, name, ns) {
const child = ns ? parent.getChild(name, ns) : parent.getChild(name);
return child ? child.getText().trim() : "";
}
// Atomの <link href="..."> からURLを取り出す
function getAtomLink(entry, ns) {
const link = entry.getChild("link", ns);
return link ? link.getAttribute("href").getValue() : "";
}
// 本文からHTMLタグを除いて短くする
function stripTags(html) {
return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim().slice(0, 400);
}Atom形式は名前空間(namespace)が付くため、getChildに名前空間を渡すのがポイントです。 本文はHTMLタグを除いて先頭だけを残し、要約に渡すトークンを節約します。
配信済みをスキップして新着だけ集める
毎日実行しても同じ記事を流さないよう、配信済みの記事ID(guidまたはURL)をスクリプトプロパティに記録します。 全フィードを回して、まだ記録にない記事だけを新着として集めます。 1回の配信が多すぎないよう、件数に上限も設けます。
// 全フィードを回して、まだ配信していない新着記事だけを集める
function collectNewItems() {
const props = PropertiesService.getScriptProperties();
const sentIds = JSON.parse(props.getProperty("SENT_IDS") || "[]");
const sentSet = {};
sentIds.forEach((id) => (sentSet[id] = true));
const fresh = [];
FEEDS.forEach((url) => {
fetchFeed(url).forEach((item) => {
if (!item.id || sentSet[item.id]) return; // 配信済みはスキップ
if (!item.title) return;
fresh.push(item);
sentSet[item.id] = true; // 同じ実行内での重複も防ぐ
});
});
return fresh.slice(0, 15); // 1回の配信は多くても15件までに絞る
}Claude APIで新着をまとめて要約する
集めた新着記事を1回のリクエストでまとめてClaude APIに渡し、それぞれのsummary(1文の要約)をJSONで受け取ります。 1件ずつ呼ぶより通信回数が減り、速く安く動きます。 RSSの本文は抜粋だけのことが多いため、「推測は書かない」と指示して要約の暴走を防ぎます。
// 新着記事の配列を渡し、1〜2行の日本語要約をまとめて受け取る
function summarizeItems(items) {
if (items.length === 0) return [];
const prompt =
"あなたはIT担当者向けのニュースキュレーターです。\n" +
"次の記事それぞれを、日本語で40字以内・1文に要約してください。\n" +
"本文が抜粋のみの場合は、分かる範囲で要約し、推測は書かないでください。\n" +
"余計な説明は出力せず、JSON配列だけを返してください。\n" +
"形式: [{\"index\":0,\"summary\":\"新機能Xが発表され無料枠が拡大\"}]\n\n" +
"記事(番号: タイトル / 本文抜粋):\n" +
items.map((it, i) => i + ": " + it.title + " / " + it.body).join("\n");
const payload = {
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
};
const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": getProp("CLAUDE_API_KEY"),
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
if (res.getResponseCode() !== 200) {
throw new Error("API error: " + res.getContentText());
}
const responseText = JSON.parse(res.getContentText()).content[0].text;
const parsed = JSON.parse(extractJson(responseText));
// 要約を元の記事にひも付ける
return items.map((it, i) => {
const found = parsed.find((p) => p.index === i);
return { title: it.title, link: it.link, summary: found ? found.summary : "" };
});
}
// レスポンスからJSON配列部分だけを取り出す(前後の余計な文字対策)
function extractJson(text) {
const start = text.indexOf("[");
const end = text.lastIndexOf("]");
return text.slice(start, end + 1);
}AIの返答には前後に説明文が付くことがあるため、[から]までを切り出してからJSON.parseしています。 返ってきた要約はindexを頼りに元記事へひも付けます。
Block KitでSlackに見やすく投稿する
要約付きの記事一覧を、SlackのBlock Kit(メッセージを部品で組み立てる仕組み)で投稿します。 見出し・区切り線・記事ごとのセクションで構成すると、朝の一覧として読みやすくなります。 タイトルはリンクにして、要約を見て気になった記事をすぐ開けるようにします。
// 要約付きの記事一覧をBlock KitでSlackに投稿する
function postToSlack(summaries) {
const blocks = [
{
type: "header",
text: { type: "plain_text", text: "☀️ 今朝の新着記事(" + summaries.length + "件)" },
},
{ type: "divider" },
];
summaries.forEach((s) => {
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: "*<" + s.link + "|" + escapeMrkdwn(s.title) + ">*\n" + s.summary,
},
});
});
UrlFetchApp.fetch(getProp("SLACK_WEBHOOK_URL"), {
method: "post",
contentType: "application/json",
payload: JSON.stringify({ blocks: blocks }),
muteHttpExceptions: true,
});
}
// Slackのmrkdwnで意味を持つ記号をエスケープする
function escapeMrkdwn(t) {
return t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}Slackのmrkdwn(Slack独自の記法)では<や>が特別な意味を持つため、タイトルに含まれると表示が崩れます。エスケープしておくと安全です。
毎朝の定期トリガーで自動配信する
収集・要約・投稿・記録を1つの関数にまとめ、毎朝自動で動かします。 新着がゼロなら投稿をスキップし、無駄な通知を出しません。 一時的なAPIエラーに備えて、失敗したら少し待って再試行するリトライも挟みます。
// 収集 → 要約 → Slack投稿 → 配信済み記録 を一気に実行する
function runDailyDigest() {
const items = collectNewItems();
if (items.length === 0) {
Logger.log("新着記事はありませんでした。");
return;
}
const summaries = summarizeWithRetry(items);
postToSlack(summaries);
// 配信済みIDを記録(直近300件だけ保持して肥大化を防ぐ)
const props = PropertiesService.getScriptProperties();
const sentIds = JSON.parse(props.getProperty("SENT_IDS") || "[]");
const merged = sentIds.concat(items.map((it) => it.id)).slice(-300);
props.setProperty("SENT_IDS", JSON.stringify(merged));
}
// 一時的なAPIエラー時に最大3回まで再試行する
function summarizeWithRetry(items, maxRetry) {
const limit = maxRetry || 3;
for (let attempt = 1; attempt <= limit; attempt++) {
try {
return summarizeItems(items);
} catch (e) {
Logger.log("試行" + attempt + "回目で失敗: " + e.message);
if (attempt === limit) throw e;
Utilities.sleep(1000 * attempt); // 待ち時間を伸ばして再試行
}
}
}あとは時間主導トリガー(時刻や間隔で自動実行する仕組み)で、毎朝の時間帯に実行するよう設定します。atHour(8)なら8〜9時の間に配信されます。
// 毎朝8時台に自動実行するトリガーを作る(初回1回だけ実行)
function createDailyTrigger() {
ScriptApp.newTrigger("runDailyDigest")
.timeBased()
.atHour(8) // 8〜9時の間に実行される
.everyDays(1) // 毎日
.create();
}実務での注意点
1. 要約は「開くかどうか」の判断材料にする
RSSの本文は冒頭の抜粋だけのことが多く、要約が全文の内容と一致しない場合があります。 数字や固有名詞など重要な情報は、必ず元記事のリンクを開いて確認してください。 要約はあくまで取捨選択のための下書きです。
2. フィードの取得マナーを守る
短い間隔で何度も取得すると、相手サーバーに負荷をかけます。配信は1日1〜数回にとどめ、常識的な頻度で利用してください。 取得したフィードの内容の利用が、各サイトの利用規約に反しないかも確認しておくと安心です。
3. 取得できないフィードもある
RSSを提供していないサイトや、独自形式・認証が必要なフィードは取得できません。 取得失敗はログに残してスキップし、他のフィードの配信は止めないようにしています。
4. 実行時間の上限に注意する
GASには1回あたり6分の実行時間制限があります。フィード数や新着件数が多い場合は、要約のバッチ処理と配信件数の上限で1回の処理量を抑えると安定します。
まとめ
RSSフィードの巡回は、XmlServiceでの解析、配信済みのスキップ、Claude APIでのバッチ要約、Block KitでのSlack投稿を組み合わせれば、毎朝自動で届く仕組みにできます。 RSSとAtomの両方に対応させ、要約と一緒に必ず元記事リンクを載せておけば、チーム全員が朝に情報を受け取り、開くべき記事だけを選べます。 要約は取捨選択の下書きとして使い、詳細は元記事で確認する、という前提だけ守って活用してください。
よくある質問
多くのニュースサイトやブログには、購読用のRSS(更新情報を配信する仕組み)が用意されています。ページ内の「RSS」「フィード」リンクや、ブラウザのアドレスバーに /feed や /rss、/atom.xml を付けたURLで見つかることが多いです。この記事のコードはRSS(rssタグ)とAtom(feedタグ)の両方に対応しているので、どちらの形式でも同じように扱えます。取得できないサイトは、配信自体を提供していない場合があります。
配信済みの記事IDを PropertiesService(スクリプトごとの設定値を保管する仕組み)に記録し、次回以降はスキップするようにしています。記事のURL(またはguid)をキーにするので、同じ記事が再配信されることはありません。記録が増えすぎないよう、この記事では直近の一定件数だけを保持する形にしています。
要約はあくまで「その記事を開くべきか」を判断するための下書きとして使ってください。AIはRSSに含まれる本文(多くは冒頭の抜粋)から要約するため、全文を読んだ場合と細部が異なることがあります。数字や固有名詞など重要な情報は、必ず元記事のリンクを開いて確認する前提で運用してください。この記事でも要約と一緒に必ず元記事URLを載せています。
GASには1回あたり6分の実行時間制限があります。フィードの取得と要約をまとめて行うため、10〜20サイト程度なら問題なく動きます。サイト数や新着件数が多い場合は、要約を複数記事まとめて1回のAPIリクエストで送るバッチ処理にすると、通信回数が減って速くなります。この記事のコードもバッチ要約に対応しています。
直接書くのは避けてください。スクリプトを共有した際に漏れる恐れがあります。Claude APIキーもSlackのWebhook URLも、PropertiesService のスクリプトプロパティに保存し、コードからは getProperty() で読み込む方法が安全です。
関連するサービス・記事
任意のURLの本文を取得して要約する実装です。RSSで拾いきれない個別ページの要約に応用できます。
GASでWebページのURLをAIで要約する方法を見る →
Incoming WebhookとBlock Kitの基本を解説します。この記事のSlack配信部分の土台になります。
GASからSlackに自動通知する方法を見る →
メール版の自動要約です。情報収集を要約でまとめる発想はRSS配信と共通しています。
GASでGmailの長文メールをAIで3行に要約する方法を見る →
情報収集・要約配信など、AIとGASを組み合わせた業務自動化の依頼内容と進め方を紹介します。
AI×GAS自動化の対応範囲を見る →
情報収集の
自動化を相談する。
RSS配信以外にも、Webページやメールの要約・定期レポート・Slack通知など、情報収集と共有を効率化するGAS×AI開発をご相談いただけます。