目录
一、问题背景1.1 常规写法1.2 奇怪问题二、发现线索2.1 前途光明2.2 道路曲折三、顺藤摸瓜3.1 找源代码3.1 分析原因3.3 取得所需四、破解办法4.1 找到"合并"单元格4.2 转换行列信息4.3 自定义规则合并附、读取xls文件中的合并单元格一、问题背景
用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):...@propertydef cells(self):return tuple(self.table.row_cells(self._index))
这里调用了一个row_cells
方法,继续追踪,该方法出现在了docx.table
的Table
类中:
class Table(Parented):...def row_cells(self, row_idx):column_count = self._column_countstart = row_idx * column_countend = start + column_countreturn self._cells[start:end]
返回了一个self._cells
值,并对数组进行切片,也就是这样的做法导致了第一行的cells
和第二行的cells
地址值无法互相确认。
继续查找_cells
值定义的位置,依然是在Table
类中的一个受@property
保护的方法返回值:
class Table(Parented):...@propertydef _cells(self):col_count = self._column_countcells = []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.CONTINUE
和grid_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._cellscols = table._column_countlength = len(cells)...
四、破解办法
4.1 找到"合并"单元格
在返回的数组cells
中,第一个出现重复的单元格,就是合并单元格区域的左上角的单元格;最后一次出现重复的单元格,就是右下角的单元格。
参考代码:
for i, cell in enumerate(cells):if cell in cells[:i]: # 如果该单元格不是在表中第一次出现则跳过continuefor j in range(length - 1, 0, -1): # 倒序查找if cell is cells[j]: # 找到"相同"的单元格,如果没有"合并"单元格,则会倒序找到"自己"breakif 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 cellfor 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 xif 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_infoxls = xlrd.open_workbook(file, formatting_info=True)data = []for sheet in xls.sheets():sheet_name = sheet.namesheet_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的格式后再做读取。