XStreamingReader - Linq to Xml形式でのストリーミングXml読み込み

CodePlex : XStreamingReader - Streaming for Linq to Xml

1クラスライブラリシリーズ。もしくはストリーミングをIEnumerableに変換していこうシリーズ。またはシンプルだけど小粒でピリッと隙間にぴったりはまるシリーズ(を、目指したい)。といったわけで、100行程度ではあるのですが、表題の機能を持つコードをCodePlexに公開しました。それとおまけとして、XMLファイルからC#クラス自動生成T4 Templateも同梱。

Linq to Xml風にXmlを読み込めるけれど、ツリーを構築せずストリームで、完全遅延評価で実行します。Linq to Xmlには、書き込み用にXStreamingElementというものがあるため、それと対比した読み込み用という位置付けのつもりです。メモリの厳しいモバイル機器や、巨大なXMLを読み込む際に使えるんじゃないかと思っています。

利用例

ぶっちゃけまるっきりXElementと同じです。例としてYahoo!天気情報のRSSから京都と東京を取り出し。

// XElement
var kyoto = XElement.Load(@"http://rss.weather.yahoo.co.jp/rss/days/6100.xml")
    .Descendants("item")
    .Select(x => new
    {
        Title = (string)x.Element("title"),
        Description = (string)x.Element("description"),
        PubDate = (DateTime)x.Element("pubDate")
    })
    .Where(a => !a.Title.StartsWith("[PR]")) // itemが広告の場合は除外
    .ToArray();

// XStreamingReader
var tokyo = XStreamingReader.Load(@"http://rss.weather.yahoo.co.jp/rss/days/4410.xml")
    .Descendants("item")
    .Select(x => new
    {
        Title = (string)x.Element("title"),
        Description = (string)x.Element("description"),
        PubDate = (DateTime)x.Element("pubDate")
    })
    .Where(a => !a.Title.StartsWith("[PR]")) // itemが広告の場合は除外
    .ToArray();

Load/Parseで生成し、ElementsやDescendantsで抽出。あとは、IEnumerable<XElement>となっているので、SelectしたりWhereしたり。完全にLinq to Xmlと同じAPIです。同じすぎてこれだけだと利点がさっぱり見えませんが、100%遅延評価+ストリーミング読み込みで逐次生成という違いがあります。詳しくは次のセクションで。

バックグラウンド

Androidでは性能のためにDOMじゃなくてSAXでXML扱うんだ。という話を良く聞いて、確かにただデータ取るためだけにDOM構築ってのは嫌だし、そりゃ避けたい。対象がDOMなら素直にそう思いますが、しかし、もしそれがLinq to Xmlならどうだろう?Windows Phone 7だったらLinq to Xml使うに決まってるよ、と言いたいのですが、これってDOMと同じく、すぐに(LoadなりParseなりした直後)ツリーを構築しています。Elements()なりDescendants()なりの戻り値がIEnumerableなため、遅延評価かと思ってしまうわけですが、遅延評価されるのはツリーの探索が、というだけであって、構築自体は即時でされています。

DOMに比べて軽量(という謳い文句)であることと、非常に軽々と書けるため抵抗感がないわけですが、考えてみれば Load.Descendants.Select みたいな、API叩いて何らかのクラスなり匿名型なりに変換するという程度の、しかしよくある定型作業は、わざわざツリー作る必要はなくストリーミングで取れるし、それならばストリーミングで取るべきではある。しかし、今時XmlReaderを直で触るなんて、時代への逆行のようなことはやりたくない。

ストリームはIEnumerableに変換するのがLinq以降のC#の常識。というのを日々連呼しているので、今回はXmlReaderをIEnumerable<T>に変換しなければなりません。しかし、困ったのが、<T>のTを何にすればいいのか、ということ。ファイル読み込みなら一行のString。データベースなら、IDataRecord(DbExecutorというライブラリとしてCodePlexに公開しています)を用いましたが、XmlReaderだと適当なのが見当たらない。XmlReaderを直接渡すのは危なっかしいし、そもそも渡したところで面倒くさいことにかわりなくてダメだ。何か適切なコンテナが……。

と、考えたり考えなかったりで、Twitterでもにょもにょと言っていたら

@neuecc Linq to Xml を使うにしても XmlReaderからReadSubtreeで切り出した断片に対してかなー、XML全体をオンメモリさせる必然性がなければStreamから読んで処理した端からGCに捨てて貰えるようにしておきたいだけだけど
http://twitter.com/kazuk/status/18193188205

うぉ!うぉぉぉぉぉ!なるほど、断片をXElementに変換してそれを渡せば、操作しやすいし感覚的にもXElement.Loadなどと変わらないしでベストだ!言われてみればそりゃそうだよねー、ですが全然頭になかった、まさにコロンブスの卵。こういうことがサラッと出てくることこそが、凄さだよね。

