openpyxl性能优化实战小记
最近做了个Excel文件处理小工具给一个项目使用,但是一个Excel文件要处理大概两分钟,我觉得这也太慢了,虽然电脑性能一般。因为就是读一个Excel文件,然后写一个Excel文件,也不涉及什么数据库、网络之类的,应该很快才对。 于是,我让AI去做性能优化,毕竟小工具的代码也是Vibe Coding出来的。起初我并没有太在意,不过,当我发现AI为了做性能优化开始修改openpyxl的源码,并且效果很显著时,我觉得有必要看一下了,学习一下。 学习了下本次的性能优化过程,整理了本次性能优化的改动点,然后就有了本篇文章。
用 cProfile 找热点
import cProfile, pstats, io
pr = cProfile.Profile()
pr.enable()
# ... 业务代码 ...
pr.disable()
s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats("cumulative").print_stats(30)
print(s.getvalue())
表头
ncalls tottime percall cumtime percall filename:lineno(function)
分别表示:函数调用次数,函数自身消耗时间,平均每次调用函数自身耗时,函数从进入到退出耗时,平均每次调用耗时,函数所在文件行号与函数名。
跑了一下,性能瓶颈主要有:样式复制、工作表读取。
性能优化
样式复制优化
之前跨工作簿复制样式时,是采用逐属性深拷贝的方式,这样非常消耗性能。更好的做法是:先把样式注册到工作簿,然后设置样式。
def register_style_to_wb(src_cell, dst_wb):
"""把 src 的样式注册到 dst wb,返回 dst wb 内的 StyleArray。"""
# ...
template_styles = [
register_style_to_wb(template_ws.cell(row=3, column=c), out_wb) for c in range(1, N_COLS + 1)
]
# ...
out_ws.cell(row=out_r, column=c)._style = template_styles[c - 1]
只读取需要的Sheet
用户上传的Excel文件有一些程序处理时并不需要的Sheet,但openpyxl会解析所有Sheet。可以给load_workbook加个only_sheets参数,只加载需要的Sheet。
# openpyxl/reader/excel.py
def load_workbook(filename, ..., only_sheets=None):
# ...
屏蔽透视表
openpyxl的WorkbookParser.pivot_caches是个@property,每次访问都会触发全部透视表解析。但是我们的场景用不上不需要透视表,因此直接屏蔽即可。
# openpyxl/reader/workbook.py
@property
def pivot_caches(self):
# ...
启用 lxml 后端
openpyxl默认用Python标准库的xml.etree.ElementTree写 XML。安装lxml后,只需设一个环境变量就切到lxml后端,写入速度会有一些提升:
import os
os.environ.setdefault("OPENPYXL_LXML", "True") # 必须在 import openpyxl 之前
import openpyxl
修复非规范属性BUG
由Excel/WPS生成的Excel文件有时会有一些不符合OOXML规范里的格式属性,如:defaultColWidthPt,这将会导致openpyxl解析出错:
TypeError: SheetFormatProperties.__init__() got an unexpected keyword argument 'defaultColWidthPt'
这个问题长期存在,openpyxl应该并不打算修复。解决方案就是在SheetFormatProperties里显式接受这些字段:
# openpyxl/worksheet/dimensions.py、
class SheetFormatProperties(Serialisable):
# ...
defaultColWidthPt = Float(allow_none=True) # 新增
补丁
直接修改openpyxl源码不太好,因为配环境时还需要替换openpyxl的源文件,且不利于库的升级。所以,可以对openpyxl进行patch,我做了一份供参考:
使用时,只需要import一下就会应用patch了。
最后,对比下优化前后的执行效果:
