Java
rdf
RDF-XML

RDF/XMLデータモデルのSAXパーサによるパース、のメモ

More than 1 year has passed since last update.

RDF/XMLのためのSAXパーサの実装(イベントハンドラの実装)をしたら大変だったのでメモ。
大変なので、可能な限りライブラリを使いましょう。たぶんrdf4jとかで大抵のことはできるような気がします。

将来の(色々なことを忘れたときの)自分用のメモなので、感想ポエムを含んだイントロになっています。
実際のソースコードは載っていません。再実装するときや実装を読み解く時のヒントとして。

イントロ

RDFとは

https://ja.wikipedia.org/wiki/Resource_Description_Framework

  • 有向グラフで表されるデータ構造。
  • 有向辺自体がPredicate、辺の始点がSubject、終点がObjectとそれぞれ呼ばれる
  • グラフを辺集合の形で、 <Subject,Predicate,Object> の対で表す
  • 頂点は「ノード要素」(名前がややこしいが、この記事では、グラフ理論上のノードは「頂点」と表記している。「ノード」とあるものは「ノード要素」を指している。)と「プロパティ要素」がある。
  • Subject は 必ずノード要素である
  • Object は ノード要素あるいはプロパティ要素がありうる
  • したがって (ノード要素) × (弧) × (ノード要素 or プロパティ要素) の集合としてデータを表している
  • ノード要素はIDを持つ
  • ノード要素のIDは明示的に与えられている場合と、そうでない場合があり、後者はblank node(一般的な訳語:空白ノード。「無名ノード」とかの方がJavaプログラマにはわかりやすい気がする)と呼ばれる

RDF/XML

  • 上記の (ノード要素) × (弧) × (ノード要素 or プロパティ要素) の集合をXMLで表したもの。
  • 基本は以下の2つで表される
Objectがノード要素.xml
<rdf:Description rdf:about="SubjectのノードID">
  <Predicate>
    <rdf:Description rdf:about="ObjectのノードID">
    </rdf:Description>
  </Predicate>
</rdf:Description>
Objectがプロパティ要素.xml
<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でインデクス(ソート木でもハッシュテーブルでも)を作っておいて、検索できるようにしておかないと、常に全部走査することになって遅くなる
  • 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でちゃんとパースできる。