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?

Gemini APIで請求書・領収書のPDFを読み取ってスプレッドシートに自動転記する【GAS】

0
Posted at

はじめに

  • 毎月メールで届く請求書PDFを開いて、発行元・金額・支払期限をスプレッドシートに手で打ち込んでいる
  • 取引先ごとにレイアウトが違うので、従来のOCRや正規表現では項目をうまく取り出せない
  • 転記ミスや二重計上に後から気づき、確認作業にまた時間がかかる

この記事では、Gmailに届いた請求書・領収書(PDF・画像)をGemini APIで読み取り、スプレッドシートに自動で転記する仕組みをGASで作ります。

「AIに読ませて終わり」にはしません。金額の整合性チェック、登録番号の形式チェック、二重登録の検知、「要確認」フラグまで入れた、実務で回せる形にします。

完成イメージ

1時間ごとにGmailを確認し、請求書の添付ファイルを見つけたら下のように1行ずつ追記します。

処理日時 ステータス 確認理由 書類種別 発行元 登録番号 請求書番号 発行日 支払期限 小計 消費税 合計
2026/10/28 10:00 OK 請求書 株式会社サンプル商事 T1234567890123 INV-0012 2026/10/25 2026/11/30 50000 5000 55000
2026/10/28 10:00 要確認 小計+消費税が合計と一致しない 請求書 〇〇デザイン T9876543210987 0045 2026/10/26 2026/11/15 30000 3000 32000

人が見るのは「要確認」の行だけで済みます。

処理の流れは次のとおりです。

Gmail検索(未処理の請求書メール)
  ↓
添付ファイルを取得(PDF / JPEG / PNG)
  ↓
ファイルのハッシュで登録済みかチェック → 登録済みならスキップ
  ↓
Gemini API に送信 → JSONで項目を受け取る
  ↓
検証(金額の整合性・登録番号・日付形式・二重請求)
  ↓
スプレッドシートに追記(問題があれば「要確認」)
  ↓
スレッドに処理済みラベルを付ける

準備 / 前提

必要なもの 備考
Googleアカウント Gmail・スプレッドシート・GASを使う
Gemini APIキー Google AI Studio から発行
転記先のスプレッドシート 新規作成でOK

⚠️ 業務の書類を扱う前に:無料枠と有料枠の違い

Gemini APIには無料枠がありますが、無料枠では送ったデータがGoogleのサービス改善に使われる場合があります。請求書には取引先名や金額が載っているので、業務で使うなら課金を有効にした有料枠を推奨します。

無料枠 有料枠
送信データのサービス改善への利用 あり得る されない
レート制限 低い 高い
費用 0円 従量課金

※ 執筆時点の規約に基づく整理です。利用前に必ず最新の利用規約と料金ページを確認してください。

費用について:請求書1〜2ページ程度なら、Flash系モデルでの1件あたりのコストはかなり小さく収まります。ただし料金表は頻繁に改定されるので、月の処理件数 × 1件あたりのトークン数で事前に試算しておきましょう。

APIキーをスクリプトプロパティに保存する

APIキーはコードに直接書かず、スクリプトプロパティに保存します。

  1. スプレッドシートを開き、「拡張機能」→「Apps Script」
  2. 左メニューの「⚙ プロジェクトの設定」→「スクリプト プロパティ」→「プロパティを追加」
  3. プロパティ名 GEMINI_API_KEY、値に発行したAPIキーを入れて保存

Step 1:設定とシートの準備

以降のコードはすべて同じ コード.gs に順番に貼り付けてください。

// ===== 設定 =====
const CONFIG = {
  SHEET_NAME: '請求書',      // ★転記先のシート名
  // ★請求書メールを探すGmail検索条件(処理済み・エラーのラベルは自動で除外される)
  GMAIL_QUERY: 'has:attachment (請求書 OR 領収書 OR invoice) newer_than:30d',
  LABEL_DONE: 'invoice-done',      // 処理済みスレッドに付けるラベル
  LABEL_ERROR: 'invoice-error',    // エラーが出たスレッドに付けるラベル
  DRIVE_FOLDER_ID: '',             // ★添付を保存するDriveフォルダID(空なら保存しない)
  MODEL: 'gemini-flash-latest',    // ★使用するモデル(Step 2で確認)
  MAX_THREADS: 10,                 // 1回の実行で処理するスレッド数の上限
  MAX_FILE_MB: 15,                 // これより大きい添付はエラー扱い
  MAX_RUNTIME_MS: 4.5 * 60 * 1000, // 実行時間の上限(GASの6分制限より手前で止める)
};

