RDF/XMLのためのSAXパーサの実装(イベントハンドラの実装)をしたら大変だったのでメモ。
大変なので、可能な限りライブラリを使いましょう。たぶんrdf4jとかで大抵のことはできるような気がします。
将来の(色々なことを忘れたときの)自分用のメモなので、感想ポエムを含んだイントロになっています。
実際のソースコードは載っていません。再実装するときや実装を読み解く時のヒントとして。
イントロ
RDFとは
- 有向グラフで表されるデータ構造。
- 有向辺自体がPredicate、辺の始点がSubject、終点がObjectとそれぞれ呼ばれる
- グラフを辺集合の形で、
<Subject,Predicate,Object>
の対で表す - 頂点は「ノード要素」(名前がややこしいが、この記事では、グラフ理論上のノードは「頂点」と表記している。「ノード」とあるものは「ノード要素」を指している。)と「プロパティ要素」がある。
- Subject は 必ずノード要素である
- Object は ノード要素あるいはプロパティ要素がありうる
- したがって
(ノード要素) × (弧) × (ノード要素 or プロパティ要素)
の集合としてデータを表している - ノード要素はIDを持つ
- ノード要素のIDは明示的に与えられている場合と、そうでない場合があり、後者はblank node(一般的な訳語:空白ノード。「無名ノード」とかの方がJavaプログラマにはわかりやすい気がする)と呼ばれる
RDF/XML
- 上記の
(ノード要素) × (弧) × (ノード要素 or プロパティ要素)
の集合をXMLで表したもの。 - 基本は以下の2つで表される
<rdf:Description rdf:about="SubjectのノードID">
<Predicate>
<rdf:Description rdf:about="ObjectのノードID">
</rdf:Description>
</Predicate>
</rdf:Description>
<rdf:Description rdf:about="SubjectのノードID">
<Predicate>
Objectのプロパティの値
</Predicate>
</rdf:Description>
- が、省略記法のルールが多く存在し、パースするのは大変である
- (たぶん、RDFモデルを人間様が見てもわかりやすいように出力するために、さまざまな省略ができるようになっているように見え、パースするコンピュータ様(のためにパーサを書くためのレベル低めのプログラマ)には優しくない仕様、に見える。※レベル低めのプログラマの感想)
プログラムで扱うときに困ること
- グラフ構造になっているので、DOM的な辿り方はできない
- DOM的なXMLツリーの場合は、「HogeのFugaのPiyoが見たい(例:A部署のBさんの電話番号)」と思ったときに、ツリー化されたXMLであれば、
Hoge/Fuga/Piyo
(/部署[@部署名=A]/社員[@氏名=B]/電話番号/text()
) 的なものを取ってくればよいが、XMLとしてはツリー化されないので、HogeのID⇒FugaのIDを取ってきて、FugaのID⇒Piyoの値を取ってくる、みたいなプロセスを踏む必要がある。データ量が大きい場合は、ノード要素のIDでインデクス(ソート木でもハッシュテーブルでも)を作っておいて、検索できるようにしておかないと、常に全部走査することになって遅くなる
- DOM的なXMLツリーの場合は、「HogeのFugaのPiyoが見たい(例:A部署のBさんの電話番号)」と思ったときに、ツリー化されたXMLであれば、
- XMLとして等価でなくても、RDFとして(表現される構造)は等価でありうる
- 省略記法がたくさんあってつらい
といったことがあるので、XMLそのままでは色々と都合が悪く、(ノード要素) × (弧) × (ノード要素 or プロパティ要素)
の集合の形に書き直す必要がある。
SAXパーサで書く時のポイント
そんなわけで、SAXパーサで頭から読んでいって、確定した(ノード要素) × (弧) × (ノード要素 or プロパティ要素)
の値を外(ファイルなりDBなり)に書き出していく。
SAXパーサ(のイベントハンドラ)を実装していくうえでのポイントは以下。
- パーサに状態を持たせて、モードに合わせて状態遷移させる必要がある
rdf:parseType="Collection", rdf:parseType="Literal", rdf:parseType="Resource"
- 特に指定のない場合に2パターン
- ↑後者の、指定のない場合に2パターンいるのは、主語が読み込み終わっている場合(次の要素をPredicateとしてパースする)と、Predicateまで読みこみ終わっている場合(次の要素をObjectのノード要素あるいはプロパティ要素)で状態を分けるため。
状態は {Root, S1, S2, Collection, Resource, Literal} があって(S1はPredicateのパースが未了、S2はPredicateのパースが済んでいる)、エレメントが開くたびに状態が遷移する。状態遷移のたびに現在の状態をスタックに積む。エレメントが閉じた場合は、スタックからポップして状態を戻す。
エレメントが開いたときの状態遷移は以下。
Root (+ 任意のエレメント)|-> S1
S1 (+ rdf:parseType="Collection|Resource|Literal" のエレメント)|-> Collection|Resource|Literal
S1 (+ rdf:Resourceを含む=ノード要素 のエレメント)|-> S1
S1 (+ rdf:Resourceを含まないエレメント)|-> S2
S2 (+ 任意のエレメント)|-> S1
Resource (+ rdf:parseType="Collection|Resource|Literal" のエレメント)|-> Collection|Resource|Literal
Resource (+ rdf:Resourceを含む=ノード要素 のエレメント)|-> S1
Resource (+ rdf:Resourceを含まないエレメント)|-> S2
Collection (+ 任意のエレメント)|-> S2
となる。
あとは読みかけの文(SubjectあるいはSubject×Predicate)と、上記の状態をスタックに積んで、PushしたりPopしたりしながら読んでいくと、SAXでちゃんとパースできる。