RDF.rbで，プロパティURIに日本語を含むRDFデータをRDF/XML形式で出力する



### 概要

RubyでGem``rdf-rdfxml``を利用しRDF/XMLファイルを出力する際，プロパティなどのURIに日本語が含まれていると正しく出力されない．この問題は，rdf-rdfxmlを利用せずraptorなどの外部ツールでRDF/XMLに変換するか，URIの名前空間部分とそれ以外を分割するための正規表現を修正することで解決できる．


### 環境

* MacOSX 10.10.5
* Ruby 2.2.2
* Gem
	* rdf 1.1.14
	* rdf-rdfxml 1.1.4



### RDF.rbとrdf-rdfxml

[RDF.rb](https://github.com/ruby-rdf/rdf)は，RubyでRDFデータを扱う際に用いる代表的なライブラリである．デフォルトではN-Triples形式とN-Quads形式という2種類の文法（フォーマット）のRDFファイルにしか対応していないが，プラグインを読み込むことで他の文法にも対応できる．RDF/XML形式に対応したプラグインとして，[rdf-rdfxml](https://github.com/ruby-rdf/rdf-rdfxml)が公開されている．



### RDF/XMLファイルの出力

RDF.rbには，RDFグラフを各種の文法で出力するメソッド``dump``がある．引数で出力時の文法を指定することができ，rdf-rdfxmlを使っていれば:rdfxmlを指定することでRDF/XML形式の出力が可能である．


```
require 'rdf'
require 'rdf/rdfxml'

# 空のグラフを作成
graph = RDF::Graph.new

# graphにRDFトリプルを入れていく…
# 割愛

# グラフの中身をRDF/XML形式で出力
puts graph.dump(:rdfxml)
```

この仕組みを利用して，以下のように文法の変換を行うこともできる．

```
require 'rdf'
require 'rdf/rdfxml'

# 空のグラフを作成
graph = RDF::Graph.new

# N-Triples形式で記述されたRDFファイル（sample.nt）を読み込む
graph.load("sample.nt")

# 読み込んだデータをRDF/XML形式で出力
puts graph.dump(:rdfxml)
```


### URIからQNameへの変換手順

rdf-rdfxmlでは，RDFデータを出力する際に，リソースやプロパティのURIを名前空間接頭辞を用いたQNameに変換する．（例：\<<http://purl.org/dc/terms/title>\> => dcterms:title）
この処理は，以下の流れで行われている．

1. BaseURIとLocalPartに分割する（<http://purl.org/dc/terms/>とtitleに分割）
2. BaseURIを名前空間接頭辞に変換する（<http://purl.org/dc/terms/>をdcterms:などに変換）
3. 名前空間接頭辞とLocalPartを結合する（dcterms:とtitleを結合し，dcterms:titleとする）

#### 手順1: BaseURIとLocalPartに分割する

手順1では，URIをBaseURI（NCNameには使用できない文字が最後に出現する位置まで）とLocalPart（NCNameには使用できない文字が最後に出現する位置より後）に分割する．

NCNameは，QNameのPrefixやLocalPartで使用できるデータ型であり，W3Cによって以下のように定義されている．

```
NCName ::= Name - (Char* ':' Char*)	/* An XML Name, minus the ":" */
Name ::= NameStartChar (NameChar)*
NameStartChar	::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
```

参考：
http://www.w3.org/TR/REC-xml-names/#NT-QName
http://www.w3.org/TR/REC-xml-names/#NT-NCName
http://www.w3.org/TR/REC-xml/#NT-Name
http://www.w3.org/TR/REC-xml/#NT-NameStartChar
http://www.w3.org/TR/REC-xml/#NT-NameChar

この定義によると，

* NameStartCharで始まる
* それに続いてNameCharが0文字以上出現する
* そこからコロンを除いた
 
ものがNCNameである．スラッシュやハッシュはNCNameに含まれていない．
<http://purl.org/dc/terms/title>の場合はtitleの直前にあるスラッシュが「NCNameには使用できない文字が最後に出現する位置」となるため，BaseURIの「<http://purl.org/dc/terms/>」とLocalPartの「title」に分割される．

#### 手順2: BaseURIを名前空間接頭辞に変換する

名前空間宣言を確認し，手順1で取得したBaseURIがそこに含まれていれば宣言された接頭辞を利用する．含まれていない場合はns0:という接頭辞で宣言を追加する．既にns0:がある場合は，ns1:のように数値を増やして宣言する．<http://purl.org/dc/terms/>が既にグラフ中でdcterms:として宣言されていれば，接頭辞としてdcterms:を利用する．宣言されていなければ，<http://purl.org/dc/terms/>を接頭辞ns0:として宣言し利用する．

#### 手順3: 名前空間接頭辞とLocalPartを結合する

手順2で取得した名前空間接頭辞と手順1で取得したLocalPartを結合してQNameを生成する．接頭辞がdcterms:，LocalPartがtitleの場合，QNameはdcterms:titleとなる．




### 問題：日本語を含むURIからQNameへの変換失敗

前述のQName生成手順1は，``lib/rdf/rdfxml/writer.rb``で実装されている．このコードでは，BaseURIとLocalPartの分割箇所を以下のように求めている．

```
separation = uri.rindex(%r{[^a-zA-Z_0-9-][a-zA-Z_][a-z0-9A-Z_-]*$})
```

本来は非NCName文字が最後に出現する位置より後をLocalPartとするべきだろうが，この実装では

* 1文字目（BaseURIとLocalPartのセパレータ）がアルファベット（大文字，小文字），数字，アンダースコア，ハイフンのいずれでもない
* 2文字目（LocalPartの1文字目）がアルファベット（大文字，小文字），アンダースコアのいずれかである
* 3文字目（LocalPartの2文字目）以降にアルファベット（大文字，小文字），数字，アンダースコア，ハイフンのいずれかが連続する

という正規表現でBaseURIとLocalPartを分割しようとしている．これはLocalPartにひらがなや漢字などが含まれていないことが前提であり，日本語を含むURIの場合はBaseURIとLocalPartとの分割がうまくいかない．

実際に以下のRDFデータを用意し，

```
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix prop-ja: <http://ja.dbpedia.org/property/> .

<http://ja.dbpedia.org/resource/羅臼岳>
    prop-ja:名称 "羅臼岳"@ja .
```

RDF/XML形式で出力しようとすると，以下のようなRDF/XMLファイルになってしまう．

```
<?xml version='1.0' encoding='utf-8' ?>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
  <rdf:Description rdf:about='http://ja.dbpedia.org/resource/羅臼岳'>
    <http://ja class='dbpedia org' xml:lang='ja'>羅臼岳</http://ja>
  </rdf:Description>
</rdf:RDF>
```

RDF/XMLファイルの4行目の開始タグとその属性，終了タグが正しく出力されていないことがわかる．

### 解決策

解決策として，

* N-Triplesなどで出力してから，外部ツールでRDF/XMLに変換する
* 名前空間URIとLocalPartを分割する正規表現を修正する

というものが考えられる．

#### 外部ツールでRDF/XMLに変換する

RDFデータを扱うライブラリとして，[Raptor](http://librdf.org/raptor/)というライブラリが公開されている．
C言語で記述されたライブラリで，コマンドラインインタフェースから利用できるrapperというツールも付属している．このツールを利用すると，以下のようなコマンドでN-Triples形式のRDFファイルをRDF/XML形式に変換して出力できる．

```
rapper -i ntriples -o rdfxml-abbrev sample.nt
```

RDF.rbで一旦N-Triples形式で出力し，それをrapperでRDF/XMLに変換することで，rdf-rdfxmlを使わずRDF/XML形式のファイルを出力できる

#### 正規表現を修正する

日本語を正しく処理できないのは，BaseURIとLocalPartの分割に利用する正規表現がNCNameのパターンと異なっていることが原因である．これを解決するには，例えば```lib/rdf/rdfxml/writer.rb```に書かれた以下のコードを

```
separation = uri.rindex(%r{[^a-zA-Z_0-9-][a-zA-Z_][a-z0-9A-Z_-]*$})
```

以下のコードに変更すればよい（テストは用意していない）．

```
# NameStartChar
# NCNameの1文字目で使用できる文字
regexp_name_start_chars = [
  ':',
  "[A-Z]",
  "_",
  "[a-z]",
  "[#{[0xC0].pack('U*')}-#{[0xD6].pack('U*')}]",
  "[#{[0xD8].pack('U*')}-#{[0xF6].pack('U*')}]",
  "[#{[0xF8].pack('U*')}-#{[0x2FF].pack('U*')}]",
  "[#{[0x370].pack('U*')}-#{[0x37D].pack('U*')}]",
  "[#{[0x37F].pack('U*')}-#{[0x1FFF].pack('U*')}]",
  "[#{[0x200C].pack('U*')}-#{[0x200D].pack('U*')}]",
  "[#{[0x2070].pack('U*')}-#{[0x218F].pack('U*')}]",
  "[#{[0x2C00].pack('U*')}-#{[0x2FEF].pack('U*')}]",
  "[#{[0x3001].pack('U*')}-#{[0xD7FF].pack('U*')}]",
  "[#{[0xF900].pack('U*')}-#{[0xFDCF].pack('U*')}]",
  "[#{[0xFDF0].pack('U*')}-#{[0xFFFD].pack('U*')}]",
  "[#{[0x10000].pack('U*')}-#{[0xEFFFF].pack('U*')}]",
]

# NameChar
# NCNameの2文字目以降で使用できる文字
## NCNameで使用できない文字は[^#{regexp_name_chars.join}]となる
regexp_name_chars = regexp_name_start_chars + [
  ".",
  "[0-9]",
  "[#{[0xB7].pack('U*')}]",
  "[#{[0x0300].pack('U*')}-#{[0x036F].pack('U*')}]",
  "[#{[0x203F].pack('U*')}-#{[0x2040].pack('U*')}]",
  "-",
]

regexp = Regexp.new("[^#{regexp_name_chars.join}][#{regexp_name_start_chars.join}][#{regexp_name_chars.join}]*$")
separation = uri.rindex(regexp)
```