const HEADERS = [
  '処理日時', 'ステータス', '確認理由',
  '書類種別', '発行元', '登録番号', '請求書番号', '発行日', '支払期限',
  '小計', '消費税', '合計', '通貨',
  '送信元', '件名', 'ファイル名', 'ファイルハッシュ', 'メールリンク', '保存先',
];

// 先頭の0が消えないよう文字列として扱う列
const TEXT_COLUMNS = ['登録番号', '請求書番号', 'ファイルハッシュ'];

function setupSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(CONFIG.SHEET_NAME) || ss.insertSheet(CONFIG.SHEET_NAME);
  sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]).setFontWeight('bold');
  sheet.setFrozenRows(1);
  getOrCreateLabel_(CONFIG.LABEL_DONE);
  getOrCreateLabel_(CONFIG.LABEL_ERROR);
  console.log('シートとラベルを準備しました');
}

function getSheet_() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) throw new Error(`シート「${CONFIG.SHEET_NAME}」がありません。setupSheet を実行してください`);
  return sheet;
}

function getOrCreateLabel_(name) {
  return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}

function getApiKey_() {
  const key = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  if (!key) throw new Error('スクリプトプロパティ GEMINI_API_KEY が設定されていません');
  return key;
}

貼り付けたら、関数選択で setupSheet を選んで実行します。初回は権限の承認画面が出るので許可してください。シートにヘッダー行ができ、Gmailにラベルが2つ作られます。

Step 2:使えるモデルを確認する

Geminiのモデルは入れ替わりが早く、古いモデル名は順次提供終了になります。記事のコードをそのまま使って 404 が出るのを避けるため、まず自分のAPIキーで使えるモデルを確認します。

function listModels() {
  const res = UrlFetchApp.fetch('https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000', {
    headers: { 'x-goog-api-key': getApiKey_() },
    muteHttpExceptions: true,
  });
  if (res.getResponseCode() !== 200) {
    throw new Error(`モデル一覧の取得に失敗 (${res.getResponseCode()}): ${res.getContentText()}`);
  }
  const models = JSON.parse(res.getContentText()).models || [];
  models
    .filter(m => (m.supportedGenerationMethods || []).includes('generateContent'))
    .forEach(m => console.log(`${m.name.replace('models/', '')}  /  ${m.displayName}`));
}

実行ログに一覧が出ます。CONFIG.MODEL の初期値 gemini-flash-latest は「最新のFlashモデル」を指すエイリアスです。

  • まず動かしてみる段階 → エイリアスのままでOK
  • 本番運用 → 一覧にあるバージョン付きのモデル名に固定するのがおすすめ(モデルが勝手に切り替わって出力の傾向が変わるのを防ぐため)

Step 3:Gemini APIで1件読み取ってみる

いきなりGmailと連携せず、Driveに置いたPDF1件で読み取り精度を確認します。

ポイントは構造化出力(responseMimeType: 'application/json' + responseJsonSchema)です。返ってくるJSONの形をスキーマで固定できるので、「AIの返答から正規表現で値を抜き出す」といった不安定な処理が要りません。

const EXTRACT_PROMPT = `添付の書類から請求情報を抽出してください。

ルール:
- 書類に書かれていない項目は出力しない(推測で埋めない)
- 金額は数値のみ(カンマ・通貨記号は付けない)
- subtotal は税抜金額。税込金額しか書かれていない場合(内税表記)は subtotal を出力しない
- tax は消費税の合計額(8%と10%が混在する場合は合算)
- total は税込の合計金額
- 日付は YYYY-MM-DD 形式。和暦は西暦に変換する
- registration_number は適格請求書発行事業者の登録番号(T + 13桁の数字)
- currency は ISO 4217 の3文字コード(日本円なら JPY)
- 読み取りに自信がない項目がある場合は needs_review を true にし、理由を review_reason に日本語で書く`;

