Wikidataficator

De Wikipast
Aller à la navigation Aller à la recherche

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. Une fois le flag Q5 trouvé, le Wikidataficator sélectionne les entrées les unes après les autres et regarde pour chacune d'entre elle si elle possède également le numéro d'identification de la Bibliothèque Nationale de France (ce numéro est aussi ajouté aux personnages par le bot GallicaSPARQLBot). Trois scénarios sont alors possibles :

  • Le numéro d'identification de la BNF est présent sur Wikipast. Une recherche d'après ce numéro est effectuée sur Wikidata. Si la recherche donne un résultat, le bot vérifie que cela corresponde effectivement à un numéro d'identification de la BNF, et non pas à autre chose. Si tel est le cas, alors le personnage trouvé sur wikidata à l'aide de ce numéro est considéré comme étant identique à celui de Wikipast, l'identifiant de la BNF étant unique. Le numéro d'identification Wikidata du personnage est alors récupéré, et inséré sur sa page Wikipast. Si aucune entrée ne correspond à cet identifiant de la BNF, l'indication "Match not found" est inscrite sur Wikipast.
  • Le numéro d'identification de la BNF n'est pas présent sur Wikipast. Une recherche d'après le nom du personnage est effectuée sur Wikidata, et le bot évalue ensuite la probabilité que le premier résultat soit le bon en se basant sur les critères suivants : année de naissance, année du décès. Si les informations ne correspondent pas entre Wikipast et Wikidata, le bot va comparer l'entrée suivante, et ainsi de suite, jusqu'à ce qu'il trouve une entrée correspondante. Lorsque cette dernière est trouvée, le bot récupère le numéro d'identification Wikidata et l'intègre à la page Wikipast du personnage. Si aucune entrée ne correspond, ou si les dates de naissance et de décès ne sont pas connues, le premier résultat de la liste sera sélectionné, et une vérification humaine sera nécessaire. Cela sera indiqué par un flag "Uncertain identification" sur la page Wikipast du personnage. Nous avons pris cette décision car nous avons jugé que le fait de nécessiter quelques vérifications humaines n'était pas trop contraignant, et que cela était plus intéressant que de simplement abandonner une entrée.
  • L'entrée n'existe pas du tout sur Wikidata (aucun nom de correspond). 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 trois scénarios.

É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 :

Wikidataficator exemple4.png

Entrée avec trop peu d'informations, pas de numéro BnF ni date de naissance/décès :

Wikidataficator exemple2.png

Entrée non-existante sur Wikidata :

Wikidataficator exemple3.png

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)