pdf2docx开发概要:解析表格

发布于:2020-08-15 | 分类:process automation


PDF中没有语义上的表格的概念,所以需要根据外观上的表格,即边框线(Stroke类型的形状)围成的区域,来识别语义上的表格。另一方面,不借助文本框的话Word不支持浮动布局,表现为一段文字默认占据一整行,所以需要表格来进行辅助布局,例如多列文本。于是,pdf2docx考虑两类表格:

  • 显式表格(Lattice Table):有边线,组织结构化的内容
  • 隐式表格(Stream Table):无边线,既可能用于组织结构化的内容(隐藏边框的表格),也可能是为了页面布局的需要

解析显式表格

从边线出发,确定表格结构->单元格属性->单元格内容

  • 以是否相交为依据分组Stroke元素,每一组即为潜在的表格区域内的边框线(table border),根据潜在表格区域获取Fill单元格背景(cell shading)。

  • 基于边框线确定表格结构

    • 判断是否存在外边框线,没有则加上假想的外边框线(白色、宽度0)。

    • 将边框线按横、纵分组得到行间隔线和列间隔线,横纵间隔线的交点组成了初始的未考虑合并单元格的表格结构。

    • 对于每一行,检测一条假想水平线与列边线的交点:不存在交点的位置即发生了行方向的合并单元格;同理检测列方向的合并单元格。

  • 基于表格结构确定单元格属性

    • 以左上角单元格表示合并单元格区域,被合并的单元格初始化为空Cell()

    • 根据每一个单元格的区域(合并单元格则考虑所有合并区域)确定边框的四个矩形:从而直接得到各个边框的宽度和颜色;并根据单元格区域检测Fill类型的矩形,存在则得到单元格背景色。

  • 基于表格结构确定单元格包含的文本

    • 将包含于单元格区域的文本/图片加入到相应单元格。有时文本块的划分与单元格区域并不严格匹配,需要深入到block>line>span>char级别拆分原始文本块。

    • 如果两个文本块物理上处于同一行,则合并为一个大文本块。目的是创建Word文档时保证正确的位置关系,因为每一个文本块将被作为独立的段落。

得到表格的位置及属性后,需要考虑能否在python-docx中重建。好在或者直接使用API或者借助openxml,以下操作都被python-docx支持:

  • 表格缩进 1
  • 合并单元格 cell.merge(other_cell) 2
  • 行高row.height及单元格宽度cell.width 2
  • 单元格内边距 3
  • 单元格边框颜色和宽度 4
  • 单元格背景色 5

解析隐式表格

实际上很难完美重建语义上的无边线表格,pdf2docx退而求其次,只是为了保证位置关系,从“看起来一样”的角度解析隐式表格。基本思路:

从文本块出发,确定分隔线即边线,接下来与显式表格的处理步骤一致

  • 基于文本块位置关系检测潜在的隐式表格区域

    • 如果同一文本块内line级别元素竖直方向有重叠但物理上不是严格处于同一行,它将被视为表格的行。因为docx重建时,普通段落无法保证这样的位置关系,所以必须加入潜在表格区域,后续进一步划分为不同列。

    • 如果同一文本块内line级别元素出现多次且相邻之间间隔一定距离,各个line将被视为潜在的单元格,因此整个文本块被加入潜在表格区域。

    • 同理,如果相邻两个文本块物理上处于同一行(竖直方向有重叠),它们也将被视为表格区域。

  • 基于以上文本块的line级别的元素递归检测边界线

    • 按列分组,相邻两组的中间线即为列边界。因为列的角度有利于docx重建,行方向会因为不同文本块另起一段而破坏位置关系。

    • 每一组内按行分组,相邻两组的中间线即为行边界。

    • 重复以上步骤,直到每组只有单一文本块。

如上得到隐式表格区域的假想边框线,接下来按照显式表格流程处理即可。

以上将相邻两组的中间线作为列边界的处理方式简单快捷,但不足之处是不同行中的各列边界参差不齐,使表格结构复杂化。另外,还有一类半显式半隐式表格,即在隐式表格的基础上,又显式给出了部分表格线,例如常见的三线表。这些都需要在上述隐式表格解析的基础上进行表格线的优化,具体参考下文:

pdf2docx开发概要:对齐隐式表格线

数据结构

表格块TableBlock继承了块元素Block的结构(详见PDF提取),同时新增了表征表格结构的RowCell。其中Cell是布局级别的对象(Layout),嵌套了下一级的块元素和形状元素,如此递归,直到块元素中不再包含表格,即只有表征文本和图片的TextBlock

{
    'type': int, # 3-explicit table; 4-implicit table
    'bbox': (float, float, float, float),
    ..., # some spacing properties same with Block
    "rows": [
        {
            'bbox': (float, float, float, float),
            'height': float,
            'cells': [
                {
                    'bbox': (float, float, float, float),
                    'bg-color':  int,
                    'border-color': (int, int, int, int), # top, right, bootom, left
                    'border-width': (float, float, float, float),
                    'merged-cells': (int, int),
                    'blocks': [
                        { # text/image blocks contained in current cell },
                        { ... }
                    ],
                    'shapes': [
                        { # shapes contained in current cell }
                        { ... }
                    ]
                },
                ... # more cells
            ]

        },
        ... # more rows
    ]

}