トップC# > XMLパーサー

XMLパーサー

1.対象とする事例

当ホームページは多くの目次ページを有するが、それらは個々に作成・編集しているわけではなく、 ホームページ全体を下に示すようなただ一つのファイル toc.xml で管理している。

一つの複数の divセクションから成り、一つの divセクションは一つ以上の tableからなる。 このファイルtoc.xmlはhtmlブラウザで表示する訳ではないが、使い慣れた htmlタグを使用している。

現在はC言語プログラムでこのファイルを解釈し、無数の目次ファイルに展開しているが、 これを C# に置き換えてみよう。

現在は厳密に XMLの規格に従う必要はないが、C#では XML関連機能を使う関係から、 必要に応じて toc.xml の記述規則を修正する。

<div name="デジタル回路" id="dc">
<table>
<caption>デジタル回路[Java Applet版]</caption>
<tr><td>dc</td><td>dc</td><td></td></tr>
<tr><td>シミュレータの操作方法</td><td>dc/manual.html</td><td></td></tr>
[中略]
</table>

<table>
<caption>デジタル回路[JavaScript版]</caption>
<tr><td>dc2</td><td>dc2</td><td></td></tr>
<tr><td>シミュレータの操作方法</td><td>dc2sim/manual01.html</td><td></td></tr>
[中略]
</table>

[中略]
</div>

2.DOM

XML 文書を扱う API として、DOM (Document Object Model) を使用する。 DOM は、W3C によって勧告されている API であり、XML 文書を予めメモリに読み込んで 仮想的な階層構造ツリー (DOM ツリー) を作成する。 そして、DOM は、階層化された XML 文書の要素や属性に対し直接アクセスするものである。

XML文書を読み込み、読み込んだ結果を表示するだけのプログラムを次に示す。

using System;
using System.Xml;

class XmlParser {
    static void Main() {
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load("c:/mh/www/toc.xml");
        Console.Write(xmlDoc.InnerXml);
    }
}

XMLの規格に合わせるために、下のように、先頭に2行、末尾に1行を追加した。

<?xml version="1.0" encoding="shift_jis"?>
<root>
[ここは従来と同じ]
</root>

従って、全体の階層構造は [root]-[div]-[table] である。 目次画面左の大項目目次は次のようにして生成している。

XmlNodeList nodeList = xmlDoc.SelectNodes("/root/div"); によって divノードが抽出できる。 このリストの要素 nodeDiv が一つの divノード(divタグ)にあたる。 divタグは <div name="デジタル回路" id="dc"> のように二つの属性 name と id を持っている。 この属性の値 "デジタル回路" は nodeDiv.Attributes.GetNamedItem("name").Value によって取り出すことができる。

  XmlNodeList nodeList = xmlDoc.SelectNodes("/root/div"); 
  for (int i = 0; i < nodeList.Count; i++) {
      XmlNode nodeDiv = nodeList[i];
      string name = nodeDiv.Attributes.GetNamedItem("name").Value;
      string id  = nodeDiv.Attributes.GetNamedItem("id").Value;
      sbHeader.AppendLine("<li><a href=\"" + id + ".html\">" + name + "</a></li>");
  }

このdivノードは、1つ以上の子ノードを持つ。子ノードは tableノードである。 右側のページメニューを作成する場合にも、一部 divノードの情報が必要であり、 目次生成プログラムでは、スキャンを二度行っている。

nodeList.Countは divノードの個数、nodeDiv.ChildNodes.Count は divノードに含まれる tableノードの個数を表す。 同様に nodeTable.ChildNodes.Count は tableノードの子ノードの個数を表す。

tableノードの子ノードとしては captionノード、trノード、hrノードの三つのタイプがある。

これらは子ノードとしては対等である。このタグ名は Nameプロパティにより得られる。 captionノードは属性を持たず、目次のタイトルは 開始タグと終了タグに挟まれた文字列である。 この文字列はInnerTextプロパティによって取り出すことができる。 captionノードはtableノードnodeTableの最初の子ノードであるから、文字列は nodeTable.ChildNodes[0].InnerText となる。 trノードは三つの td ノードを子コードとして持つ。最後の子ノードが現時点では使用していない。 tdノードも属性は待たず、開始タグと終了タグで挟まれた文字列を目次生成に使用する。 この取り出しは先に述べたように、InnerTextプロパティを使う。