const INVOICE_SCHEMA = {
  type: 'object',
  properties: {
    document_type: { type: 'string', enum: ['請求書', '領収書', 'その他'], description: '書類の種類' },
    vendor_name: { type: 'string', description: '発行元の会社名・店名' },
    registration_number: { type: 'string', description: '登録番号(T + 13桁)' },
    invoice_number: { type: 'string', description: '請求書番号・領収書番号' },
    issue_date: { type: 'string', description: '発行日(YYYY-MM-DD)' },
    due_date: { type: 'string', description: '支払期限(YYYY-MM-DD)' },
    subtotal: { type: 'number', description: '税抜の小計' },
    tax: { type: 'number', description: '消費税額の合計' },
    total: { type: 'number', description: '税込の合計金額' },
    currency: { type: 'string', description: '通貨コード' },
    needs_review: { type: 'boolean', description: '人の確認が必要か' },
    review_reason: { type: 'string', description: '確認が必要な理由' },
  },
  required: ['document_type', 'needs_review'],
};

function callGemini_(bytes, mimeType) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.MODEL}:generateContent`;
  const payload = {
    contents: [{
      parts: [
        { inlineData: { mimeType: mimeType, data: Utilities.base64Encode(bytes) } },
        { text: EXTRACT_PROMPT },
      ],
    }],
    generationConfig: {
      responseMimeType: 'application/json',
      responseJsonSchema: INVOICE_SCHEMA,
    },
  };
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': getApiKey_() },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };

  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = UrlFetchApp.fetch(url, options);
    const code = res.getResponseCode();

    if (code === 200) {
      const candidate = (JSON.parse(res.getContentText()).candidates || [])[0];
      if (!candidate) throw new Error('応答に候補がありません: ' + res.getContentText().slice(0, 500));
      if (candidate.finishReason && candidate.finishReason !== 'STOP') {
        throw new Error(`生成が途中で終了しました(finishReason: ${candidate.finishReason})`);
      }
      const text = ((candidate.content && candidate.content.parts) || [])
        .filter(p => p.text && !p.thought)
        .map(p => p.text)
        .join('');
      return JSON.parse(text);
    }

    // レート制限・サーバーエラーは待ってからリトライ
    if ((code === 429 || code >= 500) && attempt < maxAttempts) {
      console.warn(`Gemini API ${code}。${attempt * 10}秒後にリトライします`);
      Utilities.sleep(attempt * 10 * 1000);
      continue;
    }
    throw new Error(`Gemini API エラー (${code}): ${res.getContentText().slice(0, 500)}`);
  }
}

function validate_(data) {
  const reasons = [];
  if (data.needs_review) reasons.push(data.review_reason || 'AIが要確認と判定');
  if (data.document_type === 'その他') reasons.push('請求書・領収書ではない可能性');
  if (typeof data.total !== 'number') reasons.push('合計金額が読み取れない');

  const amounts = [data.subtotal, data.tax, data.total];
  if (amounts.every(v => typeof v === 'number') && Math.abs(data.subtotal + data.tax - data.total) > 1) {
    reasons.push('小計+消費税が合計と一致しない');
  }
  if (data.registration_number && !/^T\d{13}$/.test(data.registration_number)) {
    reasons.push('登録番号の形式が不正');
  }
  [['issue_date', '発行日'], ['due_date', '支払期限']].forEach(([key, label]) => {
    if (data[key] && !/^\d{4}-\d{2}-\d{2}$/.test(data[key])) reasons.push(`${label}の形式が不正`);
  });
  return reasons;
}

function testExtract() {
  const FILE_ID = 'ここにDriveのファイルIDを入れる'; // ★テスト用PDFのファイルID
  const blob = DriveApp.getFileById(FILE_ID).getBlob();
  const data = callGemini_(blob.getBytes(), blob.getContentType());
  console.log(JSON.stringify(data, null, 2));
  const reasons = validate_(data);
  console.log(reasons.length ? '要確認: ' + reasons.join(' / ') : '検証OK');
}

手元の請求書PDFをGoogleドライブにアップロードし、URLの /d/ と /view の間の文字列(ファイルID)を FILE_ID に入れて testExtract を実行します。

{
  "document_type": "請求書",
  "vendor_name": "株式会社サンプル商事",
  "registration_number": "T1234567890123",
  "invoice_number": "INV-0012",
  "issue_date": "2026-10-25",
  "due_date": "2026-11-30",
  "subtotal": 50000,
  "tax": 5000,
  "total": 55000,
  "currency": "JPY",
  "needs_review": false
}

このような結果が出れば成功です。取引先ごとに数枚ずつ試して、読み取りの傾向を確認しておきましょう。

なぜ「AIの判定」と「コードの検証」を両方入れるのか

AIの needs_review は「AI自身が自信がない」ケースしか拾えません。読み間違えたのに自信満々というケースはコード側の検証で拾います。

チェック 拾えるミスの例
小計+消費税=合計 桁の読み違い、8%/10%の取り違え
登録番号の形式 桁数不足、Tの欠落
日付形式 和暦の変換ミス
二重登録(Step 4) 同じ請求書の再送、別PDFでの二重請求

Step 4:Gmailの添付を処理して転記する

いよいよ本体です。Gmailの検索や添付ファイルの扱いの基本は「GASでGmailを自動処理する」で解説しています。

function processInvoiceEmails() {
  // 多重起動の防止(前回の実行が長引いた場合)
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(10 * 1000)) {
    console.log('前回の実行が続いているためスキップします');
    return;
  }

  const startTime = Date.now();
  try {
    const sheet = getSheet_();
    const existing = loadExisting_(sheet);
    const labelDone = getOrCreateLabel_(CONFIG.LABEL_DONE);
    const labelError = getOrCreateLabel_(CONFIG.LABEL_ERROR);

    const query = `${CONFIG.GMAIL_QUERY} -label:${CONFIG.LABEL_DONE} -label:${CONFIG.LABEL_ERROR}`;
    const threads = GmailApp.search(query, 0, CONFIG.MAX_THREADS);
    console.log(`対象スレッド: ${threads.length}件`);

    for (const thread of threads) {
      if (Date.now() - startTime > CONFIG.MAX_RUNTIME_MS) {
        console.log('実行時間の上限に近づいたため、残りは次回に回します');
        break;
      }
      let hasError = false;
      for (const message of thread.getMessages()) {
        for (const att of message.getAttachments({ includeInlineImages: false })) {
          try {
            processAttachment_(sheet, existing, message, att);
          } catch (e) {
            hasError = true;
            console.error(`処理失敗: ${message.getSubject()} / ${att.getName()} / ${e.message}`);
          }
        }
      }
      thread.addLabel(hasError ? labelError : labelDone);
    }
  } finally {
    lock.releaseLock();
  }
}

function processAttachment_(sheet, existing, message, att) {
  const mimeType = detectMime_(att);
  if (!mimeType) return; // Word・Excelなど対象外の形式は無視

  const sizeMb = att.getSize() / 1024 / 1024;
  if (sizeMb > CONFIG.MAX_FILE_MB) {
    throw new Error(`ファイルサイズが大きすぎます(${sizeMb.toFixed(1)}MB)`);
  }

  // 同じファイルは二度処理しない(冪等性)
  const bytes = att.getBytes();
  const hash = computeHash_(bytes);
  if (existing.hashes.has(hash)) {
    console.log(`登録済みのためスキップ: ${att.getName()}`);
    return;
  }

  const data = callGemini_(bytes, mimeType);
  if (data.registration_number) {
    data.registration_number = data.registration_number.replace(/[\s-]/g, '');
  }
  const reasons = validate_(data);

  // ファイルは別でも、同じ発行元・同じ番号なら二重請求を疑う
  const invoiceKey = makeInvoiceKey_(data.vendor_name, data.registration_number, data.invoice_number);
  if (invoiceKey && existing.invoiceKeys.has(invoiceKey)) {
    reasons.push('同じ発行元・番号の書類が登録済み(二重請求の可能性)');
  }

  let savedUrl = '';
  if (CONFIG.DRIVE_FOLDER_ID) {
    savedUrl = DriveApp.getFolderById(CONFIG.DRIVE_FOLDER_ID).createFile(att.copyBlob()).getUrl();
  }

  const record = {
    '処理日時': new Date(),
    'ステータス': reasons.length ? '要確認' : 'OK',
    '確認理由': reasons.join(' / '),
    '書類種別': data.document_type,
    '発行元': data.vendor_name,
    '登録番号': data.registration_number,
    '請求書番号': data.invoice_number,
    '発行日': data.issue_date,
    '支払期限': data.due_date,
    '小計': data.subtotal,
    '消費税': data.tax,
    '合計': data.total,
    '通貨': data.currency,
    '送信元': message.getFrom(),
    '件名': message.getSubject(),
    'ファイル名': att.getName(),
    'ファイルハッシュ': hash,
    'メールリンク': `https://mail.google.com/mail/u/0/#all/${message.getId()}`,
    '保存先': savedUrl,
  };
  writeRow_(sheet, record);

  existing.hashes.add(hash);
  if (invoiceKey) existing.invoiceKeys.add(invoiceKey);
  console.log(`登録: ${att.getName()}(${record['ステータス']})`);
}

