Wikidataficator
Résumés des fonctionnalités
Ce bot a pour but d'aller récupérer les numéros d'identification Wikidata de tous les personnages présents sur Wikipast, et les intégrer aux pages Wikipast de ces derniers.
Description technique
Le bot récupère la liste de tous les personnages présents sur Wikipast en effectuant un recherche du "flag" Q5. Ce flag correspond à la catégorie "human" sur Wikidata, et indique donc qu'une entrée correspond effectivement à un personnage, et non pas à une œuvre par exemple. Le flag Q5 est inséré sur Wikipast pour tous les personnages par le bot GallicaSPARQLBot. Après avoir établi la liste de tous les personnages, le bot itère sur chaque entrée, en les traitant de la manière suivante :
Il recherche sur Wikidata le nom du personnage en question, et plusieurs scénarios sont alors possibles :
- Sur Wikipast, le personnage possède un identifiant de la Banque Nationale de France (BNF ID, cet identifiant est également inséré par le GallicaSPARQLBot). Si un BNF ID est aussi disponible sur Wikidata, ces deux numéros sont comparés :
- Ces deux numéros sont égaux. Le personnage est alors séléctionné, et son numéro d'identification Wikidata est inséré sur Wikipast.
- Ces deux numéros ne sont pas égaux.
- Le BNF id n'est pas disponible sur Wikipast ou sur Wikidata. Les personnages seront alors comparés à l'aide de leur date de naissance et date de décès.
- Les deux dates correspondent.
- Une seule des deux dates correspondent
- Aucune date ne correspond
- Toutes les entrées ont été traitées et aucun match n'a été trouvé. Le bot sélectionne alors soit le guess décrit précédemment si il y en a un, ou si il n'y en a pas, "Match not found" sera indiqué sur Wikipast avec un lien vers la création d'une nouvelle page sur Wikidata.
- Aucun résultat de recherche n'apparaît sur Wikidata. Le bot va alors insérer "Match not found" sur Wikipast avec un lien vers la création d'une nouvelle page sur Wikidata.
Voir la section "Exemple de résultats" pour une illustration de ces différents résultats.
Évaluation des performances
Première version
Dans un premier temps, le bot a été testé sur les entrées issues de la page "Bibliographies". Le flag Q5 n'étant pas encore inséré à ce moment là, toutes les vérifications ont été effectuées avec les dates de naissances et de décès. Les résultats sont encore visibles sur cette page. Les résultats étaient très encourageants, avec seulement 4 entrées sur 50 nécessitant une vérification humaine. Ces 4 entrées ont été vérifiées, et sont correctes. Toutes les entrées ne nécessitant pas de vérification humaine étant correctes également, la précision étant alors de 100%. Nous avons manuellement rajouté le flag Q5 à ces pages après coup, pour une syntaxe cohérente. Après cela, nous avons pu passer à l'étape d'après impliquant la vérification à l'aide de l'identifiant de la Banque Nationale de France.
Version finale
La version finale du bot étant la plus intéressante, voici une analyse un peu plus détaillée de ses résultats.
Le bot a d'abord été lancé sur plusieurs sous-ensembles de pages, pour éviter de rencontrer des problèmes majeurs qui affecteraient un trop grand nombre de données.
En effet, le GallicaSPARQLBot ayant inséré près de 500'000 entrées sur Wikipast, nous ne voulions pas prendre le risque d'altérer autant de données. Après quelques ajustements mineurs, nous avons lancé le bot sur l'ensemble des données.
Voici les statistiques calculées :
- % de match not found
- % de uncertain identification
- BNF id manquant sur wikidata
- Numero BNF qui ne match pas
Précision
Vitesse
La phase la plus lente du bot est celle qui génère la liste des entrées à traiter. En effet, Wikipast possédant désormais plus de 500'000 personnages avec le flag Q5, le bot doit toutes les traiter. Nous avons mesuré environ 1 heure et 20 minutes pour établir la liste.
Exemple de résultats
Entrée vérifiée grâce au numéro BnF ou aux dates de naissance/décès :
Entrée avec trop peu d'informations, pas de numéro BnF ni date de naissance/décès :
Entrée non-existante sur Wikidata :
Code
import requests import json from bs4 import BeautifulSoup from urllib.request import urlopen from dateutil.parser import parse def getWikidataBirthdayDeathday(number_wiki): response = urlopen('https://www.wikidata.org/wiki/Special:EntityData/'+number_wiki+'.json') page_source=json.loads(response.read()) claims = page_source['entities'][number_wiki]['claims'] try: birthdate_wikidata = claims['P569'][0]['mainsnak']['datavalue']['value']['time'] except: birthdate_wikidata = " N/A" try: deathdate_wikidata = claims['P570'][0]['mainsnak']['datavalue']['value']['time'] except: deathdate_wikidata = " N/A" return (birthdate_wikidata, deathdate_wikidata) user='testbot' passw='******' baseurl='http://wikipast.epfl.ch/wikipast/' summary='Wikipastbot update' name='test_bot_mionscalisi' # Login request payload={'action':'query','format':'json','utf8':'','meta':'tokens','type':'login'} r1=requests.post(baseurl + 'api.php', data=payload) #login confirm login_token=r1.json()['query']['tokens']['logintoken'] payload={'action':'login','format':'json','utf8':'','lgname':user,'lgpassword':passw,'lgtoken':login_token} r2=requests.post(baseurl + 'api.php', data=payload, cookies=r1.cookies) #get edit token2 params3='?format=json&action=query&meta=tokens&continue=' r3=requests.get(baseurl + 'api.php' + params3, cookies=r2.cookies) edit_token=r3.json()['query']['tokens']['csrftoken'] edit_cookie=r2.cookies.copy() edit_cookie.update(r3.cookies) # 1. retrouver le contenu et l'imprimer url_wikipast = urlopen("http://wikipast.epfl.ch/wikipast/index.php/Biographies") source = url_wikipast.read() soup=BeautifulSoup(source,'html.parser') wikidata_addr = 'https://www.wikidata.org/wiki/' wikidata_limit = 15 def processEntry3(a): wikidata_addr = 'https://www.wikidata.org/wiki/' wikidata_limit = 15 content = '' print(a.string) ## Looking for the date on wikipast u_d = 'http://wikipast.epfl.ch' + a.get('href') url_date = urlopen(u_d) source_date = url_date.read() soup_date=BeautifulSoup(source_date,'html.parser') birth_found = False for t2 in soup_date.findAll('li'): for n in t2.findAll(title = 'Naissance'): birth_found = True break; if(birth_found): break; if(birth_found): birthdate_wikipast = n.parent.a.text death_found = False for t2 in soup_date.findAll('li'): for n in t2.findAll(title = 'Décès'): for n2 in t2.findAll(class_ = 'selflink'): death_found = True break; if(death_found): break; if(death_found): deathdate_wikipast = n.parent.a.text else: for t2 in soup_date.findAll('li'): for n in t2.findAll(title = 'Mort'): for n2 in t2.findAll(class_ = 'selflink'): death_found = True break; if(death_found): break; if(death_found): deathdate_wikipast = n.parent.a.text # WIKIDATA url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&search=" url = url + str(a.string.replace(' ', '_').replace('ü', 'u').replace('é', 'e').replace('è', 'e').replace('ö', 'o').replace('í', 'i')) url = url + "&language=en&limit="+str(wikidata_limit)+"&format=json" url = urlopen(url) source = json.load(url) matchFound = False resultFound = False # indicates that at least one page has been found for itemsQuerryNumber in range(wikidata_limit): try: number_wiki = source['search'][itemsQuerryNumber]['id'] except: break; resultFound = True ## Looking for the date on wikidata (birthdate_wikidata, deathday_wikidata) = getWikidataBirthdayDeathday(number_wiki) #try with parse(birthdate_wikidata[1:11], "yyyy.dd.mm") #print(str(itemsQuerryNumber)+" "+birthdate_wikipast[0:4] + " - "+birthdate_wikidata[1:min(5,len(birthdate_wikipast)+1)].replace('-', '.')) if((birth_found and birthdate_wikidata[1:min(5,len(birthdate_wikipast)+1)].replace('-', '.') == birthdate_wikipast[0:4]) or (death_found and deathday_wikidata[1:min(5,len(deathday_wikidata)+1)].replace('-', '.') == deathdate_wikipast[0:4])): matchFound = True break if(not matchFound and resultFound): number_wiki = source['search'][0]['id'] (birthdate_wikidata,deathday_wikidata) = getWikidataBirthdayDeathday(number_wiki) if(not resultFound): wikidata_addr = '' number_wiki = '' birthdate_wikidata = '' birthdate_wikipast = '' deathdate_wikipast = '' deathday_wikidata = '' content+= '|-'+ '\n' + '| [[' + a.string + ']]\n' content+= '|[' + wikidata_addr + number_wiki + ' ' + number_wiki+']\n' if(birth_found): content+= '|[[' + birthdate_wikipast + ']]\n' else: content+= '| -' + '\n' content+= '|[[' + str((birthdate_wikidata[1:11])).replace('-', '.') + ']]\n' if(death_found): content+= '|[['+deathdate_wikipast+']]\n' else: content+= '| -' + '\n' content+= '|[[' + str((deathday_wikidata[1:11])).replace('-', '.') + ']]\n' content+= '|'+str(not matchFound)+'\n' return content import multiprocessing as mp import sys sys.setrecursionlimit(10000) %%time joblist = [] for primitive in soup.findAll('table'): for a in primitive.findAll('a'): joblist.append(a) pool = mp.Pool(20) res = pool.map(processEntry3,joblist) pool.close() content='{| class="wikitable" \n|-\n! scope="col" | Noms\n ! scope="col" | Wikidata\n ! scope="col" | Birthdate wikipast\n ! scope="col" | Birthdate wikidata\n ! scope="col" | Deathdate wikipast\n ! scope="col" | Deathdate wikidata\n ! scope="col" | Human verification required\n' for i in range(len(joblist)): content+=res[i] content += '|}' payload={'action':'edit','assert':'user','format':'json','utf8':'','text':content,'summary':summary,'title':name,'token':edit_token} r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie)