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 が最適だとは言えないかもしれないですが、
メモリ使用量を抑える方法としてはこのような形でコントロールすることも可能であるということが分かりました。