起因:复制出来的文字顺序是错的
用户反馈:在 AetherPDF 里复制一段双栏论文的文字,粘贴出来的内容,顺序全乱了。
左栏第一行拼上右栏第一行,左栏第二行拼上右栏第二行——像把两栏用拉链拉在了一起。
原因我知道:PDF 不像 Word,它没有「段落」「分栏」这些语义结构。PDF 里只有一堆字符,每个字符带一个坐标。软件要自己判断:这些字符按什么顺序读?
如果是单栏文本,按坐标从上到下、从左到右排就行。但双栏呢?两栏的字符坐标混在一起,必须先识别出「这是两栏」,然后左栏内部排完,再排右栏。
这个问题我花了一些时间解决了。但解决的过程中,我遇到了一个更棘手的情况:
如果页面上不是分栏,而是表格呢?
表格看起来也是「左右排列的文字块」,但阅读顺序和分栏完全不同——表格要按行读(每行从左到右),分栏要按栏读(先读完左栏再读右栏)。
同样是左右并排的文字,分栏和表格的正确阅读顺序恰好相反。
我必须让软件区分出来:这块区域,到底是分栏还是表格。
识别的线索:文字之外的信息
怎么区分?答案在文字之外。
PDF 虽然没有「表格」这个语义标签,但有大量的绘制指令——线条(Path)和填充矩形(Fill)。人类看到一组水平线和垂直线围成的网格,就知道那是表格。软件也可以。
我开始收集页面上的视觉线索:
- 水平线和垂直线——表格的边框和分隔线
- 填充矩形——有些表格不画线,靠交替填充颜色来区分行(灰白灰白……)
- 字符间距——同一行内字符紧密排列,行与行之间有明显间隔
如果一块区域有明显的网格状线条或填充,那它大概率是表格,文字按行读。如果没有这些信号,只有自然流动的文字块,那它更可能是分栏,按栏读。
文字选择的问题解决了。但我盯着那些被识别出来的表格结构——行边界、列边界、每个单元格的范围——突然想到一件事:
既然我已经知道了表格有几行几列、每个格子在哪里,那把格子里的文字提取出来,不就是一张 Excel 表吗?
这不就是用户一直想要的「PDF 表格导出」功能?
而且我不需要 OCR,因为 PDF 里的文字本来就是精确的字符数据——我只需要把它们按照表格结构重新组织一下。
从「识别这是表格」到「导出成表格」
说干就干。
表格结构已经有了(行列边界),字符坐标已经有了,下一步就是把字符分配到对应的单元格里——一个矩形包含判断就够了。
但做起来才发现,现实中的 PDF 表格千奇百怪。
没有两张表格是一样的
做完第一版,拿几个真实文档一试——翻车了。
第一种翻车:有些表格只有外框线,没有内部分隔线。 行列完全靠字符间距判断。
第二种翻车:有些表格用填充色区分行,但表头是深色底+白字,数据行是浅色底。 我的填充矩形收集器把表头的深色块当成了一个独立区域,把表格切碎了。
第三种翻车:有些表格的单元格里有多行文字。 一个「缴费起始时间」的表头占了两行,我的行带检测把它切成了两行——导致后面所有数据行错位。
第四种翻车:页面背景有一个覆盖整页的浅灰色填充矩形。 我的算法把它当成了表格的一部分,凭空多出一行一列。
第五种翻车:表格每行高度不一致。 有些表格表头行很高(里面有多行文字),数据行很矮;或者某几行因为内容多而撑高了。统一的行高假设直接崩盘。
第六种翻车:某些行有高亮背景色。 比如财务报表里,重要数据行会加一个黄色或浅蓝色底色标记。这个高亮填充矩形被我的算法当成了额外的行边界信号,把一行数据切成了两行。
每一种翻车都需要对应的修复:
- 整页背景填充?只关注与文字距离较近的绘制指令,丢弃尺寸与单元格差异巨大的背景级填充
- 表头深色块?过滤掉跨多列的大块填充
- 多行单元格?用字符间距判断:如果两行字符之间的间隔小于字符高度的 1.2 倍,它们属于同一个单元格
- 只有外框线?回退到纯字符间距聚类——找到字符 Y 坐标的自然聚类作为行,X 坐标的大间隔作为列分隔
- 行高不一致?不预设统一行高,用实际的水平线位置或字符 Y 坐标聚类来独立确定每一行的边界
- 行内高亮背景?识别出「与某行完全重叠的填充矩形」是装饰性的,不是结构性的——它的边界不应该参与行列切分
最终的算法是一套混合策略:线条优先,填充补充,字符间距兜底。三种信号相互印证、相互修正。
交互设计:框选,而不是全自动
一个重要的产品决策:让用户手动框选要识别的区域,而不是自动检测整页的表格。
为什么?
因为一个 PDF 页面上可能同时有表格、正文、页眉页脚、水印。自动检测需要先区分哪些是表格、哪些不是——这本身就是一个更难的问题。而且,如果页面上有两个表格并排,统一识别会因为两表的行高不同而互相干扰。
框选的好处是:用户告诉软件「这块区域是表格」,软件只需要专注做好「识别行列结构 + 提取文字」这一件事。准确率大幅提高。
操作流程:文件菜单 → 识别并导出表格 → 在页面上拖动框选 → 选择保存位置 → 完成。
算下来,用户真正要动手的,只有「框选」和「保存」两步。剩下的交给 AetherPDF。
不需要联网,不需要上传文件到云端,不依赖 OCR——纯本地计算,毫秒级响应。
有时候,一个看似「功能更弱」的选择,恰恰是对用户最友好的选择。因为用户要的不是「全自动的惊喜」,而是「可控的确定」。
识别表格结构,有几种思路
表格识别不是新问题,业界有不同的方案:
多模态大模型方案:把 PDF 页面渲染成图片,丢给视觉模型去「看」哪里是表格、行列在哪。模型足够强的话,什么表格都能认。但代价是:需要调用云端 API(隐私风险)、推理速度慢(秒级甚至十几秒)、结果不稳定(同一张图多问几次可能给出不同答案)。
图形学分析方案:用传统图像处理的方法——霍夫变换检测直线、连通域分析检测单元格。不需要大模型,但需要先把 PDF 渲染成图片,然后在像素层面做分析。精度受渲染分辨率影响。
我的方案:直接读取 PDF 的绘制指令。
PDF 文件不是图片。它内部存储的是精确的绘制指令——「在坐标 (x1,y1) 到 (x2,y2) 画一条线」「在这个矩形区域填充灰色」「在坐标 (x,y) 放置字符'年'」。这些指令里明确包含了绘制意图:画线就是画线,填充就是填充,字符就是字符——不需要猜。
前两种方案都是先把这些精确信息渲染成像素,再从像素里反推结构。我的方案跳过了渲染这一步,直接在源数据上做结构分析。
好处是:
- 精确:线条坐标精确到小数点后几位,不存在「这条线到底在哪个像素」的模糊性
- 快:纯坐标计算,毫秒级完成,不需要跑神经网络
- 本地运行:不依赖云端 API,文件不出设备,没有隐私问题
- 确定性:同一个文件,跑一百次结果一模一样
当然,这个方案有一个前提:PDF 文件里的文字必须是真实的文字数据(而不是扫描件图片)。对于扫描件,确实需要 OCR。但大部分电子生成的 PDF——办公文档、财务报表、系统导出的单据——里面的文字都是精确的字符数据。对这类文件,直接读取绘制指令是最干净的方案。
底层能力复用,用户需求提前落地
其实「表格导出」不是意外冒出来的想法——之前已经有用户提过这个需求了。只是一直排在后面,没有开始做。
但当我为了提升文本复制的可读性去做排版分析时,底层能力自然而然地就长出来了:收集绘制指令、识别线条和填充结构、判断区域类型。这套能力本来是为文字选择服务的,但它同时就是表格识别的核心算法——行列边界检测、单元格划分、字符归属。
底层能力到位之后,表格导出功能就是在上面搭一层薄薄的产品逻辑:加一个框选交互、加一个 CSV 序列化、加一个保存对话框。用户等了挺久的功能,因为底层能力的复用,很快就落地了。
这就是自研引擎的好处:底层能力是通用的,产品功能是在上面自然生长的。
我为文字选择写的排版分析代码,同时就是表格导出的核心识别引擎。不需要单独接一个表格检测模型,不需要集成第三方 SDK。一套引擎,能力共享,功能自然组合。
用第三方库的软件做不到这种复用。它们要加一个「表格导出」,就得找一个做表格识别的 SDK 来集成,和原有的文字选择逻辑完全割裂,两套系统之间没有共享的认知。
16 年,我没有白写那一行行底层代码。它们现在开始,自己长出产品来了。
如果你也有这个痛点
遇到 PDF 里的表格想录入 Excel,你以前怎么做?
手动抄一遍?截图发给同事让他帮忙录入?上传到某个在线工具等它慢慢识别,还要担心数据安全?
现在:AetherPDF 打开文档,文件菜单 → 识别并导出表格,框选区域,保存。几秒钟,干净的 CSV 文件就在你电脑上了。本地运行,数据不出设备。