MinerU跨页合并

MinerU跨页合并

fanz Lv3

一直很好奇 Mineru 中 pipeline 模式下,跨页的段落是怎么合并的,看了一下代码,简单了解下
相关文件位置:
mineru/backend/pipeline/model_json_to_middle_json.py > mineru/backend/pipeline/para_split.py

跨页合并操作是在将model_json转换成middle_json的过程中实现的,model_json中是对图片的每一页使用 yolo 模型进行处理,提取出每页图片中各类信息(如标题、文本、表格、图片等)的文本框位置,然后再result_to_middle_json方法中使用 ocr 模型提取出文字,在此方法中,会使用para_split方法处理跨页段落的情况。

它通过将所有页面的文本块(blocks)放入同一个列表中,消除了“页”的物理边界,从而将“跨页合并”转化为了简单的“相邻块合并”问题。

以下是具体的实现步骤解析:

1. 核心步骤:打平所有页面 (Flattening)

这是跨页合并最关键的一步。在 para_split 函数中,代码并没有一页一页地处理,而是把所有页面的 block 提取出来,放入了一个名为 all_blocks 的大列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def para_split(page_info_list):
all_blocks = []
for page_info in page_info_list:
blocks = copy.deepcopy(page_info['preproc_blocks'])
for block in blocks:
# 给每个block打上页码标记,方便后续知道它来自哪一页
block['page_num'] = page_info['page_idx']
block['page_size'] = page_info['page_size']
# 【关键】将所有页面的block追加到同一个列表中
all_blocks.extend(blocks)

# 将打平后的所有blocks传入合并逻辑
__para_merge_page(all_blocks)

原理解析: 一旦放入 all_blocks,第 页的最后一个段落和第 页的第一个段落,在列表中就变成了紧挨着的两个元素。程序不需要编写复杂的“跨页查找”逻辑,只需要看“前一个”和“后一个”即可。

2. 分组策略 (Grouping)

__para_merge_page 内部,首先调用了 __process_blocks(blocks)
这个函数的作用是阻断合并。它会将连续的 text 类型块分在一个组(Group)里,一旦遇到 title(标题)或 interline_equation(行间公式),就会切断分组。

  • 意义: 跨页合并绝不会跨越标题或公式。只有连续的文本流才有资格被合并。

3. 合并判定逻辑 (Heuristics)

最复杂的逻辑在于判断两个相邻的 block 是否应该合并。这主要由 __merge_2_text_blocks 函数实现。

代码采用了启发式规则(Heuristics),必须同时满足以下条件才会合并:

  1. 非结束符结尾: 上一个 block (block2) 的最后一段文字,不能以句号、感叹号等结束符 (LINE_STOP_FLAG) 结尾。
1
2
3
# LINE_STOP_FLAG = ('.', '!', '?', '。', '!', '?', ')', ')', '"', '”', ':', ':', ';', ';')
and not last_span['content'].endswith(LINE_STOP_FLAG)

  1. 非新段落特征开头: 下一个 block (block1) 的开头,不能是数字(列表特征)或大写字母(英文新句特征)。
1
2
3
4
5
# 下一个block的第一个字符是数字
and not span_start_with_num
# 下一个block的第一个字符是大写字母
and not span_start_with_big_char

  1. 格式一致性: 两个 block 的宽度差异不能太大(abs(block1_weight - block2_weight) < min_block_weight),且垂直距离不能过远。

4. 跨页标记 (Cross-Page Tagging)

你提到的 "cross_page": true 参数正是在合并发生的时刻被标记的。

__merge_2_text_blocks 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 如果决定合并,且两个block的页码不一致
if block1['page_num'] != block2['page_num']:
for line in block1['lines']:
for span in line['spans']:
# 【关键】在这里给span打上跨页标记
span[SplitFlag.CROSS_PAGE] = True

# 将下个block (block1) 的内容追加到上个block (block2) 中
block2['lines'].extend(block1['lines'])
# 清空下个block
block1['lines'] = []
block1[SplitFlag.LINES_DELETED] = True

流程:

  1. 检测到 block1(下文)在第 2 页,block2(上文)在第 1 页。
  2. 判定需要合并。
  3. 将 block1 中所有的文本片段(spans)标记为 CROSS_PAGE: True
  4. 将 block1 的内容“搬运”到 block2 中。
  5. block1 变为空壳并被标记删除。

总结

MinerU 实现跨页合并的流程如下:

  1. Input: PDF 各页的原始 JSON 数据。
  2. Flatten: [Page1_BlockA, Page1_BlockB] + [Page2_BlockC, Page2_BlockD] -> [BlockA, BlockB, BlockC, BlockD]
  3. Check: 检查 BlockB (Page1 结尾) 和 BlockC (Page2 开头)。
  4. Decide: BlockB 没有句号结尾?BlockC 不是大写开头? -> 合并!
  5. Tag & Merge:BlockC 的内容搬到 BlockB 里面,并给 BlockC 原有的文字打上 cross_page=True 标签。
  6. Output: 最终生成的 JSON 中,原本属于 Page 2 的那段话现在位于 Page 1 的结构里,但保留了“我来自跨页”的标记。
  • 标题: MinerU跨页合并
  • 作者: fanz
  • 创建于 : 2026-02-09 20:56:45
  • 更新于 : 2026-02-09 20:59:18
  • 链接: https://redefine.ohevan.com/ta6zym/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。