Schema.org + JSON-LD を RDFLib で読み込んでみる

本記事では、Schema.org の語彙を使って記述された JSON-LD を、RDF を扱える Python ライブラリ RDFLib に読み込む方法を見てみます。

【目次】

[1]はじめに

記事『Web ページ内の JSON-LD を Python + PyLD で覗いてみる(Schema.org)』で、Web ページに組み込まれた JSON-LD を見ました。

また、JSON-LD は RDF(Resource Description Framework)のシリアライズ形式の一つであり、RDF としても解釈できる旨を書きました。

RDF として扱うと、セマンティックウェブ系の技術を使っていろいろな応用が期待できそうです。

例えば、収集した情報をマージしてナレッジグラフを構成したり、SPARQL のようなクエリ言語を利用して情報を抽出したり、新たな知識の発見につなげたり、などなど。

そこで、本記事では、Schema.org の語彙を使って記述された JSON-LD を、RDF を扱える Python ライブラリ RDFLib に読み込む方法を見てみます。

今回もセマンティックウェブ技術とか RDF には深入りせず、利用方法に絞ってます。

なお、本記事のコードは Google Colaboratory で動作を確認しています。

[2]RDFLib と JSON-LD の読み込み例

RDFLib は、RDF を操作するためのオープンソース Python パッケージです。

RDFLib を利用すると、RDF を表現できる各種形式をメモリ内の RDF ストレージに読み込んで(parse)、検索や処理を行ったり、各種形式で出力(serialize)できます。

また、RDFLib のバージョン 6.0.1 から直接 JSON-LD の読込、出力が行えるようになったようです。(それ以前は、別途プラグインが必要だったようです。)

本記事では RDF や RDFLib の仕様や利用方法は割愛しますが、下記仕様書にある簡単な JSON-LD のサンプルを例を使って利用イメージを見てみることにします。

仕様書 3. Basic Concepts の EXAMPLE 2: Sample JSON document にJSONデータがあります。

{
  "name": "Manu Sporny",
  "homepage": "http://manu.sporny.org/",
  "image": "http://manu.sporny.org/images/manu.png"
}

人間が見ると、データの内容は何となく理解できると思います。

これに @context で Schema.org の語彙に対応させる規則を与えて JSON-LD とし、RDF として解釈できるようにしてみます。

これは仕様書の EXAMPLE 7: In-line context definition に書かれています。

{
  "@context": {
    "name": "http://schema.org/name",
    "image": {
      "@id": "http://schema.org/image",
      "@type": "@id"
    },
    "homepage": {
      "@id": "http://schema.org/url",
      "@type": "@id"
    }
  },
  "name": "Manu Sporny",
  "homepage": "http://manu.sporny.org/",
  "image": "http://manu.sporny.org/images/manu.png"
}

この JSON-LD を RDFLib で読み込んでみます。

まず、RDFLib をインストールします。
!pip install rdflib

以下のコードは、JSON-LD データを RDFLib の Graph に読み込んで、Turtle 形式で出力するものです。

from rdflib import Graph

# JSON-LD 1.1 仕様書の EXAMPLE 7: In-line context definition
example_7_data = """
{
  "@context": {
    "name": "http://schema.org/name",
    "image": {
      "@id": "http://schema.org/image",
      "@type": "@id"
    },
    "homepage": {
      "@id": "http://schema.org/url",
      "@type": "@id"
    }
  },
  "name": "Manu Sporny",
  "homepage": "http://manu.sporny.org/",
  "image": "http://manu.sporny.org/images/manu.png"
}
"""

# データをGraphに読み込む
g = Graph().parse(data=example_7_data, format='json-ld')

# turtle 形式で表示してみる
print(g.serialize(format='turtle'))

これを実行すると以下のように出力されると思います。
@prefix ns1: <http://schema.org/> .

[] ns1:image <http://manu.sporny.org/images/manu.png> ;
    ns1:name "Manu Sporny" ;
    ns1:url <http://manu.sporny.org/> .

RDFLibへの読み込みは、「Graph().parse」とある1行だけです。

直感的には、元の JSON ファイルに近い形で出力されていると思います。
(ちなみに、[] は空白ノードです。)

目立つ違いは、JSONのキーがIRIに変わっているところです(ちなみに、homepage は url に変わっています)。

これらの語彙は Schema.org で定義されたものであり、その内容はネット上で確認できるものになっています。上記例の IRI をクリックすると定義が参照できます。

このことは、作成者と利用者が同じ意味を共有することにつながります。

[3]Web ページ内の JSON-LD を読み込んでみる

