EXCELのXMLデータを表示する

 多くのユーザーに利用されているマイクロソフト社のExcel(エクセル)で保存されたXML形式のデータを扱ってみます。あいにくWindows版のExcelは所有していないためMacOS X版のExcel 2004で保存したものを利用します(実際のデータを見る)。
 Excel 2004 for Macで保存されたXMLファイルを、そのまま扱おうとするとSafariでは日本語などが文字化けを起こしてしまいます。原因としてはUTF-8を示すBOM (Byte Order Mark) がないことです。このため、ここでは他のソフトを使ってBOM付きのXMLファイルにして利用することにします。サーバー側でBOMを付けてクライアント(ブラウザ側)に返すのも方法の1つかもしれません。また、XML宣言でエンコードをUTF-8であるというのも明示するようにしてあります。

 ExcelのXMLデータは、あまり複雑なものではありません。まず、最初に最初のシートに記述されたデータを表示させましょう。データはDataタグに実際のデータ値が記述されています。つまりgetElementByTagNameでDataタグを指定し、その値を読み出せば表の内容を表示することができます。ただし、この方法では最初のシートだけでなく全てのシートに含まれるDataタグが対象となってしまい全てのデータが表示されてしまいます。最初のシートだけを対象にするため、最初に記述されているTableタグの属性を参照します。Tableタグのss:ExpandedColumnCount属性には横のデータ数、ss:ExpandedRowCount属性には縦のデータ数が記述されています。この値を読み出してfor命令で繰り返せば最初のシートのデータを表示させることができます。
 属性の値を読み出すにはgetAttribute()を使います。引数に読み出したい属性名を指定すると、その属性値を返します。実際のスクリプトは以下のようになります。(スクリプトを実行する

<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=shift_jis">
<title>ExcelのXMLデータを表示する</title>
<script type="text/javascript" src="xmlhttp.js"></script>
<script type="text/javascript"><!--
function loadDataFile(fName)
{
httpObj = createXMLHttpRequest(displayData);
if (httpObj)
{
httpObj.open("GET",fName,true);
httpObj.send(null);
}
}
function displayData()
{
if ((httpObj.readyState == 4) && (httpObj.status == 200))
{
$("result").innerHTML = parseMap(httpObj.responseXML);
}
}
// ExcelのXMLデータを解析して表示
function parseMap(excelXML)
{
var cObj = excelXML.getElementsByTagName("Table");
var cellW = cObj[0].getAttribute("ss:ExpandedColumnCount");
var cellH = cObj[0].getAttribute("ss:ExpandedRowCount");
var dataTag = excelXML.getElementsByTagName("Data");
var result = "<table border='1' bordercolor='black'>";
for (h=0; h<cellH; h++)
{
result += "<tr>";
for (var w=0; w<cellW; w++)
{
var n = h * cellW + w;
result += "<td>"+dataTag[n].childNodes[0].nodeValue + "</td>";
}
result += "</tr>";
}
result += "</table>";
return result;
}
// --></script>
</head>
<body>
<h1>ExcelのXMLデータを表示する</h1>
<p>ExcelのXMLデータを読み込み表示します</p>
<form name="ajaxForm">
<input type="button" value="sample.xml読み込み" onClick="loadDataFile('sample.xml')">
</form>
<div id="result"></div>
</body>
</html>

 このスクリプト一見すると何の問題もないように見えますが、OperaとSafariでは動作しません。これは属性名が正しく取得できないためです。属性名に:が入っていると駄目みたいな感じなので、以下のように直接属性の位置を指定して読み出せばSafariとOperaでも動作するようになります。(実際のスクリプトを実行する

var cellW = cObj[0].attributes[0].value;
var cellH = cObj[0].attributes[1].value;

 しかし、これらの手法では無理があります。最初のスクリプトでは複数あるシートのデータを無視する形になりますし、次のスクリプトでは属性の記述する順番が変われば動作しなくなってしまいます。さらに、これらはDOM tree (DOMツリー、ドキュメントツリー)を無視する状態に近いため、スマートではありませんし、今後の主流にはなりえない書き方です。そこで、複数のワークシートにも対応し、ドキュメントツリーを利用したスクリプトに変更します。
 まず、スクリプトを記述する前にExcelで出力されたXMLのドキュメントツリーを見てみましょう。ワークシート部分は以下のようになります。

Worksheet
 ┗Table
   ┗Row
    ┗Cell
     ┗Data

 実際のデータはDataタグで囲まれた部分にあります。このドキュメントツリーをたどってデータ値までいきつくにはchildNodesを列記します。まず、wsObj = getElementsByTagName("Worksheet")としてワークシートタグの情報を取得します。結果は配列になります。配列の数がワークシートの数になります。ここでは最初のワークシートを参照するのでwsObj配列の0番を指定します。wsObj[0]からTableタグを指定するには

wsObj[0].childNodes[1]

とします。このchildNodes[1]の1は何なのでしょうか? 最初に出てくるタグなら0番目になるはずです。実は、ここが困った部分でInternet Explorer 6とそれ以外のブラウザ (Firefox, Opera, Safari) ではXMLデータに空白/改行が入っているかどうかでドキュメントツリーの参照順が変わってしまうのです。Excelで出力されたデータは整形されているため、タグとタグの間に空白/改行が入ってしまっています。Firefoxなどでは、これもノードの1つとしてカウントされてしまいます。このためTableタグの前の空白が0番目、Tableタグ自体が1番目となります。ところが、このような決めうちして番号を指定してしまうとInternet Explorer 6では動かなくなってしまいます。逆にInternet Explorer 6で動くようにするとFirefoxなど他のブラウザで動作しなくなってしまいます。この解決方法については後で説明するとして、まずはFirefoxなどで動作するスクリプトを作ります。
 Tableタグの次にたどるのはRowタグです。しかし、前にも書いたようにタグの間に空白が入っているとタグ情報が正しく取得できません。このような場合には、ノードの種類とタグ名を調べ該当するものであれば処理を行います。ノードの種類を調べるにはnodeTypeプロパティを参照します。このプロパティの値が1であればエレメントノード、つまりタグです。値が3であればテキスト、つまり空白や改行および文字ということになります。nodeTypeが1だったらタグ名を調べます。タグ名はtagNameプロパティを参照します。Rowタグの次にたどるのはCellタグです。同様にノードの種類を調べます。Cellタグの次にたどるのはDataタグです。やはり同様の方法で調べます。
 DataタグまでたどることができればDataタグの最初のノードの値を読み出します。これが実際のデータの値になります。以下のスクリプトのn = cellObj.childNodes[cnt].childNodes[0].nodeValue;が、そのデータを読み出す部分です。値はnodeValueプロパティで読み出す事ができます(dataプロパティでもできる)。
 データを読み出したら、出力するタグと連結していきます(本当は以前の項にも書いたように、データとプログラムが分離していないため好ましくない。ただInternet Explorer 6の都合があるので、ここではデータとプログラムは分離しないままにします)。
 これでデータを表示することができます。(実際のスクリプトを実行する

<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=shift_jis">
<title>ExcelのXMLデータを表示する3</title>
<script type="text/javascript" src="xmlhttp.js"></script>
<script type="text/javascript"><!--
function loadDataFile(fName)
{
httpObj = createXMLHttpRequest(displayData);
if (httpObj)
{
httpObj.open("GET",fName,true);
httpObj.send(null);
}
}
function displayData()
{
if ((httpObj.readyState == 4) && (httpObj.status == 200))
{
$("result").innerHTML = parseMap(httpObj.responseXML);
}
}
// ExcelのXMLデータを解析して表示 (DOM Tree)
function parseMap(excelXML)
{
var wsObj = excelXML.getElementsByTagName("Worksheet");
tableTag = wsObj[0].childNodes[1]; // Table
var result = "<table border='1' bordercolor='black'>";
for (h=0; h<tableTag.childNodes.length; h++)
{
// ■Rowタグか?
if (tableTag.childNodes[h].nodeType == 1 )
{
result += "<tr>";
rowObj = tableTag.childNodes[h];
for (w=0; w<rowObj.childNodes.length; w++)
{
// ■Cellタグか?
if ((rowObj.childNodes[w].nodeType == 1) && (rowObj.childNodes[w].tagName == "Cell"))
{
cellObj = rowObj.childNodes[w];
for (cnt=0; cnt<cellObj.childNodes.length; cnt++)
{
// ■Dataタグか?
if ((cellObj.childNodes[cnt].nodeType == 1) && (cellObj.childNodes[cnt].tagName == "Data"))
{
n = cellObj.childNodes[cnt].childNodes[0].nodeValue;
result += "<td>"+n+ "</td>";
}
}
}
}
result += "</tr>";
}
}
result += "</table>";
return result;
}
// --></script>
</head>
<body>
<h1>ExcelのXMLデータを表示する3</h1>
<p>ExcelのXMLデータを読み込み表示します</p>
<form name="ajaxForm">
<input type="button" value="sample.xmlのSheet1を読み込み" onClick="loadDataFile('sample.xml')"><br>
</form>
<div id="result"></div>
</body>
</html>

 このスクリプトではInternet Explorer 6では動作しません。前にも書いたようにTableタグの位置を決めうちしてしまっているためです。これを解決するには、Worksheetに含まれるノードの数だけ繰り返しTableタグなのかどうかを調べます。つまり

tableTag = wsObj[0].childNodes[1];

の行を以下のように変更します。

for (i=0; i<wsObj[0].childNodes.length; i++)
{
if ((wsObj[0].childNodes[i].nodeType == 1) && (wsObj[0].childNodes[i].tagName == "Table"))
{
tableTag = wsObj[0].childNodes[i];
break;
}
}

 ノードの種類を調べてタグ名がTableかどうか調べていくわけです。これでInternet Explorer 6でもFirefoxなど他のブラウザでも動作するようになります。(実際のスクリプトを実行する
 最初のワークシートを表示することができたので、XMLデータに含まれている任意のワークシートを表示できるように変更します。指定されたワークシートが存在しない場合には、ワークシートはありません、といったエラーメッセージを表示します。
 ワークシート名ですが、これはWorksheetタグのss:Name属性に記述されています。Excel 2004 for Macで出力されたXMLの場合、Worksheetタグには他に属性は記述されていません。ss:Nameとなっている属性はgetAttribute()を使うとSafariとOperaでは読み出す事ができないのでattributes配列を参照します。これは、すでに説明しました。あとはfor命令を使ってWorksheetタグの数だけ繰り返し指定されたシート名が存在するかどうか調べます。実際のスクリプトは以下のようになります。(スクリプトを実行する

<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=shift_jis">
<title>ExcelのXMLデータを表示する5</title>
<script type="text/javascript" src="xmlhttp.js"></script>
<script type="text/javascript"><!--
sheetName = "";
function loadDataFile(fName,sName)
{
sheetName = sName;
httpObj = createXMLHttpRequest(displayData);
if (httpObj)
{
httpObj.open("GET",fName,true);
httpObj.send(null);
}
}
function displayData()
{
if ((httpObj.readyState == 4) && (httpObj.status == 200))
{
$("result").innerHTML = parseMap(httpObj.responseXML);
}
}
// ExcelのXMLデータを解析して表示 (複数シート)
function parseMap(excelXML)
{
var wsObj = excelXML.getElementsByTagName("Worksheet");
var flag = false;
for (var j=0; j<wsObj.length; j++)
{
if (wsObj[j].attributes[0].value == sheetName)
{
for (var i=0; i<wsObj[j].childNodes.length; i++)
{
if ((wsObj[j].childNodes[i].nodeType == 1) && (wsObj[j].childNodes[i].tagName == "Table"))
{
tableTag = wsObj[j].childNodes[i];
flag = true;
break;
}
}
}
}
if (!flag) return "指定したシートは存在しません";
var result = "<table border='1' bordercolor='black'>";
for (h=0; h<tableTag.childNodes.length; h++)
{
// ■Rowタグか?
if (tableTag.childNodes[h].nodeType == 1 )
{
result += "<tr>";
rowObj = tableTag.childNodes[h];
for (w=0; w<rowObj.childNodes.length; w++)
{
// ■Cellタグか?
if ((rowObj.childNodes[w].nodeType == 1) && (rowObj.childNodes[w].tagName == "Cell"))
{
cellObj = rowObj.childNodes[w];
for (cnt=0; cnt<cellObj.childNodes.length; cnt++)
{
// ■Dataタグか?
if ((cellObj.childNodes[cnt].nodeType == 1) && (cellObj.childNodes[cnt].tagName == "Data"))
{
n = cellObj.childNodes[cnt].childNodes[0].nodeValue;
result += "<td>"+n+ "</td>";
}
}
}
}
result += "</tr>";
}
}
result += "</table>";
return result;
}
// --></script>
</head>
<body>
<h1>ExcelのXMLデータを表示する5</h1>
<p>ExcelのXMLデータを読み込み表示します</p>
<form name="ajaxForm">
<input type="button" value="sample.xmlのSheet1を読み込み" onClick="loadDataFile('sample.xml','Sheet1')"><br>
<input type="button" value="sample.xmlのSheet2を読み込み" onClick="loadDataFile('sample.xml','Sheet2')"><br>
<input type="button" value="sample.xmlのSheet3を読み込み" onClick="loadDataFile('sample.xml','Sheet3')"><br>
<input type="button" value="sample.xmlの存在しないシートを読み込み" onClick="loadDataFile('sample.xml','mz721')"><br>
</form>
<div id="result"></div>
</body>
</html>

 DOMツリー/ドキュメントツリーを、うまくたどることができれば上記のような手法でブラウザを問わずにXMLデータにアクセスすることができます。
 次項ではデータベースソフトであるFile Makerで出力したXMLデータを表示します。

[第三章 5:ファイルメーカーのXMLデータを表示するへ]
[目次へ]

(2006.1.6)