GASでWebページのURLを
AIで要約してスプレッドシートに保存する方法

気になる記事のURLをスプレッドシートに並べておくだけで、UrlFetchApp(GASから外部にアクセスする標準機能)でページを取得し、Claude API(Anthropic社の生成AI)で 3行要約と要点を自動でまとめる方法を、そのまま動くコードで解説します。HTMLタグの除去から 要約結果のJSON受け取り、要約済みURLのスキップまで一通り扱います。

|対象: GAS / スプレッドシート / Claude API / 情報収集の自動化

Table of Contents

なぜWeb要約をGAS×AIで自動化するのか

結論から言うと、業界ニュースや競合サイトのチェックは、GAS(Google Apps Script。Googleサービスを操作する無料のプログラム実行環境)とAIで自動化できます。 読みたい記事のURLをスプレッドシートのA列に並べておけば、GASがページを取得し、AIが本文を3行に要約して要点を箇条書きにし、隣の列へ書き込んでくれます。 1本ずつ開いて読む手間を減らし、要点だけを一覧で見比べられます。

スプレッドシート単体では、Webページの取得はできても文章の要約まではできません。GASからClaude APIを呼べば、 「取得」と「要約」をひとつの流れにまとめられます。次のような使い方が現実的です。

情報収集の効率化

業界ニュースや競合の更新を、要点だけ一覧で把握できる

要点の抽出

3行要約に加えて、箇条書きの要点と分類を自動で付けられる

履歴が残る

要約をシートに蓄積するので、あとから検索・並べ替えできる

定期実行できる

トリガーで回せば、追加したURLだけを自動で要約できる

この記事では、A列のURLを読み取り、ページを取得してタグを取り除き、Claude APIで要約して、 タイトル・3行要約・要点・分類を各列に書き込む流れを実装します。要約済みの行は自動でスキップするので、 途中で止まっても再実行するだけで続きから埋められます。

準備:APIキーの保管と要約シートの用意

必要なのは2つです。1つ目は要約シート。A列にURL、B列をタイトル、C列を3行要約、D列を要点、E列を分類の書き込み先とし、 1行目はヘッダー(URL / タイトル / 要約 / 要点 / 分類など)にしておきます。シート名はここでは「要約」とします。2つ目はClaude APIのキーです。 このキーは料金が発生する権限そのものなので、コードに直書きしてはいけません。GASの「スクリプトプロパティ」(PropertiesServiceで読み書きする、スクリプト単位の設定保存領域)に入れ、コードからはキー名で読み出します。

// 一度だけ実行してAPIキーを安全に保存する
function saveApiKey() {
  // 実行後、この関数のキー文字列は削除しておく
  PropertiesService.getScriptProperties()
    .setProperty("ANTHROPIC_API_KEY", "sk-ant-xxxxxxxx");
}

// コードからはキー名で読み出す(直書きしない)
function getApiKey() {
  const key = PropertiesService.getScriptProperties()
    .getProperty("ANTHROPIC_API_KEY");
  if (!key) throw new Error("APIキーが未設定です。saveApiKeyを実行してください。");
  return key;
}

saveApiKeyを一度だけ実行したら、キー文字列はコードから消しておきましょう。以降はどの関数からもgetApiKey()で安全に取り出せます。

URLからページ本文を取り出す(HTMLの除去)

まずはURLを渡すと、ページのタイトルと本文テキストを返す関数を作ります。UrlFetchApp.fetch()が返すのはHTMLそのものです。そのままAIに渡すとタグやCSS・JavaScriptがノイズになるので、タグを取り除いてテキストだけを抽出してから渡します。あわせて、長すぎるページは先頭を切り詰めて、料金と処理時間を抑えます。

// URLのページを取得し、HTMLタグを除いた本文テキストを返す
function fetchPageText(url) {
  const res = UrlFetchApp.fetch(url, {
    muteHttpExceptions: true,       // エラー時も例外にせずレスポンスを受け取る
    followRedirects: true,          // リダイレクトを追う
    validateHttpsCertificates: true
  });

  const code = res.getResponseCode();
  if (code !== 200) {
    throw new Error("取得失敗 (" + code + "): " + url);
  }

  let html = res.getContentText();  // 文字コードは基本UTF-8で解釈される

  // タイトルを先に取り出しておく(要約の手がかりになる)
  const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
  const title = titleMatch ? titleMatch[1].trim() : "";

  // script と style は中身ごと削除する(ノイズを消す)
  html = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
  html = html.replace(/<style[\s\S]*?<\/style>/gi, " ");

  // 残りのタグをすべて取り除く
  let text = html.replace(/<[^>]+>/g, " ");

  // よく使う実体参照を戻し、連続する空白を1つにまとめる
  text = text
    .replace(/&nbsp;/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/\s+/g, " ")
    .trim();

  // 長すぎるページは先頭6000文字までに切り詰めてから渡す
  const MAX = 6000;
  if (text.length > MAX) text = text.substring(0, MAX);

  return { title: title, text: text };
}

ポイントは3つです。まず<script><style>を中身ごと削除し、その後に残りのタグを外します。次に&nbsp;などの実体参照(記号をHTML用に置き換えた表記)を元に戻し、連続する空白を1つにまとめます。最後に長さを6000文字までに切り詰めます。なお、この方式で取得できるのは最初に返るHTMLだけです。表示をJavaScriptで 組み立てるページ(SPA)では本文が取れないことがあります。

Claude APIで要約する関数を作る(JSONで受け取る)

次に、本文テキストを渡すと3行要約・要点・分類を返す関数を作ります。要約結果は複数の項目に分けたいので、 JSON(データを構造化して表す形式)で受け取ります。プロンプト(AIへの指示文)で出力するJSONの形を明示し、 「summaryは3行以内」「pointsは3〜5個の箇条書き」と具体的に伝えるのがコツです。

// 本文テキストを要約し、結果をJSONで返す
// 戻り値: { summary: "3行要約", points: ["要点1", ...], category: "分類" }
function summarizePage(title, text) {
  const url = "https://api.anthropic.com/v1/messages";

  const prompt =
    "あなたはWeb記事の要約者です。\n" +
    "次のページ本文を日本語で要約してください。\n" +
    "・summaryは全体を3行以内でまとめる。\n" +
    "・pointsは重要な要点を3〜5個の短い箇条書きにする。\n" +
    "・categoryは内容を表す短い分類名を1つ(例: 製品発表 / 技術解説 / 事例)。\n" +
    "・下のJSON形式だけを出力し、前置きや注釈は書かない。\n\n" +
    'JSON形式: {"summary":"...","points":["..."],"category":"..."}\n\n' +
    "タイトル: " + title + "\n\n本文:\n" + text;

  const payload = {
    model: "claude-haiku-4-5",
    max_tokens: 1000,
    messages: [{ role: "user", content: prompt }]
  };

  const options = {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": getApiKey(),
      "anthropic-version": "2023-06-01"
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const res = UrlFetchApp.fetch(url, options);
  const code = res.getResponseCode();
  if (code !== 200) {
    throw new Error("API エラー (" + code + "): " + res.getContentText());
  }

  const answer = JSON.parse(res.getContentText()).content[0].text;
  return parseResult(answer);
}

// 返答からJSON部分だけを取り出して読み込む
function parseResult(answer) {
  // いちばん外側の { ... } を抜き出す(前置きが混ざっても解析できる)
  const start = answer.indexOf("{");
  const end = answer.lastIndexOf("}");
  if (start === -1 || end === -1) {
    throw new Error("JSONが見つかりません: " + answer);
  }
  return JSON.parse(answer.substring(start, end + 1));
}

返答はcontent配列の先頭textに入ります。それを直接JSON.parseせず、parseResult()で最初の{から最後の}までを取り出しています。こうすると、AIが前置きを付けてもJSON部分だけを安全に読み込めます。モデルは軽量なclaude-haiku-4-5から始め、精度を上げたいときに高性能なclaude-opus-4-8へ差し替えるだけで切り替えられます。

エラーに強くする:リトライとJSON抽出

外部APIは、一時的な混雑や通信の不調で失敗することがあります。特にURLを大量に処理すると、 短時間に呼び出しが集中して一時的なエラー(レート制限)が返ることがあります。そこで、失敗したら 少し待ってから再試行する仕組みを入れます。待ち時間を1秒・2秒・4秒と倍にしていく方式を 「指数バックオフ」と呼びます。

// 一時的なエラー時に少し待って再試行する
function summarizeWithRetry(title, text, maxRetry) {
  maxRetry = maxRetry || 3;

  for (let i = 0; i < maxRetry; i++) {
    try {
      return summarizePage(title, text);
    } catch (e) {
      // 最後の試行で失敗したら、そのまま呼び出し元に投げる
      if (i === maxRetry - 1) throw e;
      // 待ち時間を1秒・2秒・4秒と伸ばす(指数バックオフ)
      Utilities.sleep(1000 * Math.pow(2, i));
    }
  }
}

一時的な失敗はこのリトライで自動回復します。加えて、前の関数のparseResult()がJSONの取り出しを担うので、「APIの失敗」と「返答が壊れている」の両方に備えられます。 要約の本体はこのsummarizeWithRetry()を呼ぶようにすれば、安定性がぐっと上がります。

URL列をまとめて要約し、メニューとトリガーで実行する

いよいよ本体です。A列(URL)〜E列(分類)をまとめて読み込み、URLがあってC列(要約)が空の行だけを処理します。取得と要約を行い、タイトル・要約・要点・分類を各列に書き込み、済んだ行はスキップするので、 途中で止まっても再実行すれば未処理分だけを埋められます。

// A列のURLを取得・要約し、B〜E列に書き込む
// シート構成: A=URL, B=タイトル, C=3行要約, D=要点, E=分類
function summarizeUrlColumn() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("要約");
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return; // データなし(1行目はヘッダー想定)

  // A(URL)〜E(分類)をまとめて読み込む
  const rows = sheet.getRange(2, 1, lastRow - 1, 5).getValues();

  let count = 0;
  for (let i = 0; i < rows.length; i++) {
    const url = String(rows[i][0]).trim();
    const done = String(rows[i][2]).trim();

    if (!url) continue;             // URLが空ならスキップ
    if (!/^https?:\/\//.test(url)) continue; // http/https以外はスキップ
    if (done) continue;             // すでに要約済みならスキップ

    try {
      const page = fetchPageText(url);
      if (!page.text) throw new Error("本文が空です(動的ページの可能性)");

      const result = summarizeWithRetry(page.title, page.text);
      const points = (result.points || []).map(function (p) { return "・" + p; }).join("\n");

      sheet.getRange(i + 2, 2).setValue(page.title || result.category || "");
      sheet.getRange(i + 2, 3).setValue(result.summary || "");
      sheet.getRange(i + 2, 4).setValue(points);
      sheet.getRange(i + 2, 5).setValue(result.category || "");
    } catch (e) {
      // 取得やAPIで失敗した行はC列に記録して次へ進む(全体は止めない)
      sheet.getRange(i + 2, 3).setValue("エラー: " + e.message);
      continue;
    }

    SpreadsheetApp.flush();  // 途中経過をすぐ画面に反映する
    count++;
    if (count >= 15) break;  // 1回15件までに区切る(実行時間制限対策)
    Utilities.sleep(1000);   // 取得先に配慮して間隔をあける
  }
}

1行ごとにtry...catchで囲み、取得やAPIで失敗した行はC列にエラー内容を残して次へ進みます。1件の失敗で全体を止めません。 取得先のサーバーに配慮して、呼び出しの間にUtilities.sleep(1000)で1秒あけ、1回の実行は15件までに区切っています。最後に、実行をワンクリックにするメニューを追加します。

// シートを開いたときに要約メニューを追加する
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("AI要約")
    .addItem("未処理のURLを要約する", "summarizeUrlColumn")
    .addToUi();
}

