0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIツール、何を使っていますか? ― GAS×Geminiなら無料で手書き帳票をAI-OCR、スプレッドシートへ自動入力

0
Last updated at Posted at 2026-06-26

本記事は PROMPT-X 技術ブログ(blog.prompt-x.jp)に掲載した内容をQiita用に再構成したものです。

業務でAIを使おうとすると、まず「どのツールを入れるか」で悩みがちです。ワークフロー自動化ならDifyやn8n、コーディングや業務自動化ならAnthropicのClaude Code / Claude Cowork、OpenAIのCodex——選択肢は年々増えています。

ですが、新しいツールをわざわざ導入しなくても、すでに多くの現場で使い慣れている GAS(Google Apps Script)だけでAIを活用できます。GASはGoogleアカウントがあれば無料で使えるサーバーレスの実行環境で、UrlFetchAppで外部APIをそのまま叩けるため、Gemini APIもそのまま呼び出せます。スプレッドシートやドライブとの連携も標準で備わっているので、「撮った写真をAIで読み取ってシートに整える」といった処理が、追加のサービスなしで完結します。

この記事ではGASの基本から順番に、Step 1(Hello World)→ Step 2(スプレッドシート連携と権限承認)→ Step 2発展(外部API・定期実行)→ Step 4(Gemini連携と手書きOCR・構造化出力)→ 片付け の流れを、実際の操作画面とともに進めます。GASを触ったことがなくても、Googleアカウントさえあれば費用ゼロで最後まで試せます。

手書きの営農記録表。これをGAS×Geminiでスプレッドシートに自動入力する
手書きの営農記録表。これをGAS×Geminiでスプレッドシートに自動入力する

Step 1: GASはじめの一歩(Hello World)

まずはプロジェクトを作り、実行ログにメッセージを出すところから始めます。script.google.com を開き、「新しいプロジェクト」を作成するとエディタが開きます。

GASのホーム画面。「新しいプロジェクト」から始める
GASのホーム画面。「新しいプロジェクト」から始める

新規プロジェクトのエディタ初期画面
新規プロジェクトのエディタ初期画面

次のコードを貼り付けます。

function hello() {
  Logger.log('Hello, GAS! 👋');
  Logger.log('現在時刻: ' + new Date().toLocaleString('ja-JP'));
}

コードを貼り付けた状態
コードを貼り付けた状態

Ctrl + S(Macは⌘+S)で保存し、上部の ▶ 実行 をクリックします。Logger.logだけなら権限承認は不要で、画面下の「実行ログ」にメッセージが表示されれば成功です。

▶ 実行 → 実行ログにメッセージが表示される
▶ 実行 → 実行ログにメッセージが表示される

Step 2: スプレッドシートに書き込む(権限承認フロー)

次はSpreadsheetAppで新しいスプレッドシートを作り、値を書き込みます。ここで初めてGoogleアカウントのデータにアクセスするので、権限承認を1回だけ通します。

function writeToSheet() {
  // 新しいスプレッドシートを作成
  const ss = SpreadsheetApp.create('GASデモシート');
  const sheet = ss.getActiveSheet();

  // ヘッダ行
  sheet.getRange('A1').setValue('日時');
  sheet.getRange('B1').setValue('メッセージ');

  // データ行
  sheet.getRange('A2').setValue(new Date());
  sheet.getRange('B2').setValue('Hello from GAS!');

  // 結果(URL)をログ出力 → 開いて確認できる
  Logger.log('URL: ' + ss.getUrl());
}

▶ 実行を押すと「承認が必要です」と出ます。「権限を確認」→ アカウント選択と進むと、「Google hasn't verified this app(このアプリは確認されていません)」という警告が出ます。これは自分で書いた未公開スクリプトなら正常な表示です。

「承認が必要です」ダイアログ
「承認が必要です」ダイアログ

「Google未確認」警告。自作スクリプトでは正常
「Google未確認」警告。自作スクリプトでは正常

左下の「詳細」を展開し、「[プロジェクト名](安全ではないページ)に移動」をクリック。続いて要求される権限(スプレッドシートの作成・編集)を確認して「続行」を押します。

「詳細」を展開すると安全でないページへのリンクが出る
「詳細」を展開すると安全でないページへのリンクが出る

要求される権限スコープを確認して「続行」
要求される権限スコープを確認して「続行」

承認すると自動で実行が再開し、実行ログにスプレッドシートのURLが出力されます。この承認は初回だけで、次回以降は不要です。

実行完了。ログにスプレッドシートのURLが出る
実行完了。ログにスプレッドシートのURLが出る

生成されたスプレッドシート
生成されたスプレッドシート

Step 2発展: 外部APIから取得して並べる/定期実行でためる

UrlFetchApp.fetchで外部のHTTPS APIを叩けます。これは後段のGemini呼び出しと同じ部品です。ここでは気象庁の公開JSON(APIキー不要)で東京都の週間天気を取得し、setValuesでまとめてシートに書き込みます(地域コードを変えれば任意の都道府県で動きます)。

