はじめに
PythonでExcel形式のファイルを作るとき、openpyxlを使うことが多いと思います。
しかし、そのままのopenpyxlは遅い&メモリー効率が悪いという欠点があります。
解決方法
上述の欠点はデフォルトのXMLモジュールの性能が原因です。
実は、openpyxlはlxmlというより性能の良いXMLモジュールの使用にも対応しています。
https://foss.heptapod.net/openpyxl/openpyxl/-/blob/branch/3.0/openpyxl/cell/_writer.py#L72
lxmlはデフォルトのモジュールより高速かつ省メモリーに動作するため、lxmlの使用を有効化するとopenpyxlの書き込み処理の高速化&省メモリー化を期待できます。
lxmlの使用の有効化
openpyxlはlxmlモジュールを自動で読み込むため、
(https://foss.heptapod.net/openpyxl/openpyxl/-/blob/branch/3.0/openpyxl/xml/__init__.py#L8)
pip install lxml
してlxmlモジュールをインストールするだけで有効化されます。
性能測定
測定コード(時間)
from line_profiler import LineProfiler
import memory_profiler
import openpyxl
def time():
wb = openpyxl.Workbook(write_only=True)
ws = wb.create_sheet()
for _ in range(10000):
ws.append(['%d' % i for i in range(100)])
wb.save("test.xlsx")
prof = LineProfiler()
prof.add_function(time)
prof.runcall(time)
prof.print_stats()
結果(時間)
Timer unit: 1e-06 s
Total time: 61.3903 s
File: /xxx/openpyxl_test.py
Function: time at line 6
Line # Hits Time Per Hit % Time Line Contents
==============================================================
6 def time():
7 1 1633.0 1633.0 0.0 wb = openpyxl.Workbook(write_only=True)
8 1 724.0 724.0 0.0 ws = wb.create_sheet()
9 10001 8888.0 0.9 0.0 for _ in range(10000):
10 10000 20477116.0 2047.7 33.4 ws.append(['%d' % i for i in range(100)])
11 1 40901916.0 40901916.0 66.6 wb.save("test.xlsx")
Timer unit: 1e-06 s
Total time: 27.064 s
File: /xxx/openpyxl_test.py
Function: time at line 6
Line # Hits Time Per Hit % Time Line Contents
==============================================================
6 def time():
7 1 1612.0 1612.0 0.0 wb = openpyxl.Workbook(write_only=True)
8 1 759.0 759.0 0.0 ws = wb.create_sheet()
9 10001 10565.0 1.1 0.0 for _ in range(10000):
10 10000 26444344.0 2644.4 97.7 ws.append(['%d' % i for i in range(100)])
11 1 606738.0 606738.0 2.2 wb.save("test.xlsx")
wb.save("test".xml)
が、lxmlなしの場合は40.9秒かかっているのに対して、lxmlありの場合は 0.6秒 です。
測定コード(メモリー)
from line_profiler import LineProfiler
import memory_profiler
import openpyxl
@memory_profiler.profile
def memory():
wb = openpyxl.Workbook(write_only=True)
ws = wb.create_sheet()
for _ in range(10000):
ws.append(['%d' % i for i in range(100)])
wb.save("test.xlsx")
memory()
結果(メモリー)
Filename: /xxx/openpyxl_test.py
Line # Mem usage Increment Occurences Line Contents
============================================================
5 46.6 MiB 46.6 MiB 1 @memory_profiler.profile
6 def memory():
7 46.6 MiB 0.0 MiB 1 wb = openpyxl.Workbook(write_only=True)
8 46.6 MiB 0.0 MiB 1 ws = wb.create_sheet()
9 767.4 MiB 0.6 MiB 10001 for _ in range(10000):
10 767.4 MiB 720.1 MiB 1030000 ws.append(['%d' % i for i in range(100)])
11 102.6 MiB -664.8 MiB 1 wb.save("test.xlsx")
Filename: /xxx/openpyxl_test.py
Line # Mem usage Increment Occurences Line Contents
============================================================
5 49.0 MiB 49.0 MiB 1 @memory_profiler.profile
6 def memory():
7 49.0 MiB 0.0 MiB 1 wb = openpyxl.Workbook(write_only=True)
8 49.0 MiB 0.0 MiB 1 ws = wb.create_sheet()
9 49.1 MiB 0.0 MiB 10001 for _ in range(10000):
10 49.1 MiB 0.1 MiB 1030000 ws.append(['%d' % i for i in range(100)])
11 49.7 MiB 0.5 MiB 1 wb.save("test.xlsx")
lxmlなしの場合は、ループ中に764.4-46.6=717.8MiB、 wb.save("test".xml)
時点でも102.6-46.6=56MiB使っているのに対し、
lxmlなしの場合は、 最大でも49.7-49.0=0.7MiB です。
ほか
StackOverflowのこの質問によるとlxmlなしでメモリーを17.2GB使っていた ものが、 lxmlありにすると40.1MBになった との報告もあります。
結論
openpyxlで書き込み処理する場合は何も考えずに pip install lxml
しておこう。