みなさん、こんにちは!業務ハックLabのようです。
最近、寒くなってきましたね。 (布団から出るのが毎日戦いですw)
さて、今回は「AIを使った業務自動化」のネタを持ってきました!
これ、わけわからなくなりますよね…
みなさん、契約書のチェックってどうしてますか? 細かい条文を目を皿のようにして読んで、リスクがないか確認する……。
正直なところ、めんどくさいですよね?
もちろん、最終的には人間の目で見る必要がありますが、「まずはざっくりリスクが高い箇所だけ教えてほしい!」と思うこと、ありませんか?
ということで、今回は「フォルダに契約書を入れるだけで、自動でスプレッドシートにリスク分析結果が書き込まれ、ファイルが整理されるシステム」を作っていきたいと思います!
実は「Power Automate × Azure OpenAI × SharePoint構成版」の記事もあります
「あれ?このネタ見たことあるぞ?」と思った方、鋭いですねw 実は以前、Power Automate × Azure OpenAI × SharePoint という「企業向けガチ構成」で同じようなシステムを作る連載記事を書いています。
会社で本格的にセキュリティを固めて導入するならAzure版がおすすめなので、興味がある方はこちらもぜひチェックしてみてください。
- Power AutomateとAzure AIで実現する契約書リスク自動分析システム(第1回:システム概要・準備編)
- Power AutomateとAzure AIで実現する契約書リスク自動分析システム(第2回:基礎実装編)
- Power AutomateとAzure AIで実現する契約書リスク自動分析システム(第3回:AI分析実装編)
- Power AutomateとAzure AIで実現する契約書リスク自動分析システム(第4回:完成・運用編)
しかし、記事を読んだ方の中にはこう思った方もいるかもしれません。
「うちはGoogle Workspaceを使っとるんじゃ!Power Automate × Azure OpenAI × SharePointは使えないんじゃあ!!」
そんな方向けに、今回は Google Workspace × Gemini でも同じ仕組み を考えてみました! これならGoogle派の皆さんも安心ですね。
今回作る仕組みの全体像

いきなりコードを書く前に、まずはどういう仕組みにするか、ロジックを整理しましょう。 ここをサボると、後でコードを書く時に迷子になりますからね。(自戒)
やりたいことはシンプルです。
- 入口: 特定のフォルダに契約書(PDFや画像)を入れる。
- 処理: GASがファイルを検知して、テキストを抜き出す。
- 頭脳: Gemini APIにテキストを投げて、「リスク分析して!」と頼む。
- 出口: 返ってきた結果をスプレッドシートに書き込み、ファイルを「処理済」フォルダへ移動する。
この流れをGoogle Apps Script (GAS) で実装していきます。
ステップ1:Googleドライブとスプレッドシートの準備
まずはデータの「入り口」と「出口」を作ります。
フォルダの作成
Googleドライブに、以下の2つのフォルダを作ってください。
契約書_未処理(ここが入口になります)契約書_処理済(処理が終わったらここに移動させます)
今回は共有ドライブにこのフォルダを作成しました!
フォルダIDの取得
プログラムからフォルダを特定するために「ID」が必要です。 作成した「契約書_未処理」フォルダを開き、URLバーを見てみましょう。
drive.google.com/drive/folders/ の後ろにある、長い英数字の羅列。 これがフォルダIDです。これをコピーして、メモ帳などに控えておいてください。 (「契約書_処理済」のIDも同じように控えてくださいね!)
スプレッドシートの作成
次に、結果を書き出すためのスプレッドシートを新規作成します。 名前は 契約書レビュー管理表 としておきましょう。 こちらもURLの /d/ と /edit の間にある文字列(スプレッドシートID)を控えておきます。
ステップ2:Gemini APIキーの取得
次は、AIの頭脳を使うためのパスポートを取得します。
- Google AI Studio にアクセスします。
- 左下の 「Get API key」 をクリック。
- 「API キーを作成」をクリック
- 「キー名の設定」にキーの名前を入力し
- 「インポートしたプロジェクトを選択」で「プロジェクトを作成」 を選択しプロジェクト名を入力。
- 最後に「キーを作成」をクリック

