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。用 PDFBox importPage 实现,落到临时文件,用完即删:

复制代码
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 小结

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

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