function fetchWeatherToSheet() {
  // 気象庁 東京都の週間予報(APIキー不要の公開JSON。130000=東京, 270000=大阪 など)
  const url = 'https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json';
  const data = JSON.parse(UrlFetchApp.fetch(url).getContentText());

  const weekly = data[1].timeSeries[0];
  const temps  = data[1].timeSeries[1];
  const dates  = weekly.timeDefines;
  const pops   = weekly.areas[0].pops;
  const tmin   = temps.areas[0].tempsMin;
  const tmax   = temps.areas[0].tempsMax;

  const rows = [['日付', '降水確率(%)', '最低気温(℃)', '最高気温(℃)']];
  for (let i = 0; i < dates.length; i++) {
    if (pops[i] === '') continue;
    rows.push([dates[i].slice(0, 10), pops[i], tmin[i], tmax[i]]);
  }

  // まとめて1回で書き込む(行数が増えてもコードは変わらない)
  const ss = SpreadsheetApp.create('週間天気シート');
  ss.getActiveSheet().getRange(1, 1, rows.length, 4).setValues(rows);
  Logger.log('URL: ' + ss.getUrl());
}

取得した週間天気がシートに並ぶ
取得した週間天気がシートに並ぶ

さらに、時間トリガーを使えば「定期的に実況データをためる」こともできます。ScriptApp.newTrigger(...).timeBased().everyMinutes(10).create() で10分ごとに関数を自動実行し、気象庁AMeDAS(実況)の値をシートに1行ずつ追記すれば、時系列ログが育っていきます。

// 10分ごとに logAmedasToSheet を自動実行するトリガーを作る(1回だけ ▶ 実行)
function createTenMinLog() {
  ScriptApp.newTrigger('logAmedasToSheet')
    .timeBased()
    .everyMinutes(10)
    .create();
}

定期実行で実況(気温など)が時系列でたまっていく
定期実行で実況(気温など)が時系列でたまっていく

設定したトリガーは「トリガー」画面で確認できる
設定したトリガーは「トリガー」画面で確認できる

Step 4: Gemini API を統合する

ここからが本題のAI連携です。まずGemini APIキーを発行します。Google AI Studioを開き、「Create API key」からキーを作成します。

Google AI Studio の APIキー発行画面
Google AI Studio の APIキー発行画面

APIキーが発行された
APIキーが発行された

発行したキーはコードに直書きしません。GASの「プロジェクトの設定」→「スクリプトプロパティ」にGEMINI_API_KEYという名前で保存し、コードからはPropertiesServiceで読み出します。

スクリプトプロパティに GEMINI_API_KEY を追加する
スクリプトプロパティに GEMINI_API_KEY を追加する

APIキーをスクリプトプロパティに保存した状態(値はマスク)
APIキーをスクリプトプロパティに保存した状態(値はマスク)

まずGeminiに文章を渡してみる

まずは「文章を渡す → 答えが返る」だけの最小構成を動かします。やることはUrlFetchApp.fetchでGemini APIを叩くだけです。

const GEMINI_MODEL = 'gemini-3.1-flash-lite';

// 実行用:▶ から run を選ぶと、ログに答えが出力される
function run() {
  Logger.log(askGemini('Google Apps Scriptでできることを3つ教えてください'));
}

function askGemini(prompt) {
  // APIキーはコードに直書きせず、スクリプトプロパティから読む
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + GEMINI_MODEL + ':generateContent';

  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
    muteHttpExceptions: true
  });
  return JSON.parse(res.getContentText()).candidates[0].content.parts[0].text;
}

run を実行すると、実行ログにGeminiの回答が表示される
run を実行すると、実行ログにGeminiの回答が表示される

モデルには、安価・高速な軽量モデル「gemini-3.1-flash-lite」を指定しています。無料枠が充実しているので、この記事の内容でしたら、無料枠に十分収まります。

手書きの記録を画像で読ませる(Gemini OCR)

次に「文章 + 画像」を渡して、手書きの記録表を読み取らせます。画像処理もすべてGAS側で完結できます。写真はGoogleドライブに置くだけでよく、DriveAppで読み込んでUtilities.base64Encodeし、inline_dataとしてプロンプトと一緒にpartsへ入れて渡します。画像を別途アップロード(Files API)する手順が要らず、1回のリクエストにまとめて送れるので構成がシンプルです。

写真はGoogleドライブにアップロードするだけ
写真はGoogleドライブにアップロードするだけ

ファイルIDはドライブのURLから取得する(赤枠の部分)
ファイルIDはドライブのURLから取得する(赤枠の部分)

// 実行用:Googleドライブの画像ファイルIDを指定して読み取る
function runImage() {
  const fileId = 'ここにGoogleドライブの画像ファイルIDを入れる';
  Logger.log(readImage(fileId, 'この表を読み取って、内容をテキストで教えてください'));
}