function detectMime_(att) {
  const type = att.getContentType();
  const name = att.getName().toLowerCase();
  // PDFが application/octet-stream で届くことがあるため拡張子でも判定する
  if (type === 'application/pdf' || name.endsWith('.pdf')) return 'application/pdf';
  if (type === 'image/jpeg' || /\.jpe?g$/.test(name)) return 'image/jpeg';
  if (type === 'image/png' || name.endsWith('.png')) return 'image/png';
  return null;
}

function computeHash_(bytes) {
  const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, bytes);
  return digest.map(b => ((b + 256) % 256).toString(16).padStart(2, '0')).join('');
}

function makeInvoiceKey_(vendor, regNo, invoiceNo) {
  if (!invoiceNo) return '';
  return `${regNo || vendor || ''}|${String(invoiceNo).trim()}`;
}

function loadExisting_(sheet) {
  const hashes = new Set();
  const invoiceKeys = new Set();
  const lastRow = sheet.getLastRow();
  if (lastRow >= 2) {
    const col = name => HEADERS.indexOf(name);
    sheet.getRange(2, 1, lastRow - 1, HEADERS.length).getValues().forEach(r => {
      if (r[col('ファイルハッシュ')]) hashes.add(String(r[col('ファイルハッシュ')]));
      const key = makeInvoiceKey_(r[col('発行元')], r[col('登録番号')], r[col('請求書番号')]);
      if (key) invoiceKeys.add(key);
    });
  }
  return { hashes, invoiceKeys };
}