と、感嘆しつつ、それそのまま頂き、というわけで、TをXElementにするという形で解決しました。

public IEnumerable<XElement> Descendants(XName name)
{
    using (var reader = readerFactory())
    {
        while (reader.ReadToFollowing(name.LocalName, name.NamespaceName))
        {
            yield return XElement.Load(reader.ReadSubtree());
        }
    }
}

Descendantsの実装はこんな感じで、断片から都度XElement生成しているという、それだけの単純明快な代物です。そのため挙動はXElement.Load.Descendantsと完全同一というわけじゃありません。例えばサブツリー中に同名の要素がある場合、XElementでDescendantsの場合はサブツリー中の要素も列挙しますが、XStreamingReaderではトップ要素のものだけが拾われます。

他に注意点としては、それぞれのXElementは完全に独立しているため、ParentやNextNodeなどは全てnullです。よってAncestorsで先祖と比較しながらの複雑な抽出、などといったことも出来ません。TwitterのAPIのような、ウェブサービスとして用意されているXMLなら素直な構造なので問題はありませんが、SGMLReaderでLinq to HTMLなどといった場合は、結構複雑なクエリで抽出することになるため使えないでしょう。その場合は素直にXElement.Loadを使うのが良いと思います。

おまけ(Xml→自動クラス生成)

Xmlから人力でClass作るのって定型作業で面倒だよねー。ということで、自動生成するT4 Templateも同梱しました。プロパティ定義だけではなく(ちゃんとPascalCaseに直します)、コンストラクタにXElementを投げるとマッピングもしてくれます。つまりは、XStreamingReaderの仕様に合わせたものです。

.ttの上の方にある3つの項目を適当に書き換えると

string XmlString = new WebClient().DownloadString("http://twitter.com/statuses/public_timeline.xml");
const string DescendantsName = "status"; // select class root
const string Namespace = "Twitter"; // namespace
namespace Twitter
{
    public class Status
    {
        public string CreatedAt { get; set; }
        public string Id { get; set; }
        // snip...
        public User User { get; set; }
        public string Geo { get; set; }

        public Status(XElement element)
        {
            this.CreatedAt = (string)element.Element("created_at");
            this.Id = (string)element.Element("id");
            this.User = new User(element.Element("user"));
            this.Geo = (string)element.Element("geo");
        }
    }
    
    public class User
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string ScreenName { get; set; }
        // snip...
        public string FollowRequestSent { get; set; }

        public User(XElement element)
        {
            this.Id = (string)element.Element("id");
            this.Name = (string)element.Element("name");
            this.ScreenName = (string)element.Element("screen_name");
            this.FollowRequestSent = (string)element.Element("follow_request_sent");
        }
    }
}

こんなのが生成されます。型は全部stringになるので、手動で直してください。半自動生成。T4で生成→新しいクラスファイル作って生成結果をコピペ→型を直す。みたいな使い方をイメージしています。完全自動生成じゃないと変更に対する自動追随ってのが出来ないので、自動生成する意味が半減。しかし、型かあ、スキーマないと無理ですな。まあ、ウェブサービスのAPIなどは基本的には固定で変化がないでしょうから、ある程度は手間を省けるんじゃないかと思われます。

まとめ

断片とはいえ、XElement作るのは無駄じゃないの?というと、無駄ではあります。抽出したらすぐ用済みでポイなわけなので、純粋にパフォーマンスの観点から言えばXmlReaderを直で触ったほうが良いに決まっています。しかし、さすがにそこまで来ると無視して良いと思うわけです。例えばLinqで一時的な匿名型は使わないって?ああ、むしろLinqなんてやめて全部forループにでもします?言いだいたらキリがない。

今回で大事なのは、ストリーミング化しても、決して使いやすさは損なわれていないということです。ツリー構築型と全く同じように快適に書ける。それが何より大切。「性能のために書きやすさが犠牲になるぐらいなら性能なんていらない!」と、現実は言えなくても心では言ってしまいます。ユーザー視点だと逆ですが……。ただ、中長期的には、スパゲティコードは開発者を幸せにしない→機能追加速度低下/洗練が鈍る→ユーザーも不幸せになる、のループが回るので綺麗さは重要。勿論、そこが性能上本当にボトルネックになっているならば気合入れて叩く必要がありますが、気分的に、もしくはマイクロベンチマーク的にちょっと性能Downな程度でパフォーマンスチューニングとか言い出すのならシバいてよし。

といったわけかで、私なりにWindows Phone 7プログラミングへの準備を進めています。これで、準備になってる?……だと?ご冗談を。ですね、はい、すみません。開発キットのベータ版が出たので、次回はWindows Phone 7で何か作ろう紹介でも書く予定は未定。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive