图片表格变结构化数据:OCR 后的三个坑

图片表格变结构化数据:OCR 后的三个坑

街道公示、年报、招标结果,越来越多以图片形式发布。要把它们变成能统计的数据,OCR 只是起点。本文记录一次 18 期、32 张图的还原过程,重点在**版面还原**和**校验**,代码可直接跑。

一、先判断有没有文字层

很多人一上手就截图丢给识别工具。先做一步更省事的:`Ctrl+F` 搜表里任意一个数字。

命中就是文字层,直接复制,不用 OCR。未命中才是图片。

二、版面还原:表头 → 列 → 行

OCR 输出的是文字块,每块带坐标。要变成表格,做三件事:

  1. **定列**:按表头文字块的 x 坐标聚类,得到列中心;

  2. **定行**:以「序号」列为行锚点(每行唯一),其余文字块按 y 距离归到最近序号;

  3. **归并**:同一锚点内的块按 x 排序拼接,解决单元格折行。

三个具体坑:

  • **双金额列**:存在「报送金额 / 实际划转金额」时取最右列,财务表惯例比字面匹配可靠;
  • **续页无表头**:以首图列位置为基准,按图宽比例缩放套用;
  • **列名被切断**(如「实际划转金」+「额」):列名匹配用前缀包含,不用全等。

下面是行锚点归并的最小实现,输入用合成块演示规则(真实场景里这些块来自识别引擎输出)。保存为 `anchor_demo.py`,直接跑:

text 复制代码
$ python anchor_demo.py
exit code: 0

全程无异常日志输出,结果是纯文本,可再转 JSON。

text 复制代码
BLOCKS = [
    ("序号", 40, 30, 30), ("项目", 260, 30, 60), ("金额", 620, 30, 50),
    ("1", 40, 80, 20), ("1栋屋面防水", 300, 80, 160), ("23,045.67", 620, 80, 120),
    ("2", 40, 130, 20), ("2栋电梯导向装置", 300, 130, 200), ("45,120.00", 620, 130, 120),
    ("3", 40, 180, 20), ("3栋外墙保温砂浆", 280, 176, 180), ("脱落维修(紧急)", 430, 196, 180),
    ("35,754.47", 620, 185, 120),
    ("4", 40, 235, 20), ("4栋主排污管", 300, 235, 160), ("23,850.36", 620, 235, 120),
]
ANCHOR_COL_X, ANCHOR_TOL, ROW_TOL = 40, 12, 18


def group_rows(blocks, anchor_x=ANCHOR_COL_X, tol=ANCHOR_TOL):
    anchors = {}
    for text, x, y, _w in blocks:
        if abs(x - anchor_x) <= tol and text.isdigit():
            anchors[y] = text
    rows = {y: [] for y in anchors}
    for text, x, y, _w in blocks:
        if y in anchors and abs(x - anchor_x) <= tol:
            continue
        if y < min(rows) - 40:
            continue
        nearest = min(rows, key=lambda ay: abs(ay - y))
        if abs(nearest - y) <= ROW_TOL:
            rows[nearest].append((x, text))
    out = []
    for y in sorted(rows):
        cells = sorted(rows[y])
        project = "".join(t for x, t in cells if x < 560)
        money = "".join(t for x, t in cells if x >= 560)
        out.append((anchors[y], project, money))
    return out


rows = group_rows(BLOCKS)
print("识别到行数:", len(rows))
for no, project, money in rows:
    print(f"序号{no} | {project} | {money}")
max_no = max(int(n) for n, _p, _m in rows)
print("校验:行数 =", len(rows), ";序号最大值 =", max_no, ";一致 =", len(rows) == max_no)

实跑输出:

text 复制代码
识别到行数: 4
序号1 | 1栋屋面防水 | 23,045.67
序号2 | 2栋电梯导向装置 | 45,120.00
序号3 | 3栋外墙保温砂浆脱落维修(紧急) | 35,754.47
序号4 | 4栋主排污管 | 23,850.36
校验:行数 = 4 ;序号最大值 = 4 ;一致 = True

第 3 行的项目名在图上折成两块(y=176 和 y=196),靠 18 像素的行容差拼回了同一行。这就是锚点法相对于「按 y 切行」的价值。

三、三道校验

| 校验 | 方法 | 不通过怎么办 |

|---|---|---|

| 行数 | `max(序号) == len(rows)` | 回查漏行,重跑该图 |

| 合计 | `sum(金额) == 图内印刷合计` | 逐行比对原图,标注差异 |

| 缺口 | 无法识别的单元格单列清单 | 标注「原图不可辨」,不插补 |

关键原则:**差异要保留,不要平滑**。合计对不上有两种原因------漏行,或原表本身未对齐,两者都要落进报告。

工程上还有两个细节:

  • 金额解析要统一全角半角、去千分位、括号负数转负号;
  • 每行保留「来源图 + 坐标」,便于人工回查。

四、这套方法的失效边界

写清楚做不到的部分,比吹方案有用:

  • **没有印刷合计的表**:缺校验标尺,只能靠行号,可靠性下降一档;
  • **每期格式都变的表**:列位置无法复用,解析规则要重写,人工看反而更快;
  • **手写体、盖章遮挡**:识别率断崖式下跌,必须人工录入;
  • **多栏排版**(左右两栏各一张表):纵向锚点会串行,要先做版面切分。

五、结论

瓶颈不是识别准确率,是**校验与缺口披露**。

没有校验环节的自动提取,产出的是「看起来像数据的东西」。一份能用的表,不是没有空缺的表,是空缺写在哪里的表。

我们是谁:做企业知识库与 AI 客服落地,也做「图片与散落文件变成可核对数据」这类活;交付原始文件、还原表、缺口清单三样。本文的示例数据与输出均为本机实跑,文字由作者与 AI 辅助整理,观点与结论由作者负责。

相关推荐
小小张说故事10 分钟前
Python 里弱引用到底弱在哪:拆开 weakref.ref、回调陷阱和 WeakValueDictionary
python
用户0190275816137 分钟前
用 DuckDB 存查 A 股 K 线:零服务、SQL 直接分析
python
程序员果子2 小时前
本体构建方法
人工智能·python·知识图谱·agent框架·本体
天天被压力3 小时前
【A股分钟级高频数据完全手册 #11】券商实时行情数据接口:实时交易(券商源)接入与字段
java·人工智能·python
总裁余(余登武)3 小时前
【python项目】:1操作EXCEl自动化开发
python·自动化·excel
芝梵家电数据笔记3 小时前
用字段表核对暖菜板长期使用条件:结构、暖区、质保边界
python·家电·数据校验·dataclass·选购
天天被压力3 小时前
【A股分钟级高频数据完全手册 #12】股票五档盘口数据接口:买卖五档实时盘口解析
java·人工智能·python
卷无止境15 小时前
RAGFlow 项目全景报告:从文档解析引擎到 Agent 原生数据库
后端·python
灿飞屋16 小时前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