HOME > ブログ > JavaScriptでJSON-LDを操作する話

JavaScriptでJSON-LDを操作する話

2017年06月13日 (火)
  • JavaScriptでCONSTRUCTクエリを使用し、そこからデータを取り出す方法を紹介いたします。

このプログラムの挙動はGoogle Chrome 59.0.3071.86で確認を行いました。

例として、odpに登録されている、バス停データを用います。
今回使用するクエリは以下のとおりです。

PREFIX jrrk: <http://purl.org/jrrk#>
construct {
 ?busstop ?p ?o.
 ?o ?q ?r.
} where {
 {
 select ?busstop {
 ?busstop a jrrk:BusStop.
 } limit 10
 }
 ?busstop ?p ?o.
 optional {
 ?o ?q ?r.
 filter(isBlank(?o)).
 }
}

取得に、Fetch APIを利用します。Fetch APIの詳細については、MDNによるFetch APIの解説をご覧ください。
まず、SPARQLクエリを準備します。

const sparql = `
PREFIX jrrk: <http://purl.org/jrrk#>
construct {
 ?busstop ?p ?o.
 ?o ?q ?r.
} where {
 {
 select ?busstop {
 ?busstop a jrrk:BusStop.
 } limit 10
 }
 ?busstop ?p ?o.
 optional {
 ?o ?q ?r.
 filter(isBlank(?o)).
 }
}
`;

次に、Fetch API用の設定を追加します。

const call_sparql_config = {
 method: "GET",
 headers: {
 "Accept": "application/ld+json"
 },
 mode: "cors",
 cache: "default",
};

この時、headersにAccept: application/ld+jsonを指定していることに注意してください。
JSON形式でRDFを表現する JSON-LD で取得を行うには、Acceptヘッダにこのように指定をする必要があります。

以下のコードで、通信を行います。

const odp_endpoint = "https://sparql.odp.jig.jp/data/sparql";
document.addEventListener("DOMContentLoaded", function(e){
 main();
});
function main(){
 let params = new URLSearchParams();
 params.append("query", sparql);
 fetch(odp_endpoint+"?"+params.toString(), call_sparql_config).then(function(response){
 return response.json();
 }).then( json => console.log(json));
}

URLSearchParamsは、クエリストリングを生成するAPIです。詳細はMDNの解説ページをご覧ください。
上記のmain関数は、odpエンドポイントから、サンプルクエリを発行し、結果をコンソールにJSON形式で受信するものです。バス停を1件だけ取得するようにした結果は以下の通りです。

