lxml を使った python での省メモリ XML 解析
こちらのブログをみつけ、動作確認を行ってみました。
コード
テストコード等は以下になります。
概要
公開された 109MB のXMLデータを用いて、
次の3つのパターンにおけるメモリ使用量と要素数の計数にかかる時間を測定しました。
(詳細な内容は冒頭のブログかテストコードをご参照ください)
lxml.etree.parseメソッドで一度に全体をパースするlxml.etree.iterparseメソッドで逐次パースするlxml.etree.iterparseメソッドで逐次パースする + メモリ使用量の最適化を行う
結果
(.venv) tseki:~/dev/parse-xml-python (master) $ python main.py 1
elapsed_time(use_parse): 0:00:06.395076
count: 50000
Filename: main.py
Line # Mem usage Increment Occurences Line Contents
============================================================
7 21.8 MiB 21.8 MiB 1 @profile
8 def use_parse():
9 21.8 MiB 0.0 MiB 1 measurement = Measurement()
10
11 1577.5 MiB 1555.8 MiB 1 obj = lxml.etree.parse('SwissProt.xml')
12 1577.5 MiB 0.0 MiB 1 root = obj.getroot()
13 1577.5 MiB 0.0 MiB 50001 for entry in root.iter('Entry'):
14 1577.5 MiB 0.0 MiB 50000 measurement.increment_count()
15
16 1577.5 MiB 0.0 MiB 1 print(f'elapsed_time(use_parse): {measurement.elapsed_time}')
17 1577.5 MiB 0.0 MiB 1 print(f'count: {measurement.count}')
(.venv) tseki:~/dev/parse-xml-python (master=) $ python main.py 2
elapsed_time(use_iter): 0:00:06.411065
count: 50000
Filename: main.py
Line # Mem usage Increment Occurences Line Contents
============================================================
20 21.7 MiB 21.7 MiB 1 @profile
21 def use_iter():
22 21.7 MiB 0.0 MiB 1 measurement = Measurement()
23
24 21.7 MiB 0.0 MiB 1 context = lxml.etree.iterparse('SwissProt.xml', events=('end',), tag='Entry')
25
26 1577.3 MiB 1555.7 MiB 50001 for event, elem in context:
27 1577.3 MiB 0.0 MiB 50000 measurement.increment_count()
28
29 1577.3 MiB 0.0 MiB 1 print(f'elapsed_time(use_iter): {measurement.elapsed_time}')
30 1577.3 MiB 0.0 MiB 1 print(f'count: {measurement.count}')
(.venv) tseki:~/dev/parse-xml-python (master=) $ python main.py 3
elapsed_time(use_iter_optimized): 0:00:02.433571
count: 50000
Filename: main.py
Line # Mem usage Increment Occurences Line Contents
============================================================
33 21.8 MiB 21.8 MiB 1 @profile
34 def use_iter_optimized():
35 21.8 MiB 0.0 MiB 1 measurement = Measurement()
36
37 21.8 MiB 0.0 MiB 1 context = lxml.etree.iterparse('SwissProt.xml', events=('end',), tag='Entry')
38 23.5 MiB 1.7 MiB 1 fast_iter(context, measurement.increment_count)
39
40 23.5 MiB 0.0 MiB 1 print(f'elapsed_time(use_iter_optimized): {measurement.elapsed_time}')
41 23.5 MiB 0.0 MiB 1 print(f'count: {measurement.count}')
| 方法 | 時間[s] | メモリ増加[MiB] |
|---|---|---|
| parse | 6.40 | 1555.8 |
| iterparse | 6.41 | 1555.7 |
| iterparse with optimization | 2.43 | 1.7 |
3 の方法が時間・メモリ増加量ともに圧倒的に少なくなりました。
結局全体をメモリに載せているためか、1 と 2 の違いは見られませんでした。
(何度か繰り返して平均を取ったりすると変わるかもしれないけど)
要件よっては必ずしも 3 が最適だとは言えないかもしれないですが、
メモリ使用量を抑える方法としてはこのような形でコントロールすることも可能であるということが分かりました。