画像内の表データをCSVに抽出する方法
表、注文書、納品書の写真から、クリーンなCSVファイルを作成します。space-ocr が明細行をどう読み取り、行単位の検証が確認すべき値をどう表に出すかを解説します。
スキャンした納品書や注文書の表データを、スプレッドシートに入力するのは骨の折れる作業です。鮮明な画像があっても、結局はピクセルデータに過ぎません。多くの場合、項目、数量、価格などを一行ずつ手でコピー&ペーストする地道な作業が待っています。このプロセスは時間がかかるだけでなく、たった一つの入力ミスがデータセット全体を狂わせてしまう原因にもなります。

より良いアプローチは、表の構造をスキーマとして定義することです。テキストブロックを丸ごと抜き出すのではなく、必要な列を宣言します。繰り返し現れる明細行のセクションは array 型のフィールドとし、その子要素(children)に列を並べます。常に数値が入る列には型を宣言します。
{
"name": "items",
"type": "array",
"children": [
{ "name": "name", "type": "string" },
{ "name": "qty", "type": "integer" },
{ "name": "price", "type": "number" },
{ "name": "amount", "type": "number" }
]
}行数は指定しません。何行返るかはページ側が決めます。各行は添字付きのパス(values.items[0]、values.items[1] …)で返り、同じパスがそのまま値ごとの座標・検証マップのキー cells["items[0].price"] になります。型を宣言しても抽出される値は変わりません。型がモデルに渡ることはないからです。宣言した型が足すのは、values の隣に並ぶ決定的な第二の層 data.normalized です。
この方法は、値が密集した表でも有効です。システムはまず大規模言語モデルを使って抽出テキストの候補を生成しますが、処理はそこで終わりません。例えば品名の「刻みたくあん」や単価の「580」など、個々の値に対してクロス検証を行います。言語モデルの読みを文書の列構造と照合し、ページ上で元々検出されたOCRシンボルと文字単位でマッチングさせます。値が隣の行にずれ込むと、その座標の文字は多くの場合一致しなくなり、そのまま通らずに要確認として記録されます。
"data": {
"values": {
"items": [
{ "name": "刻みたくあん", "qty": "3",
"price": "580", "amount": "1,740" }
]
},
"cells": {
"items[0]": {
"box": { "xmin": 263, "ymin": 460,
"xmax": 738, "ymax": 523 },
"quad": [ { "x": 263, "y": 460 }, { "x": 738, "y": 460 },
{ "x": 738, "y": 523 }, { "x": 263, "y": 523 } ],
"verified": null, "review": null
},
"items[0].name": {
"box": {…}, "quad": […],
"verified": true, "review": null,
"evidence": { "text_match": true, "match_ratio": 1.0 }
},
"items[0].qty": { "box": {…}, "quad": […],
"verified": true, "review": null },
"items[0].price": {
"box": { "xmin": 693, "ymin": 460,
"xmax": 738, "ymax": 488 },
"quad": […],
"verified": false,
"review": { "reasons": ["text_mismatch"] },
"evidence": { "text_match": false, "match_ratio": 0.62 }
}
},
"review": {
"unit": "field",
"flagged": [
{ "path": "items[0].price", "reasons": ["text_mismatch"] }
]
},
"normalized": {
"items": [ { "qty": 3, "price": 580, "amount": 1740 } ]
}
}要確認リストがそのまま作業待ち行列です。data.review.flagged がパスと理由を名指しし、cells["items[0].price"] には照合した座標が入っています。ただし全件を捕まえる仕組みではありません。隣の行に同じ数字が印字されていれば、箱がどちらに付いても文字は一致しますし、両エンジンが同じ誤読で一致した場合は突き合わせる相手がいません。そのため、文字照合では見えない行・列の取り違えは、宣言できる規則として渡しておく価値があります。コード列には pattern、マスタが既に持っている値には enum、妥当な範囲には min と max。違反はいずれも同じ review.flagged に載ります。
最後の層は自前の検算で、これは下流の仕事です。qty を integer、金額列を number として宣言してあるので、data.normalized には解釈済みの数値が並びます("1,740" は 1740)。行の検算は掛け算ひとつで足ります。
const { values, normalized, review } = data;
const rows = values.items.map((item, i) => {
const n = normalized.items[i];
const flagged = review.flagged.some((f) =>
f.path.startsWith(`items[${i}]`)
);
return {
name: item.name,
qty: n.qty,
price: n.price,
amount: n.amount,
// 人が見る行: 要確認が立った、または検算が合わない
check:
flagged || n.qty == null || n.qty * n.price !== n.amount,
};
});解釈できなかった葉は normalized で null になり、失敗の種類は cells[path].normalized.error に、要確認リストには type_mismatch として載ります。計算に使う値は normalized から取り、表示する文字は values のままにしてください。座標と検証が付いているのは values の方です。
各値は、その値が載っていたページに突き合わせます。モデルの読みを、そこで実際に検出されたOCRシンボルと文字単位でマッチングし、どれだけ一致したかが evidence.match_ratio に残ります。0.85 以上であれば信頼性の高い一致です。判定そのものは verified で、これは review の鏡です。何かが立てば false、照合が走って何も立たなければ true、照合する相手が無ければ(行のユニオンボックスなど)null になります。座標は一致したシンボルから box と quad として導かれ、data.image を基準に 0–1000 のスケールで正規化されます。これは値がどこから来たかの証拠であって、求めていた値であることの証明ではありません。確認が取れなかった値は review.flagged に並びます。
料金は従量課金制で、画像処理1枚あたり¥10です。毎月100枚分の無料スキャン枠がアカウントに付与されます。何らかの理由で抽出に失敗した場合は、料金は一切かかりません。
- シートスキーマを定義する新しいシートを作成し、列を定義します。明細行には「配列」タイプを使用し、商品名、数量、価格などの子列を追加します。
- 画像をアップロードするドラッグ&ドロップまたはAPIを使用して、表が写った画像をシートにアップロードします。
- 抽出データを確認する画像がスキーマに沿って処理されます。表の各明細行が、シート上で構造化された行として表示されます。
- 必要に応じて修正する任意のセルをクリックすると、画像上の対応する領域が表示されます。グリッド上で直接、値を手動で修正できます。
- CSVにエクスポートする「エクスポート」ボタンをクリックし、CSVを選択します。すべての明細行を含む表データが、クリーンで構造化されたファイルとしてダウンロードされます。