Sunday Query : utiliser SPARQL et Python pour corriger des coquilles sur Wikidata

Ash_Crow Wikidata English

À mon tour de faire une #SundayQuery! Comme Harmonia Amanda l’a dit dans son propre billet, j’envisageais de faire un article expliquant comment créer un script Python permettant de corriger les résultats de sa requête.. Finalement, j’ai préféré en faire un autre, au fonctionnement similaire mais plus court et plus simple à comprendre. Le script pour Harmonia est cependant disponible en ligne ici.

Jeudi, j’ai publié un article au sujet des batailles du Moyen Âge, et depuis, j’ai commencé à corriger les éléments correspondants sur Wikidata

L’une des corrections les plus répétitives était la capitalisation des libellés en français : comme ils ont été importés de Wikipédia, ils ont une majuscule inutile au début ( « Bataille de Saint-Pouilleux en Binouze » au lieu de « bataille de Saint-Pouilleux en Binouze »…)

La requête

Commençons par trouver tous les éléments présentant cette coquille.

SELECT ?item ?label WHERE {
  ?item (wdt:P31/(wdt:P279*)) wd:Q178561;
    rdfs:label ?label.
  FILTER((LANG(?label)) = "fr")
  FILTER(STRSTARTS(?label, "Bataille "))
}

http://tinyurl.com/jljf6xr

Quelques explications de base :

  • ?item wdt:P31/wdt:P279* wd:Q178561 . cherche les éléments qui sont des batailles ou des sous-classes de batailles, pour être bien sûr que je ne vais pas virer sa majucule à un bouquin intitulé  « Bataille de Perpète-les-Olivettes »…
  • Sue la ligne suivante, je demande les libellés pour les éléments en question ?item rdfs:label ?label .  et les filtre pour ne garder que ceux en français FILTER(LANG(?label) = « fr ») . . Comme j’ai besoin d’utiliser le libellé dans la requête et pas juste de l’afficher (et comme Harmonia Amanda l’a expliqué dans son billet de dimanche), je ne peux pas utiliser le service wikibase:label, et je me rabats donc sur le standard du web sémantique rdfs:label.
  • La dernière ligne est un FILTER  (filtre), qui ne garde que les résultats qui répondent à la fonction à l’intérieur. Ici, STRSTARTS  vérifie si ?label commence avec « Bataille ».

Au moment où j’ai écrit la version anglaise de ce texte, la requête renvoyait 3521 résultats. Beaucoup trop pour les corriger à la main, et je ne connais aucun outil déjà existant qui pourrait faire ça pour moi… Je suppose qu’il est temps de dégainer Python, du coup !

Le script Python

J’aime Python. J’adore carrément Python, même. Ce langage est génial pour créer une une application utile en une poignée de minutes, facile à lire (pour peu qu’on lise l’anglais), pas constellé de séries d’accolades ou de points-virgules, et a des libs géniales pour les choses que je fais le plus avec : récupérer le contenus de pages web, trier des données, vérifier des ISBNs[1] et faire des sites web. Oh, et pour faire des requêtes SPARQL et traiter les résultats[2].

Et puis le nom du langage a un petit côté « charmeur de serpents » 😉 (Snake Charmers, CC-BY-SA 2.0, Frank Long)

Premières remarques

Si vous ne connaissez pas du tout le Python, cet article n’est pas le bon endroit pour ça, mais il y a de nombreuses ressources disponibles en ligne[3]. Assurez-vous juste qu’elles sont à jour et pensées pour Python 3. La suite de cet article part du principe que vous avez une connaissance basique de Python (indentation, variables, chaînes de caractères, listes, dictionnaires, imports et boucles for.), et que Python 3 et pip sont installés sur votre machine.

Pourquoi Python 3 ? Parce que nous allons manipuler des chaînes qui viennent de Wikidata et sont donc encodées en UTF-8 et que Python 2 n’est pas hyper pratique pour ça. Et puis mince, on est en 2016, par Belenos !

Pourquoi pip ? Parce qu’on a besoin d’une libraire non-standard pour faire des requêtes SPARQL, appelée SPARQLwrapper, et que cette commande est le moyen le plus simple de l’installer :

pip install sparqlwrapper

Allez, on commence à scripter !

Pour commencer, un script qui fait une requête Sparql retournant la liste des sièges à corriger[4] :

#!/usr/bin/env python3

from SPARQLWrapper import SPARQLWrapper, JSON
from pprint import pprint

endpoint = "https://query.wikidata.org/bigdata/namespace/wdq/sparql"

sparql = SPARQLWrapper(endpoint)
sparql.setQuery("""
SELECT ?item ?label WHERE {{
  ?item wdt:P31/wdt:P279* wd:Q178561 .
  ?item rdfs:label ?label . FILTER(LANG(?label) = "fr") .
  FILTER(STRSTARTS(?label, "Siège ")) .
}}
""")  # Link to query: http://tinyurl.com/z8bd26h