ページメニュー生成プログラムを下に示す。

  for (int i = 0; i < nodeList.Count; i++) {
      XmlNode nodeDiv = nodeList[i];
      string div = nodeDiv.Attributes.GetNamedItem("name").Value;
      string id = nodeDiv.Attributes.GetNamedItem("id").Value;
      string path = wwwdir + "/_toc/" + id + ".html";
      StringBuilder sbToc = new StringBuilder();
      sbToc.Append(header.Replace("$title$", div));

      // table: captionがある. また, 先頭レコードは特別扱い
      for (int j = 0; j < nodeDiv.ChildNodes.Count; j++) {
          XmlNode nodeTable = nodeDiv.ChildNodes[j];
          // caption
          string caption = nodeTable.ChildNodes[0].InnerText;     // #0
          XmlNode nodeDir = nodeTable.ChildNodes[1];              // #1
          // 先頭レコード
          string code = nodeDir.ChildNodes[0].InnerText;  // 現在は使用していない
          string dir = nodeDir.ChildNodes[1].InnerText;
          sbToc.AppendLine("<a name=\"" + dir + "\"></a>");
          sbToc.AppendLine("<h1>" + caption + "</h1>");
          sbToc.AppendLine("<ul style=\"list-style-type: disc;\">");
          for (int k = 2; k < nodeTable.ChildNodes.Count; k++) {
              XmlNode nodeRecord = nodeTable.ChildNodes[k];
              if (nodeRecord.Name == "hr") {
                  sbToc.AppendLine("<hr/>");
                  continue;
              }
              // <tr>
              string title = nodeRecord.ChildNodes[0].InnerText;
              string url = nodeRecord.ChildNodes[1].InnerText;
              sbToc.AppendLine("<li><a href=\"../"+ url +"\">"+ title +"</a></li>");
          }
          sbToc.AppendLine("</ul>");
          genRefresh(caption, id, dir);
      }
  }

目次生成プログラム全体のソースコードを下に示す。

using System;
using System.IO;
using System.Text;
using System.Xml;

class XmlParser {
    const string idxheadfile = "c:/mh/www/idxhead.txt";
    const string idxbodyfile = "c:/mh/www/idxbody.txt";
    const string topfile = "c:/mh/www/top.txt";
    const string xmlfile = "c:/mh/www/toc.xml";
    const string wwwdir  = "c:/mh/www";
    static Encoding SJIS = Encoding.GetEncoding("Shift_JIS");