{
 "@graph" : [ {
 "@id" : "_:b0",
 "identifier" : "2",
 "label" : {
 "@language" : "ja",
 "@value" : "京福バス池田線 下り"
 }
 }, {
 "@id" : "_:b1",
 "表記" : {
 "@language" : "ja",
 "@value" : "稲荷"
 },
 "ic:表記" : {
 "@language" : "ja",
 "@value" : "稲荷"
 }
 }, {
 "@id" : "http://odp.jig.jp/rdf/jp/fukui/imadate/ikeda/741#2/%E4%BA%AC%E7%A6%8F%E3%83%90%E3%82%B9%E6%B1%A0%E7%94%B0%E7%B7%9A%E3%80%80%E4%B8%8B%E3%82%8A/%E7%A8%B2%E8%8D%B7/35.885572/136.343482/",
 "@type" : "jrrk:BusStop",
 "名称" : "_:b1",
 "地理座標" : "http://odp.jig.jp/res/geopoint/+35.885571/+136.343475/",
 "ic:名称" : {
 "@id" : "_:b1"
 },
 "ic:地理座標" : {
 "@id" : "http://odp.jig.jp/res/geopoint/+35.885571/+136.343475/"
 },
 "http://odp.jig.jp/odp/1.0#hasRoute" : {
 "@id" : "_:b0"
 },
 "hasRoute" : "_:b0",
 "label" : {
 "@language" : "ja",
 "@value" : "稲荷"
 },
 "lat" : "35.885572",
 "long" : "136.343482"
 } ],
 "@context" : {
 "hasRoute" : {
 "@id" : "http://purl.org/jrrk#hasRoute",
 "@type" : "@id"
 },
 "地理座標" : {
 "@id" : "http://imi.go.jp/ns/core/rdf#地理座標",
 "@type" : "@id"
 },
 "label" : "http://www.w3.org/2000/01/rdf-schema#label",
 "lat" : {
 "@id" : "http://www.w3.org/2003/01/geo/wgs84_pos#lat",
 "@type" : "http://www.w3.org/2001/XMLSchema#float"
 },
 "long" : {
 "@id" : "http://www.w3.org/2003/01/geo/wgs84_pos#long",
 "@type" : "http://www.w3.org/2001/XMLSchema#float"
 },
 "名称" : {
 "@id" : "http://imi.go.jp/ns/core/rdf#名称",
 "@type" : "@id"
 },
 "表記" : "http://imi.go.jp/ns/core/rdf#表記",
 "identifier" : "http://purl.org/dc/terms/identifier",
 "schema" : "http://schema.org/",
 "cc" : "http://creativecommons.org/ns#",
 "icnew" : "http://imi.go.jp/ns/core/rdf#",
 "jrrk" : "http://purl.org/jrrk#",
 "uncefactISO4217" : "urn:un:unece:uncefact:codelist:standard:ISO:ISO3AlphaCurrencyCode:2012年08月31日#",
 "dct" : "http://purl.org/dc/terms/",
 "owl" : "http://www.w3.org/2002/07/owl#",
 "xsd" : "http://www.w3.org/2001/XMLSchema#",
 "rdfs" : "http://www.w3.org/2000/01/rdf-schema#",
 "ic" : "http://imi.ipa.go.jp/ns/core/rdf#",
 "foaf" : "http://xmlns.com/foaf/0.1/"
 }
}

この結果は、@graph@contextの2つによって成り立っております。@graphは実際に取得したトリプルです。@contextはそれぞれのプロパティがどのIRIを意味するか、型は何の型か、といった情報が含まれております。詳細は JSON-LDの仕様をご確認ください。
JSON-LDには@contextの内容を全て@graphに埋め込むようにしたExpanded書式、逆に@contextに可能な限り情報を入れるようにし、@graphが最小になるようにしたCompacted書式等があります。
上記の書式は、主語1つにつき配列1要素を割り当てるようにしたFlattened書式です。
これはそれぞれの主語に対して処理を行うときに便利な書式です。
帰ってくる書式はサーバの実装により異なるため、統一して扱えるようにjsonld.jsなどで事前に書式を変えておくと良いでしょう。
odpのサーバでは、デフォルトでFlattened形式を取得してくれるようなので、このまま進めます。

@graph内から、型がjrrk:BusStopであるトリプルの抽出を行います。@contextの中から、jrrk:BusStopがどのようなプロパティで入っているかを求めます。@context内には省略名がフルパスの参照であるケースと、短縮IRIのプリフィックスの参照であるケースがあります。
例えば、

{
 "hasRoute" : {
 "@id" : "http://purl.org/jrrk#hasRoute"
 }
}

上記の要素が@context内にあった場合、結果内のhasRoutehttp://purl.org/jrrk#hasRouteを示します。

{
 "jrrk" : "http://purl.org/jrrk#"
}

上記の要素が@context内にあった場合、結果内のjrrk:XXXhttp://purl.org/jrrk#XXXを示します。

戦略として、まず絶対パスが参照されているケースを求め、次に相対参照されているケースを求めます。もしなければ、@graph内に絶対パスで指定されています。
@contextでは、キーに対して純粋にIRIの文字列が格納されているか、@id要素にIRIが格納されています。相対参照の場合は、直接文字列が指定されています。
以下の関数によって、使われている値を検索することが可能です。

