先把长文档按版面区块切好——Unlimited OCR 2 在复杂排版上的取舍

文章导读
把上百页文档整份送进 OCR 时,中断和拼接错误往往来自不同层级:有的是页数多导致单次任务过长、资源被占满,有的是版面本身复杂(多栏、跨页表格、通栏标题)导致区块识别被拆开。这两种失败的判断信号不同,处理方式也不同。先不要急着改切分粒度,先用两组小规模对照测试把失败来源区分开,再决定是按页切、按版面区块切,还是先缩小单次输入规模。下面按“定位失败—选边界—写脚本—处理拼接—抽样核对”的顺序给出可落
📋 目录
  1. 一 先分清失败来自页数、版面复杂度还是内存占用
  2. 二 按页切与按版面区块切的边界分别落在哪
  3. 三 写出切分脚本骨架与批次记录
  4. 四 处理跨页表格与跨栏段落的拼接
  5. 五 抽样核对拼接后的文本连续性
A A

把上百页文档整份送进 OCR 时,中断和拼接错误往往来自不同层级:有的是页数多导致单次任务过长、资源被占满,有的是版面本身复杂(多栏、跨页表格、通栏标题)导致区块识别被拆开。这两种失败的判断信号不同,处理方式也不同。先不要急着改切分粒度,先用两组小规模对照测试把失败来源区分开,再决定是按页切、按版面区块切,还是先缩小单次输入规模。下面按“定位失败—选边界—写脚本—处理拼接—抽样核对”的顺序给出可落地的做法。

若整份送入在中途固定页数附近中断、内存持续上涨,问题偏向输入规模,先缩小批次而不是换切法;若中断总出现在多栏页、跨页表格或通栏标题处,问题偏向版面结构,应按版面区块切并在拼接阶段按页码加坐标回接。按页切实现简单、恢复容易,但容易把跨栏段落和跨页表格拦腰截断;按版面区块切保留版面完整性,代价是切分单元不规则、需要额外的合并判定和更细的断点记录。两种切法都建议保留批次清单和已完成标记,并用目录或条款编号做抽样核对。

先分清失败来自页数、版面复杂度还是内存占用

把资源问题和版面问题混在一起排查,最容易出现的误判是“再多切几页就好了”。实际上,如果失败原因是内存或超时,减少每批页数确实能让任务跑完,但拼接错误不会因此减少;如果失败原因是版面结构被切断,缩小批次只是让错误发生得更早、更零碎。

建议做两组对照输入,规模不必大,但要能看出差异:

  • 少页复杂版面:取 5-10 页包含双栏正文、跨页表格、通栏标题的页面,整份送入。
  • 多页简单版面:取 80-100 页以单栏正文为主的文档,同样整份送入。

两组都要记录三类信息:中断出现在第几页或第几个区块、进程内存或显存随页数增长的趋势、单页处理耗时是否随页数明显变长。这些信息通常可以从运行日志、进程监控(如 top、nvidia-smi 的周期采样)和任务返回结构里拿到,具体字段要结合实际使用的仓库和运行环境确认。

判断顺序建议如下:

先把长文档按版面区块切好——Unlimited OCR 2 在复杂排版上的取舍
  1. 先看中断位置是否与页数相关。多页简单版面在某个页数附近稳定中断,指向输入规模或资源限制。
  2. 再看中断是否与特定版面相关。少页复杂版面在表格页、多栏页稳定失败,指向版面解析而非规模。
  3. 最后看耗时曲线。单页耗时随页数上升明显,说明存在累积状态或内存压力,应按批次切分并控制单批页数。

只有第 1、3 类信号出现时,“先降低输入规模”才是对的解法;如果主要是第 2 类信号,缩小规模解决不了拼接错误,需要换切分粒度。

按页切与按版面区块切的边界分别落在哪

切分粒度的选择依据不是页数多少,而是文档里跨边界结构出现的密度。跨栏段落需要同一页内左右栏按阅读顺序接续;跨页表格需要上一页的表尾与下一页的表头对齐;通栏标题通常标志着一个新章节的开始,被切到独立单元时容易丢失与后文的层级关系。下面这张对照表用于判断两种切法各自的落点:

结构类型按页切按版面区块切
跨栏段落整页保留,栏内顺序由解析器自行处理,风险集中在解析器是否理解分栏按栏切出独立区块,需要在拼接时按 x 坐标还原左右栏顺序
跨页表格必然被页边界截断,需在后处理中按表头特征合并若表格区块跨越页边界,仍要跨单元合并,但表体结构更完整
通栏标题与所在页正文同批处理,层级关系通常保留可能被切成独立小区块,需按页码和纵坐标挂回后文
恢复与重跑粒度规整,按页号续跑简单单元不规则,需要稳定的单元 ID 才能续跑