画面に AIza から始まるキーが表示されるので、これをコピーして控えてください。
⚠️ 【超重要】実運用するなら「有償枠」への切り替えが必須!
ここで一つ、めちゃくちゃ大事な話をします。 (これを知らずに会社で使うと、情シスに怒られる案件ですw)
今回の手順で取得したAPIキーは、デフォルトだと「無料枠」の状態です。 個人の学習用ならこれで全く問題ないのですが、「会社の契約書」を扱う場合は、必ずGoogle Cloudプロジェクトと紐付けた「有償枠(Pay-as-you-go)」に切り替えてください。
理由は以下の2点です。
理由①:入力データがAIの学習に使われないようにするため
これが最大の理由です。 Googleの規約上、無料枠のデータは「モデルの改善(学習)」に使われる可能性があります。 つまり、あなたの会社の秘密保持契約書の内容が、巡り巡ってGemini賢くなるための餌になってしまうかもしれないのです。 有償枠に切り替えると、「入力データは学習に使われない」という規約が適用されるため、セキュリティ的に安心です。
理由②:APIの回数制限(レートリミット)を回避するため
無料枠は、1分間にリクエストできる回数に厳しい制限があります。 「月末に契約書を50件まとめて処理したい!」と思っても、無料枠だと途中で「回数制限オーバーです」とエラーが出て止まってしまう可能性があります。
【結論】 まずは無料枠で試してみて、「これは使える!」となったら、必ずGoogle Cloudの請求先アカウントを設定して、有償枠に切り替えてから運用してくださいね。
ステップ3:Google Apps Script (GAS) の設定
はい、ここからが本番です。
スクリプトエディタを開く
スプレッドシートのメニューから 「拡張機能」 > 「Apps Script」 をクリックします。
【重要】Drive API サービスの追加
ここ、一番のハマりポイントです! PDFから文字を読み取る機能を使うために、APIサービスを追加する必要があります。
- エディタ左側、「サービス」の横の 「+」 をクリック。
- 「Drive API」 を選択して 「追加」。
- 【重要】バージョンは必ず「v2」を選択してください。 (今回のコードはv2の仕様で書かれているため、v3だと動きません!)
- 「追加」 をクリック。