function find_key(context, full_iri){
 // 短縮IRIによる探索準備
 let splited = /^(.*[/#])([^/#]*)$/.exec(full_iri);
 let prefix_iri = splited[1];
 let suffix = splited[2];
 let shorten_result = null;
 let full_result = null;
 // フルIRIによる探索
 for (i in context) {
 let compare_target = typeof(context[i]) === "string" ?
 context[i] :
 context[i]["@id"];
 if( compare_target === full_iri ){
 // fullは一意
 full_result=i;
 break;
 }
 if( compare_target === prefix_iri ){
 shorten_result = i + ":" + suffix;
 }
 }
 if (full_result !== null ){
 return full_result;
 }
 if (shorten_result !== null){
 return shorten_result;
 }
 return full_iri;
}

準備が整ったので、抜き出しを行います。結果jsonに対して、@graphからjrrk:BusStop型のトリプルの抽出を行います。

let busstops_graphs = json["@graph"].filter(triple => triple["@type"] === find_key(json, "http://purl.org/jrrk#BusStop"));

今回は、IRIと名前の表示を行います。名前はhttp://imi.go.jp/ns/core/rdf#名称/http://imi.go.jp/ns/core/rdf#表記を辿り、日本語のものを表示します。
トリプルから、IRIと名前を持つプロパティへの加工を行います。

let Name = find_key(json["@context"], "http://imi.go.jp/ns/core/rdf#名称");
let Transcribe = find_key(json["@context"], "http://imi.go.jp/ns/core/rdf#表記");
let busstops = busstop_graphs.map(triples => {
 let names = json["@graph"].filter(t => t["@id"] === triples[Name]);
 let name = undefined;
 if (names.length > 0) {
 name = names[0][Transcribe]["@value"];
 }
 return {
 iri: triples["@id"],
 name: name,
 };
});

最初の2行は、@contextによるキーを求めています。
3行目から、各トリプルに対し、名前を抽出、オブジェクトへの変換処理を行っています。
4行目で名前を持つグラフの抜き出しを行います。名前は複数持っている可能性もありますが、
今回は簡略化のため単一の名前を持っているものとします。
7行目で、実際の表示するための名前の抜き出しを行います。
多言語化されたものを含めても、名前が単一であれば、以下のようなオブジェクトが格納されます。
もし名前が複数あったのであれば、以下のオブジェクトが配列形式で格納されます。

{
 "@value": "名前",
 "@lang": "ja"
}

@valueは、そのままトリプルに対するリテラルの値を指します。@langは、それが何語で示すかを表しています。
今回は単一であることがわかっているので、そのまま格納されているオブジェクトの@valueを参照します。

これにて、IRIと名前が格納されたオブジェクトを作成することができました。

今回は言語が日本語のものしかありませんでしたが、データが多言語対応したときのために、アプリ側も多言語化できるようにしましょう。
もし多言語化されていた場合は、以下のような形式で格納されています。

[
 {
 "@value": "名前",
 "@lang": "ja"
 },
 {
 "@value": "Name",
 "@lang": "en"
 }
]

ja, enの順に優先して表示することを考えます。はじめに、優先順位のテーブルを用意します。
そこから、フィルタ・並び替えを行います。

let langPriority = {
 "ja": 0,
 "en": 1,
};
name_values = names[0][Transcribe];
if(Array.isArray(name_values) && name_values.length > 0) {
 name = name_values
 .filter(value => allowLang.indexOf(value["@lang"]) >= 0)
 .sort( (a, b) => langPriority[a["@lang"]] - langPriority[b["@lang"]])[0]["@value"];
} else {
 name = name_values["@value"];
}

上記のコードを応用すれば、国際化に対応したアプリの作成が容易になります。

このように、RDFをJSONで表した形式であるJSON-LDを加工すると、SPARQLの結果や、元々のRDFデータから柔軟にデータを取り出すことができます。

カテゴリー

(c) B Inc. All rights reserved.

AltStyle によって変換されたページ (->オリジナル) /