API, URIとPythonを使って、Wikidataからデータを取得する (5) 実装の要点とコード

Wikidata

はじめに

この記事は、Wikidataから「日本の首都は?」のように、「○○の××は何か?」ということを調べる方法についての記事です。

他の記事

第4回:APIで得られたJSONの内容

このシリーズの他の記事は、「Wikidata」カテゴリーから飛んでください。
スマホの方は画面右下の「サイドバー」ボタンから、それ以外の方は画面右側のサイドバーから。

今回のテーマ

今回は、いよいよコードを書いていきます。
前回までの知識を総動員します。

その前に、いったんソースコードの全容を共有しておきます。
(ここクリックするとダウンロード始まります。嫌な方は以下に書いとくので見てください)

プログラムは十分注意して作成しておりますが、予期せぬ動作を引き起こす可能性があるので、実行にあたっては自己責任でお願いします。
また、短いスパンで繰り返しプログラムを実行することは、Wikidata側のサーバ等に負荷を与えることになります。節度を持った利用を心がけてください。

(上の「ダウンロード」ボタンをクリックするとダウンロード始まります。嫌な方は以下に書いとくので見てください)

main.pyの全文

import http.client
import urllib.parse
import json

def get_data(url):
    """URLで指定された場所からデータを取得する。

    URLで指定された先に、HTTPのGETリクエストを送信し、レスポンスをUTF-8文字列として返す。

    Args:
        url (str): URLを表す文字列。クエリが後ろにくっついている。

    Returns:
        str: レスポンス文字列。

    """
    # URLを構成要素に解析
    parsed_url = urllib.parse.urlparse(url)
    host = parsed_url.hostname
    path = parsed_url.path + "?" + parsed_url.query
    scheme = parsed_url.scheme
    
    if not path:
        path = '/'
    
    # HTTPSが使えれば使う
    if scheme == 'https':
        connection = http.client.HTTPSConnection(host)
    else:
        connection = http.client.HTTPConnection(host)
    
    # GETリクエスト送信
    connection.request('GET', path)
    
    # レスポンス取得
    response = connection.getresponse()
    data = response.read()
    
    # 接続を閉じる
    connection.close()
    
    return data.decode('utf-8')

def entity_name_to_id(query):
    """エンティティ名をエンティティIDに変換する。

    エンティティ名をAPIで検索する。見つかればそのエンティティIDを文字列として返す。

    Args:
        query (str): 検索文字列。

    Returns:
        str: エンティティID。1つも検索にヒットしなかった場合は、"NULL"が返る。

    """
    encoded_query = urllib.parse.quote(query, encoding='UTF-8')
    url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&language=ja&format=json&search=" + encoded_query

    json_str = get_data(url)

    try:
        json_dict = json.loads(json_str) # json文字列をpythonの辞書に
        entity_list = json_dict['search']

        if type(entity_list) == None: # 検索にヒットせず
            return "NULL"
        else:
            # 大抵は最も若い番号のものが探してるやつだが、不安なら完全一致するかしっかり調べたほうが良い
            return entity_list[0].get('id') 
        
    except json.JSONDecodeError as e:
        print(f"Error decoding JSON: {e}")
        exit()

def property_name_to_id(query):
    """プロパティ名をプロパティIDに変換する。

    プロパティ名をAPIで検索する。見つかればそのプロパティIDを文字列として返す。

    Args:
        query (str): 検索文字列。

    Returns:
        str: プロパティID。1つも検索にヒットしなかった場合は、"NULL"が返る。

    """
    encoded_query = urllib.parse.quote(query, encoding='UTF-8')
    url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&language=ja&format=json&type=property" + "&search=" + encoded_query
    json_str = get_data(url)
    try:
        json_dict = json.loads(json_str) # 辞書に変換
        prop_list = json_dict['search']
        if type(prop_list) == None:
            return "NULL"
        else:
            return prop_list[0].get('id') # ここも不安なら、完全一致するか調べたほうが良い
    except json.JSONDecodeError as e:
        print(f"Error decoding JSON: {e}")
        exit()

def id_to_name(id):
    """エンティティ、プロパティIDをエンティティ、プロパティ名に変換する。

    URIで該当するIDのデータを取得し、ラベルを取得する。

    Args:
        id (str): 検索ID。

    Returns:
        str: ラベル。

    """
    id_head = id[0]
    if (id_head == 'P' or id_head == 'Q'):
        url = "https://www.wikidata.org/wiki/Special:EntityData/" + id +  ".json"
        entity_dict = json.loads(get_data(url))
        value_dict = entity_dict['entities'][id]['labels']['ja']
        if (value_dict == None):
            print("NULL")
            exit()
        else:
            return value_dict['value']
    else:
        print("IDが不正です。")
        exit()

def entity_prop_to_vals(entity_id, prop_id):
    """「エンティティ」の「プロパティ」に該当する「値」を返す。

    エンティティのデータをURIで取得し、JSONの中から該当するデータをプロパティIDを使って見つける。

    Args:
        entity_id (str): エンティティID。
        prop_id (str): プロパティID。

    Returns:
        str: 値のリスト。

    """
    vals = []

    url = "https://www.wikidata.org/wiki/Special:EntityData/" + entity_id +  ".json"
    entity_dict = json.loads(get_data(url))

    claim_dict = entity_dict['entities'][entity_id]['claims']
    if (claim_dict != None):
        if (prop_id in claim_dict):
            prop_list = claim_dict[prop_id]
            for prop_dict in prop_list:
                val = prop_dict['mainsnak']['datavalue']['value']
                if (type(val) == str):
                    vals.append(val)
                elif (type(val) == dict):
                    id = val['id']
                    jaLabel = id_to_name(id)
                    vals.append(jaLabel)
                else:
                    vals.append(val.to_string())
        else:
            return []
    else:
        exit()
    return vals