sparql.setReturnFormat(JSON)

results = sparql.query().convert()

pprint(results)

Ça fait un bon petit paquet de lignes, mais que font-elles ? Comme on va le voir, la plupart vont en fait être incluses à l’identique dans tout script qui fait une requête  SPARQL.

  • Pour commencer, on importe deux choses du module  SPARQLWrapper : la classe SPARQLWrapper elle-même et la constante « JSON » qu’elle va utiliser plus tard (pas d’inquiétude, on n’aura pas à manipuler du json directement.)
  • On import aussi le module « Pretty printer » pour afficher les résultats de manière plus lisible.
  • Ensuite, on crée une variable qu’on nomme « endpoint », qui contient l’URL complète vers le point d’accès SPARQL de Wikidata[5].
  • Ensuite, on crée une instance de la classe SPARQLWrapper qui utilisera ce point d’accès pour faire des requêtes, et on les met dans une variable simplement appelée  « sparql ».
  • On applique à cette variable la fonction setQuery, qui est l’endroit où l’on rentre la requête de tout à l’heure. Attention, il faut doublonner les accolades (remplacer { et } par {{ et }}, car elles sont des caractères réservés dans les chaînes Python.
  • sparql.setReturnFormat(JSON)  dit au script que le résultat sera retourné en json.
  • results = sparql.query().convert(), enfin, fait la requête elle-même et convertit la réponse dans un dictionnaire Python appelé  « results ».
  • Et pour l’instant, on va juste afficher le résultat à l’écran pour voir ce qu’on obtient.

Ouvrons un terminal et lançons le script :

$ python3 fix-battle-labels.py 
{'head': {'vars': ['item', 'label']},
 'results': {'bindings': [{'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q815196'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Pskov',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q815207'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Silistra',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q815233'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Tyr',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q608163'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Cracovie',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q1098377'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Narbonne',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q2065069'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Hloukhiv',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q4087405'},
                           'label': {'type': 'literal',
                                     'value': "Siège d'Avaricum",
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q2284279'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Fort Pulaski',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q4337397'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Liakhavitchy',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q4337448'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Smolensk',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q701067'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Rhodes',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q7510162'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Cracovie',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q23013145'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Péronne',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q10428014'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Pskov',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q3090571'},
                           'label': {'type': 'literal',
                                     'value': 'Siège du Hōjūjidono',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q3485893'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Fukuryūji',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q4118683'},
                           'label': {'type': 'literal',
                                     'value': "Siège d'Algésiras",
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q5036985'},
                           'label': {'type': 'literal',
                                     'value': 'Siège de Berwick',
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q17627724'},
                           'label': {'type': 'literal',
                                     'value': "Siège d'Ilovaïsk",
                                     'xml:lang': 'fr'}},
                          {'item': {'type': 'uri',
                                    'value': 'http://www.wikidata.org/entity/Q815112'},
                           'label': {'type': 'literal',
                                     'value': "Siège d'Antioche",
                                     'xml:lang': 'fr'}}]}}

C’est un gros paquet de résultats mais on peut voir que c’est un dictionnaire qui contient deux entrées :

  • « head », qui contient les noms des deux variables renvoyées par la requête,
  • et « results », qui contient lui-même un autre dictionnaire avec la clef « bindings », associée avec la liste des résultats eux-mêmes, chacun d’entre eux étant lui-même un dictionnaire  Python. Pfiou…

Examinons un desdits résultats :

{'item': {'type': 'uri',
          'value': 'http://www.wikidata.org/entity/Q17627724'},
 'label': {'type': 'literal', 
           'value': "Siège d'Ilovaïsk",
           'xml:lang': 'fr'}}

C’est un dictionnaire avec deux clefs (label et item), chacune ayant pour valeur un autre dictionnaire qui à son tour a une clef « value » associée avec, cette fois, la valeur qu’on veut au final. Enfin !

Parcourir les résultats

Parcourons la liste « bindings » avec une boucle « for » de Python, pour pouvoir en extraire les résultats.

for result in results["results"]["bindings"]:
    qid = result['item']['value'].split('/')[-1]
    label = result['label']['value']

    print(qid, label)

Rapide explication sur la ligne qid = result[‘item’][‘value’].split(‘/’)[-1]  : comme l’identifiant de l’élément est en fait stocké sous la forme d’une URL complète (« https://www.wikidata.org/entity/Q17627724 » et pas juste « Q17627724 »), il nous faut séparer cette chaîne à chaque caractère ‘/’, ce qu’on fait à l’aide de la fonction « split()« , qui transforme la chaîne en une liste Python contenant ceci :

['https:', '', 'www.wikidata.org', 'entity', 'Q17627724']

Nous ne voulons que le dernier élément de cette liste. En Python, c’est celui avec l’index -1, d’où le [-1] à la fin de la ligne. Enfin, nous stockons cette valeur dans la variable qid.

Lançons le script ainsi modifié :

$ python3 fix-battle-labels.py 
Q815196 Siège de Pskov
Q815207 Siège de Silistra
Q815233 Siège de Tyr
Q608163 Siège de Cracovie
Q1098377 Siège de Narbonne
Q2065069 Siège de Hloukhiv
Q4087405 Siège d'Avaricum
Q2284279 Siège de Fort Pulaski
Q4337397 Siège de Liakhavitchy
Q4337448 Siège de Smolensk
Q701067 Siège de Rhodes
Q7510162 Siège de Cracovie
Q23013145 Siège de Péronne
Q10428014 Siège de Pskov
Q3090571 Siège du Hōjūjidono
Q3485893 Siège de Fukuryūji
Q4118683 Siège d'Algésiras
Q5036985 Siège de Berwick
Q17627724 Siège d'Ilovaïsk
Q815112 Siège d'Antioche

Corriger le problème

On y est presque ! Maintenant, il reste à remplacer cet orgueilleux « S » majuscule par un plus modeste « s » minuscule :

label = label[:1].lower() + label[1:]

Que se passe-t-il ici ? Une chaîne Python fonctionne comme une liste, on peut donc lui demander de prendre la partie située entre le début de la chaîne « label » et la position qui suit le premier caractère (« label[:1] ») et forcer celui-ci en bas-de-casse (« .lower() »). Ensuite, on y concatène le reste de la chaîne (de la position 1 à la fin, donc « label[1:] ») et on réassigne ce résultat à la variable « label ».

Dernière chose, formater le résultat de manière compatible à QuickStatements:

out = "{}\tLfr\t{}".format(qid, label) print(out)

Cette ligne semble barbare ? Elle est en fait assez simple : « {}\tLfr\t{} »  est une chaîne qui contient un premier emplacement pour le résultat d’une variable (« {} »), puis une tabulation, (« \t »), puis le mot-clef Quickstatements pour le libellé français (« Lfr »), une autre tabulation et enfin le second emplacement pour une variable. Ensuite, la fonction « format() » se charge de mettre le contenu des variables « qid » et « label » dedans. Le script final devrait ressembler à ça :

#!/usr/bin/env python3

from SPARQLWrapper import SPARQLWrapper, JSON

endpoint = "https://query.wikidata.org/bigdata/namespace/wdq/sparql"

sparql = SPARQLWrapper(endpoint)
sparql.setQuery("""
SELECT ?item ?label WHERE {{
  ?item wdt:P31/wdt:P279* wd:Q178561 .
  ?item rdfs:label ?label . FILTER(LANG(?label) = "fr") .
  FILTER(STRSTARTS(?label, "Siège ")) .
}}
""")  # Link to query: http://tinyurl.com/z8bd26h

sparql.setReturnFormat(JSON)

results = sparql.query().convert()

for result in results["results"]["bindings"]:
    qid = result['item']['value'].split('/')[-1]
    label = result['label']['value']

    label = label[:1].lower() + label[1:]

    out = "{}\tLfr\t{}".format(qid, label)
    print(out)

C’est parti :

$ python3 fix-battle-labels.py 
Q815196	Lfr	siège de Pskov
Q815207	Lfr	siège de Silistra
Q815233	Lfr	siège de Tyr
Q2065069	Lfr	siège de Hloukhiv
Q2284279	Lfr	siège de Fort Pulaski
Q1098377	Lfr	siège de Narbonne
Q608163	Lfr	siège de Cracovie
Q4087405	Lfr	siège d'Avaricum
Q4337397	Lfr	siège de Liakhavitchy
Q4337448	Lfr	siège de Smolensk
Q701067	Lfr	siège de Rhodes
Q10428014	Lfr	siège de Pskov
Q17627724	Lfr	siège d'Ilovaïsk
Q23013145	Lfr	siège de Péronne
Q815112	Lfr	siège d'Antioche
Q3090571	Lfr	siège du Hōjūjidono
Q3485893	Lfr	siège de Fukuryūji
Q4118683	Lfr	siège d'Algésiras
Q5036985	Lfr	siège de Berwick

On est bons ! Il ne reste plus qu’à copier-coller le résultat dans QuickStatements et attendre qu’il fasse le boulot tout seul.

Image d’en-tête:

Photographie de fontes de caractères par Andreas Praefcke (domaine public)

Mots-clefs :

0 commentaire publié.

Notes de bas de page

  1. J’espère pouvoir bientôt publier quelque chose ici sur ce sujet.

  2. En plus, les exemples dans la doc officielle sont basés sur Firefly. Yes sir, Captain Tightpants.

  3. Par exemple, https://www.codecademy.com/learn/python ou https://docs.python.org/3.5/tutorial/.

  4. Oui, les sièges, j’ai déjà corrigé les batailles avant d’écrire le billet 😉

  5. Et non son accès web qui est simplement « https://query.wikidata.org/ »

Commentaires

Les commentaires sont fermés.