    static void Main() {
        // xmlファイルを読み込む
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(xmlfile);

        // ① 全目次ファイル共通部. $title$は後で置き換え
        StringBuilder sbHeader = new StringBuilder();
        sbHeader.Append(File.ReadAllText(idxheadfile,SJIS));
        sbHeader.AppendLine("<!--先頭からここまで idxhead.txt をコピー-->");
        sbHeader.AppendLine("<!--タイトルはプログラムにより生成-->");
        sbHeader.AppendLine("<title>$title$</title>\r\n</head>");
        sbHeader.AppendLine("<!--これよりidxbody.txtをコピー-->");
        sbHeader.Append(File.ReadAllText(idxbodyfile,SJIS));
        sbHeader.AppendLine("<!--これよりプログラムにより生成-->");

        // 左の大項目メニュー部
        XmlNodeList nodeList = xmlDoc.SelectNodes("/root/div"); 
        // div
        for (int i = 0; i < nodeList.Count; i++) {
            XmlNode nodeDiv = nodeList[i];
            string name = nodeDiv.Attributes.GetNamedItem("name").Value;
            string id  = nodeDiv.Attributes.GetNamedItem("id").Value;
            sbHeader.AppendLine("<li><a href=\"" + id + ".html\">" + name + "</a></li>");
        }
        sbHeader.AppendLine("</ul>");
        sbHeader.AppendLine("</td>");
        sbHeader.AppendLine("<td valign=\"top\">");
        string header = sbHeader.ToString();    // メニューファイル共通部
      
        // ② 個別部. 右のページメニュー部
        string tail = "<!--これよりプログラムにより生成-->\r\n";
        tail += "</td></tr></table>\r\n";
        tail += "</div>\r\n";
        tail += "</body>\r\n</html>\r\n";
        // 最上位のページ
        string tocTop = header.Replace("$title$", "Hatada's Home Page");
        tocTop += "<!--これよりtop.txtをコピー-->\r\n";
        tocTop += File.ReadAllText(topfile,SJIS);
        tocTop += tail;
        File.WriteAllText(wwwdir + "/_toc/index.html", tocTop, SJIS);

        // 個々の目次ページ
        for (int i = 0; i < nodeList.Count; i++) {
            XmlNode nodeDiv = nodeList[i];
            string div = nodeDiv.Attributes.GetNamedItem("name").Value;
            string id = nodeDiv.Attributes.GetNamedItem("id").Value;
            string path = wwwdir + "/_toc/" + id + ".html";
            StringBuilder sbToc = new StringBuilder();
            sbToc.Append(header.Replace("$title$", div));

            // table: captionがある. また, 先頭レコードは特別扱い
            for (int j = 0; j < nodeDiv.ChildNodes.Count; j++) {
                XmlNode nodeTable = nodeDiv.ChildNodes[j];
                // caption
                string caption = nodeTable.ChildNodes[0].InnerText;     // #0
                XmlNode nodeDir = nodeTable.ChildNodes[1];              // #1
                // 先頭レコード
                string code = nodeDir.ChildNodes[0].InnerText;  // 現在は使用していない
                string dir = nodeDir.ChildNodes[1].InnerText;
                sbToc.AppendLine("<a name=\"" + dir + "\"></a>");
                sbToc.AppendLine("<h1>" + caption + "</h1>");
                sbToc.AppendLine("<ul style=\"list-style-type: disc;\">");
                for (int k = 2; k < nodeTable.ChildNodes.Count; k++) {
                    XmlNode nodeRecord = nodeTable.ChildNodes[k];
                    if (nodeRecord.Name == "hr") {
                        sbToc.AppendLine("<hr/>");
                        continue;
                    }
                    // <tr>
                    string title = nodeRecord.ChildNodes[0].InnerText;
                    string url = nodeRecord.ChildNodes[1].InnerText;
                    sbToc.AppendLine("<li><a href=\"../"+ url +"\">"+ title +"</a></li>");
                }
                sbToc.AppendLine("</ul>");
                genRefresh(caption, id, dir);
            }
            sbToc.Append(tail);
	    string newToc = sbToc.ToString();
            string oldToc = File.ReadAllText(path, SJIS);
	    if (newToc != oldToc) File.WriteAllText(path, newToc, SJIS);
	    else Console.WriteLine("no change: " + path);
        }
    }

    static void genRefresh(string title, string id, string dir) {
        string url = (dir.IndexOf('/') > 0 ? "../" : "") + "../_toc/" + id + ".html#" + dir;
        string path = wwwdir + "/" + dir + "/index.html";
        if (File.Exists(path)) return;
        StringBuilder sbHtml = new StringBuilder();
        sbHtml.AppendLine("<html>");
        sbHtml.AppendLine("<head>");
        sbHtml.AppendLine("<title>" + title + "</title>");
        sbHtml.AppendLine("<meta http-equiv=\"REFRESH\" content=\"0;url=" + url + "\">");
        sbHtml.AppendLine("</head>");
        sbHtml.AppendLine("<body>");
        sbHtml.AppendLine("自動転送します。<br>");
        sbHtml.AppendLine("転送されない場合はこちらをクリックして下さい。<br>");
        sbHtml.AppendLine("<a href=\"" + url + "\">クリック</a>");
        sbHtml.AppendLine("</body>");
        sbHtml.AppendLine("</html>");
        File.WriteAllText(path, sbHtml.ToString(), SJIS);
    }

}

A.リファレンス

[1] XML 操作入門[msdn]