这次的需求是解析一个用表格布局的word的文件内的信息;word模板采用了两层表格的样式,因此采用python-docx包来进行解析,打算先把表格的内容按行列先读取出来。最早版本的代码如下:
def table_nested_parsing(cell, tb, cells):
try:
cell_table = tb.cell(cell[0], cell[1])
_table_list = []
if len(cell_table.tables) > 0:
block = cell_table.tables[0]
for row in block.rows: # 读每行
row_content = []
for cell in row.cells: # 读一行中的所有单元格
c = cell.text
row_content.append(c)
_table_list.append(row_content)
return _table_list
except Exception as e:
print(e)
return '-1'
def doc_parsing(doc):
doc_list = []
for doc_part in doc.tables:
tb1 = doc_part
# 单线程模式
for row in range(len(tb1.rows)):
for col in range(len(tb1.columns)):
tb = table_nested_parsing((row, col), tb1, cells)
if tb == '-1':
return result
if tb is None:
continue
if tb not in result:
result.append(tb)
if __name__ == '__main__':
docx = Document('./test.docx')
result = doc_parsing(docx)
print(result)
可以看到doc_parsing
中对最外层的表格进行遍历,并且将每个单元格作为一个表格对象,调用table_nested_parsing
来解析其中的数据,并将行列的数据返回;由于python-docx在处理拆分合并的表格时,会按最多列的情况来解析导致数据重复,因此在添加表格数据到result
之前做了去重
这段代码可以成功的按需求解析出表格中的数据,但有一个问题,解析一份文档需要耗时 Total time: 225.461 s 。对于需要批量处理的情况下,效率太低,于是开始着手优化。
对主要的处理函数doc_parsing
函数打上@profile
注释,并使用kernprof运行
> kernprof -l -v ./test.py
......此处忽略输出
Total time: 227.168 s
Function: doc_parsing at line 34
Line # Hits Time Per Hit % Time Line Contents
==============================================================
34 @profile
35 def doc_parsing(doc):
36 1 7.0 7.0 0.0 doc_list = []
37 2 198.0 99.0 0.0 for doc_part in doc.tables:
38 2 3.0 1.5 0.0 tb1 = doc_part
39 # 单线程模式
40 121 339.0 2.8 0.0 for row in range(len(tb1.rows)):
41 2967 6814.0 2.3 0.0 for col in range(len(tb1.columns)):
42 2848 227154137.0 79759.2 100.0 tb = table_nested_parsing((row, col), tb1)
43 2848 2147.0 0.8 0.0 if tb == '-1':
44 1 1.0 1.0 0.0 return result
45 2847 1251.0 0.4 0.0 if tb is None:
46 2504 1010.0 0.4 0.0 continue
47 343 1863.0 5.4 0.0 if tb not in result:
48 33 22.0 0.7 0.0 result.append(tb)
可以看到table_nested_parsing
方法占用了几乎全部的运行时间,而且这个方法调用了2848次,经过和实际的word比对,发现第一层表格完全没有那么多的单元格,于是第一个思路就是降低无效的table_nested_parsing
方法调用。
于是,在下方处理结果数据的地方,打印出存在有效数据的行和列数,发现当行数>=1时,所有有效数据都存在第14列上 ,于是加上对应的剪枝语句,再次运行
Total time: 14.5656 s
Function: doc_parsing at line 34
Line # Hits Time Per Hit % Time Line Contents
==============================================================
34 @profile
35 def doc_parsing(doc):
36 1 6.0 6.0 0.0 doc_list = []
37 2 125.0 62.5 0.0 for doc_part in doc.tables:
38 2 1.0 0.5 0.0 tb1 = doc_part
39 # 单线程模式
40 121 232.0 1.9 0.0 for row in range(len(tb1.rows)):
41 2967 6806.0 2.3 0.0 for col in range(len(tb1.columns)):
42 2848 1234.0 0.4 0.0 if row > 1 and col is not 14: # 去除无效访问
43 2682 937.0 0.3 0.0 continue
44 166 14555643.0 87684.6 99.9 tb = table_nested_parsing((row, col), tb1)
45 166 288.0 1.7 0.0 if tb == '-1':
46 1 1.0 1.0 0.0 return result
47 165 82.0 0.5 0.0 if tb is None:
48 112 48.0 0.4 0.0 continue
49 53 203.0 3.8 0.0 if tb not in result:
50 33 22.0 0.7 0.0 result.append(tb)
可以发现时间已经由原来的227.168 s
缩短到14.5656 s
,效率提升了16倍;同时table_nested_parsing
方法的调用次数也减少到了166次,基本符合实际数据的结构。但是99%的时间消耗还是在table_nested_parsing
方法上。
对子单元格的处理函数table_nested_parsing
函数打上@profile
注释,并使用kernprof运行
Total time: 14.2929 s
Function: table_nested_parsing at line 16
Line # Hits Time Per Hit % Time Line Contents
==============================================================
16 @profile
17 def table_nested_parsing(cell, tb):
18 166 108.0 0.7 0.0 try:
19 166 13428371.0 80893.8 94.0 cell_table = tb.cell(cell[0], cell[1])
20 165 317.0 1.9 0.0 _table_list = []
21 165 6587.0 39.9 0.0 if len(cell_table.tables) > 0:
22 53 996.0 18.8 0.0 block = cell_table.tables[0]
23 316 2809.0 8.9 0.0 for row in block.rows: # 读每行
24 263 128.0 0.5 0.0 row_content = []
25 4676 669253.0 143.1 4.7 for cell in row.cells: # 读一行中的所有单元格
26 4413 180856.0 41.0 1.3 c = cell.text
27 4413 3193.0 0.7 0.0 row_content.append(c)
28 263 145.0 0.6 0.0 _table_list.append(row_content)
29 53 25.0 0.5 0.0 return _table_list
30 1 2.0 2.0 0.0 except Exception as e:
31 1 65.0 65.0 0.0 print(e)
32 1 1.0 1.0 0.0 return '-1'
发现94%的时间花在了Table对象获取单元格的过程,于是去看cell()
方法的实现
def cell(self, row_idx, col_idx):
"""
Return |_Cell| instance correponding to table cell at *row_idx*,
*col_idx* intersection, where (0, 0) is the top, left-most cell.
"""
cell_idx = col_idx + (row_idx * self._column_count)
return self._cells[cell_idx]
看起来貌似只是一个简单的从数组中取指定值的操作,应该不需要那么长的时间,于是继续查看self._cells
这个数组的处理方式,发现这是个用@property装饰器的方法
def _cells(self):
"""
A sequence of |_Cell| objects, one for each cell of the layout grid.
If the table contains a span, one or more |_Cell| object references
are repeated.
"""
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
原来每次调用cell()
都会导致这个_cells()
重算一遍,于是考虑把_cells()
的结果缓存下来,避免每次调用都重取
def doc_parsing(doc):
...
cells = tb1._cells # 缓存这张表格的单元格列表
...
def table_nested_parsing(cell, tb, cell): # 将缓存的列表传入
# cell_table = tb.cell(cell[0], cell[1])
cell_idx = cell[1] + (cell[0] * tb._column_count) # 使用原.cell()方法的方式计算idx
cell_table = cells[cell_idx] # 直接取
再运行测试
Total time: 0.877686 s
Function: table_nested_parsing at line 16
Line # Hits Time Per Hit % Time Line Contents
==============================================================
16 @profile
17 def table_nested_parsing(cell, tb, cells):
18 166 109.0 0.7 0.0 try:
19 # cell_table = tb.cell(cell[0], cell[1])
20 166 5482.0 33.0 0.6 cell_idx = cell[1] + (cell[0] * tb._column_count)
21 166 148.0 0.9 0.0 cell_table = cells[cell_idx]
22 165 90.0 0.5 0.0 _table_list = []
23 165 3357.0 20.3 0.4 if len(cell_table.tables) > 0:
24 53 919.0 17.3 0.1 block = cell_table.tables[0]
25 316 2244.0 7.1 0.3 for row in block.rows: # 读每行
26 263 126.0 0.5 0.0 row_content = []
27 4676 686406.0 146.8 78.2 for cell in row.cells: # 读一行中的所有单元格
28 4413 175236.0 39.7 20.0 c = cell.text
29 4413 3317.0 0.8 0.4 row_content.append(c)
30 263 172.0 0.7 0.0 _table_list.append(row_content)
31 53 26.0 0.5 0.0 return _table_list
32 1 1.0 1.0 0.0 except Exception as e:
33 1 51.0 51.0 0.0 print(e)
34 1 2.0 2.0 0.0 return '-1'
效果立竿见影,总的运行时间缩短到了0.87s,已经基本能满足日常使用了。综合下来,剪枝+缓存让这个脚本的效率提高了约256倍。