tseki blog

my study room

lxml を使った python での省メモリ XML 解析

www.ibm.com

こちらのブログをみつけ、動作確認を行ってみました。

コード

テストコード等は以下になります。

github.com

概要

公開された 109MB のXMLデータを用いて、
次の3つのパターンにおけるメモリ使用量と要素数の計数にかかる時間を測定しました。
(詳細な内容は冒頭のブログかテストコードをご参照ください)

  1. lxml.etree.parse メソッドで一度に全体をパースする
  2. lxml.etree.iterparse メソッドで逐次パースする
  3. 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 が最適だとは言えないかもしれないですが、
メモリ使用量を抑える方法としてはこのような形でコントロールすることも可能であるということが分かりました。