「XMLからJSONへの変換をどうすべかねぇ」のコメント欄にて、amanoさんより、GoogleがGDataで採用しているXML-to-JSON変換を教えていただきました。
「なんか似たことをやっているな」とは思ったのですが、GDataの方式は、残念ながら我々の目的には使えません。例えば、次のXHTML断片を考えてみます。
<p>うら<em>には</em>にはには<br /><em>には</em>とりがいます。</p>
CatyのXML-to-XJSON変換を使うと次のようになります。
@p {"":
["うら", @em {"" :["には"]}, "にはには", @br {"":[]},
@em { "" :["には"]}, "とりがいます。"]
}
略記(ショートハンド)を認めれば、かなり簡潔に書けます。
@p ["うら", @em "には", "にはには", @br {}, @em "には", "とりがいます。"]
GDataの方式では、この例をJSONに変換できません。変換ルールを補って無理やりに変換してみると、結果は次のとおり。
{
"p" : {
"em" : [{"$t" : "には"}, {"$t": "には"}],
"br" : {},
"$t" : "うらにはにはにはにはとりがいます。"
}
}
もとのXML構造は壊れてしまい、再現不可能です。という事情で、GData方式の変換に興味は湧かなかったのですが、特定のXMLデータだけに制限すれば、GData方式は効率的な変換ができるので、GData方式も悪くないと最近思い直しました。
GDataのXML-JSON変換方式
例題として、次のXMLデータを使います。
<person gender="male">
<age>26</age>
<hobby>釣り</hobby>
<hobby>盆栽</hobby>
<name><family>坂東</family><given>トン吉</given></name>
<address type="home">東京都目黒区北目黒大目玉 0-100-99</address>
</person>
GDataの方式に従うと、XML要素はJSONオブジェクトのプロパティに変換されます。基本的な形は次のとおりです。
- "タグ名" : { <属性と子ノード> }
上の例では次のようになります。
"person" : { <属性と子ノード> }
属性と子ノードには次があります。
- gender属性
- age子要素
- hobby子要素(2つ)
- name子要素
- address子要素
まず、属性は文字列値のプロパティとします。
"person" : {
"gender" : "male",
<子ノード>
}
age要素を "age" : "26"
とやってしまうと属性と区別が付かななくなります。要素のときは、値を文字列ではなくて必ずオブジェクトとします。
"person" : {
"gender" : "male",
"age" : {<ageの子ノード>},
<personの残りの子ノード>
}
ageの子ノードはテキストノードです。テキストノードには、属性名や要素タグ名のような名前がありません。そこで、"$t" という特別な名前を与えて属性と同じように記述します。これは、「内容テキストは、$t という名前の属性値とみなす」と言ってもいいでしょう。
"person" : {
"gender" : "male",
"age" : {"$t" : "26"},
<personの残りの子ノード>
}
hobby要素も同じようにすると:
"person" : {
"gender" : "male",
"age" : {"$t" : "26"},
"hobby" : {"$t" : "釣り"},
"hobby" : {"$t" : "盆栽"},
<personの残りの子ノード>
}
おっと、これはダメです。JSONオブジェクトに同じ名前のプロパティを複数は入れられません。こんなときは、値を配列にします。
"person" : {
"gender" : "male",
"age" : {"$t" : "26"},
"hobby" : [{"$t" : "釣り"}, {"$t" : "盆栽"}],
<personの残りの子ノード>
}
<name><family>坂東</family><given>トン吉</given></name>
の部分は次のようですね。
"name" : {
"family" : {"$t": "坂東"},
"given": {"$t": "トン吉"}
}
これを入れ子にしてはめ込むと:
"person" : {
"gender" : "male",
"age" : {"$t" : "26"},
"hobby" : [{"$t" : "釣り"}, {"$t" : "盆栽"}],
"name" : {
"family" : {"$t": "坂東"},
"given": {"$t": "トン吉"}
},
<personの残りの子ノード>
}
<address type="home">東京都目黒区北目黒大目玉 0-100-99</address>
は、typeという名の属性と、$tという名の属性があるように扱えばいいので:
"person" : {
"gender" : "male",
"age" : {"$t" : "26"},
"hobby" : [{"$t" : "釣り"}, {"$t" : "盆栽"}],
"name" : {
"family" : {"$t": "坂東"},
"given": {"$t": "トン吉"}
},
"address" : {
"type" : "home",
"$t" : "東京都目黒区北目黒大目玉 0-100-99"
}
}
JSONでは、プロパティだけではデータと認められないので、単一のプロパティを持つオブジェクトの形にします。
{
"person" : {
"gender" : "male",
"age" : {"$t" : "26"},
"hobby" : [{"$t" : "釣り"}, {"$t" : "盆栽"}],
"name" : {
"family" : {"$t": "坂東"},
"given": {"$t": "トン吉"}
},
"address" : {
"type" : "home",
"$t" : "東京都目黒区北目黒大目玉 0-100-99"
}
}
}
はい、できあがり。
どのようなXMLデータなら変換できるのか
XML要素の内容モデルは、次の3種に分類できます。
- 要素内容モデル -- 子ノードとして要素ノードしか出現しない
- テキスト内容モデル -- (DOM正規化の後で)子ノードとしてただ1つのテキストノードしか出現しない
- 混合内容モデル -- 子ノードとして要素ノードとテキストノードが混じる可能性がある
GData方式では、混合内容モデルの要素はJSONに変換できません。要素内容モデルまたはテキスト内容モデルである要素(の型)が変換対象となります。さらに、子要素の出現順序に意味があると順序情報が失われるのでダメです。変換可能である条件をまとめると次のようになります。
- 要素の子ノードは単一のテキスト、またはいくつかの要素ノードであり、テキストと要素が混じることはない。
- 要素の内容が子要素の列であるとき、それらの子要素の出現順序に意味がない。
この条件を満たすXMLデータはけっこう多いので、実用上の適用範囲は狭くはないでしょう。では、この変換可能性の条件を満たせば、何も考えなくても変換できるでしょうか? 実はそうでもないんです。
空白類の扱いをどうするか
XML処理をするときに毎度頭が痛いのが空白類(ホワイトスペース)の扱い方です。インスタンスだけを見ていては、空白類だけのテキストを捨てていいかどうかサッパリわからないので、安全のためには保存するしかありません。xml:space属性が処理のヒントになりますが、xml:spaceを常に期待できるわけじゃありません。
例題であった次のXMLインスタンスをDOMツリーに展開すると、空白類だけのテキストノードがたくさんできます。
<person gender="male">
<age>26</age>
<hobby>釣り</hobby>
<hobby>盆栽</hobby>
<name><family>坂東</family><given>トン吉</given></name>
<address type="home">東京都目黒区北目黒大目玉 0-100-99</address>
</person>
となると、内容モデルは混合内容モデルとなり、GData方式では変換不可能となります。
じゃ、あらゆる空白類を捨ててしまうことにするとどうなるでしょうか。次は、プリティプリンタの設定ファイルだとします。
<prettyPrint>
<indentString> </indentString>
<delimiter> </delimiter>
<terminator></terminator>
</prettyPrint>
この設定の意味は:
- インデントには間隔空白(0x20)4つを使う
- 区切り記号には間隔空白1つを使う
- 終端記号には改行を2つ使う
空白類を削除してしまうと、設定情報がまったく失われてしまいます。
じゃあどうするの? って話は、何年も前に何度も書いたりしゃべったりしたことがあるんで、僕としては食傷気味。気が向いたら書くかもしれませんが、特にうまい方法があるわけじゃなくて、処理系に空白処理の方法を別途与えるしかないのです。