如何从图片中提取表格数据并转换为CSV
将表格、订单或送货单的照片转换成干净的CSV文件。了解 space-ocr 如何读取分项明细,以及行级校验如何把需要人工核对的值挑出来。
将扫描件或图片里的表格数据录入到电子表格中,是一项费时费力的传统工作。你可能有一张清晰的送货单或采购单图片,上面列满了各种项目,但它们都只是像素点。接下来的步骤通常是繁琐的手工录入:逐字逐句地将品名、数量和价格复制到新的一行里。这个过程不仅慢,而且一个打字错误就可能让整份数据出错。

一个更高效的方法是,把表格的结构定义为一份 schema(列结构)。你不再是简单地提取一大块文本,而是预先声明所需要的列。重复出现的分项区块定义为 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 里。
费用按使用量计算,每处理一张图片收费$0.05。您的账户每月包含100次免费扫描。如果因任何原因提取失败,我们不会收取任何费用。
- 定义工作表列结构 (Schema)创建一个新的工作表(Sheet)并定义您需要的列。对于分项列表,请使用'array'类型,并为其添加名称、数量、价格等子列。
- 上传您的图片通过拖拽或使用API将表格图片上传到该工作表。
- 检查提取的数据图片将根据您定义的列结构进行处理。表格中的每个分项都会在工作表中显示为一行结构化数据。
- 按需修正点击任意单元格,即可在图片上看到对应的区域。您可以直接在网格中手动修正任何数值。
- 导出为CSV点击“导出”按钮并选择CSV格式。您的表格数据(包括所有分项)将被下载为一个干净、结构化的文件。