続いて、Web ページに組み込まれている JSON-LD を RDFLib に読み込んでみます。

例として、前の記事で書いたコードを利用して Wikipedia の記事に組み込まれている JSON-LD を取得します。


まず「https://en.wikipedia.org/wiki/JSON-LD」に組み込まれている JSON-LD を取得します。
jsonld_data = get_jsonld_from_html_url("https://en.wikipedia.org/wiki/JSON-LD", False)

以下のコードで内容を確認してみます。
print_json( jsonld_data )

すると以下のような JSON-LD が表示されると思います。
{
  "@context": "https://schema.org",
  "@type": "Article",
  "name": "JSON-LD",
  "url": "https://en.wikipedia.org/wiki/JSON-LD",
  "sameAs": "http://www.wikidata.org/entity/Q6108942",
  "mainEntity": "http://www.wikidata.org/entity/Q6108942",
  "author": {
    "@type": "Organization",
    "name": "Contributors to Wikimedia projects"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Wikimedia Foundation, Inc.",
    "logo": {
      "@type": "ImageObject",
      "url": "https://www.wikimedia.org/static/images/wmf-hor-googpub.png"
    }
  },
  "datePublished": "2011-12-30T17:43:17Z",
  "dateModified": "2022-06-15T04:03:41Z",
  "headline": "a method of encoding Linked Data using JSON"
}

[2]と同様に、この JSON-LDデータを RDFLib に読み込んでみます。
from rdflib import Graph
g = Graph().parse(data=jsonld_data, format='json-ld')

# turtle 形式で表示してみる
print(g.serialize(format='turtle'))

あれれ…以下の例外になって読み込むことができませんでした(号泣)。
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte

(注意)

本記事で利用している RDFLib のバージョンは、6.1.1 です。

RDFLib の古いバージョン5系と JSON-LD のプラグインの組み合わせを利用すれば問題なく読み込めるのかもしれません(未確認です)。

また将来のバージョンでは問題なく読み込めるかもしれません。

もしかすると、読込失敗は、私の使い方が間違っているだけかもしれませんが。。。

以降では、現行バージョンで読み込みに失敗している理由を探って、RDFLib に読み込むための一時的な回避策を考えてみることにします。


<<お知らせ(2022/07/27)>>
2022/7/27日時点で pip で rdflib をインストールすると、バージョンは 6.2.0 になっていました。このバージョンでは上記エラーは発生せず、parse が成功して読み込むことができました。よって、上記エラーはバージョン6.1.1(それ以前も?)固有の問題だと思います。バージョン 6.2.0 であれば、本記事の後半にある回避策は不要です。

以下はバージョン6.2.0で上記コードを実行した出力結果です。
@prefix schema: <http://schema.org/> .

