aria / blog

重写报告批改前,先拿 14 份真实样本验证选型

新版批改系统的解析层选型记录:xberg、PaddleOCR-VL、PyMuPDF、OnlyOffice 各就其位的过程,不是看文档定的是实测定的。包括一个元素数对账发现不了的段落级漏检,和一堆只有真实文档才会教你的坑。

现网的报告批改系统要重写了。绿地项目,不背兼容包袱。这句话听着爽,也意味着解析层这座地基要从头打。而解析层是整个批改系统里决策最密的地方:学生提交的东西有 .docx、有 .doc 老格式、有原生数字 PDF、有手机拍照的扫描件;候选工具有一大把,文档个个都说自己什么都能干。

这次的教训可以提前说:选型是拿真实样本喂出来的,评测文档只能参考。项目一开始先立了个规矩:不写完整实现,每个技术卡点做一个独立 demo,用真实学生报告实测,出了数字再定稿。这篇记的是解析层(内部编号 D1)的验证过程。

样本

合作方提供了三十多个学生、八个流体力学实验的真实报告:docx 和 pdf 成对、教师打过分和批注的版本、.doc 老格式、模板与评分规则。验证子集选了 14 份:8 个 docx、2 个 .doc、2 个原生数字 PDF、2 份自制扫描件(150dpi 图片型 PDF,模拟拍照提交)。

顺带一提,真实学生数据有出校合规问题,demo 阶段全部用脱敏样本,云 API 只碰这些。

定式:结构来自原始文档,坐标来自渲染文档

四类输入分流到统一元素模型

选型表上最亮的新星是 xberg,Rust 内核的多语文档引擎,能输出层级章节树。认真查证后发现它是真的,但有一个致命差距:不给元素级页面坐标。而批改系统的高亮回标、样式量测、查重框选,全都要坐标。

于是解析栈的定式浮出来了:

  • 结构树从原始 docx 里取(章节、段落、表格、公式、样式,这是内容的真源);
  • 坐标从转换后的 PDF 里取(OnlyOffice 把 docx 渲染成 PDF,PyMuPDF 读行级 bbox——渲染结果就是学生肉眼所见);
  • 两条线在解析阶段对齐一次,合并成统一的元素模型,bbox 落库。

另一个诱惑是 OCR:PaddleOCR-VL 这代模型很能打,“全都 OCR 一遍”似乎能统一所有输入。验证给了它机会,也给了它教训,见下文。

实测下来这条定式的成绩单:OnlyOffice 转换 10/10 成功、约 2 秒一份;docx 路径段落定位 89–97%、表格 93%(42/45)、图片配对后 100%;本地解析路径每份不到 0.1 秒。.doc 老格式先转 docx 再走同一条路,文本覆盖 99.1–99.3%,零 OCR 误差,零串位。

计划里的 xberg 最终让位给了现网解析器的迁移版。它本身没毛病,只是这个场景里,自己控结构加 OnlyOffice 控坐标,已经把两条真源都占住了。

OCR 在数字 PDF 上漏了整段

PaddleOCR-VL 对扫描件的表现接近完美:bbox 覆盖 100%,章节切分 5/5,扫描版的还原度和数字版几乎一致(94 对 93 个元素)。照这个数据,让它通吃所有 PDF 似乎顺理成章。

翻车翻在逐段对账上。有一份 .doc 转出来的数字 PDF,按元素数对账是 94 对 93,看着健康;逐段核对才发现,“三、仪器列表”和”五、体会”两段,PyMuPDF 的文本层里完整存在,Paddle 的版面结果里整段没有。标题检出了,正文段丢了。

这是整个验证里最值钱的一个发现:按元素数对账发现不了段落级丢失。94 对 93 看着是误差,逐字对账才知道是整块内容消失。架构因此改了一条硬规则:

  • 数字 PDF 的文本用 PyMuPDF 文本层直提(渲染产物,无损);
  • Paddle 只负责版面和 bbox,与文本层对账回填;
  • 只有扫描件和拍照——没有文本层的输入——才全靠 OCR。

一句话:有真源的时候不要用识别结果覆盖真源。

真实文档才会教你的坑

验证过程攒了一串小坑,每一条都来自具体样本:

  • OnlyOffice 转出来的 PDF 里,图片是内联的。常规的 get_images()(按 xref 取对象)返回零,必须按 bbox 渲染裁剪才能拿到图。换了转换器这条路就变了,工具链的耦合点往往藏在这种地方。
  • 章内重号会把章节切分带沟里。某组样本的一级标题用中文数字(一、二、),二级标题用阿拉伯数字(1. 2. 3.),纯正则按编号切章,会把二级切成一级,多切出五个”章”。层级判定最终靠版面分析的 parent/children 树,不靠正则硬猜。
  • 矢量图在 PDF 里根本不是位图对象。Word 里画的自选图形,转成 PDF 后没有位图可提取,只能靠”图N”题注锚定、按区域渲染兜底。
  • 带网格线的曲线图会被版面模型判成表格。实体类型判定不可靠,图表的归属和提取一律用题注锚定兜底。
  • 跨页表格会被拆成两个碎片。一份望远镜参数表跨了 3、4 两页,版面分析切成一尾一头。好消息是按阅读序归属,两个碎片都正确落进了同一章。按”相邻页同类碎片、中间无新标题”合并为逻辑实体,这条规则直接进了正式设计。

计划是活的

验证计划本身也变了:原定十四个 demo,执行中解析栈对比提前定稿、某个转换器对比直接取消、图片查重和 AI 代写检测暂缓。计划文档的状态栏老实写着”历史存档,现行结论以定稿文档为准”。

计划改了不算失败:demo 的产出本来就是”哪些问题不用再纠结了”。解析层定稿时附了一份迁移实现方案,工作量估到一周半。如果没做这些验证,直接按选型表开写,xberg 的坐标缺失、OCR 的段落漏检、内联图的坑,每一个都会在正式实现的半路上等着,返工远不止一周半。

这笔账算下来,验证阶段花的时间,大约是它省下的时间的零头。下次再遇到”选型表写得很漂亮”的局面,我还是会先问一句:拿真实样本跑过吗。

← 返回列表