相关文件
想学Python的小伙伴可以关注小编的公众号【Python日志】 有很多的资源可以白嫖的哈,不定时会更新一下Python的小知识的哈!!
前言
把 Excel 转换成 Word 格式的文档大家肯定都干过吧?是不是十分的枯燥无味而且浪费时间? 你可能在想:用 Excel 、Word 转换器啊!是啊,对于普通的 Excel 文档来说,这可能是个好方法。但是,如果是一个数据很多并且存在着很多人为设定的查看规则呢?普通的转换器碰上这种高度定制化的 Excel 就显得无能为力了。
项目描述
用文字表述不清,各位看图吧。
看懂要怎么看这张表了嘛…… 整张表分三个 Sheet ,每个 Sheet 下存储一种数据。而这三个 Sheet 中的数据是相互关联的。例如,第一张图里的设备编号是引用的第二张图里的设备,第一张图里每一个检测对象都属于 A 列的大类别编号所对应的大类别…… 不仅如此,整张表还有层级关系。对于 Sheet 1,有着三级或更多的层级嵌套。别说机器了,就是人也不能一下子明白这张表咋看。 这还不够,转换出来的 Word 还有四种不同的形式,虽然有两份是大同小异的,但剩下的可都不太一样。 然而,这表的数据量还大到离谱 —— 转换出来的 Word 文档最少也得一百页,多的甚至上千页。这要是人干,恐怕肝是要爆啊……
开发历程
读取 Excel
既然接下了这个活,那肯定得干啊。先看看怎么用 Python 读写 Excel 吧。 一开始以为 openpyxl 就可以解决问题,但仔细一看这是 Excel 2003 的工作簿…… 没办法,只能转用 xlrd 和 xlwt 。 读取的过程还是比较愉快的,但期间我还是掉了不少头发…… 首先,是怎么设计这个存储结构。因为最终的文档是以大类别作为一级标题的,且嵌套等级太多,我构思了好久才搞出来这个读取器。 最终,我把读取出来的数据转换为 JSON 文件,方便后续生成器进行二次读取。
生成 Word
现在最困难的事情到了。我在网上搜索了一圈后,发现了 python-docx 这个 Word 操作库。 幸运的是,python-docx 支持 Word 内表格的各种操作,没有必要再自己鼓捣了。 在看完不那么好懂的英文文档后,我一气呵成,做出了生成器的第一个版本。 我兴奋的运行了起来,在跑了一个小时之后,我发现了事情的不对劲:为啥跑这么久啊喂! 果断加上进度条再次运行,结果: 好家伙七个小时还让不让人活啦!不行,必须优化!
多进程
根据我爬虫的思路,一旦什么东西跑的太慢,那就用多进程 / 多线程优化! 研究库的底层实现 在有了多进程的失败过后,我又查看了一遍日志。而这次,我发现了一个很重要的信息! 每次执行一开始的时候,速度是非常快的,但是越往后,速度就越慢。而且变慢的速度是飞速的。 这就让我想起了指数爆炸。难道有一个深层递归在暗中执行? 我翻遍了官方文档,没有发现一句有关性能的提示。我决定深入看看 python-docx 的底层实现。 在 table.py 中找到 Table 类很容易,于是我就挨个函数地看源代码。这一看不要紧,关键是很多我认为是 O(1) 的方法,实际实现其实是 O(n) 的!怪不得慢。 但是那也不应该这么慢呀。继续往下看,一切都是那么的正常,直到我看到了 _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
不想多说啥了,找到原因了。这个如此频繁使用的函数,复杂度竟然是 O(n^2) 的!小规模数据还不要紧,数据量一大,几万几万的数据写到表格里,_cells 函数的遍历次数也越来越多,执行时间也越来越长。 一个 8000 行的表格数据,写入到 Word 里可能变成 10000 行左右。这意味着什么?这意味着这段代码将会遍历整个表格!10 * 10000 的数据可能看起来没有那么可怕,但运行次数一多了,时间耗费就会非常大。 其实库的开发者这样做也是为了保证 merged cells 的正确性。但是,就是太耗时了…… 知道了问题的源头,我就去 GitHub 上寻找答案。最终,我在 python-docx 的官方仓库中找到了 这个 Issue 。Issue 中有人说,一个 150 * 8 的表格需要 60 秒来创建。看来找对地方了。幸运的是,Issue 的发起者给出了一个 可行的解决方案 。但是,这个临时方案并不支持有合并单元格需求的表格。 很明显,我这个表格用不了这个方案。但是,既然有了一个样例,我可以照猫画虎嘛! 于是,我加上了我自己做的临时缓存。一开始的效果还不错,速度保持在每秒处理 5 行左右。但是跑了半个小时之后,速度又开始急速下降:每 5 秒处理一行。
这说明什么?
- 我的缓存方案有效
- 它还不够有效
- 我需要更有效的方案
继续优化
仔细思考一下就能知道,如果我们不能减少执行这个循环的次数,那就缩小循环范围呀!缩小循环范围,就是让我们的表格变小呀! 实现方式很简单,就是每隔一会就切断当前表格,建立一个新的。 但是,因为生成的表格中有很多合并的单元格,所以很难直接分开。最终,在实现难度和速度的综合考量下,我选择了在每个合并长度最大的单元格合并完成时拆分表格。 再次运行程序,哇!简直神速!原来跑一天不完的程序,现在只需要十分钟就可跑完! 最终,我得到了一个大小高达 674 KB 的 Word 文件(页数太多电脑短时间加载不完了)!
Victory!
UI 界面
总不能给用户用命令行吧…… 我用 Flask 快速搭建了一个 API ,并部署到了云平台上可供前端调用。 前端方面我使用了 React + Next.js + Mantine 的技术架构,简单做了一个前端 UI 。
于是,这个项目的搭建就到此告一段落了。
想学Python的小伙伴可以关注小编的公众号【Python日志】
有很多的资源可以白嫖的哈,不定时会更新一下Python的小知识的哈!!
|