連載

.NETで簡単XML

第16回 川俣流XMLプログラミングの定石(2)

株式会社ピーデー 川俣 晶
2004/03/24
Page1 Page2 Page3 Page4

DOMツリーからデータを抜き取る方法

 DOMツリーは、DOMツリーを作っただけでは意味がなく、そこに含まれる情報を活用しなければならない。では、DOMツリー内に含まれる情報にアクセスするにはどうすればよいのかというと、迷うほどに多くの方法が用意されている。DOM自身が、もともと情報にアクセスするために複数の方法を用意しているのに加えて、.NET FrameworkのDOMの実装には、W3Cが勧告する標準のDOMにはない機能が付け加えられているためだ。ここでは、主にどのような方法があるのか、それらにはどのような相違があるのかを見てみよう。

 ある名前の要素の内容から文字列を取り出す処理を5種類記述したサンプル・プログラムを以下に示す。

Private Const sampleDocument As String = "<item xmlns='http://sample/'>" _
  & "<code>cod0123456789</code>" _
  & "<user>Taro</user>" _
  & "<user>Jiro</user>" _
  & "<user>Hanako</user>" _
  & "</item>"

Private Sub walk(ByVal node As XmlNode)
  If node.NodeType = XmlNodeType.Element _
    AndAlso node.LocalName = "user" AndAlso node.NamespaceURI = "http://sample/" Then
    System.Diagnostics.Trace.WriteLine(node.InnerText)
  End If
  For Each child As XmlNode In node.ChildNodes
    walk(child)
  Next
End Sub

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  Dim doc As XmlDocument = New XmlDocument
  doc.LoadXml(sampleDocument)
  Dim nsmgr As XmlNamespaceManager = New XmlNamespaceManager(doc.NameTable)
  nsmgr.AddNamespace("myns", "http://sample/")

  ' 子を順番で決め打ち

  System.Diagnostics.Trace.WriteLine(doc.ChildNodes(0).ChildNodes(0).InnerText)
  ' 木を歩く
  walk(doc)
  ' GetElementsByTagNameを使う
  Dim list1 As XmlNodeList = doc.GetElementsByTagName("user", "http://sample/")
  For Each node As XmlNode In list1
  System.Diagnostics.Trace.WriteLine(node.InnerText)
  Next
  ' SelectNodesを使う
  Dim list2 As XmlNodeList = doc.SelectNodes("//myns:user", nsmgr)
  For Each node As XmlNode In list2
    System.Diagnostics.Trace.WriteLine(node.InnerText)
  Next
  ' SelectSingleNodeを使う
  System.Diagnostics.Trace.WriteLine(doc.SelectSingleNode("//myns:code", nsmgr).InnerText)
