PDF-OCR文件识别篇(三):PDF 切分与表格还原

为什么先讲切分?因为「按表」是整条流水线的最小处理单元。一份 PDF 可能有几十张表,只有先把它拆成一张一张,后面的并行抽取、单表重试、按表注入字段定义才有可能。本章对应包com.example.pdfextraction.pdf

3.1 按表切分PdfTableSlicer

逐页用 PDFBox 提取文本,识别行首形如「表1 …」的标题,把文档切成若干PdfSection

@Component public class PdfTableSlicer { // 表格标题:行首「表 + 数字」,例如「表1 ...」「表 12 ...」 private static final Pattern TABLE_TITLE = Pattern.compile("^\\s*表\\s*\\d+\\b.*"); public List<PdfSection> slice(byte[] pdfBytes) { try (PDDocument document = PDDocument.load(pdfBytes)) { PDFTextStripper stripper = new PDFTextStripper(); stripper.setSortByPosition(true); // 按坐标排序,减少跨栏错乱 // ① 先逐页抽取文本 List<String> pageTexts = 每页 stripper.getText(document); // ② 收集所有「表N」标题出现位置,按归一化标题去重 // ③ 由相邻标题确定每段页范围 } } }

三个关键处理,每一个都对应一类真实坑:

  • 跨页标题去重。跨页表的标题会在每页顶部重复出现。若不处理,同一张表会被切成多段。做法是把标题「去表号 + 去空白」归一化成 key,用Set.add(key)只保留首次出现:
String key = normalizeTitle(t); // 去「表N」前缀与空白 if (StringUtils.isEmpty(key) || !seen.add(key)) continue; // 已见过则跳过
  • 边界页共享。下一张表的标题页,往往同时印着上一张表的尾部(跨页表尾与下表标题同页)。所以相邻两段共享这个边界页,都包含进去,避免漏掉落在该页上半部的尾部数据:
end = (i + 1 < titles.size()) ? titlePages.get(i + 1) : pages; // 截到下一个标题页(含)
  • 可配置大标题切分。除了「表N」,有些章节没有表号但也要单独成段(如「补充说明」)。通过pdf.ai.split-headings配关键词,isConfiguredHeading去掉可选序号前缀(「八、」「9.」)后按关键词匹配: 最终可获取到 八、补充说明 大标题下内容。

本人测试分割文件格式为:

表1 xxxxxx表

表2xxxxx表

八、补充登记信息

九、xxxxx信息

private static final Pattern HEADING_NUM_PREFIX = Pattern.compile("^[一二三四五六七八九十百零\\d]+[、..]\\s*"); private static final int BIG_HEADING_MAX_LEN = 60; // 行太长视为正文,避免误判

切出的每个PdfSection持有:

title // 表格标题 startPage // 起始页 endPage // 结束页 text // 整段合并文本 pageTexts // 逐页文本(供按页分块时附「表头参考」用)

3.2 单表导出PdfSplitter

当走「文件抽取」路径(useFile=true,把单张表 PDF 直接上传给模型让它先解析)时,需要把单张表的页范围导出成独立 PDF。用 PDFBoximportPage实现,落到临时文件,用完即删:

public String splitToTempPdf(byte[] pdfBytes, int start, int end) { try (PDDocument src = PDDocument.load(pdfBytes); PDDocument out = new PDDocument()) { int s = Math.max(1, start), e = Math.min(src.getNumberOfPages(), end); for (int p = s; p <= e; p++) out.importPage(src.getPage(p - 1)); File tmp = File.createTempFile("ai-table-", ".pdf"); out.save(tmp); return tmp.getAbsolutePath(); } } // 调用方在 finally 里 Files.deleteIfExists(tmpPath) 清理

3.4 小结

输入输出用在哪
PdfTableSlicerPDF 字节List<PdfSection>(按表切段)所有路径的第一步
PdfSplitterPDF + 页范围单表临时 PDF 路径文件抽取路径

由于pdf会出现大文件,某个表格就几十近百行。如果一整个文件去执行显然不合适的。故以异步思想,将互不相关的表格和特殊表头对应的数据进行分割,也提高执行效率、准确率。如果你也面临同样的需求,将本文章交给ai,ai会给详细的解释。这里仅提供实现思想😀。