私たち製造ビジネステクノロジー部では、製造業に関するエンドユーザや事業会社さんに対して、システムやサービスを事業会社さんと一緒に作成・運用しています。そんな製造業において、PLCなどのデータをエクセルにまとめるため、プログラムでエクセルを作る機会がありました。そこで、openpyxlで大量データを持つエクセルを作成した際の時間を調べてみました(AWS Lambda)。
おすすめの方
- AWS Lambdaでopenpyxlを利用したい方
- AWS Lambdaでopenpyxlを利用して大量データを扱った場合の時間を知りたい方
openpyxlでエクセルを作成するLambdaをデプロイする
sam init
sam init \
--runtime python3.11 \
--name lambda-openpyxl-sample \
--app-template hello-world \
--no-tracing \
--no-application-insights \
--structured-logging \
--package-type Zip
requirements.txt
にライブラリを記載する
SAMテンプレート
タイムアウトは最大の900秒に設定します。メモリは次の値をそれぞれ利用します。
- 128
- 256
- 512
- 1024
- 2048
- 4096
- 8192
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: lambda-openpyxl-sample
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: lambda-openpyxl-sample-function
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.11
MemorySize: 8192
Timeout: 900
Architectures:
- x86_64
HelloWorldFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${HelloWorldFunction}
Lambdaコード
下記は「50万行 x 10列」の場合です。
app.py
import openpyxl
import time
def lambda_handler(event, context):
wb = openpyxl.Workbook()
ws = wb.active
begin_write = time.perf_counter()
for i in range(500_000):
ws.append([f"Row {i}"] + [f"Column {j}" for j in range(1, 10)])
end_write = time.perf_counter()
print(f"Write time: {(end_write - begin_write)} s")
begin_save = time.perf_counter()
wb.save("/tmp/hello_world.xlsx")
end_save = time.perf_counter()
print(f"Save time: {end_save - begin_save} s")
デプロイ
sam build --use-container
sam deploy
Lambdaを実行してみる
aws lambda invoke \
--function-name lambda-openpyxl-sample-function \
output.txt
データ行数とメモリによる実行時間
データ量が多くなると、エクセル書き込みよりも保存時間が長くなりました。
以下にMarkdownのTable形式でデータをまとめました。なお、「???」の部分は、Lambdaのメモリ不足またはLambdaのタイムアウトにより不明を意味します。
128 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
5.7 |
20.0 |
25.7 |
90 |
5万 (Error: Runtime exited with error: signal: killed) |
??? |
??? |
21.2 |
128 |
10万 (Error: Runtime exited with error: signal: killed) |
??? |
??? |
22.0 |
128 |
20万 (Error: Runtime exited with error: signal: killed) |
??? |
??? |
23.0 |
128 |
30万 (Error: Runtime exited with error: signal: killed) |
??? |
??? |
22.2 |
128 |
40万 (Error: Runtime exited with error: signal: killed) |
??? |
??? |
22.1 |
128 |
50万 (Error: Runtime exited with error: signal: killed) |
??? |
??? |
22.8 |
128 |
256 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
2.9 |
10.0 |
12.9 |
90 |
5万 |
16.3 |
51.3 |
67.6 |
237 |
10万 (Task timed out after 901.73 seconds) |
??? |
??? |
900 |
256 |
20万 (Task timed out after 901.63 seconds) |
??? |
??? |
900 |
256 |
30万 (Task timed out after 901.92 seconds) |
??? |
??? |
900 |
256 |
40万 (Task timed out after 901.92 seconds) |
??? |
??? |
900 |
256 |
50万 (Task timed out after 903.10 seconds) |
??? |
??? |
900 |
256 |
512 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
1.4 |
4.9 |
6.3 |
89 |
5万 |
7.9 |
24.2 |
32.1 |
237 |
10万 |
16.1 |
50.2 |
66.3 |
423 |
20万 (Task timed out after 900.34 seconds) |
??? |
??? |
900 |
512 |
30万 (Task timed out after 900.49 seconds) |
??? |
??? |
900 |
512 |
40万 (Task timed out after 900.87 seconds) |
??? |
??? |
900 |
512 |
50万 (Task timed out after 903.09 seconds) |
??? |
??? |
900 |
512 |
1024 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
0.7 |
2.4 |
3.1 |
90 |
5万 |
3.9 |
12.3 |
16.2 |
237 |
10万 |
8.1 |
24.8 |
32.9 |
423 |
20万 |
16.2 |
49.1 |
65.3 |
798 |
30万 (Error: Runtime exited with error: signal: killed) |
25.1 |
??? |
47.7 |
1024 |
40万 (Task timed out after 900.69 seconds) |
??? |
??? |
900 |
1024 |
50万 (Task timed out after 900.71 seconds) |
??? |
??? |
900 |
1024 |
2048 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
0.4 |
1.4 |
1.8 |
90 |
5万 |
2.2 |
6.9 |
9.1 |
237 |
10万 |
4.6 |
14.1 |
18.7 |
424 |
20万 |
8.2 |
24.3 |
32.5 |
798 |
30万 |
14.1 |
41.0 |
55.1 |
1195 |
40万 |
18.2 |
55.1 |
73.3 |
1535 |
50万 |
23.2 |
69.1 |
92.3 |
1857 |
4096 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
0.4 |
1.3 |
1.7 |
91 |
5万 |
2.2 |
7.0 |
9.2 |
238 |
10万 |
4.6 |
14.2 |
18.8 |
424 |
20万 |
9.1 |
27.7 |
36.8 |
799 |
30万 |
14.2 |
41.4 |
55.6 |
1195 |
40万 |
17.8 |
54.3 |
72.1 |
1536 |
50万 |
22.8 |
69.3 |
92.1 |
1857 |
8192 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
0.4 |
1.3 |
1.7 |
91 |
5万 |
1.9 |
6.0 |
7.8 |
238 |
10万 |
4.5 |
14.0 |
18.5 |
424 |
20万 |
9.3 |
27.9 |
37.2 |
799 |
30万 |
14.3 |
41.6 |
55.9 |
1196 |
40万 |
18.4 |
54.6 |
73.0 |
1537 |
50万 |
22.9 |
68.0 |
90.9 |
1858 |
Write-only mode を試してみる
公式で紹介されているWrite-only modeを試してみます。
Lambdaコード
下記は「50万行 x 10列」の場合です。write_only=True
を設定してます。
app.py
import openpyxl
import time
def lambda_handler(event, context):
wb = openpyxl.Workbook(write_only=True)
ws = wb.create_sheet()
begin_write = time.perf_counter()
for i in range(500_000):
ws.append([f"Row {i}"] + [f"Column {j}" for j in range(1, 10)])
end_write = time.perf_counter()
print(f"Write time: {(end_write - begin_write)} s")
begin_save = time.perf_counter()
wb.save("/tmp/hello_world.xlsx")
end_save = time.perf_counter()
print(f"Save time: {end_save - begin_save} s")
データ行数とメモリによる実行時間(Write-only mode)
全体的にエクセルへのデータ書き込みの時間が増えた代わりに保存時間が短くなっています。処理の合計時間は、若干ですが短くなっています。そしてメモリ使用量がかなり減っています。
128 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
22.3 |
1.4 |
23.7 |
57 |
5万 |
111.9 |
6.5 |
118.4 |
81 |
10万 |
223.6 |
13.0 |
236.6 |
111 |
20万 |
441.5 |
27.1 |
468.6 |
124 |
30万 |
629.5 |
37.8 |
667.3 |
117 |
40万 (Task timed out after 900.09 seconds) |
887.8 |
??? |
900 |
118 |
50万 (Task timed out after 900.11 seconds) |
??? |
??? |
900 |
117 |
256 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
11.3 |
0.7 |
12.0 |
57 |
5万 |
55.2 |
3.2 |
58.4 |
81 |
10万 |
110.3 |
6.4 |
116.7 |
111 |
20万 |
223.1 |
12.9 |
236.0 |
172 |
30万 |
332.1 |
19.2 |
351.3 |
234 |
40万 |
445.6 |
26.8 |
472.4 |
256 |
50万 |
570.4 |
34.3 |
604.7 |
251 |
512 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
5.3 |
0.3 |
5.6 |
57 |
5万 |
25.5 |
1.5 |
37.0 |
81 |
10万 |
54.5 |
3.2 |
57.7 |
111 |
20万 |
106.9 |
6.2 |
113.1 |
173 |
30万 |
164.8 |
9.5 |
174.3 |
234 |
40万 |
217.2 |
12.6 |
229.8 |
296 |
50万 |
268.8 |
15.8 |
284.6 |
357 |
1024 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
2.6 |
0.2 |
2.8 |
57 |
5万 |
13.1 |
0.8 |
13.9 |
81 |
10万 |
26.0 |
1.6 |
27.6 |
111 |
20万 |
54.1 |
3.2 |
57.3 |
173 |
30万 |
79.5 |
4.8 |
84.3 |
234 |
40万 |
105.8 |
6.3 |
112.1 |
296 |
50万 |
130.9 |
7.8 |
138.7 |
357 |
2048 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
1.5 |
0.1 |
1.6 |
57 |
5万 |
7.3 |
0.4 |
7.7 |
81 |
10万 |
14.5 |
0.9 |
15.4 |
111 |
20万 |
28.9 |
1.8 |
30.7 |
173 |
30万 |
44.5 |
2.7 |
47.2 |
234 |
40万 |
58.6 |
3.6 |
62.2 |
296 |
50万 |
73.9 |
4.5 |
78.4 |
357 |
4096 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
1.4 |
0.1 |
1.5 |
58 |
5万 |
7.2 |
0.4 |
7.6 |
82 |
10万 |
14.6 |
0.9 |
15.5 |
112 |
20万 |
29.5 |
1.8 |
31.3 |
173 |
30万 |
43.5 |
2.7 |
46.2 |
235 |
40万 |
58.4 |
3.6 |
62.0 |
296 |
50万 |
73.4 |
4.5 |
77.9 |
358 |
8192 MB
データ行数 |
行追加[s] |
保存[s] |
全体[s] |
メモリ使用量[MB] |
1万 |
1.5 |
0.1 |
1.6 |
58 |
5万 |
7.2 |
0.5 |
7.7 |
82 |
10万 |
14.7 |
0.9 |
15.6 |
112 |
20万 |
29.2 |
1.8 |
31.0 |
174 |
30万 |
44.5 |
2.7 |
47.2 |
235 |
40万 |
58.4 |
3.6 |
62.0 |
297 |
50万 |
73.4 |
4.4 |
77.8 |
358 |
さいごに
openpyxlを利用してエクセルを作成する場合は、想定する通常のデータ量や想定する最大のデータ量で事前に実験することをおすすめします。
参考