function readImage(fileId, prompt) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const blob = DriveApp.getFileById(fileId).getBlob();   // ドライブの画像を読む
  const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + GEMINI_MODEL + ':generateContent';

  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify({ contents: [{ parts: [
      { text: prompt },                                  // 質問(プロンプト)
      { inline_data: { mime_type: blob.getContentType(),
                       data: Utilities.base64Encode(blob.getBytes()) } }  // 画像
    ] }] }),
    muteHttpExceptions: true
  });
  return JSON.parse(res.getContentText()).candidates[0].content.parts[0].text;
}

これで手書きの表を読み取って文章で返してくれます。ただし、返ってくるのは「表っぽいテキスト」のみ。あとで集計やグラフ化をしたいなら、これでは足りません。

スプレッドシートにAIの結果を「使える列」で書き込む(responseSchema)

AI×スプレッドシートを実用的に使いたいなら、Geminiの構造化出力(responseSchema)が役に立ちます。「ほしい列の形」を指定すると、その形のJSONだけを返してくれます。あとはsetValuesで「1行=1レコード」に展開すれば、そのまま合計・平均・グラフ化できる“使えるデータ”になります。

function readImageToSheet(fileId, sheetId) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + GEMINI_MODEL + ':generateContent';

  const blob = DriveApp.getFileById(fileId).getBlob();
  const imagePart = { inline_data: {
    mime_type: blob.getContentType(),
    data: Utilities.base64Encode(blob.getBytes())
  } };

  // ほしい「列の形」を指定する(Gemini はこの形の JSON だけを返す)
  const schema = {
    type: 'ARRAY',
    items: {
      type: 'OBJECT',
      properties: {
        '':       { type: 'INTEGER' },
        '天気':     { type: 'STRING'  },
        '給液差分量': { type: 'INTEGER' },
        '排液率':   { type: 'INTEGER' }
      }
    }
  };

  const prompt = 'この給液・排液記録表を、各行を1レコードとして読み取ってください';
  const res = UrlFetchApp.fetch(url, {
    method: 'post', contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify({
      contents: [{ parts: [ { text: prompt }, imagePart ] }],
      generationConfig: {                       // ← 構造化出力のキモ
        responseMimeType: 'application/json',   //   「JSON で返して」
        responseSchema: schema                  //   「この列の形で」
      }
    }),
    muteHttpExceptions: true
  });

  // 応答の中身はJSON文字列なので、もう一度parseして配列に戻す
  const rows = JSON.parse(
    JSON.parse(res.getContentText()).candidates[0].content.parts[0].text
  );

  // シートに「ヘッダ + 全データ行」を一気に書き込む → そのまま集計できる
  const headers = ['', '天気', '給液差分量', '排液率'];
  const values  = [headers].concat(rows.map(r => headers.map(h => r[h])));
  SpreadsheetApp.openById(sheetId).getSheets()[0]
    .getRange(1, 1, values.length, headers.length).setValues(values);
}

手書き記録から構造化されたスプレッドシート(responseSchemaでの出力結果)
手書き記録から構造化されたスプレッドシート(responseSchemaでの出力結果)

紙の手書き記録 → 写真 → 構造化データ(表)まで、GASだけで一気通貫です。あとはこの関数を時間トリガーで回せば「撮ってフォルダに入れる → 自動で表になりシートに溜まる」まで全自動にできます。

プロンプトとモデルのコツ

かつては高いモデルで複雑なプロンプトを書かなければ読み取れなかった資料も、安いモデルでシンプルなプロンプトで、現場の手書き帳票がここまで読めるようになりました。AIによる帳票の電子化が、特別な投資なしに現場の手で始められる時代になってきたことを実感できると思います。

片付け

時間トリガーを設定した場合は、必ず止めておきます。「トリガー」画面から削除するか、ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)) を実行します。使わなくなったGemini APIキーも、Google AI Studio側で無効化しておくと安心です。

「トリガー」画面から不要なトリガーを削除する
「トリガー」画面から不要なトリガーを削除する

使わないAPIキーは無効化しておく
使わないAPIキーは無効化しておく

応用アイデア

この記事の内容でハンズオンを行ったところ「電話などの接客応対メモなど様々なデータに使えそう。もっとちょっとしたメモのような用途にも」「AIのいいところが見えた。手書きのものを読ませるところに手応えがあった」。「小さく始められるものがいい。スクリプトが長くなりすぎると本末転倒で、簡単であること自体が価値」という声をいただきました。

結びに

現場主導こそが最も効果的なAI導入のあり方だと考えています。書籍やオンライン記事には残りにくい試行錯誤の過程や、実際に動かしてみる体験をしてみることが大切です。AIがどのように動いているかという理解が深まれば、どんなAIツールであっても使いこなせると思います。ぜひこの記事の内容を試して、感想をコメントなどにいただけると幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?