Node.js+SPARQLでRDFデータベースから「日本史人物の家系図」を表示する

# 目的
　[Wikipediaから情報をスクレイピングする際の代替手段](http://qiita.com/pika_shi/items/eb56fc205e2d670062ae)として、["DBpedia Japanese"](http://ja.dbpedia.org/)があります。
　このデータベースへのアクセスには、SQLライクな言語**"SPARQL（スパークル）"**を用いなければならず、記法について学習が必要でした。

　本記事の目的は、**SPARQLを用いたRDFデータベースからの情報取得について手法をまとめ**、自分への備忘録作成および情報共有をすることです。

# 結論
　**指定した歴史上の人物の、「子、親、兄弟」を、それぞれ検索表示してくれるNode.jsアプリケーション**を作成しました。
　動作は以下の通り。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">「日本史人物」家系図検索アプリケーション <a href="https://t.co/6HFPF1Zgt2">pic.twitter.com/6HFPF1Zgt2</a></p>&mdash; すいばり@&#39;16年度9試合2勝2敗 (@Suibari_cha) <a href="https://twitter.com/Suibari_cha/status/837869876253351937">2017年3月4日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

# 内容
## コード

```getFamily.js
function getFamily(person) {
  var uri      = "http://ja.dbpedia.org/sparql";
  var children = [];
  var parents  = [];
  var siblings = [];
  
  console.log("YUKI.N > " + person + " の親族を調査中...");
  
  async.series([
    function(callback) {
      // 子供を検索
      client.fetch(uri, getParam(person, "child"), function(err, $, res, body){
        children = getJson(body);
        callback(err, children);
      });
    },
    function(callback) {
      // 親を検索
      client.fetch(uri, getParam(person, "parent"), function(err, $, res, body){
        parents = getJson(body)
        callback(err, parents);
      });
    },
    function(callback) {
      // 兄弟を検索
      siblings = compareChildren(parents);
      callback(null, siblings);
    }
  ], function(err, results) {
    if (err) throw err;
    results.forEach(function(v, i){
      switch (i) {
        case 0: console.log("YUKI.N > 子は、"); break;
        case 1: console.log("YUKI.N > 親は、"); break;
        case 2: console.log("YUKI.N > 兄弟は、"); break;
      }
      display(v);
    });
  });
  
  // ----------------------------
  // SPARQLクエリを生成する
  function getParam(name, cp) {
    var obj = {};
    var q;
    var f = "application/json";
    
    if (cp == "child") {
      // nameの子供を検索するSPARQLを返す
      q  = "SELECT DISTINCT ?name ?abstract WHERE {{ ?w dbpedia-owl:parent <http://ja.dbpedia.org/resource/"+name+"> . ";
      q += "} UNION { <http://ja.dbpedia.org/resource/"+name+"> prop-ja:子 ?w . } ";
    } else if (cp == "parent") {
      // nameの親を検索するSPARQLを返す
      q = "SELECT DISTINCT ?name ?abstract WHERE { <http://ja.dbpedia.org/resource/"+name+"> dbpedia-owl:parent ?w . ";
    }
    q += "?w rdfs:label ?name . ";
    q += "?w dbpedia-owl:abstract ?abstract . }";
    obj.query  = q;
    obj.format = f;
    return obj;
  }
  
  // SPARQLendpointからの応答である4バイト文字JSONを変換する
  function getJson(body) {
    return JSON.parse(decodeUnicode(body)).results.bindings;
  
    function decodeUnicode(unicode) {
      return unicode.replace(/(\\u)([0-9A-F]{4})/g, function(match,p1,p2){
        return String.fromCharCode(parseInt(p2, 16));
      });
    }
  }
  
  // 子供の数を比較して多いほうを返す
  function compareChildren(p) {
    var result = [];
    
    p.forEach(function(v) {
      var n = v.name.value;
      var tmp = getJson(client.fetchSync(uri, getParam(n, "child")).body);
      if (tmp.length > result.length) {
        result  = tmp; // 子供を代入
        parents = v;   // 親を再代入
      }
    });
    return result;
  }
  
  function display(arr) {
    arr.forEach(function(v){
      console.log(v.name.value);
    });
  }
}
```

## 【ポイント】SPARQLについて
　SPARQLの記法について記載します。
　[コード](#コード)のgetParam関数では、**人物名を引数としてSPARQLクエリを生成する役割を持っています。**
　例えば、"織田信長"が与えられた場合、**「"織田信長"の子」を表すSPARQLクエリ**は以下になります。
　このクエリを、["DBpedia Japanese"のSPARQLエンドポイント](http://ja.dbpedia.org/sparql)に対し、URLパラメータに加えてGETすれば、結果を得られます。
　[このような応答です。](http://ja.dbpedia.org/sparql?query=SELECT+DISTINCT+%3Fname%0D%0AWHERE+%7B%0D%0A++%7B%0D%0A++++%3Fw+dbpedia-owl%3Aparent+%3Chttp%3A%2F%2Fja.dbpedia.org%2Fresource%2F%E7%B9%94%E7%94%B0%E4%BF%A1%E9%95%B7%3E+.%0D%0A++%7D+UNION+%7B%0D%0A++++%3Chttp%3A%2F%2Fja.dbpedia.org%2Fresource%2F%E7%B9%94%E7%94%B0%E4%BF%A1%E9%95%B7%3E+prop-ja%3A%E5%AD%90+%3Fw+.%0D%0A++%7D%0D%0A++%3Fw+rdfs%3Alabel+%3Fname+.%0D%0A%7D)

```sql
SELECT DISTINCT ?name
WHERE {
  {
    ?w dbpedia-owl:parent <http://ja.dbpedia.org/resource/織田信長> . --1.
  } UNION {
    <http://ja.dbpedia.org/resource/織田信長> prop-ja:子 ?w . --2.
  }
  ?w rdfs:label ?name . --3.
}
```

　コード中の番号はそれぞれ以下の処理を意味しています。
　1.と2.は重複した検索を行っているように見えますが、このようにしないと**検索結果に漏れができます**。詳細は割愛します。

1. **「織田信長」を親とする人物（信長の子）**を検索し"?w"に格納
2. **「織田信長」の子である人物**を検索し"?w"に格納、UNIONで1.の結果とマージする
3. **"?w"の名前**を"?name"に格納

# 展望
1. コンソール上の表示だけではなく、["OrgChart.js"](https://github.com/dabeng/OrgChart)などを用いて、**図示および芋づる式の家系調査**（ある人の子供の子供の子供の...と辿っていける）が行えると、物凄く有用であると思う。

# 所見
* **私は日本史が大好き（趣味：お城巡り）なので、このようなツールがあればいいな**と以前から思っていた。実現でき、とても嬉しい。
* この処理をWikipediaへのスクレイピングで実現しようとすると、**アクセス回数がかなり多くなり、実行速度低下などが予想される**。こちらの案を実現できて良かった。
* 基本情報技術者試験を17年春季に受験予定なので、SQLの勉強方法に悩まされていた。**SPARQLの学習を通してSQLの文法を学ぶことができ**良かった。

# 参考
1. ["Wikipedia からスクレイピングして… とか言ってる人におすすめしたい，DBPedia からの情報抽出": Qiita](http://qiita.com/pika_shi/items/eb56fc205e2d670062ae) @pika_shi 氏
2. ["直感RDF!!　その１-RDFとは。": Qiita](http://qiita.com/maoringo/items/4742b5cd01c9e698260d) @maoringo 氏
3. ["cheerio-httpcli": GitHub](https://github.com/ktty1220/cheerio-httpcli) @ktty1220 氏