function writeRow_(sheet, record) {
  const row = sheet.getLastRow() + 1;
  TEXT_COLUMNS.forEach(name => {
    sheet.getRange(row, HEADERS.indexOf(name) + 1).setNumberFormat('@');
  });
  const values = HEADERS.map(h => (record[h] === undefined || record[h] === null) ? '' : record[h]);
  sheet.getRange(row, 1, 1, HEADERS.length).setValues([values]);
}

processInvoiceEmails を手動で実行してみましょう。追加の権限(Gmail・Drive)を求められたら許可します。

この設計で押さえているポイントは3つです。

  • ハッシュで冪等性を担保:途中で止まって再実行しても、同じファイルが二重に登録されない
  • エラーはスレッド単位でラベル分け:invoice-error ラベルで絞り込めば、失敗したメールだけ確認できる
  • 1件の失敗で全体を止めない:添付ごとに try/catch しているので、壊れたPDFが1つあっても他は処理される

Step 5:トリガーで定期実行する

function createTrigger() {
  ScriptApp.getProjectTriggers()
    .filter(t => t.getHandlerFunction() === 'processInvoiceEmails')
    .forEach(t => ScriptApp.deleteTrigger(t));
  ScriptApp.newTrigger('processInvoiceEmails').timeBased().everyHours(1).create();
  console.log('1時間ごとのトリガーを設定しました');
}

createTrigger を1回だけ実行すれば完了です。何度実行しても既存のトリガーを消してから作り直すので、重複はしません。

実用例

1. 月次締めの確認作業を「要確認だけ」に絞る

シートのヘッダー行でフィルタを作り、ステータスを「要確認」で絞り込みます。メールリンク列から元のメールに飛んで原本と照合し、修正したらステータスを「OK」に書き換える運用にすれば、全件を目で追う作業がなくなります。

