Python读取docx表格中的合并单元格信息

一、问题背景

用pywin32运行速度太慢,我一般用docx库对Word表格进行处理。

注意docx库的安装库名是python-docx而不是docx,pip安装方法是:

pip install python-docx

1.1 常规写法

读取表格一般用这样的写法:

def ReadDocx(file):
    doc = docx.Document(file)
    data = []
    for table in doc.tables:
        data.append([])
        for row in table.rows:
            data[-1].append([])
            for cell in row.cells:
                data[-1][-1].append(cell.text)
    return data

1.2 奇怪问题

有的时候,读取的表格有一些离谱,比如,当表格是这样子的时候:
在这里插入图片描述
读取出来的结果却是:

>>> data
[[['苹果', '苹果', '香蕉'],
  ['阿猫', '阿狗', '阿狗']]]

问题出现在了哪里呢?docx库读取的表格,认为里面存在合并单元格,把它看成了2×3的表格。
在这里插入图片描述
如果是Excel,在读取表格的同时也可以获取合并单元格的信息,但是在docx库中不支持这样做的方法

在docx库中,读取合并单元格内的文本,获得的全部都是同一内容

但是通过单元格的文本来判断是否是合并单元格,显然是不合适的。因为在不同单元格里,文本也可以是“相同”的,所以这样的判断是不够“严谨”的。

二、发现线索

2.1 前途光明

但是在绝境之中我发现了一个线索。在合并单元格中,获取第一行的内容:

row = table.rows[0].cells

判断在表格中被合并的两个单元格,是否指向同一地址,返回结果是True

>>> row[0] is row[1]
True

这样可以解决同一行中合并单元格的判断,同理用table.columns[c].cells,也可以判断纵向单元格的合并情况。

2.2 道路曲折

但是,如果存在单元格,既有横向合并,也有纵向合并,比如row1是第一行,row2是第二行:
在这里插入图片描述
计算row1[0] is row2[1],却不能得到True的结果。

同样如果用table.cell(r, c)的方式进行访问:

>>> table.cell(0, 0) is table.cell(1, 1)
False

也不能得到预期的结果。

三、顺藤摸瓜

所以是为什么呢?

查看row的信息,输出结果为:

>>> row
<docx.table._Row object at 0x00000000039FDF98>

3.1 找源代码

找到docx.table库的_Row类,找到对应的源代码:

class _Row(Parented):
    ...
    @property
    def cells(self):
        return tuple(self.table.row_cells(self._index))

这里调用了一个row_cells方法,继续追踪,该方法出现在了docx.tableTable类中:

class Table(Parented):
    ...
    def row_cells(self, row_idx):
        column_count = self._column_count
        start = row_idx * column_count
        end = start + column_count
        return self._cells[start:end]

返回了一个self._cells值,并对数组进行切片,也就是这样的做法导致了第一行的cells和第二行的cells地址值无法互相确认。

在这里插入图片描述

继续查找_cells值定义的位置,依然是在Table类中的一个受@property保护的方法返回值:

class Table(Parented):
    ...
    @property
    def _cells(self):
        col_count = self._column_count
        cells = []
        for tc in self._tbl.iter_tcs():
            for grid_span_idx in range(tc.grid_span):
                if tc.vMerge == ST_Merge.CONTINUE:
                    cells.append(cells[-col_count])
                elif grid_span_idx > 0:
                    cells.append(cells[-1])
                else:
                    cells.append(_Cell(tc, self))
        return cells

3.1 分析原因

图穷匕见,看到这里就很容易理解为什么合并单元格里的单元会被认为是同一地址值的原因了。

当满足条件tc.vMerge == ST_Merge.CONTINUEgrid_span_idx > 0时,最终结果数组的cells都是直接添加了一个原本数组中已经含有的元素。而这导致了判断c1 is c2的时候,得到了True的返回值。

