昨日の続きで、XmlNode に navigator の機能を付けてみます。
xml に関するナビゲータは、既に XPathNavigator クラスがあって、XPATH の記法を使って XML のツリー構造を探索できるのですが…当たり前なのですが「XPATHを使わないといけない」という制約があります。
XPathNavigator クラス (System.Xml.XPath)
http://msdn.microsoft.com/ja-jp/library/system.xml.xpath.xpathnavigator(v=vs.110).aspx
まあ、xpath で細々と書ればいいんだけど、もっと大雑把に C# の文法に近く、というか LINQ 近い形で探索がしたいなぁと思った…というのは嘘で、成り行き上こうなりましたって感じです。HTML の探索方法を色々模索していったら、LINQ の Where メソッドを使う(IEnumable<> を使う)ほうが一番妥当かというパターンなのです。
■IEnumable<>部分を HtmlDoc からコピペ
以前作ったところから、そのままコピーして手直し。
IEnumerator<XmlNode> のところは、内部クラスにして XmlNavigator.Enumerator にします。
public class XmlNavigator : IEnumerable<XmlNode> { XmlNode _root; public XmlNavigator(XmlNode root) { _root = root; } public IEnumerator<XmlNode> GetEnumerator() { return new Enumerator(_root); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return new Enumerator(_root); } protected class Enumerator : IEnumerator<XmlNode> { XmlNode _root; XmlNode _cur; public Enumerator(XmlNode root) { _root = root; _cur = null; } public XmlNode Current { get { return _cur; } } public void Dispose() { } object System.Collections.IEnumerator.Current { get { return _cur; } } public bool MoveNext() { if (_cur == null) { _cur = _root; return true; } if (_cur.Children.Count > 0) { _cur = _cur.Children[0]; return true; } if (_cur.NextSubling != null) { _cur = _cur.NextSubling; return true; } XmlNode cur = _cur; while (true) { XmlNode pa1 = cur.Parent; if (pa1 == null) { _cur = null; return false; } if (pa1.NextSubling != null) { _cur = pa1.NextSubling; return true; } cur = pa1; } } public void Reset() { _cur = null; } } }
MoveNext メソッドが若干ややこしくて、XML のツリー構造を親子関係を伝って探索していきます。バイナリサーチの要領ですね。
次の兄弟要素を拾うことが必要なので、XmlNode.NextSubling メソッドを追加しています。
public XmlNode NextSubling { get { if (this.Parent == null) { return null; } #if true var it = this.Parent.Children.GetEnumerator(); while ( it.MoveNext() ) { if ( it.Current == this ) { if ( it.MoveNext() ) { return it.Current; } else { return null; } } } return null; #else for (int i = 0; i < this.Parent.Children.Count; i++) { XmlNode nd = this.Parent.Children[i]; if (this.Equals(nd)) { if (i < this.Parent.Children.Count - 1) { return this.Parent.Children[i + 1]; } else { return null; } } } return null; #endif } }
こんな感じでいちいち探索しているのでスピード的には遅いのですが、用途は足りるのでOKとします。
最初は for 文で書いておいて、後から GetEnumerator で書き直してありますが…多分スピードは変わらないでしょう。
■テストコードを書く
例によって、テストコードを書きます。昨日書いた XmlNode の構築メソッドチェーンを使って、ぽちぽちと移植。
[TestClass] public class TestXmlNavi { [TestMethod] public void TestNormal() { var root = new XmlNode("root") .Node("person") .Node("name", "masuda") .Root; // タグが検索できた場合 var q = new XmlNavigator(root) .Where(n => n.TagName == "name") .FirstOrDefault(); Assert.AreEqual("masuda", q.Value); // タグが見つからなかった場合 q = new XmlNavigator(root) .Where(n => n.TagName == "error") .FirstOrDefault(); Assert.AreEqual(null, q); } [TestMethod] public void TestNormal2() { var root = new XmlNode("root") .Node("person") .AddNode("name", "masuda") .AddNode("name", "yamada") .AddNode("name", "yamasaki") .Root; // タグが検索できた場合 var q = new XmlNavigator(root) .Where(n => n.TagName == "name") ; Assert.AreEqual(3, q.Count()); Assert.AreEqual("masuda", q.First().Value); } [TestMethod] public void TestNormal3() { var root = new XmlNode("root") .Node("person").AddAttr("id", "1") .AddNode("name", "masuda") .AddNode("age", "44") .Parent .Node("person").AddAttr("id", "2") .AddNode("name", "yamada") .AddNode("age", "20") .Parent .Node("person").AddAttr("id", "3") .AddNode("name", "tanaka") .AddNode("age", "10") .Root; // タグが検索できた場合 var q = new XmlNavigator(root) .Where(n => n.Attrs["id"] == "2") .FirstOrDefault(); Assert.AreEqual("person", q.TagName); // ExDoc記述 Assert.AreEqual("yamada", q / "name"); Assert.AreEqual("20", q / "age"); } [TestMethod] public void TestNormal4() { var root = new XmlNode("root") .Node("person").AddAttr("id", "1") .AddNode("name", "masuda") .AddNode("age", "44") .Parent .Node("person").AddAttr("id", "2") .AddNode("name", "yamada") .AddNode("age", "20") .Parent .Node("person").AddAttr("id", "3") .AddNode("name", "tanaka") .AddNode("age", "10") .Root; // クエリ文にしてみる var q = from n in new XmlNavigator(root) where n.Attrs["id"] == "2" select n; Assert.AreEqual("person", q.First()); // ExDoc記述 Assert.AreEqual("yamada", q.First() / "name"); Assert.AreEqual("20", q.First() / "age"); } [TestMethod] public void TestNormal5() { var root = new XmlNode("root") .Node("person").AddAttr("id", "1") .AddNode("name", "masuda") .AddNode("age", "44") .Parent .Node("person").AddAttr("id", "2") .AddNode("name", "yamada") .AddNode("age", "20") .Parent .Node("person").AddAttr("id", "3") .AddNode("name", "tanaka") .AddNode("age", "10") .Root; // クエリ文にしてみる var nd = from n in new XmlNavigator(root) where n % "id" == "2" select n; XmlNode xn = nd.FirstOrDefault(); // 本来は、以下のようにしたのだが暗黙キャストが作れない /* XmlNode xn = from n in new XmlNavigator(root) where n % "id" == "2" select n; */ Assert.AreEqual("person", xn); // ExDoc記述 Assert.AreEqual("yamada", xn / "name"); Assert.AreEqual("20", xn / "age"); } }
TestNormal3 あたりからある Assert.AreEqual(“yamada”, q / “name”); は、子要素から「name」タグを取得するという ExDoc の文法です…つーか、operator の多重定義。
XmlNode クラスに以下の operator を追加しておきます。
#region sweet operator public static XmlNode operator / ( XmlNode node, string tag ) { var nd = node.Children.Find( n => n.TagName == tag ) ; return nd ?? XmlNode.Empty ; } public static implicit operator string(XmlNode node) { return node.Value; } public static string operator %(XmlNode node, string key) { return node.Attrs[key]; }
ExDoc を作ったときは等価オペレータ(==演算子)も多重定義していましたが、今回は LINQ に任せるので、/演算子と%演算子だけを多重定義します。
TestNormal5 メソッドの暗黙のキャストはうまく定義できないので、断念してます。何か逃れることはできないかと、後でチェック。
■できあがったコード
XmlNavigator クラスは最初に示した通りです。XmlNode クラスは多少手を加えてテストはOKということで。
IEnumerator<XmlNode>.MoveNext さえきっちりと実装していけば、LINQ 文法で動くのでかなりコーディングが楽です。
■IEnumerable<XmlNode> からの暗黙のキャストが作れない
ExDoc で作ったように、LINQ の結果を XmlNode で直接受けたい(暗黙のキャストを受けたい)のですが、これが実装できません。
XmlNode xn = from n in new XmlNavigator(root) where n % "id" == "2" select n;
下記のように作るのですが、
class XmlNode { ... public static implicit operator XmlNode(IEnumerable<XmlNode> lst) { return lst.FirstOrDefault() ?? XmlNode.Empty; } }
コンパイルするとこんなエラーが。
エラー 1 'Moonmile.XmlDom.XmlNode.implicit operator Moonmile.XmlDom.XmlNode(System.Collections.Generic.IEnumerable<Moonmile.XmlDom.XmlNode>)': インターフェイスとの間におけるユーザー定義の変換は許可されていません。
ってな具合で、インターフェースからキャストはできないそうです。うーむ残念。
なんかいい方法はないですかねぇ。多分、ToList メソッドのように拡張メソッドをつければいいんでしょうが
XmlNode xn = (from n in new XmlNavigator(root) where n % "id" == "2" select n ).ToXmlNode();
のように折角のクエリ文法に括弧を入れないと駄目なのでいまいち過ぎ。
まあ、メソッド的に
var xn = new XmlNavigator(root) .Where(n => n % "id" == "2") .FirstOrDefault();
と書くと、ひとつの XmlNode が取れるんので十分なんですけどね。後で考えますか。