こんにちは!今回は、Google Apps Script(GAS)を使って Gemini APIを呼び出し、特定のニュースサイトの情報をユニークなキャラクター風に要約させ、Googleドキュメントに自動出力・整形するプログラムを実装しました。
コードの構造や、API連携、Googleドキュメントの自動操作(テキスト挿入やURLの自動リンク化)の仕組みについて、備忘録を兼ねて詳しく解説します。
概要
このプログラムは、大きく分けて以下の3つの処理(関数)で構成されています。
-
CallAnoGemini():メイン処理。APIを叩き、ドキュメントに日付や要約テキストを書き込む。 -
callGeminiAPI(promptText):Gemini API(gemini-flash-latestなど)へリクエストを送信する共通関数。 -
convertUrlsToLinks():ドキュメント内の生のURL(https://...)を検出し、クリック可能なハイパーリンクへ自動変換する。
解説するソースコード
今回対象とするコードの全容は以下の通りです。
/**
* メイン処理:Geminiからテキストを取得しドキュメントへ書き込む
*/
function CallAnoGemini() {
const now = new Date();
const today = Utilities.formatDate(now, "Asia/Tokyo", "yyyy/MM/dd");
// サブ関数を使ってAPIからテキストを取得
var resultText = callGeminiAPI();
Logger.log(resultText);
// 1. 書き込み先のドキュメントIDを指定
const docId = '自身のGoogleドキュメントID';
// 2. ドキュメントを開く
const doc = DocumentApp.openById(docId);
// 3. 本文(Body)を取得
const body = doc.getBody();
// 追加改ページ
body.appendPageBreak();
// タイトル用パラグラフを追加
var par1 = body.appendParagraph(today+'のAIあのちゃん');
par1.setHeading(DocumentApp.ParagraphHeading.HEADING1);
// メインのモデル指定と再リクエスト部分
const model = "gemini-3.1-flash-lite-preview";
const API_KEY = '自身のGeminiのAPIキー(GoogleAIStudioより取得)';
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${API_KEY}`;
const textToSend = "[https://www.asahi.com/のトップページのニュース記事を具体的に取り上げてあのちゃん風に書いて](https://www.asahi.com/のトップページのニュース記事を具体的に取り上げてあのちゃん風に書いて)、元のニュースへのパーマネントリンクと謝辞も書いて #や*を使わないで生成AIとわかる記述はやめて";
const payload = {
"contents": [{
"parts": [{
"text": textToSend
}]
}]
};
const options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
try {
const response = UrlFetchApp.fetch(url, options);
const resText = response.getContentText();
const json = JSON.parse(resText);
if (response.getResponseCode() !== 200) {
Logger.log("エラー詳細: " + resText);
return "エラーが発生しました。";
}
// 取得したテキストをドキュメントの末尾に追加
body.appendParagraph(json.candidates[0].content.parts[0].text);
const resultText = json.candidates[0].content.parts[0].text;
return resultText;
} catch (e) {
return "実行エラー: " + e.toString();
}
// ドキュメントを保存して閉じる
doc.saveAndClose();
}
/**
* Gemini API 呼び出し用の共通関数
*/
function callGeminiAPI(promptText) {
const API_KEY = 'AIzaSyCkwxaRRES9lrNYaromf5fyyGYUGQXczs4';
const model = "gemini-flash-latest";
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${API_KEY}`;
const textToSend = promptText || "[https://www.asahi.com/のトップページのニュースをあのちゃん風に書いて](https://www.asahi.com/のトップページのニュースをあのちゃん風に書いて)、元のニュースのパーマネントURLも書いて #や*を使って生成AIとわかる記述はやめて";
const payload = {
"contents": [{
"parts": [{
"text": textToSend
}]
}]
};
const options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
try {
const response = UrlFetchApp.fetch(url, options);
const resText = response.getContentText();
const json = JSON.parse(resText);
if (response.getResponseCode() !== 200) {
Logger.log("エラー詳細: " + resText);
return "エラーが発生しました。";
}
const resultText = json.candidates[0].content.parts[0].text;
return resultText;
} catch (e) {
return "実行エラー: " + e.toString();
}
}
/**
* ドキュメント内のURL文字列を検索し、ハイパーリンクへ変換する
*/
function convertUrlsToLinks() {
const docId = '1XDg91vP1cyXDbj5ZlPt4-DANRjn5Bt1807lQklhqgr0';
const doc = DocumentApp.openById(docId);
const body = doc.getBody();
// URL文字列を抽出するための正規表現パターン
const urlPattern = "https?://[\\w!?/+\\-_~=;.,*&@#$%\\[\\]()]+";
let searchResult = body.findText(urlPattern);
while (searchResult !== null) {
const element = searchResult.getElement().asText();
const startOffset = searchResult.getStartOffset();
const endOffset = searchResult.getEndOffsetInclusive();
const url = searchResult.getElement().getText().substring(startOffset, endOffset + 1);
// 見つけた文字列の範囲に対してリンクを設定
element.setLinkUrl(startOffset, endOffset, url);
// 次の候補を検索
searchResult = body.findText(urlPattern, searchResult);
}
}
コードの詳細解説
- 外部APIへのHTTPリクエスト(Gemini APIとの連携)
GASから外部のWEB API(Gemini API)を叩くために、UrlFetchApp.fetch(url, options) を使用しています。
エンドポイント(URL)とペイロード:
v1beta の generateContent エンドポイントに対し、POST メソッドでデータを送信しています。
プロンプト(指示文)の工夫:
textToSend に仕込まれた指示が非常に特徴的です。
「あのちゃん風に書いて」「#や*を使わないで」「生成AIとわかる記述はやめて」
このように出力時のマークダウン装飾(#や*)やAI特有の定型文を禁止する制約をプロンプト側でチューニングすることで、ドキュメントにそのまま貼り付けても違和感のない自然なテキスト(キャラクター風)を取得しています。
- Googleドキュメントの自動操作(DocumentApp)
DocumentApp.openById(docId) を使って、指定した既存のGoogleドキュメントをスクリプト経由で直接操作しています。
body.appendPageBreak():実行ごとに新しいページへ書き出すための「改ページ」を自動挿入します。
body.appendParagraph(...):テキストを新しい段落として追加します。
.setHeading(DocumentApp.ParagraphHeading.HEADING1):追加した日付タイトルに対し、Googleドキュメントの「見出し1」のスタイルを適用し、文書構造を整えています。
- 正規表現を用いたURLの自動リンク化
Geminiが返してきたニュースの元リンク(URL文字列)は、そのままドキュメントに書き込むとただの「テキスト」になってしまい、クリックできません。それを解決しているのが convertUrlsToLinks() 関数です。
urlPattern = "https?://..." という正規表現を用いて、文中のURLを検出します。
body.findText(urlPattern) を使ってドキュメント内を検索し、while ループでヒットするURLがなくなるまですべて走査します。
element.setLinkUrl(startOffset, endOffset, url) を実行することで、検出された文字列の開始位置から終了位置の範囲だけに、ピンポイントでクリック可能なハイパーリンクを付与しています。
まとめ
今回のスクリプトは、GASの強力なGoogleサービス連携機能とGemini APIの柔軟なテキスト生成能力が組み合わさった素晴らしい自動化の例です。
これを応用すれば、毎朝自動でニュースを収集して自分好みのマイノート(Googleドキュメント)を作成する「自分専用のAI新聞」のような仕組みをトリガー(タイマー実行)機能を使って簡単に構築できます。
ぜひ試してみてください!
