Pour accéder à une version interactive de ce Jupyter Notebook (et au fichier .ipynb), suivre ce lien Binder. Pour plus d'infos, consulter cette page.
Interroger sa propre instance Wikibase en Python¶
Cette étape vise à explorer comment nous pouvons interroger / manipuler / enregistrer les données contenues dans une instance Wikibase en faisant appel à un Jupyter Notebook et aux librairies Python Requests, Pandas et SPARQLWrapper.
Objectif¶
Le but ici est de tester les deux façons dont nous pouvons utiliser le langage Python pour interroger la Wikibase sans devoir passer par l'interface graphique :
- 1/ En passant par l'API MediaWiki
- 2/ En passant par le SPARQL Endpoint
1. MediaWiki API¶
Inspiration : Wikidata Training: the Mediawiki API
Cette méthode offre des possibilités plus limitées que le SPARQL endpoint (qui permet de formuler des requêtes plus complexes, après la conversion des données en RDF) et son usage est soumis à des limites*, mais les modules wbgetentities et wbsearchentities permettent toutefois d'accéder facilement au "JSON canonique des pages d'entités". Cela peut être utile par exemple si l'on souhaite obtenir toutes les informations disponibles sur une entité en particulier.
import requests
import pandas as pd
SSL_VERIFY = True
# maybe set SSL_VERIFY to False if connection to https://www.wikidata.org doesn't work (e.g. because of a proxy)
# To disable the SSL verification, remove comment sign (#) from next line
#SSL_VERIFY = False
#if not SSL_VERIFY:
# import urllib3
# urllib3.disable_warnings()
1.1 Obtenir les données d'entités Wikibase¶
Dans cet exemple, nous lançons un appel à l'API à l'aide de l'action wbgetentities afin d'obtenir toutes les données concernant Andrée de Jongh possédant l'identifiant [Q10] (https://adochs.arch.be/w/index.php?title=Item:Q10), en français. Sachez toutefois qu'il est possible d'obtenir les données de plusieurs entités à la fois (jusqu'à 50). Il est également possible de récupérer les données dans toutes les langues disponibles (il suffit de ne rien préciser).
get_dejongh = 'https://adochs.arch.be/w/api.php?action=wbgetentities&ids=Q10&format=json&languages=fr'
res = requests.get(get_dejongh, verify=SSL_VERIFY)
result1 = res.json()
print(result1)
Dataframe Pandas¶
Nous allons profiter de la librairie Pandas pour tenter de visualiser ces données JSON de façon peut-être plus lisible... Mais il faut se rappeler que ces données sont avant tout destinées à être lisibles par des machines : la valeur des propriétés (colonne 'claims') n'apparaît d'ailleurs pas directement dans le dataframe ci-dessous.
DeJongh = pd.DataFrame(result1['entities']["Q10"])
DeJongh.head() # pour voir tout l'entièreté du dataframe, supprimer le _.head()_
Valeur d'une propriété¶
Pour obtenir la valeur d'une certaine propriété (colonne claims), nous pouvons donc revenir au JSON en ciblant ce que l'on souhaite afficher (ici une date de naissance (P31) qui apparaît après datavalue) :
result1['entities']['Q10']['claims']['P31']
Parsing avancé¶
Comme vous pouvez le constater ci-dessus, l'extraction de valeurs associées à une propriété en particulier se révèle complexe en raison de la structure du JSON utilisée par Wikidata (voir par exemple ce post de Steve Baskauf).
En raison des spécificités du modèle de données Wikidata et de la complexité induite par les différents types de données (data-type) associées aux propriétés Wikidata - qui nécessiteraient des traitements sur mesure -, nous n'approfondissons donc pas ici le parsing de données récursives et conseillons plutôt de se référer à des outils pré-existants comme wikibase-cli (the Command-line interface interface to Wikibase instances), Wikidata Integrator exemple ou Wikidata GraphQL voir cette version de Tpt + exemples ou celle-ci (avec exemples). Il pourrait également être envisagé de passer par OpenRefine.
Extraire les données de plusieurs entités¶
Il est également possible de rechercher de lancer un appel à l'API pour plusieurs entités, c'est également possible.
Admettons par exemple que l'on recherche les descriptions de différentes communes belges (Q100, Q101, Q103, Q104) dans toutes les langues disponibles :
get_communes = 'https://adochs.arch.be/w/api.php?action=wbgetentities&ids=Q100|Q101|Q103|Q104&props=descriptions&format=json'
res = requests.get(get_communes, verify=SSL_VERIFY)
result2 = res.json()
print(result2)
1.2 Rechercher une entité¶
Dans cet exemple, nous lançons un appel à l'API à l'aide de l'action wbsearchentities afin de voir si la Wikibase contient une entité associée à the Postman, qui n'est autre que l'un des noms de code qui fut porté par Andrée de Jongh dans le cadre des ses activités de résistance.
Comme ce nom de code a été documenté parmi ses alias, l'entité Q10 de la Wikibase devrait normalement nous être renvoyée.
NB : il est obligatoire de préciser la langue de recherche et l'API ne semble pas utiliser de fuzzy matching, il faut donc que la même graphie soit utilisée que celle encodée dans la Wikibase.
find_postman = 'https://adochs.arch.be/w/api.php?action=wbsearchentities&format=json&search=the Postman&language=fr'
res = requests.get(find_postman, verify=SSL_VERIFY)
result3 = res.json()
result3
ThePostman = pd.DataFrame(result3["search"])
ThePostman.head()
2. SPARQL Endpoint¶
Ce deuxième point est directement inspiré d'un script développé par Steve Baskauf (the Vanderbilt Libraries) ainsi que de ses posts de blogs (Getting Data Out of Wikidata using Software et, dans une moindre mesure SPARQL: Retrieving SPARQL query data using HTTP). L'idée est d'utiliser le service de requêtes de la Wikibase comme une API en tirant parti d'un script Python.
Ici nous utiliserons plus spécifiquement la librairie Pandas, a fast, powerful, flexible and easy to use open source data analysis and manipulation tool, built on top of the Python programming language.
Prérequis : installer SPARQLWrapper, peut être fait via Anaconda :
coda install -c conda-forge sparqlwrapper
import pandas as pd
import json
from SPARQLWrapper import SPARQLWrapper, JSON
Objectif¶
Le but est d'extraire la liste de toutes les communes belges stockées dans la Wikibase, avec leur libellé et leur description en français, ainsi que leur identifiant AGR et leur code INS.
endpoint_url = "https://query-adochs.arch.be/proxy/wdqs/bigdata/namespace/wdq/sparql"
#si nous interrogions Wikidata, ça serait "https://query.wikidata.org/sparql"
#le code d'une requête peut être obtenue sur l'interface graphique du SPARQL endpoint en cliquant en bas à droite sur </>code
# et en choisissant ensuite 'Python', voir l'exemple ci-dessous: https://tinyurl.com/yb22fbym
query = """
PREFIX wb: <https://adochs.arch.be/entity/>
PREFIX wbt: <https://adochs.arch.be/prop/direct/>
SELECT ?place ?placeLabel ?placeDescription ?identifiantAGR ?codeINS ?identifiantWikidata WHERE {
?place wbt:P1 wb:Q25. #je cherche des communes
?place wbt:P57 ?identifiantAGR.
OPTIONAL { ?place wbt:P53 ?codeINS. }
OPTIONAL { ?place wbt:P2 ?identifiantWikidata. }
SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,nl" } .
}
ORDER BY xsd:integer (?identifiantAGR) """
Nous présentons en détail le processus pour récupérer les données au format JSON sous le point A/.
Nous présentons également une méthode (inspirée de cette fonction) pour stocker les données dans un dataframe Pandas (exportable en fichier CSV) sous le point B.
A/ Obtenir les données en JSON¶
Pour obtenir des détails sur la fonction qui suit, se référer à ce post.
def get_results(endpoint_url, query):
sparql = SPARQLWrapper(endpoint_url, agent='User:Annette')
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
return sparql.query().convert()
results = get_results(endpoint_url, query)
#print(results)
Ci-dessous, un petit extrait de ce qu'on obtient lorsqu'on lance la commande *print(results) :
Si nous regardons le contenu de cet extrait, nous voyons qu'il s'agit d'un dictionnaire Python avec deux entrées : head et results.
L'entrée head contient le noms des six variables renvoyées par la requête (place, placeLabel, etc.).
L'entrée results contient un autre dictionnaire, contenant la clé bindings, contenant une liste des résultats en tant que tels, présents chacun dans un nouveau dictionnaire Python.
Si nous observons les premiers résultats correspondant à l'entité Anvers|Q39, nous pouvons inventorier ce que ce dictionnaire contient :
les 6 clés correspondants aux variables renvoyées par la requête (place, identifiantAGR, codeINS, identifiantWikidata, placeDescription)
pour chacune de ces 6 clés, une valeur correspondant à un autre dictionnaire (...), dont la clé value contient la valeur que nous recherchons !
L'étape suivante vise donc à parser les résultats afin de ne garder que ce qui nous intéresse.
Parser les résultats¶
Cette étape vise à parser la liste bindings à l'aide d'une boucle Python (for) afin d'extraire la valeur recherchée. Nous allons extraire de chaque dictionnaire de résultat (results["results"]["bindings"]) la valeur de value.
for result in results["results"]["bindings"]:
place = result['place']['value']
label = result['placeLabel']['value']
description = result['placeDescription']['value']
AGR = result['identifiantAGR']['value']
INS = result['codeINS']['value']
Wikidata = result['identifiantWikidata']['value']
print(place, label, description, AGR, INS, Wikidata)
Script final A/¶
(Version condensée des étapes précédentes)
#!/usr/bin/env python3
from SPARQLWrapper import SPARQLWrapper, JSON
endpoint_url = "https://query-adochs.arch.be/proxy/wdqs/bigdata/namespace/wdq/sparql"
query = """
PREFIX wb: <https://adochs.arch.be/entity/>
PREFIX wbt: <https://adochs.arch.be/prop/direct/>
SELECT ?place ?placeLabel ?placeDescription ?identifiantAGR ?codeINS ?identifiantWikidata WHERE {
?place wbt:P1 wb:Q25. #je cherche des communes
?place wbt:P57 ?identifiantAGR.
OPTIONAL { ?place wbt:P53 ?codeINS. }
OPTIONAL { ?place wbt:P2 ?identifiantWikidata. }
SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,nl" } .
}
ORDER BY xsd:integer (?identifiantAGR) """
def get_results(endpoint_url, query):
sparql = SPARQLWrapper(endpoint_url)
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
return sparql.query().convert()
results = get_results(endpoint_url, query)
for result in results["results"]["bindings"]:
place = result['place']['value']
label = result['placeLabel']['value']
description = result['placeDescription']['value']
AGR = result['identifiantAGR']['value']
INS = result['codeINS']['value']
Wikidata = result['identifiantWikidata']['value']
print(place, label, description, AGR, INS, Wikidata)
B) Charger les résultats dans un dataframe Pandas¶
NB : Pour éviter les redondances, nous réutilisons les variables (endpoint_url et query, voir ci-dessus).
def get_sparql_dataframe(endpoint_url, query):
"""
Helper function to convert SPARQL results into a Pandas data frame.
"""
sparql = SPARQLWrapper(endpoint_url)
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
result = sparql.query()
processed_results = json.load(result.response)
cols = processed_results['head']['vars']
out = []
for row in processed_results['results']['bindings']:
item = []
for c in cols:
item.append(row.get(c, {}).get('value'))
out.append(item)
return pd.DataFrame(out, columns=cols)
df_places = get_sparql_dataframe(endpoint_url, query)
df_places.head()
Exporter les données dans un fichier CSV¶
df_places.to_csv("df_places.csv", sep = '\t', index = False, encoding = 'utf-8')
Script final B/¶
(Version condensée des étapes précédentes)
#!/usr/bin/env python3
import pandas as pd
from SPARQLWrapper import SPARQLWrapper, JSON
endpoint_url = "https://query-adochs.arch.be/proxy/wdqs/bigdata/namespace/wdq/sparql"
query = """
PREFIX wb: <https://adochs.arch.be/entity/>
PREFIX wbt: <https://adochs.arch.be/prop/direct/>
SELECT ?place ?placeLabel ?placeDescription ?identifiantAGR ?codeINS ?identifiantWikidata WHERE {
?place wbt:P1 wb:Q25. #je cherche des communes
?place wbt:P57 ?identifiantAGR.
OPTIONAL { ?place wbt:P53 ?codeINS. }
OPTIONAL { ?place wbt:P2 ?identifiantWikidata. }
SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,nl" } .
}
ORDER BY xsd:integer (?identifiantAGR) """
def get_sparql_dataframe(endpoint_url, query):
"""
Helper function to convert SPARQL results into a Pandas data frame.
"""
sparql = SPARQLWrapper(endpoint_url)
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
result = sparql.query()
processed_results = json.load(result.response)
cols = processed_results['head']['vars']
out = []
for row in processed_results['results']['bindings']:
item = []
for c in cols:
item.append(row.get(c, {}).get('value'))
out.append(item)
return pd.DataFrame(out, columns=cols)
df_places = get_sparql_dataframe(endpoint_url, query)
Aller plus loin¶
Évidemment, il est possible de lancer des requêtes plus complexes, croisant davantage d'éléments et de propriétés, par exemple dans le but de générer une heat map basée sur des données contenues dans la Wikibase ici ou encore visant à récupérer un identifiant Wikidata à partir d'un identifiant VIAF ici.
De plus, une fois que les données sont chargées dans un dataframe Pandas, il est possible d'en profiter pour faire d'autres choses, comme par exemple compter le nombre de personnes partageant la même description ici, ré-ordonner l'ordre des éléments, combiner des colonnes, etc. ici, .