コードの記述
以下のコードをコピーして貼り付けてください。
ちなみにMODEL_NAMEに関しては僕は「gemini-flash-latest」を使用しました。
// ==========================================
// ★設定エリア(ここだけ書き換えてください)
// ==========================================
const API_KEY = "ここに取得したAPIキーを貼り付け";
const FOLDER_ID_INPUT = "契約書_未処理のフォルダIDを貼り付け";
const FOLDER_ID_PROCESSED = "契約書_処理済のフォルダIDを貼り付け";
const SPREADSHEET_ID = "スプレッドシートIDを貼り付け";
const MODEL_NAME = "あなたの環境で動作確認済みのモデル名";
// ==========================================
// メイン処理
// ==========================================
function main() {
const inputFolder = DriveApp.getFolderById(FOLDER_ID_INPUT);
const processedFolder = DriveApp.getFolderById(FOLDER_ID_PROCESSED);
// 1. ファイル処理開始
processFiles_(inputFolder, processedFolder);
// 2. 一時ファイルのお掃除
Logger.log("フォルダの清掃を行っています...");
cleanUpTempFiles_(inputFolder);
Logger.log("すべての処理が完了しました。");
}
// ==========================================
// ファイル処理ループ
// ==========================================
function processFiles_(inputFolder, processedFolder) {
const files = inputFolder.getFiles();
while (files.hasNext()) {
const file = files.next();
const fileName = file.getName();
// OCR用の一時ファイルなどはスキップ
if (fileName.includes("temp_doc_for_text_extraction")) continue;
Logger.log(`[処理開始] ${fileName}`);
try {
// ★ポイント1:PDFからテキストを引っこ抜く
const text = extractTextFromFile_(file.getId());
if (!text || text.length < 50) {
writeToSheet_(fileName, "エラー(テキスト抽出不可)", []);
Logger.log(`[スキップ] テキスト抽出不可: ${fileName}`);
continue;
}
// ★ポイント2:Geminiに分析してもらう
Logger.log("Geminiに分析を依頼中...");
const rawResponse = callGemini_(text);
// AIがたまにMarkdown記法(```json ... ```)を含めるので掃除する
const cleanedJson = cleanJson_(rawResponse);
let riskItems = [];
try {
riskItems = JSON.parse(cleanedJson);
} catch (e) {
writeToSheet_(fileName, "エラー(JSON解析失敗)", []);
Logger.log("JSONパースエラー");
continue;
}
// 結果の書き込み
if (!riskItems || riskItems.length === 0) {
writeToSheet_(fileName, "完了(リスクなし)", []);
} else {
writeToSheet_(fileName, "完了(リスクあり)", riskItems);
}
// 処理済フォルダへ移動
file.moveTo(processedFolder);
Logger.log(`[完了] 処理済フォルダへ移動しました`);
// 連続実行でAPI制限にかからないよう少し休憩
Utilities.sleep(2000);
} catch (e) {
writeToSheet_(fileName, `システムエラー: ${e.toString()}`, []);
Logger.log(`[エラー] ${e.toString()}`);
}
}
}
// ==========================================
// スプレッドシート書き込み(指定の列構成)
// ==========================================
function writeToSheet_(fileName, status, riskItems) {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getActiveSheet();
// ヘッダー作成(初回のみ)
if (sheet.getLastRow() === 0) {
sheet.appendRow([
"ID", "分析ステータス", "対象条項", "危険度", "リスク理由", "修正案", "処理日時"
]);
sheet.setFrozenRows(1);
sheet.getRange("1:1").setFontWeight("bold");
}
const timestamp = new Date();
// リスクがない、またはエラーの場合
if (!riskItems || riskItems.length === 0) {
sheet.appendRow([fileName, status, "-", "-", "-", "-", timestamp]);
return;
}
// リスク項目がある場合、行を分けて書き込み
riskItems.forEach(item => {
sheet.appendRow([
fileName,
status,
item.clause || "",
item.risk_level || "",
item.reason || "",
item.suggestion || "",
timestamp
]);
});
}
// ==========================================
// 共通機能(ここが裏側のキモ!)
// ==========================================
// ゴミ掃除関数
function cleanUpTempFiles_(folder) {
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
if (file.getName().includes("temp_doc_for_text_extraction")) {
try { file.setTrashed(true); } catch (e) {}
}
}
}
// ★OCR処理の核心部
function extractTextFromFile_(fileId) {
let tempFile;
try {
// Drive APIを使って、PDFをGoogleドキュメントとしてコピー(これでOCRがかかる!)
const resource = { title: "temp_doc_for_text_extraction", mimeType: MimeType.GOOGLE_DOCS };
const optionalArgs = {ocr: true, supportsTeamDrives: true, supportsAllDrives: true};
tempFile = Drive.Files.copy(resource, fileId, optionalArgs);
// ドキュメントを開いてテキストを取得
const doc = DocumentApp.openById(tempFile.id);
const text = doc.getBody().getText();
return text;
} catch (e) { return null; } finally {
// 終わったら一時ファイルはゴミ箱へ(これ大事)
if (tempFile) {
try { Drive.Files.patch({trashed: true}, tempFile.id, {supportsTeamDrives: true, supportsAllDrives: true}); } catch (e) {}
}
}
}
// Gemini呼び出し関数
function callGemini_(text) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL_NAME}:generateContent?key=${API_KEY}`;
// プロンプト:ここで「どう振る舞うか」を定義
const systemPrompt = `
あなたは優秀な企業法務の専門家です。
ユーザーから提供される契約書のテキストをレビューし、自社(契約を受ける側)にとって不利な条項やリスクのある条項を抽出してください。
以下の4点を特に注意してチェックしてください:
1. 損害賠償の範囲と上限
2. 契約の解除条件
3. 知的財産権の帰属
4. 支払条件と遅延損害金
出力は以下のJSON形式のみで行ってください。
[{"clause":"条項名","risk_level":"高/中/低","reason":"理由","suggestion":"修正案"}]
`;
const payload = {
"contents": [{"parts": [{"text": systemPrompt}, {"text": `契約書内容:\n${text}`}]}],
// レスポンスをJSONに固定する設定
"generationConfig": {"response_mime_type": "application/json"}
};
const options = { "method": "post", "contentType": "application/json", "payload": JSON.stringify(payload), "muteHttpExceptions": true };
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
if (json.error) throw new Error(`Gemini API Error: ${json.error.message}`);
return json.candidates[0].content.parts[0].text;
}
function cleanJson_(text) { return text.replace(/```json/g, "").replace(/```/g, "").trim(); }
設定値の入力
コード冒頭の API_KEY などを、ステップ1・2で控えた値に書き換えてください。
💻 コードの詳細解説:何をしているの?
「コピペで動くのはわかったけど、中身はどうなってるの?」 という勉強熱心な方のために、今回のコードのキモとなる部分を解説します!
① 目を作る:PDFから文字を読む裏技
実はGAS標準の機能には「PDFの文字を読む」機能はありません。 そこで、このコードではDrive APIの裏技を使っています。
const resource = { mimeType: MimeType.GOOGLE_DOCS };
const optionalArgs = {ocr: true};
tempFile = Drive.Files.copy(resource, fileId, optionalArgs);
これは、「PDFファイルをGoogleドキュメントとしてコピーを作成する」という処理なんですが、その時に ocr: true(光学文字認識をオン)にすることで、コピーされたドキュメントの中身がテキスト化されるんです。 このテキストを取得して、最後に用済みになったドキュメントをゴミ箱に捨てています。 (これをしないと、フォルダが謎のドキュメントだらけになりますw)
② 脳を作る:Geminiへの指示出し
AIを使う上で一番大事なのが「プロンプト」と「出力形式」です。
const systemPrompt = `
...
出力は以下のJSON形式のみで行ってください。
[{"clause":"条項名","risk_level":"高/中/低" ... }]
`;
ここで「法務の専門家になりきって」と指示しつつ、「JSON形式以外は認めない」という強い制約をかけています。 さらに、APIのリクエスト設定でも:
"generationConfig": {"response_mime_type": "application/json"}
と指定することで、Gemini側にも「プログラムが読みやすい形式で返してね」と念押ししています。これにより、GAS側でのデータ処理(JSON.parse)がエラーになりにくくなるんです。
③ 手を動かす:結果の書き込み
riskItems.forEach(item => {
sheet.appendRow([ ... ]);
});
Geminiから返ってきたリスク一覧(配列データ)を、forEach で一つずつ取り出して、スプレッドシートの行に追加しています。 こうすることで、「一つの契約書にリスクが3つあったら、3行分追加する」という柔軟な動きを実現しています。
ステップ4:自動実行(トリガー)の設定
コードの理解が深まったところで、最後に自動実行の設定をしましょう。
- エディタ画面左側の 「時計のマーク(トリガー)」 をクリック。
- 右下の 「+ トリガーを追加」 をクリック。
- 以下のように設定して「保存」します。
- 実行する関数:
main - イベントのソース:
時間主導型 - 時間ベースのトリガーのタイプ:
分ベースのタイマー - 時間の間隔:
1分おき(※テスト用。本番なら10分おきとかでもOK)
- 実行する関数:
⚠️ トリガー設定の注意点(やりすぎ注意!)

今回はテストなのですぐに動いてほしいから「1分おき」にしていますが、これをずっと動かし続けると危険です!
- GASの実行時間制限: 1日の総実行時間(無料版だと90分/日)を超えてしまい、止まる可能性があります。
- APIのスロットリング: 短時間に何度もアクセスしすぎると、Gemini API側から「落ち着け!」と怒られてエラーになることがあります。
本番運用する時は、「10分おき」や「1時間おき」など、業務のペースに合わせて余裕を持った設定にすることをおすすめします。
完成!使い方の確認
さあ、動かしてみましょう!
「契約書_未処理」フォルダに、PDFファイル(または画像)をドラッグ&ドロップでポイッと入れます。 (今回はテストなので、ダミーの契約書などを入れてみてくださいね)
〜1分後〜
スプレッドシートを開いてみましょう。
…どうですか?
リスク分析結果が自動的に行として追加されていませんか? そして、「契約書_未処理」フォルダを見ると空になっていて、ファイルは「契約書_処理済」フォルダに移動されているはずです。

最後に
はい、いかがでしたでしょうか?
今回はGeminiとGASを組み合わせて、「契約書レビューの自動化」に挑戦してみました。 コードの解説も挟みましたが、この仕組みを応用すれば「請求書の読み取り」や「アンケートの自動集計」なんかもできちゃいます。
ぜひ、皆さんの業務に合わせてカスタマイズしてみてください。
それでは皆さん、良い業務ハックライフを~


コメント