onOpen()(スプレッドシートを開いたときに自動で走る特別な関数)でメニューを追加すると、シート上部の「AI要約」から実行できます。 URLが随時追加されるシートなら、時間主導トリガー(決まった間隔で関数を自動実行するGASの仕組み)でsummarizeUrlColumnを定期的に回すのも有効です。C列が空の行だけを処理する作りなので、二重に要約することはありません。 件数が多く実行時間制限(無料枠で6分)に届きそうなときは、関連記事の分割実行が役立ちます。

まとめ

URLの取得とClaude APIをGASでつなぐと、Webページの要約を丸ごと自動化できます。ポイントは5つです。 APIキーはスクリプトプロパティで安全に保管し、取得したHTMLはタグを除いてテキストにしてから渡し、 要約結果はJSONで受け取って要約・要点・分類に分け、空の行だけを対象にリトライと件数の区切りでAPI制限と 実行時間制限をかわし、取得先への配慮として間隔をあける——この型を押さえれば、要約に限らず、翻訳・分類・ キーワード抽出といったAI活用にも同じ流れで応用できます。

よくある質問

スプレッドシートの標準関数に文章要約はありません。IMPORTXMLはWebページの一部を取り込めますが、取り込んだ文章を要約するには生成AIが必要です。この記事のようにGASからClaude APIを呼べば、取得したページ本文を「3行で」「要点を箇条書きで」といった指示付きで要約できます。取得と要約をひとつのGASにまとめられるのが利点です。

UrlFetchAppが返すのはHTMLそのものなので、そのままAIに渡すとタグやCSS・JavaScriptがノイズになります。本文のコードでは、scriptタグとstyleタグを中身ごと削除し、残りのタグを取り除いてから空白を整え、テキストだけを抽出しています。長すぎるページは先頭の一定文字数に切り詰めてから渡すと、安定して要約できます。

できません。UrlFetchAppはブラウザのようにJavaScriptを実行しないため、取得できるのは最初に返るHTMLだけです。SPA(表示をJavaScriptで組み立てるサイト)では本文が空になることがあります。その場合は、RSSフィードや公式のAPI、または本文がHTMLに含まれるページを対象にするのが確実です。

取得先の利用規約やrobots.txt(クローラー向けの取得可否の指示)を確認し、常識的な間隔をあけてアクセスしてください。本文のコードは1回の実行件数を区切り、呼び出しの間に待ち時間を入れています。短時間に大量アクセスすると相手のサーバーに負荷をかけるため、対象は自社サイトや取得が許可された情報源にとどめるのが安全です。

書いてはいけません。APIキーは料金が発生する権限そのものです。共有スプレッドシートのスクリプトに直書きすると、編集権限を持つ他の人にキーが見えてしまいます。GASのスクリプトプロパティ(PropertiesServiceで読み書きする設定保存領域)に保管し、コードからはキー名で読み出してください。

Claude APIは処理した文字数(トークン)に応じた従量課金です。要約は出力が短く、入力もページ本文を切り詰めれば抑えられるため、軽量モデルのclaude-haiku-4-5を使えば1件あたりの費用はごくわずかです。まずは少量で試し、1件あたりの消費量を確認してから対象を広げるのがおすすめです。

AI×GASの業務自動化を
相談する。

Web要約のほかにも、翻訳・分類・問い合わせ対応・返信下書きの自動生成など、 AI APIとGASを組み合わせた自動化をご相談いただけます。既存のスプレッドシート運用にもそのまま組み込めます。

AI×GAS自動化の対応範囲を見る