Java で XML を処理するやり方は色々ある。で、今回、500MB超の XML ファイルを処理しないといけない必要に迫られた。
まず、処理対象となる XML のイメージはこんな感じ。
<?xml version="1.0" encoding="UTF-8"?> <accounts> <account id="1"> <item> <itemid>a001</itemid> <summary>hoge1</summary> </item> <item> <itemid>a002</itemid> <summary>hoge2</summary> </item> <item> <itemid>a003</itemid> <summary>hoge3</summary> </item> </account> <account id="2"> <item> <itemid>a004</itemid> <summary>hoge3</summary> </item> <item> <itemid>a005</itemid> <summary>hoge4</summary> </item> </account> <account id="3"> ... </account> ..以下、延々とaccount が並ぶ </accounts>
accounts/account が延々と大量に出現すると思っていただければ。
つまり、やりたいのは accounts/account 単位で上から順に処理をしたい。accounts/account の単位で一つのインスタンスが出来て、対応する閉じタグが来たらそのインスタンスは消滅してもらえればそれで充分、というわけです。
ファイルサイズがアレなので DOM は論外となると SAX 系になるのだが、SAX もやっぱりめんどくさいなぁと思い、勝手知ったる Digester でなんとかなんねーかな、と考え始めた。
んで、Digester のドキュメント を見ていると、こんなことが書いてある。
オブジェクトスタック
使い方を簡単にするために、 Digester はスタックを開示していて、(中略)
(中略)
pop() - スタック内の一番上のオブジェクトを返し、 スタックから削除します。
(中略)
そのネストが処理されている間オブジェクトはそこに置かれ、そして要素の最後に到達したときにポップされます。
org.apache.struts.digester (Apache Struts API Documentation) から抜粋
Digester は、基本的に(設定するルール次第ではあるが)タグのネストがあるたびにオブジェクトをスタックしていく構造になっている。で、そのスタックに外部からアクセス出来るし、スタック操作のタイミングをフックすることも出来るようだ。
てことは、accounts/account の閉じタグで pop() が出すオブジェクトをフックすればよさそうである。
そんなわけでソースコード。
まずは Digester の呼び出し側でコレは Digester のお作法どおり。ちょっと違うのは Digester のインスタンスを独自クラスの ExtendedDigester にしている点。Account と Item クラスのソースは省略。
Digester digester = new ExtendedDigester(); digester.addRule("accounts/account", new ObjectCreateRule(Account.class)); digester.addRule("accounts/account", new SetPropertiesRule()); digester.addRule("accounts/account/item", new ObjectCreateRule(Item.class)); digester.addSetNext("accounts/account/item", "addItem"); digester.addRule("accounts/account/item/itemid", new BeanPropertySetterRule("id")); digester.addRule("accounts/account/item/summary", new BeanPropertySetterRule("summary")); digester.parse(new File("C:test.xml"));
で、ExtendedDigester がコチラ。
public class ExtendedDigester extends Digester { @Override public Object pop() { Object o = super.pop(); if (o instanceof Account) { Account r = (Account)o; System.out.print("Object pop()="+ "+" + r.getId() + ":" + r.items.size()); for (Item i : r.items) { System.out.print(i.getId() + ":" + i.getSummary() + ","); } System.out.println(); } return o; } }
Digester#pop() の実装をオーバーライドしてしまうことで、パース時に閉じタグが出現したときの pop の動作をフックする。で、スタックから出てきたオブジェクトが Account クラスなら何がしかの処理をする。
出力結果は下記の通り。Digester#parse を呼ぶと、パース途中で呼ばれる Digester#pop() は ExtendedDigester#pop にすりかわることが確認できる。
Object pop()=+1:3a001:hoge1,a002:hoge2,a003:hoge3,
Object pop()=+2:2a004:hoge3,a005:hoge4,
500MB の XML で試してみたところ、メモリは大して食わない(Digester の中身は SAX らしいから当然だが)、処理時間はソレナリにかかるがまぁこんなもんかな、といった範囲内。
とはいえ……pop メソッドを実装継承するのがちょっとアレでナニな感じはする。スタックが露出してるのはあくまでもパース処理のために開放されてるわけだし。それに Digester の本来的な役割は XML 形式のちょっとした設定ファイルを Java のオブジェクトにマッピングするためであり、巨大な XML ファイルを逐次処理する目的で作られたプロダクトじゃないし。
そんなわけで、このエントリで書いたよーなことはどうみても黒魔術です本当にありがとうございました。けど、まぁ……とりあえずの実験は成功したのでコレで終わり。