阅读源代码,也比较好理解,cells中的各个单元格在表中按照从上至下、从左至右的顺序排列。当tc.vMerge == ST_Merge.CONTINUE的时候,单元格纵向重复,grid_span_idx > 0时,单元格横向重复。

在合并单元格中,所有的引用的都是来自于合并区域的第一个“格”,那么通过c1 is c2就可以判断两个单元格是否是同属于一个区域的合并单元格。

3.3 取得所需

访问@property保护的变量实际上是运行了一次函数,所以将返回值赋值给临时变量,然后进行后续运算会更具有效率,并且代码具有可读性。

获取所有单元格列表表宽度、和单元格数目

doc = docx.Document(file)
for table in doc.tables:
    cells = table._cells
    cols = table._column_count
    length = len(cells)
    ...

四、破解办法

4.1 找到"合并"单元格

在返回的数组cells中,第一个出现重复的单元格,就是合并单元格区域的左上角的单元格;最后一次出现重复的单元格,就是右下角的单元格。

参考代码:

for i, cell in enumerate(cells):
    if cell in cells[:i]: # 如果该单元格不是在表中第一次出现则跳过
        continue
    for j in range(length - 1, 0, -1): # 倒序查找
        if cell is cells[j]: # 找到"相同"的单元格,如果没有"合并"单元格,则会倒序找到"自己"
            break
    if i != j: # 如果正序查找和倒序查找的索引值不同,则说明是"合并"单元格
        ...

这样可以判断出“合并”单元格的第一个“格子”,和最后一个“格子”。

4.2 转换行列信息

行数也是已知的,那么获取到合并单元格的“合并”区域就很容易知道了:

if i != j:
    r1, c1 = divmod(i, cols) # 合并单元格区域的"起始"位置,同时也是左上角单元格的行列坐标
    r2, c2 = divmod(j, cols) # 合并单元格区域的"结束"位置,同时也是右下角单元格的行列信息
    merge = r1, r2 + 1, c1, c2 + 1 # 转为xlrd风格的单元格合并行列信息

最后,我按照xlrd库的风格,将合并单元格的信息整理为xlrd库中的顺序。

4.3 自定义规则合并

有了合并单元格的信息,再对单元格合并,就很简单了。

根据需要,可以实现纵向重复横向重复横向重复区域缩紧,或组合的方式进行“合并”:

def MergeCell(data, merge, merge_x=True, merge_y=True, strip_x=False):
    data2 = []
    for sheet_data, sheet_merge in zip(data, merge):
        # merge cell
        for r1, r2, c1, c2 in sheet_merge:
            for r in range(r1, r2):
                for c in range(c1, c2):
                    if (not merge_x and c > c1) or (not merge_y and r > r1):
                        sheet_data[r][c] = None if strip_x else ''
                    else:
                        sheet_data[r][c] = sheet_data[r1][c1]
        # strip x
        if strip_x:
            sheet_data = [[cell for cell in row if cell is not None] for row in sheet_data]
        data2.append(sheet_data)
    return data2

附、读取xls文件中的合并单元格

另外我前面提到多次的读取Excel的xlrd库,这个库的运行速度比pywin32要快很多,我常用的读取和清洗方法也分享一下:

def ReadExcel(file):
    # only ".xls" type contain merge_info
    xls = xlrd.open_workbook(file, formatting_info=True)
    data = []
    for sheet in xls.sheets():
        sheet_name = sheet.name
        sheet_data = []
        for row in range(sheet.nrows):
            rows = sheet.row_values(row)
            for c, cell in enumerate(rows):
                if isinstance(cell, float):
                    if cell.is_integer():
                        rows[c] = str(int(cell))
                    else:
                        rows[c] = str(cell)
            sheet_data.append(rows)
        data.append(sheet_data)
    merge = [sheet.merged_cells for sheet in xls.sheets()]
    return data, merge

但是需要注意,xlrd库只有读取xls格式的Excel文件才有合并单元格的信息。若xlsx格式的表格也要读取合并单元格的信息,可以先将文件转为xls的格式后再做读取

  • 18
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值