End Sub
5種類の方法で要素内容の文字列を取り出して出力するサンプル・プログラム(VB.NET版C#版

 これを実行すると以下のような結果が統合開発環境の出力ウィンドウに出力される。

cod0123456789
Taro
Jiro
Hanako
Taro
Jiro
Hanako
Taro
Jiro
Hanako
cod0123456789
「要素内容の文字列を取り出して出力するサンプル・プログラム」の出力結果
統合開発環境の出力ウィンドウに出力される結果。

 まず最初に、「子を順番で決め打ち」とコメントを書いた個所を見ていただきたい。子ノードの何番目という形で指定を行うことで、DOMツリーのどのノードにもアクセスすることができる。例えば、ドキュメント・ノードの2番目の子ノードの3番目の子ノードが目的の情報を格納するものである、と分かっている場合には単純明快で便利である。しかし、この方法は現実的にはあまり使えない。ある情報を持ったノードが何番目に来るかは、実は簡単に決定することができない。例えば、コメントが1つ入るだけで、ノードの順番が1つずれてしまうことがある。もちろん、それはXML文書としての正しさを損なうものではないが、順番を決め打ちすることは、好ましくないことだといえる。

 しかし、例外的ではあるが、まれにこの方法でもOKというケースがある。例えば、テスト・プログラムなどで、特定のXML文書を処理することが前もって分かっている場合には、それに沿ったプログラムを書くことができる場合もある。ごく例外的なことではあるが。こういう楽な方法もあると覚えておくと、たまに役に立つかもしれない。

 次に、「木を歩く」とコメントを書いた個所を見ていただきたい。これの本体は、walkメソッドである。walkメソッドは、再帰呼び出しを使って、木(DOMツリー)の全体を歩いてまわる。そして、指定されたノードの場合は、ノードの値を出力するという処理が記述されている。これは、DOMツリーに限らず木構造を持ったデータを扱う場合の定番的な処理方法であり、もちろんDOMツリーでも最も基本的かつ定番的な処理方法である。しかし、再帰呼び出しを行うためには、独立したメソッドを1つ作成する必要があり、多用するとソース・コードが膨らんでしまって、扱いにくくなる。基本的には、この後出てくる3つの方法がうまく機能しない場合に限って活用する定石と思えばよいだろう。

 次に「GetElementsByTagNameを使う」とコメントを書いた個所を見ていただきたい。このメソッドは、XML文書中で指定した名前の要素のノードのリストを返してくれる便利なものである。単純に、要素の名前からデータを引き出すには、最も便利なメソッドといえる。しかし、物足りない面もある。このメソッドは常にXML文書全体を対象にしているので、あるノードの子孫だけを対象に使うことができない。例えば、ある名前を持つ要素が100個あるXML文書でその名前を指定して呼び出すと、必ず100個のノード・リストが返されてしまう。あるノードの子孫に1個しかないと分かっている場合でも、対象を絞り込めない。また、対象は要素の名前だけしか指定することができない。例えば、ある名前の要素というだけでなく、ある属性を持っているものに限定したいと思っても、名前による選択しか行うことができない。ほかの条件を加えたければ、得られたノード・リストをさらに調べるために新しいリストを作成する必要が生じ、あまり便利ではない。この不便さを解消するものとして、次に出てくるSelectNodesメソッドがある。

 次に「SelectNodesを使う」とコメントを書いた個所を見ていただきたい。SelectNodesメソッドは、GetElementsByTagNameメソッドと同じように指定条件のノードのリストを返すが、GetElementsByTagNameメソッドの不便さを解消している。SelectNodesメソッドは任意のノードを起点にして、ノードを調べることができるので、あるノードの子孫だけを対象にすることも容易である。そして、条件の指定は要素名ではなく、XPath式なので、要素ではなく属性を対象に調べることや、複数の条件を組み合わせたノードを調べることも容易にできる。

 このことから、SelectNodesメソッドがあればGetElementsByTagNameメソッドは不用であるように思えるかもしれない。しかし、筆者はSelectNodesメソッドよりも、GetElementsByTagNameメソッドの方を多用している。それはなぜか。

 GetElementsByTagNameメソッドと比較して、SelectNodesメソッドの方が不便な点が1つだけ存在する。それは、SelectNodesメソッドで名前空間を含むXPath式を記述する場合、XmlNamespaceManagerクラスのインスタンスを渡さねばならないことである。これは、名前空間と名前空間接頭辞の関係の情報を指定するために必要とされるものである。一方、GetElementsByTagNameメソッドで名前空間を指定する場合は、単純に名前空間URIをパラメータに渡すだけでよく、ほかのインスタンスを作成する必要はない。その点で、コーディング的にはGetElementsByTagNameメソッドの方がずっと手軽なのである。そして、DOMツリーから情報を抜き出すニーズの大半は、要素名(と名前空間URI)を指定するだけで十分といえる(もちろん目的にもよるが)。そのため、GetElementsByTagNameメソッドを使う定石と、SelectNodesメソッドを使う定石を使い分ける境界線をはっきり把握しておき、GetElementsByTagNameメソッドで十分ならそれを使うことで、コードを短くシンプルにすることができる。

 最後に「SelectSingleNodeを使う」とコメントを書いた個所を見ていただきたい。これは、SelectNodesメソッドと同様の機能を持つメソッドだが、返されるノードが1つきりである点で異なる。結果として返されるノードが1個と決まっている場合に使うと便利である。戻り値がNothing(null)であるかどうか調べるコードを追加すれば、0個または1個の場合にも使用できる。このような条件の場合には、SelectNodesメソッドではなく、SelectSingleNodeを使うようにすれば、リストを受け取る手順を省けるため、コードを短くシンプルにすることができる。

ファイルと1対1に対応させるクラス

 一般的にXML文書を扱うプログラムは、ほかの方法(CSVやRDBMSを使う方法など) を使うプログラムと比較して、処理が重いとされる。XML文書を扱うプログラムで共通の問題としてしばしば指摘されるのは、解析(パース)の重さである。XML文書は、一種のテキスト・ファイルとして保存されるため、これを解析する処理は効率が悪い。しかし、この特徴は、相互運用性のある標準という立場上、どうしても外せないものである。もし、XMLである必然性がなければ、XMLよりも効率のよい技術を使うことも可能なので、どうしても性能面で問題があればそれも候補として考える価値がある。ここでは、XMLでなければならない状況を抱えていると仮定して話を進めよう。

 解析の遅さの問題は主に2つの種類に分けられる。1つは、全体の処理時間に対する解析が費やした時間の割合である。例えば、あるXML文書に何回もアクセスするプログラムがあったとき、アクセスするごとに解析を行えば、「解析に要する時間×アクセス回数」の時間が費やされる。しかし、解析した結果を捨てずに保存しておき、繰り返し使うようにすれば、解析に要する時間は何回アクセスする場合でも、1回分で済む。このようなテクニックは、XMLに限らずよく使われるものだが、特にXMLでは高い確率で必要とされる。

 もう1つは、データをリクエストしてから、それが得られるまでに要する時間である。例えば、1万件のデータがあるとしよう。これを処理する方法として、「1つのXML文書に入れてしまう方法」と「1件のデータに付き1個のXML文書を作成して1万個のファイルを作成する方法」が考えられる。どちらの方法を選択しても構わないなら、「1万個のファイルを作成する方法」を選択するのが望ましい。そうすると、データをリクエストしてから得られるまでに要する時間を短縮することができるからだ。「1つのXML文書にすべてを入れる方法」を選んだ場合、DOMを使うと、1万個のデータすべての解析が終わるまで待たされることになる。XmlReaderで読み取る場合は、すべて解析し終わる前にデータが得られるが、もし1万個の中の最後の1個が目的のデータである場合、結局、1万個を解析することになる。それに対して、小さなXML文書を多数作る方法であれば、目的のデータを含むXML文書を解析するわずかな時間で目的を達成できる。

 しかし、「1万個のファイルを作成する方法」は、要求されたデータがどのファイルに入っているかを容易に判断できない場合には使えないテクニックである。筆者は、ファイル名に必要なデータを識別するための情報(IDや検索キーワードなど)を含めておくという対処方法を取っている。これを行うにはファイル名の付け方まで含めた慎重な設計が必要となるが、条件が複雑な場合にはうまく実現できない可能性もある。


 INDEX
  .NETで簡単XML
  第16回 川俣流XMLプログラミングの定石(2)
    1.データを処理しやすい形に変換するタイミング
  2.DOMツリーからデータを抜き取る方法
    3.XML文書とクラスの1対1対応およびXML文書のキャッシュ
    4.文字列定数にXML文書を書き込む場合の改行とインデント
 
インデックス・ページヘ  「連載 :.NETで簡単XML」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間