NAKED WETWARE

「わがままになるのが怖い奴に宇宙は拓けねェさ」

【Python/openpyxl】openpyxlで遭遇したMemoryErrorへの対処方法

TL;DR

  • openpyxlはxlsxファイルをメモリ内に展開してあれこれする
  • 書き込み専用モードにすれば、メモリへの展開をいい感じにしてくれる
  • とりまwb = openpyxl.Workbook(write_only=True)を使っとけばOK

環境

Tools Versions
openpyxl 3.0.3
Python 3.6.9

発端

tsvファイルをxlsxファイルに変換するPythonスクリプトを書いた。

実際に動かしてみると、数十万件のデータを扱うと MemoryError 例外が投げられる。

どうやら、一般的なopenpyxlの使い方だと、大量のデータを扱えないようだ。

結果

元処理のイメージは以下。

import openpyxl

def read_date(file_path):
    # ... ファイルからデータを読み込む処理
    # ... 二次元リストで返却
    return data

if __name__ == '__main__':

    # ...

    input_data = read_data(in_file_path)
    wb = openpyxl.Workbook()                          # ☆
    ws = wb.create_sheet(sheet_name)

    for row in input_data:
        data = []
        for col in row:
            cell = openpyxl.cell.Cell(ws, value=col)
            data.append(cell)
        ws.append(data)

    wb.save(out_file_path)
    wb.close()

大量のデータを扱えるように改修したイメージは以下。

lxmlのパッケージが必要みたいだけど、エラーや警告が出たら入れればいいと思う。

import openpyxl

def read_date(file_path):
    # ... ファイルからデータを読み込む処理
    # ... 二次元リストで返却
    return data

if __name__ == '__main__':

    # ...

    input_data = read_data(in_file_path)
    wb = openpyxl.Workbook(write_only=True)                          # ☆ここを書き換えた
    ws = wb.create_sheet(sheet_name)

    for row in input_data:
        data = []
        for col in row:
            cell = openpyxl.cell.Cell(ws, value=col)
            data.append(cell)
        ws.append(data)

    wb.save(out_file_path)
    wb.close()

ここ を読むと、どうやら書き込み専用でWorkbookを開けば問題は解決する模様。

公式ドキュメント によれば、write_only=Trueにすれば、

It is able to export unlimited amount of data (even more than Excel can handle actually), while keeping memory usage under 10Mb.

メモリ使用量を10MB以下に抑えながら、上限なくデータを出力できます。(意訳)

らしい。

他にも公式ドキュメントに記載されている注意点としては以下。

(書き込み専用で)開いたxlsxファイルは、ひとつもシートを持ってないのでcreate_sheetメソッドで作ってやる必要がある。

試してみる。

import openpyxl

wb = openpyxl.Workbook(write_only=True)

print(wb.sheetnames)
# => []

データの書き込みに使えるのは、rowsのappendメソッドだけ。他は使えない。

>>> import openpyxl
>>> wb = openpyxl.Workbook(write_only=True)
>>> ws = wb.create_sheet('hoge')
>>> print(wb.sheetnames)
['hoge']
>>> ws.append([1,2,3])
>>> ws.cell(row=2, col=1).value = 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'WriteOnlyWorksheet' object has no attribute 'cell'
>>>
>>> wb_general = openpyxl.Workbook()
>>> ws_general = wb_general.create_sheet('general')
>>> print(type(ws_general).__name__)
Worksheet
>>> print(type(ws).__name__)
WriteOnlyWorksheet

作成したwsインスタンスcellなんてアトリビュートねーですけど、と怒られる。

write_only=Trueを付けるかどうかで、そもそも作成されるWorksheetのクラスが違うらしい。

(書き込み専用で)開いたxlsxファイルは、1回しかsaveメソッドは呼べない。saveメソッドを呼んだ後に再度saveメソッド呼んだり、Worksheetに変更を加えると、例外を飛ばします。

>>> import openpyxl
>>> wb = openpyxl.Workbook(write_only=True)
>>> ws = wb.create_sheet('hoge')
>>> print(wb.sheetnames)
['hoge']
>>> ws.append([1,2,3])
>>> wb.save('hoge.xlsx')
>>> wb.save('hoge.xlsx')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  ... 中略 ...
FileNotFoundError: [Errno 2] No such file or directory: 

発展的話題

公式ドキュメント にある、

It is able to export unlimited amount of data (even more than Excel can handle actually), while keeping memory usage under 10Mb.

メモリ使用量を10MB以下に抑えながら、上限なくデータを出力できます。(意訳)

を信じるならば、Excelでの上限である、

機能 最大数
ワークシートの行と列の合計数 1,048,576 行、16,384 列

(Excel の仕様と制限より作成)

を超えて、Worksheetにデータを書き込めるということでしょうか……。

書き込めたとしても、そのWorkbookはExcelでは二度と開けない気がする。

(今度試してみましょう)