PDF表格解析复杂场景知识沉淀

PDF 表格解析复杂场景知识沉淀

适用于政府报告、统计月报、监测数据等结构化 PDF 的自动化提取(对上一篇PDF解析知识的拓展)


一、单元格合并的三种形态

合并类型 视觉表现 PDF 底层真相 处理策略
行内合并(横向) 一个单元格跨多列 该位置只画一次文字,无竖线分隔 检测列边界缺失,将多列内容合并
列内合并(纵向) 一个单元格跨多行 该位置只在第一行画文字,后续行该列为空 检测 Y 坐标连续性,向下填充直到新记录开始
行列交叉合并 大块区域合并 既无横线也无竖线,文字只出现一次 结合 X/Y 双向检测,标记为合并区域

关键难点:PDF 没有"合并单元格"的语义,只有"某些位置没画文字"。程序无法区分是"合并"还是"数据缺失"。


二、单元格内容多行(最隐蔽的问题)

2.1 视觉表现

一个单元格里文字换行了,比如:

复制代码
┌─────────┐
│ 贵阳市  │
│ 南明区  │
│ 云岩区  │
└─────────┘

2.2 PDF 底层

实际上是 3 个独立的文字块,Y 坐标不同,但被框在同一个矩形内。

2.3 处理策略

  1. 第一步 :先按 extract_words 提取所有文字块,记录每个块的 (x0, y0, x1, y1)
  2. 第二步:判断这些块是否属于同一个单元格 ------ 看它们的 X 范围是否高度重叠(重叠度 > 80%)
  3. 第三步 :同一单元格内的多行内容,用换行符 \n 拼接,或按业务规则合并(如"贵阳市\n南明区" → "贵阳市南明区")

场景关联:水源地名称可能出现"原北郊水库\n(备用)"这种多行备注,需要识别为同一个单元格。


三、跨页表格的处理

3.1 跨页的两种模式

模式 特征 处理策略
续表模式 第二页有"续表"或"接上页"字样,有表头 检测到"续表"关键词后,将两页数据拼接,表头去重
无标识续页 第二页直接继续数据,无表头无标识 用记录边界检测(序号连续性)判断是否为同一表格的延续

3.2 跨页后表头的四种情况

情况 示例 处理策略
每页都有完整表头 第1页有表头,第2页也有表头 提取时识别表头行,去重或跳过
仅首页有表头 第1页有表头,第2页直接是数据 用首页表头做列定位,后续页直接按坐标对齐
续页有特殊表头 第2页表头简化为"续上表:XX月报" 正则匹配"续"字,跳过该行
完全无表头 第2页直接是数据,无任何标识 最困难,需要用内容特征推断列语义

3.3 跨页数据拼接的关键逻辑

python 复制代码
def merge_cross_page_tables(pages_data):
    """
    跨页表格合并
    """
    all_records = []
    last_seq = 0

    for page_idx, page_records in enumerate(pages_data):
        if not page_records:
            continue

        # 检测是否为新表格开始(有序号重置)
        first_seq = page_records[0].get("序号")

        if first_seq and first_seq <= last_seq:
            # 序号重置 = 新表格,不拼接
            all_records.extend(page_records)
        else:
            # 序号连续 = 续表,拼接
            all_records.extend(page_records)

        # 更新最后序号
        if page_records:
            last_seq = page_records[-1].get("序号", 0)

    return all_records

四、综合处理框架

复制代码
PDF 输入
    │
    v
[Step 1] 逐页提取文字块 (extract_words)
    │    └── 记录每个词的 (text, x0, y0, x1, y1, page_num)
    │
    v
[Step 2] 检测表格区域
    │    ├── 有边框 → 用 lines 策略检测单元格边界
    │    └── 无边框 → 用文字对齐 + 空白间隙推断列边界
    │
    v
[Step 3] 单元格重建
    │    ├── 单格单词 → 直接赋值
    │    ├── 单格多词(同行)→ X 重叠检测,合并为同一单元格
    │    ├── 单格多行(同列)→ Y 连续性检测,用 \n 拼接
    │    └── 合并单元格 → 标记为 merged,向下/向右填充
    │
    v
[Step 4] 记录边界检测
    │    ├── 有序号 → 新记录开始(最可靠)
    │    ├── 有城市名 → 新记录开始
    │    └── 无标识 → 上一条记录的续行(合并到上一条)
    │
    v
[Step 5] 跨页处理
    │    ├── 检测"续表"/"接上页"关键词
    │    ├── 检测序号连续性(1,2,3... 不中断)
    │    └── 拼接数据,去除重复表头
    │
    v
[Step 6] 后处理
         ├── 合并单元格内容填充(向下/向右)
         ├── 多行内容合并(按业务规则)
         └── 输出结构化数据

五、关键代码片段

5.1 检测单元格内多行内容

python 复制代码
def group_words_into_cells(words, col_boundaries):
    """
    将文字块按单元格分组,处理多行内容
    """
    cells = {}

    for word in words:
        # 找到该词属于哪一列
        col_idx = find_column(word["x0"], col_boundaries)
        # 找到该词属于哪一行(用锚点对齐,非简单Y聚类)
        row_key = find_row_anchor(word, words)

        key = (row_key, col_idx)
        if key not in cells:
            cells[key] = []
        cells[key].append(word)

    # 同一单元格内的词按 Y 排序,拼接
    result = {}
    for (row, col), word_list in cells.items():
        word_list.sort(key=lambda w: w["top"])
        text = "\n".join([w["text"] for w in word_list])
        result[(row, col)] = text

    return result

5.2 合并单元格检测与填充

python 复制代码
def detect_and_fill_merged_cells(table_data):
    """
    检测合并单元格并填充
    """
    # 向下填充(列合并)
    for col in range(len(table_data[0])):
        last_value = None
        for row in range(len(table_data)):
            if table_data[row][col]:
                last_value = table_data[row][col]
            elif last_value:
                # 当前为空且上方有值 → 可能是合并单元格
                table_data[row][col] = last_value

    # 向右填充(行合并)------ 视业务需要
    # ...

    return table_data

5.3 跨页连续性检测

python 复制代码
def is_continuation_page(current_page, next_page):
    """
    判断下一页是否是当前表格的续页
    """
    # 方法1:检测序号连续性
    current_last_seq = get_last_sequence(current_page)
    next_first_seq = get_first_sequence(next_page)

    if next_first_seq and current_last_seq:
        return next_first_seq == current_last_seq + 1

    # 方法2:检测"续"关键词
    next_header = extract_header(next_page)
    if "续" in next_header or "接上页" in next_header:
        return True

    # 方法3:列数一致且格式相似
    return len(current_page[0]) == len(next_page[0])

六、校验清单

处理复杂场景后必须检查:

校验项 方法 失败处理
序号连续性 set(range(1, n+1)) - set(实际序号) 标记缺失行,人工复核
城市名不重复(同一行) 同一记录中城市名只出现一次 检测合并单元格是否未填充
列数一致性 所有记录列数相同 标记异常行,检查多行内容拆分
跨页后记录数 总记录数 = 各页之和 - 表头行数 检查表头去重逻辑
合并单元格填充 随机抽样检查填充值是否正确 标记异常,人工复核

七、一句话总结

PDF 表格解析的本质是"视觉还原工程":合并单元格是"空值填充",多行内容是"同格拼接",跨页是"连续性拼接",表头是"重复去重"。没有魔法,只有对坐标、空白、文字的精细工程。