はじめに
機械可読なデータをやり取りするときはJSONが主流になって久しい世の中ですが、XMLでデータが配布されることは時々あります(古くからある機関が公開してくれているデータとか)。
あるいは自然言語処理をしていると、たとえば構文解析機の CaboCha
には解析結果をXML形式で出力するオプション (-f 3
) があるので、結果の処理がいわゆる lattice
の形式より楽になるという意味で使ったりすることがあるのではないでしょうか。
私は後者のクチで、大きめのコーパスの構文解析結果をXMLに落としてあれこれしようとしていたのですが、その8GBのXMLを手元にあるメモリ64GBのマシンで処理しようとしたところ、メモリがいっぱいになって途中から進まなくなってしまいました(エラーすら吐かない)。
がんばってメモリを増やしたぞーというつもりで64GBにしたのでちょっとびっくりしました。
問題のXMLは、<root>
のタグの下に、<item>
というタグが何個もぶら下がっている、リスト形式状のものです。レコード形式ということもあるようです。
<root>
<item>...</item>
<item>...</item>
...
<item>...</item>
</root>
一つ一つの item
を処理するときは他の item
とは無関係で、順々に見ていけたらよいものとします。そういうタイプのデータが巨大なときには iterator (generator)
を使うとメモリにやさしいというのは多くの方がご存知かと思います。
もちろん XML を扱うライブラリにも、XMLファイルを iterator
で読み込めるメソッドはあるのですが、ちょっとコツが必要でした。
Python 標準ライブラリ で XML
Python で XML を処理するときは標準の xml.etree.ElementTree
を使うのが手軽です。他にも有名ドコロで BeautifulSoup というのもありますが、HTMLに特化している関係で私が扱いたいXMLでは解析エラーになる箇所があり1、ハマってしまったので標準ライブラリに落ち着いています。
本稿ではこの標準ライブラリ xml
で iterator
なXMLパースをするときの注意について述べます。
ふだんの使い方(全部メモリに入れる)
iterator
とかしないで普通に使うときはこうです。
import xml.etree.ElementTree as ET
tree = ET.parse('path/to/xml')
for item in tree.iterfind('item'):
# do something on item
.iterfind()
でXMLツリーのなかの <item>
タグを iterator
しながら読み込んでいますね。でも、その直前の ET.parse()
はいうなれば file.readlines()
みたいなものです。メモリをたくさん食べます。
iter するとき(でもメモリは食べる)
iter
しながら読みたいときはこうです。
import xml.etree.ElementTree as ET
context = ET.iterparse('path/to/xml')
for event, elem in context:
if elem.tag == 'item':
# do something on item
ET.parse()
を ET.iterparse()
にすると、引数のパスにあるXMLを iterator
形式で読み込みます。1タグごとに読んでいくのですが、タグの終わりに差し掛かったときにだけ context
は event
と elem
を返します。event == "end"
で、elem
は エレメントですね。
これで省メモリに処理ができる!と思ったら大間違いなのです。実は # do something on item
のところがpass
であっても、「ふだんの使い方」と同じくらいのメモリを使います。
iter
しながらも、それまで読んだタグを context
はすべて保存しているんです。
どこかというと、context.root
というローカル変数が iterator の中に隠れているんです。そんなことは公式ドキュメントにすら書いてなかったので知りませんでした。普通の generator
と違って、あとから繰り返しアクセスできるという意味ではうれしい人もいるかもしれないでしょうか。まぁXMLの入れ子構造を読みながら保持するためにはそういう仕組みも必要そうなのは言われてみれば想像できます。
iter するとき(メモリを食べない)
ではどうすればよいかというと、その昔 ElementTree
という名前のライブラリとして Python 2.5 で標準に組み込まれる前までの公式ページに Tips がありました。Pythonは3からの新参者だったので全然しりませんでした。
import xml.etree.ElementTree as ET
context = ET.iterparse('path/to/xml', events=('start', 'end'))
_, root = next(context) # 一つ進めて root を得る
for event, elem in context:
if event == 'end' and elem.tag == 'item':
# do something on item
root.clear() # 用が済んだらrootを空に
ET.iterparse()
にはevents
というキーワード引数を指定できて、これに'start'
を指定すると開きタグを教えてくれるんだそうです。最初の開きタグは<root>
なので、これを変数にとっておきます。このとき_
で捨てている値には件の文字列'start'
が入っています。
root
さえ取ってしまえば2、都度中身を.clear()
すればエレメントの情報がメモリから退散します。めでたいですね。