def main():
    """今までの関数のテスト。日本の首都を求めさせる。
    """
    entity_id = entity_name_to_id("日本")
    print(entity_id)
    prop_id = property_name_to_id("首都")
    print(prop_id)
    vals = entity_prop_to_vals(entity_id, prop_id)
    print(vals)


if __name__ == "__main__":
    main()

ついでに、簡易的なhtmlドキュメントも公開します。

URI, APIでGETリクエストを送信する際のURL

今シリーズでWikidataから情報を取得する際には、URIとAPIを使用すると説明しました。
ですが、コードから呼び出す際はどちらもHTTPのGETメソッドを呼び出してるだけなので、URL文字列がどのようになっているか理解しなければなりません。

URI

第2回記事の説明のまんまです。

API

大元となるURLは以下。

https://www.wikidata.org/w/api.php

第2回記事のパラメータを、このURL(に”?”を加えたもの)の後ろ側に、”&”区切りで入力していくことになります。

例えば、第2回記事の設定をそのまま利用して、「潮岬」と調べたい場合は、以下のようになります1

https://www.wikidata.org/w/api.php?action=wbsearchentities&language=ja&format=json&search=%E6%BD%AE%E5%B2%AC

searchパラメータの、

%E6%BD%AE%E5%B2%AC

は、「潮岬」のUTF-8コードを表しています2

作成する関数の説明

get_data

今シリーズでWikidataから情報を取得する際には、URIとAPIを使用すると説明しました。
ですが、コードから呼び出す際はどちらもHTTPのGETメソッドを呼び出してるだけなので、そこら辺の処理を行う関数を作っておきます。

引数とかの説明はdocstringに譲るとして、簡単に動作を説明します。

関数の前半では、URLを解析したり、HTTPSの処理をしたりしてますが、本質はそこではなく、ホスト(この場合Wikidata)とHTTP(S)コネクションを確立し、GETリクエストを送信している、ということです。

どのようなデータが欲しいか、などはすべてURLに記述してあるため、やってることは

  1. コネクション確立
  2. GETリクエスト送信
  3. レスポンス取得
  4. コネクション終了

だけです。

entity_name_to_id, property_name_to_id

エンティティ、プロパティ名で検索をかけて、エンティティ、プロパティIDを返します。
APIを使った情報取得を行っています。

この2つの関数の違いは、ほぼほぼURLだけで、基本的な関数の構造はそっくりです。

情報を取得する系の関数は、主に情報を取ってくる部分と、その情報(JSON)を解析する部分に分かれます。

情報を取得する部分は、get_data関数でJSON形式の文字列が手に入ります。

JSONの解析には、JSONの構造を思い出す必要があります。不安な方、ぜひ第4回記事へ。

つまり、IDを得るためには、

  1. 一番外側の辞書
  2. 「search」がキーになっている値(リスト)
  3. リストの要素になっている各辞書
  4. 各辞書の「id」がキーになっている値(文字列)

と、内側に潜っていく必要があるわけです。

なお、ソースコードでは、より「元」になる概念(「日本」と調べたら、国家としての日本のエンティティ)がリストの頭にくるだろうと踏んで、リストの先頭の辞書を調べるようにしています。実行速度も大事なので。
ただ、確実性に欠けますから、不安な方はリスト内をforループして、各エンティティ、プロパティのラベルが検索文字列と一致するかどうか調べたほうがいいです。

id_to_name

さっきとは逆方向の変換です。
URIを使った情報取得を行っています。

エンティティやプロパティのJSONをURLで取得して、その中からラベルの文字列を取り出す操作をしています。

第3回記事の内容(画像の青枠で囲んだところ)を思い出すと、

  1. 一番外側の辞書
  2. 「エンティティID文字列」
  3. labels
  4. ja
  5. value

と潜れば、エンティティやプロパティのラベルを得られることがわかります。

entity_prop_to_vals

いよいよ、エンティティIDとプロパティIDを用いて、該当する「値」を検索する関数です。

実は、この関数は2つのIDを用いて特別な操作をしているわけではなく、URIでエンティティのデータを持ってきて、そのJSONテキストを調べて、該当するプロパティと値を取り出しているだけです。

第3回記事の内容(特に画像の緑枠赤枠で囲んだところを意識する)を思い出すと、

  1. 一番外側の辞書
  2. 「エンティティID文字列」がキーになっている値(辞書)
  3. “claims”がキーになっている値(辞書)

と潜ると緑枠に到達し、さらに

  1. プロパティID文字列(個々の赤枠に到達)
  2. mainsnak
  3. datavalue
  4. value
  5. id

と潜ると、与えられた2つのIDから値が得られました。なお、値も複数存在するので、注意してください。

あとは、id_to_nameでラベルに変換して返せば終了です。

main

以上の関数を使うと、「○○(エンティティ)」の「××(プロパティ)」はなにか、という問いに対しても、

  1. entity_name_to_idとproperty_name_to_idで、「○○」と「××」をそれぞれIDに変換する
  2. entity_prop_to_valsで、値を求める

という手順で、目的の操作ができるはずです。

サンプルでは、日本の首都を求めさせました。実行すると日本の首都オールスターズみたいな出力になるはずです。

最後に

Wikidata、意外となんでもある。


  1. パラメータの順番は関係ありません。 ↩︎
  2. 「E6BDAE」=「潮」、「E5B2AC」=「岬」。 ↩︎

コメント

タイトルとURLをコピーしました