GASでPDF請求書をAIで読み取り
自動転記する方法
請求書PDFを開いて、金額と支払期日をスプレッドシートに打ち込む——この作業は自動化できます。DriveApp(GASからGoogleドライブを操作する標準機能)でPDFを取り出し、Claude API(Anthropic社の生成AI)に ファイルごと渡して項目を読み取らせます。OCRライブラリも正規表現も使わない実装を、動くコードで解説します。
Table of Contents
請求書の転記が自動化しにくかった理由
結論から言うと、請求書PDFの読み取りはGAS(Google Apps Script。Googleサービスを操作する無料のプログラム実行環境)と AIで自動化できます。これまで難しかった理由は、はっきりしています。請求書のレイアウトが会社ごとに違うからです。
合計金額は右下にあるとは限りません。「合計」「ご請求金額」「Total」と呼び名も揺れます。 PDFからテキストを抜き出して正規表現で探す従来のやり方では、取引先が増えるたびにルールを足す羽目になりました。 今回の方法は、その分岐をすべてAIに任せます。人間が新しい請求書を見て理解できるなら、AIも同じように読めます。
取り出す
Driveの未処理フォルダからPDFを取得。1回の実行で扱う件数に上限をかける
読ませる
PDFをBase64に変換してClaude APIへ送信。項目をJSONで返させる
残す
シートに転記し、PDFを処理済みフォルダへ移動。失敗した行は理由を記録する
ただし、ひとつだけ譲れない前提があります。金額は人が確認すること。この記事の実装は「入力の手間をゼロにする」ものであって、「確認をゼロにする」ものではありません。 そのために、AIが読み取った根拠の文字列と確信度も一緒にシートへ残します。
準備:フォルダを2つ用意し、APIキーを保管する
Googleドライブに「請求書_未処理」「請求書_処理済み」の2つのフォルダを作ります。 受け取ったPDFを未処理フォルダに置くだけで取り込まれ、成功したものは処理済みへ移動する——という流れです。フォルダの位置が、そのまま処理状態を表します。管理用のフラグ列を作るより、目で見て分かるぶん壊れにくい設計です。
転記先のスプレッドシートを開き、拡張機能 → Apps Script からスクリプトを作ります。 シート名は「請求書」とし、1行目に見出し(取込日時・ファイル名・請求元・請求書番号・発行日・支払期日・金額・根拠・確信度・リンク・エラー)を入れておきます。 APIキーとフォルダIDは、コードに直接書かずスクリプト プロパティへ保存します。
// スクリプトプロパティに以下を保存しておく
// ANTHROPIC_API_KEY : AnthropicのAPIキー
// INBOX_FOLDER_ID : 未処理PDFを置くDriveフォルダのID
// DONE_FOLDER_ID : 処理済みPDFの移動先フォルダのID
function getProp(name) {
const value = PropertiesService.getScriptProperties().getProperty(name);
if (!value) throw new Error(name + " が未設定です。");
return value;
}
// フォルダIDは、Driveでフォルダを開いたときのURL末尾の文字列
// https://drive.google.com/drive/folders/【この部分】フォルダIDは、Driveでフォルダを開いたときのURLの末尾です。 APIキーをコードに直書きすると、スクリプトを共有した相手に鍵ごと渡ることになります。PropertiesService(スクリプトごとの設定値を保存するGASの仕組み)を使ってください。
DriveからPDFを取り出す(件数の上限を決める)
フォルダ内のPDFを列挙します。ここで大事なのは、1回の実行で処理する件数に上限をかけることです。GASの1回の実行は最大6分。PDF1件の読み取りに数秒〜十数秒かかるため、 月末に50件まとめて置かれると途中で強制終了します。
// 未処理フォルダからPDFだけを取り出す
// 1回の実行で扱う件数に上限を設け、6分の実行時間制限に収める
const MAX_FILES_PER_RUN = 10;
function listPdfFiles() {
const folder = DriveApp.getFolderById(getProp("INBOX_FOLDER_ID"));
const it = folder.getFilesByType(MimeType.PDF);
const files = [];
while (it.hasNext() && files.length < MAX_FILES_PER_RUN) {
files.push(it.next());
}
return files;
}getFilesByType(MimeType.PDF)でPDFだけに絞れるので、フォルダにメモや画像が混ざっていても無視されます。 上限に達しなかったPDFは未処理フォルダに残ります。後述のトリガーで15分ごとに実行すれば、次の回で自動的に片付きます。 一度に全部やろうとしないのが、GASで長い処理を安定させるコツです。
PDFをそのままAIに渡して項目を抽出する
この記事の核心です。Claude APIは、メッセージの中身としてdocument型のブロックを受け取れます。PDFのバイナリをBase64(バイナリを文字列に変換する方式)にして渡せば、 テキストもレイアウトも画像も含めて解釈してくれます。 テキスト情報を持たないスキャンPDFでも読める理由がこれです。
// PDFファイルそのものをClaude APIに渡し、項目をJSONで受け取る
function extractInvoice(file) {
const url = "https://api.anthropic.com/v1/messages";
// PDFのバイナリをBase64(バイナリを文字列に変換する方式)にする
const base64 = Utilities.base64Encode(file.getBlob().getBytes());
const instruction =
"添付のPDFは請求書です。以下のJSONだけを出力してください。\n" +
"前置きも説明文もコードブロックの記号も付けないでください。\n\n" +
"{\n" +
' "issuer": "請求元の会社名",\n' +
' "invoiceNo": "請求書番号",\n' +
' "issuedOn": "発行日 yyyy-MM-dd",\n' +
' "dueOn": "支払期日 yyyy-MM-dd",\n' +
' "totalAmount": "税込合計金額。半角数字のみ。カンマや円記号を含めない",\n' +
' "totalSource": "合計金額の根拠になったPDF上の文字列をそのまま",\n' +
' "confidence": "high | medium | low"\n' +
"}\n\n" +
"ルール:\n" +
"・PDFに書かれていない項目は空文字 \"\" にする。推測で埋めない。\n" +
"・税込と税抜が両方ある場合は税込を採用する。\n" +
"・文字がかすれて読み取りに自信がない場合は confidence を low にする。";
const payload = {
model: "claude-haiku-4-5",
max_tokens: 1000,
messages: [{
role: "user",
content: [
{
type: "document",
source: {
type: "base64",
media_type: "application/pdf",
data: base64
}
},
{ type: "text", text: instruction }
]
}]
};
const res = UrlFetchApp.fetch(url, {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": getProp("ANTHROPIC_API_KEY"),
"anthropic-version": "2023-06-01"
},
payload: JSON.stringify(payload),
muteHttpExceptions: true // エラー時も例外にせずレスポンスを受け取る
});
if (res.getResponseCode() !== 200) {
throw new Error("API エラー (" + res.getResponseCode() + "): " + res.getContentText());
}
const text = JSON.parse(res.getContentText()).content[0].text;
return parseJson(text);
}プロンプト(AIへの指示文)で決めているのは4点です。出力をJSONだけに固定すること。書かれていない項目は推測で埋めず空文字にすること。税込と税抜が併記されていたら税込を採るという業務ルール。そして、自信がなければconfidenceを low と申告させること。3点目までは正確さ、4点目は「確認すべき行」を機械的に選ぶための仕掛けです。
金額を「半角数字のみ」で返させているのは、¥132,000-のような文字列をGAS側で毎回パースしたくないからです。 同時に、根拠となったPDF上の文字列(totalSource)も返させます。数字だけを見ても正しいか分かりませんが、「合計 ¥132,000」という原文が隣にあれば一目で判断できます。
AIの返答を安全にJSONへ変換する
「JSONだけを出力して」と指示しても、まれに前後に説明文やコードブロックの記号が付きます。 返答をそのままJSON.parse()に渡すと、そこで処理が落ちます。最初の{から最後の}までを切り出してから変換します。
// AIの返答からJSONを取り出す
// 「JSONだけ」と指示しても前後に文が付くことがあるため、最初の { から最後の } までを拾う
function parseJson(text) {
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start === -1 || end === -1) {
throw new Error("JSONが見つかりません: " + text.slice(0, 200));
}
return JSON.parse(text.slice(start, end + 1));
}
// 金額文字列を数値にする。数値化できなければ null を返す(勝手に0にしない)
function toAmount(value) {
if (!value) return null;
const cleaned = String(value).replace(/[^0-9.]/g, "");
if (cleaned === "") return null;
const num = Number(cleaned);
return isNaN(num) ? null : num;
}金額の変換で最も重要なのは、数値化に失敗したときに 0 を返さないことです。0円の請求書と「読み取れなかった請求書」は、まったく別物です。 前者を後者として集計すれば、支払漏れが静かに発生します。nullを返し、シートのセルを空欄のまま残す。人が気づける形で失敗させるのが正解です。
シートに転記し、処理済みへ移動する
3つをつなぐ本体です。1件ずつtryで囲むのがポイントです。 1件の請求書が壊れていても、残りの9件は処理されます。
// 本体:PDFを1件ずつ読み取り、シートに転記して、処理済みフォルダへ移す
function importInvoices() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("請求書");
const doneFolder = DriveApp.getFolderById(getProp("DONE_FOLDER_ID"));
const files = listPdfFiles();
files.forEach(function (file) {
try {
const data = extractInvoice(file);
const amount = toAmount(data.totalAmount);
sheet.appendRow([
new Date(), // 取込日時
file.getName(), // ファイル名
data.issuer || "", // 請求元
data.invoiceNo || "", // 請求書番号
data.issuedOn || "", // 発行日
data.dueOn || "", // 支払期日
amount, // 金額(数値化できなければ空欄になる)
data.totalSource || "",// 金額の根拠となったPDF上の文字列
data.confidence || "", // 確信度
file.getUrl(), // 元PDFへのリンク
amount === null ? "金額を数値化できません" : ""
]);
// 転記に成功したPDFだけを処理済みへ移動する
file.moveTo(doneFolder);
} catch (e) {
// 1件の失敗で全体を止めない。PDFは未処理フォルダに残す
console.error(file.getName() + ": " + e.message);
sheet.appendRow([
new Date(), file.getName(), "", "", "", "", null, "", "", file.getUrl(),
"読み取り失敗: " + e.message
]);
}
});
}file.moveTo(doneFolder)をappendRow()の後に置いているのには理由があります。順序が逆だと、転記に失敗したPDFが処理済みフォルダへ消えていきます。記録してから移動する。この順序を守れば、失敗したPDFは未処理フォルダに残り、原因を直したあと再実行するだけで復旧します。
失敗時もシートに1行を残し、末尾のエラー列に理由を書きます。 沈黙して消えるのが、自動化で最も困る挙動だからです。 なお同じPDFを2回置いてしまうと2行転記されます。ファイル名や請求書番号での重複チェックを足したい場合は、 重複データの記事で扱っている考え方がそのまま使えます。
15分おきの自動実行と、人が見るべき行
最後に、時間主導トリガー(決まった間隔で関数を自動実行するGASの仕組み)を仕掛けます。 トリガー登録の関数は一度だけ手動で実行してください。
// 15分おきに importInvoices を自動実行する
// この関数は一度だけ手動で実行する(実行のたびに重複登録されるため)
function createImportTrigger() {
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === "importInvoices") {
ScriptApp.deleteTrigger(t);
}
});
ScriptApp.newTrigger("importInvoices")
.timeBased()
.everyMinutes(15)
.create();
}
// 確信度が low の行、または金額が空の行だけを目視確認すればよい
// スプレッドシートの条件付き書式で confidence 列が "low" の行に色を付けておくと早い15分おきなら、PDFを未処理フォルダに入れてコーヒーを淹れて戻る頃には転記が終わっています。 既存の同名トリガーを消してから作り直しているのは、実行のたびにトリガーが増殖して同じPDFが複数回処理されるのを防ぐためです。
運用のコツは、全行ではなく「怪しい行」だけを見ることです。確認すべきなのは、確信度が low の行、金額が空欄の行、エラー列に文字が入っている行の3種類だけ。 スプレッドシートの条件付き書式で色を付けておけば、月末の確認は数分で終わります。 残りの行は、根拠の文字列を眺めて流すだけで構いません。
まとめ
PDF請求書の転記は、GASとClaude APIで自動化できます。押さえるべきは5つです。 PDFはテキスト抽出せずdocumentブロックでそのまま渡す。書かれていない項目は空文字にさせ、推測させない。 数値化に失敗したら 0 ではなく空欄にする。記録してからPDFを移動する。 そして確信度を返させ、人が見る行を絞る。
この仕組みで消えるのは、キーボードを叩く時間だけではありません。「請求書の山を前にした、どこから手をつけるかの憂鬱」が消えます。フォルダに放り込めば表になっている状態から始められる。 同じ型は、領収書の経費精算、納品書の照合、名刺のデータ化にもそのまま応用できます。 読み取りをAIに、判断を人に——分担を守るかぎり、この自動化は安全に効きます。
よくある質問
読み取れます。Claude APIはPDFを画像としても解析するため、テキスト情報を持たないスキャンPDFでも項目を抽出できます。ただし精度は元の画質に依存します。傾いた紙をスマホで撮った画像より、複合機で200dpi以上でスキャンしたPDFのほうが確実です。読み取り結果に自信が持てない場合は、後述の confidence(確信度)を出力させて低いものだけ人が確認する運用が現実的です。
PDFのファイルそのものをAIに渡しているためです。従来はPDFからテキストを抽出し、正規表現で「合計」の近くの数字を探す、といった処理が必要でした。この記事の方法では、PDFをBase64(バイナリを文字列に変換する方式)に変換してClaude APIへ送り、「請求元と合計金額を返して」と指示するだけです。レイアウトが会社ごとに違っても、同じコードで動きます。
金額は必ず人が確認する前提で組んでください。本文のコードでは、AIに数値を「区切り文字なしの半角数字」で返させ、GAS側で Number() に通してから書き込んでいます。数値化に失敗した行はエラー列に記録され、金額セルは空のままです。加えて、AIが読み取った金額の根拠となる文字列(例: 「合計 ¥132,000」)も一緒に返させ、スプレッドシートに並べて残すと、目視確認が数秒で済みます。
Claude APIは複数ページのPDFをまとめて解釈します。明細が2ページにまたがる請求書1通なら、そのまま1件として抽出できます。1ファイルに別々の請求書が複数入っている場合は、プロンプトで「請求書ごとに1オブジェクトの配列で返す」と指示し、GAS側でも配列として受け取ってループしてください。ページ数が多いPDFはトークン消費が増えるため、料金と実行時間に注意します。
取引先の請求書は機密情報なので、社内規程と契約の確認が先です。AnthropicのAPI経由で送信したデータは、標準の設定では学習に使われません(詳細は公式の利用規約・プライバシーポリシーを確認してください)。社外送信が認められない場合は、この方法ではなく社内サーバーでのOCRを検討することになります。判断を省略しないでください。
GASの1回の実行は最大6分(無償版)です。PDF1件あたり数秒〜十数秒かかるため、数十件を一度に回すと超えます。対策は、1回の実行で処理する件数に上限を設ける(例: 10件)ことです。未処理のPDFはフォルダに残るので、トリガーで15分おきに実行すれば自然に消化されます。詳しくは実行時間制限の記事で解説しています。
AI×GASの業務自動化を
相談する。
請求書の読み取りのほかにも、領収書のデータ化・議事録の要約・問い合わせ分類・翻訳など、 AI APIとGASを組み合わせた自動化をご相談いただけます。既存のドライブ・スプレッドシート運用にもそのまま組み込めます。