確認が終わったデータは、会計ソフトのCSVインポート形式に合わせて書き出せば、仕訳入力まで手作業なしでつなげられます。

2. 支払期限が近い請求書を毎朝メールで通知する

転記したデータを使って、支払い漏れを防ぎます。

function notifyDueSoon() {
  const DAYS_BEFORE = 3; // ★何日前から通知するか
  const sheet = getSheet_();
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  const col = name => HEADERS.indexOf(name);
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const limit = new Date(today.getTime() + DAYS_BEFORE * 24 * 60 * 60 * 1000);

  const lines = sheet.getRange(2, 1, lastRow - 1, HEADERS.length).getValues()
    .filter(r => {
      const v = r[col('支払期限')];
      if (!v) return false;
      const due = v instanceof Date ? v : new Date(v);
      return !isNaN(due) && due >= today && due <= limit;
    })
    .map(r => {
      const v = r[col('支払期限')];
      const due = Utilities.formatDate(v instanceof Date ? v : new Date(v), 'Asia/Tokyo', 'yyyy/MM/dd');
      return `・${due}  ${r[col('発行元')]}  ${r[col('合計')]} ${r[col('通貨')]}`;
    });

  if (lines.length === 0) return;
  MailApp.sendEmail(
    Session.getEffectiveUser().getEmail(),
    `【支払期限${DAYS_BEFORE}日以内】${lines.length}件`,
    lines.join('\n')
  );
}

トリガーで「日付ベース・毎朝8〜9時」に設定すれば、毎朝リマインドが届きます。

つまずきやすいポイント

症状 原因 対処
404 / models/xxx is not found モデル名の誤り、またはモデルの提供終了 listModels で使えるモデルを確認し、CONFIG.MODEL を変更
429 が頻発する レート制限(特に無料枠) MAX_THREADS を減らす。リトライは組み込み済みだが、継続するなら有料枠へ
400 でペイロードサイズのエラー 添付が大きすぎる MAX_FILE_MB を下げる。大きなPDFは Files API 経由でのアップロードを検討
finishReason: MAX_TOKENS 出力が途中で切れた 明細行まで抽出しようとしていないか確認。項目を絞る
JSON.parse でエラー 応答がJSONとして不完全 上の finishReason を確認。ログに出る応答本文を見る
実行が6分で強制終了 GASの実行時間制限 MAX_RUNTIME_MS で手前で止めて次回に回す設計になっている。MAX_THREADS も調整
請求書以外のメールまで拾う 検索条件が広すぎる from:billing@example.com のように送信元で絞る
パスワード付きPDFが失敗する Geminiが中身を読めない invoice-error ラベルが付くので手動対応。取引先にパスワードなしでの送付を相談
請求書番号の先頭の0が消える 数値として扱われた TEXT_COLUMNS で文字列書式にしている。列を追加したらここにも追加
同じスレッドに後から届いた請求書が処理されない スレッドに処理済みラベルが付いている 対象スレッドのラベルを外せば再処理される(ハッシュで重複は防がれる)

エラーに気づける仕組みも入れておく

トリガー実行のエラーは、放っておくと誰も気づきません。invoice-error ラベルの件数を定期的にチェックする、失敗時にSlackやメールへ通知するなどの仕組みを入れておくと安心です。具体的な作り方は「自動化スクリプトが止まっても誰も気づかない問題:エラー通知と死活監視の作り方」にまとめています。

まとめ

  • Gemini APIはPDF・画像を直接読めるので、レイアウトが取引先ごとに違う請求書でも項目を抽出できる
  • **構造化出力(responseJsonSchema)**を使えば、返ってくるJSONの形を固定できる
  • AIの自己申告(needs_review)だけに頼らず、金額の整合性・登録番号・日付形式をコードで検証する
  • ファイルのハッシュで冪等性を確保し、発行元+番号で二重請求を検知する
  • モデル名は入れ替わりが早いので、listModels で確認してから固定する
  • 業務の書類を扱うなら、データの扱いを確認したうえで有料枠を使う

帳票・書類作成の自動化のご相談

「毎月の請求書処理を自動化したい」「AIで書類を読み取って既存のシステムに取り込みたい」などのご相談を受け付けています。

🌐 https://datarou.com

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?