[] a schema:Article ;
    schema:author [ a schema:Organization ;
            schema:name "Contributors to Wikimedia projects" ] ;
    schema:dateModified "2022-06-15T04:04:15Z"^^schema:Date ;
    schema:datePublished "2011-12-30T17:43:17Z"^^schema:Date ;
    schema:headline "a method of encoding Linked Data using JSON" ;
    schema:mainEntity "http://www.wikidata.org/entity/Q6108942" ;
    schema:name "JSON-LD" ;
    schema:publisher [ a schema:Organization ;
            schema:logo [ a schema:ImageObject ;
                    schema:url <https://www.wikimedia.org/static/images/wmf-hor-googpub.png> ] ;
            schema:name "Wikimedia Foundation, Inc." ] ;
    schema:sameAs <http://www.wikidata.org/entity/Q6108942> ;
    schema:url <https://en.wikipedia.org/wiki/JSON-LD> .

[4]Schema.org のコンテキスト

(1)"@context": "https://schema.org" の解釈

JSON-LD のコンテキストの詳細は(深いので)割愛しますが、ここでは成功した例と失敗した例の違いを見ておきます。

まず[2]の @context は以下のように書かれていました。
  "@context": {
    "name": "http://schema.org/name",
    "image": {
      "@id": "http://schema.org/image",
      "@type": "@id"
    },
    "homepage": {
      "@id": "http://schema.org/url",
      "@type": "@id"
    }
  },

一方[3]の @context は以下のように書かれていました。
  "@context": "https://schema.org",

(注)ちなみに前の記事のブログサンプルの @context の値は「http://schema.org」でした。(httpsではありません。)

違いは、@context の値として、語彙などの変換規則が直接組み込まれているか、URLが書かれているかです。

JSON-LDの仕様では、@context の情報は URL で参照できることになっています。

つまり[3]のパターンは、@context の実体は「https://schema.org」から得られますよ、といっています。この仕様はメリットが大きいところでもあります。

(2)コンテキスト情報のありかを探る

@context の内容は「https://schema.org」にあるとのことなので、まずは、このURLにブラウザでアクセスしてみます。

(注)「http://schema.org」の場合は「https://schema.org」にリダイレクトされます。

すると、予想通りブラウザの画面には Schema.org のホームページが表示されるだけです。

また右クリックで「ページのソースを表示」でソースを見ても、それらしき情報は無いと思います。

そこで JSON-LD 1.1 の仕様書を見ると、「6. Modifying Behavior with Link Relationships」、特に「6.2 Alternate Document Location」にドキュメント位置の代替に関する記載がありました。

これによると、HTTP の link ヘッダを参照する必要がありそうです。

実際に curl でヘッダの内容を見てみます。
!curl --dump-header - https://schema.org

すると以下のようなレスポンスが得られます。
HTTP/2 200 
access-control-allow-credentials: true
access-control-allow-headers: Accept
access-control-allow-methods: GET
access-control-allow-origin: *
access-control-expose-headers: Link
link: </docs/jsonldcontext.jsonld>; rel="alternate"; type="application/ld+json"
content-encoding: gzip
server: Google Frontend
content-length: 2223
cache-control: public, max-age=600
content-type: text/html
(以下省略)

仕様書にある通り、link ヘッダに、「rel="alternate"」、「type="application/ld+json"」があり、ロケーションが「/docs/jsonldcontext.jsonld」となっています。

これを絶対URLに直すと以下のURLを見てね、ということのようです。
https://schema.org/docs/jsonldcontext.jsonld

実際に、上記 URL にブラウザでアクセスすると、JSON 形式でコンテキストの内容が表示されます。
{
  "@context": {
        "type": "@type",
        "id": "@id",
        "HTML": { "@id": "rdf:HTML" },

        "@vocab": "http://schema.org/",
        "csvw": "http://www.w3.org/ns/csvw#",
        "dc": "http://purl.org/dc/elements/1.1/",

  (長いので、途中省略)

        "xpath": { "@id": "schema:xpath"},
        "yearBuilt": { "@id": "schema:yearBuilt"},
        "yearlyRevenue": { "@id": "schema:yearlyRevenue"},
        "yearsInOperation": { "@id": "schema:yearsInOperation"},
        "yield": { "@id": "schema:yield"}
    }
}

途中省略していますが、name、image、url の定義も含まれています。

(3)コンテキストの内容をダウンロードしてみる

ダウンロードして利用するだけなら wget や curl で十分なのですが、本記事の以降の内容との関係で、Schema.org のコンテキストをダウンロードする簡単なサンプルコードを書いておきます。

import json

def get_schema_org_context():
  res = requests.get("https://schema.org/docs/jsonldcontext.jsonld")
  if res.status_code == 200:
    return json.loads(res.text)
  else:
    return None

なお、上記コードは例示なのでベタにロードしているだけですが、ちゃんと使うものとして作るなら、キャッシュしたり、ヘッダを確認するなどの考慮は必要かと思います。

実際に、RDFLib のローダなどのソースコードを見ると、一度解決したものはちゃんとキャッシュして再利用するようになっているようですので、RDFLib でそのまま読み込めるようになれば有難いです。。。

[5]RDFLib 6.1.1 における、とりあえず回避策?

<<お知らせ(2022/07/27)>>
2022/7/27日時点で pip で rdflib をインストールすると、バージョンは 6.2.0 になっていました。このバージョンでは以下の回避策は不要です。

本記事で利用している RDFLib のバージョン 6.1.1 の読み込み動作をデバッガで追ってみたところ、なんとなく @context で参照している「https://schema.org」で返される HTML ファイルをそのまま JSON として解釈しようとして失敗しているように思えました。

(私の使い方が間違っていなければ、という前提なのですが。)

他のライブラリを利用することも考えてみましたが、RDFLib の Graph にロードできれば、SPARQL その他のツールが簡単に使えるなどのメリットも大きいと思います。

そこで、ライブラリの内部に手を入れない形で、現状の RDFLib に読み込む方法を2つほど考えてみましたので、問題回避のサンプルコードをメモしておきます。

ちなみに、本来はコンテキスト情報のローダーを差し替える(フックをかける)ことができれば奇麗だと思いましたが、ざっと RDFLib のコードを見た程度ではそのようなフックがかけられるかどうかよくわからなかったため、@context の値が「https://schema.org」の場合のみ予めダウンロードしたコンテキストを利用する、というショボい方法としています。

新しいバージョンで読み込めるようになったら(あるいは正しい使い方が分かれば)、以下に書いている内容は無視してください。

(1)@context を置き換える

まずは、@context の値をダウンロードしたコンテキストの内容で置き換えてしまうという強引な方法です。

# JSON-LDを取得
jsonld_data = get_jsonld_from_html_url("https://en.wikipedia.org/wiki/JSON-LD", False)

ctx_url = jsonld_data["@context"]
if ctx_url == "http://schema.org" or ctx_url == "https://schema.org":
  # Schema.org のURLが指定されている時
  # Schema.orgのContext情報を取得
  schema_org_context = get_schema_org_context()

  # "@context"を置き換える
  jsonld_data["@context"] = schema_org_context["@context"]

# RDFLib に読み込む
g = Graph().parse(data=jsonld_data, format='json-ld')

# turtle形式で表示してみる
print(g.serialize(format='turtle'))

これで RDFLib に読み込んで、Turtle形式で出力できました。
@prefix schema1: <http://schema.org/> .

[] a schema1:Article ;
    schema1:author [ a schema1:Organization ;
            schema1:name "Contributors to Wikimedia projects" ] ;
    schema1:dateModified "2022-06-15T04:03:41Z"^^schema1:Date ;
    schema1:datePublished "2011-12-30T17:43:17Z"^^schema1:Date ;
    schema1:headline "a method of encoding Linked Data using JSON" ;
    schema1:mainEntity "http://www.wikidata.org/entity/Q6108942" ;
    schema1:name "JSON-LD" ;
    schema1:publisher [ a schema1:Organization ;
            schema1:logo [ a schema1:ImageObject ;
                    schema1:url <https://www.wikimedia.org/static/images/wmf-hor-googpub.png> ] ;
            schema1:name "Wikimedia Foundation, Inc." ] ;
    schema1:sameAs <http://www.wikidata.org/entity/Q6108942> ;
    schema1:url <https://en.wikipedia.org/wiki/JSON-LD> .

(2)@context を削除+パーサにコンテキストを渡す

続いて、RDFLib の JSON-LD のパーサのソースコードを見て知ったのですが、parse メソッドに context を渡すことができます。

しかし、元の JSON-LD に @context があるとそちらが優先されてしまうため、@contextを削除して parse メソッドでダウンロードしたコンテキストを利用する方法です。

# JSON-LDを取得
jsonld_data = get_jsonld_from_html_url("https://en.wikipedia.org/wiki/JSON-LD", False)

ctx_url = jsonld_data["@context"]
if ctx_url == "http://schema.org" or ctx_url == "https://schema.org":
  # Schema.org のURLが指定されている時
  # Schema.orgのContext情報を取得
  schema_org_context = get_schema_org_context()

  # データにある@context を削除
  del jsonld_data["@context"]

  # RDFLib にcontextを指定して読み込む
  g = Graph().parse(data=jsonld_data, format='json-ld', context=schema_org_context)
else:
  # それ以外はcontext指定なし
  g = Graph().parse(data=jsonld_data, format='json-ld')

# turtle形式で表示してみる
print(g.serialize(format='turtle'))

この方法でも RDFLib に読み込んで、Turtle形式で出力できました。
@prefix schema1: <http://schema.org/> .

[] a schema1:Article ;
    schema1:author [ a schema1:Organization ;
            schema1:name "Contributors to Wikimedia projects" ] ;
    schema1:dateModified "2022-06-15T04:03:41Z"^^schema1:Date ;
    schema1:datePublished "2011-12-30T17:43:17Z"^^schema1:Date ;
    schema1:headline "a method of encoding Linked Data using JSON" ;
    schema1:mainEntity "http://www.wikidata.org/entity/Q6108942" ;
    schema1:name "JSON-LD" ;
    schema1:publisher [ a schema1:Organization ;
            schema1:logo [ a schema1:ImageObject ;
                    schema1:url <https://www.wikimedia.org/static/images/wmf-hor-googpub.png> ] ;
            schema1:name "Wikimedia Foundation, Inc." ] ;
    schema1:sameAs <http://www.wikidata.org/entity/Q6108942> ;
    schema1:url <https://en.wikipedia.org/wiki/JSON-LD> .


コメント

このブログの人気の投稿

VirtualBoxのスナップショット機能

Google Document AIで画像から表形式データを抽出する(Vision API OCRとの違い)

Vision API OCR事始め(1):TEXT_DETECTIONとDOCUMENT_TEXT_DETECTIONの違い