容易在两种切法下被切断的结构包括:跨页续表、跨栏的引言或注释、图注与图分离、页脚注释与正文引用、多级标题与首段。判断是否要改用版面区块切,可以先统计一份文档里跨页表格和双栏页的占比;如果这类页面占多数,按页切的后处理成本会明显上升,按版面区块切更合适。

写出切分脚本骨架与批次记录

切分过程必须可追踪、可续跑,核心是三样东西:稳定的切分单元标识、批次清单、已完成标记。下面的骨架是通用的,具体的 OCR 调用、PDF 解析和版面检测接口需要按实际仓库的 API 替换。

先把长文档按版面区块切好——Unlimited OCR 2 在复杂排版上的取舍
# 结构示意,非可直接运行的完整实现
JOBS = "jobs.jsonl"      # 批次清单
DONE = "done.jsonl"      # 已完成标记
FAIL = "failed.jsonl"    # 失败记录,便于单独重跑

def build_jobs(doc_path, mode, batch_size=8):
    units = split_by_page(doc_path) if mode == "page" else split_by_layout(doc_path)
    for u in units:
        yield {
            "doc": doc_path,
            "unit_id": u.id,          # 稳定 ID,例如 p0123-b04
            "page": u.page,
            "bbox": u.bbox,           # 按页切时可为整页范围
            "batch": u.page // batch_size,
        }

def run():
    done = load_ids(DONE)
    for job in build_jobs(DOC, MODE):
        if job["unit_id"] in done:
            continue
        try:
            result = ocr(job)         # 调用方式以实际仓库为准
            save_json(result, f"out/{job['unit_id']}.json")
            append(DONE, job["unit_id"])
        except Exception as e:
            append(FAIL, {**job, "err": str(e)})

几点落地建议:unit_id 用页码加区块序号构成,不要用自增序号,否则插页后全部错位;批次大小以“单批能稳定跑完”为准,先用小批次跑通再逐步放大;失败记录单独落盘,重跑时只读 failed.jsonl 而不重跑已完成单元。版面区块切分依赖版面检测,如果检测结果本身不稳定,同一个 unit_id 在两次运行中可能对应不同内容,建议把切分结果单独落盘一次,后续复用同一份切分清单。

处理跨页表格与跨栏段落的拼接

拼接的顺序规则建议固定为:先按页码升序,同一页内按区块的纵坐标升序,同一行内的多个栏按横坐标升序。这样得到的顺序与常规阅读顺序一致,也便于人工核对。跨栏段落的回接,重点看上一栏最后一个区块的末行是否以句末标点结束、下一栏首块是否以续行特征开头(如小写字母、连接词、缺少缩进的首行)。

跨页表格的合并判定可以按以下条件逐条检查:

先把长文档按版面区块切好——Unlimited OCR 2 在复杂排版上的取舍
  • 下一页首个区块是否为表格,且列数与上一页表体一致。
  • 下一页首行是否重复了上一页的表头文字;重复表头通常是续表信号,合并时保留一份表头。
  • 上一页表末行是否存在明显的截断,例如单元格内文字无结尾标点且下一页首行紧接同列内容。
  • 两页之间是否夹有通栏标题或分节符;如果有,通常说明是新表而非续表,不应合并。

段落被截断时的判定要保守:如果无法确认是续接还是新段落,宁可保留为两段并在中间加标记,也不要直接拼成一句。拼接错误一旦被拼进正文,后续抽样很难再发现。

抽样核对拼接后的文本连续性

拼接完成后需要有独立的核对动作,而不是只看输出是否非空。建议按目录或条款编号定位抽查点:从目录里挑出跨页的章节和含表格的章节,在输出文本中找到对应标题,比较标题后的首段起止是否与原文一致,特别注意段落开头是否被截掉半句、段尾是否与下一段混在一起。

抽查时可以用这样的对照方式:取原文某一页的最后一个段落和下一页的第一个段落,在拼接结果里搜索这两段的衔接处,确认中间没有插入多余的表头、页码或页脚文字。对表格,比较行数与列数是否与原文一致,检查表头是否只出现一次。

抽查比例不需要固定,但至少覆盖三类位置:跨页表格所在页、双栏页面、通栏标题之后的首页。若这三类位置都出现衔接异常,说明拼接规则需要调整而不是继续扩大抽查;若只有个别位置异常,记录下来作为已知边界,避免在后续批次里重复踩坑。