openpyxlで書き込み処理を行うときはlxmlと一緒に使うと高速化・省メモリー化できる

# はじめに

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モジュールをインストールするだけで有効化されます**。

# 性能測定

## 測定コード（時間）

```openpyxl_test.py
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()
```

## 結果（時間）

```lxmlなし.txt:
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")
```


```lxmlあり.txt:
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秒** です。


## 測定コード（メモリー）

```openpyxl_test.py
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()
```

## 結果（メモリー）


```lxmlなし.txt:
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")
```


```lxmlあり.txt:
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使っていた](https://stackoverflow.com/questions/41043672/openpyxl-writing-large-file-memory-issue) ものが、 [lxmlありにすると40.1MBになった](https://stackoverflow.com/questions/41043672/openpyxl-writing-large-file-memory-issue#comment69296269_41043818) との報告もあります。

# 結論

openpyxlで書き込み処理する場合は何も考えずに `pip install lxml` しておこう。