« Wikidataficator » : différence entre les versions

De Wikipast
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
 
(47 versions intermédiaires par 3 utilisateurs non affichées)
Ligne 1 : Ligne 1 :
{| class="wikitable"
|Langue
|'''Français'''
|[[(en)_Wikidataficator|English]]
|}


== Résumés des fonctionnalités ==
== 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.
Ce bot a pour but d'aller récupérer les numéros d'identification [https://www.wikidata.org/wiki/Wikidata:Main_Page Wikidata] de tous les personnages présents sur Wikipast, et les intégrer aux pages Wikipast de ces derniers.


== Description technique ==
== 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 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 Wikidataficator itère sur chaque entrée, en les traitant de la manière suivante :


* 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.
Il recherche sur Wikidata le nom du personnage en question, et deux premiers scénarios sont alors possibles :


* 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.
*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.


* 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.
*Des résultats de recherche sont disponibles sur Wikidata.  


Voir la section "Exemple de résultats" pour une illustration de ces trois scénarios.
Si tel est le cas, voici l'algorithme de recherche du Wikidataficator :
 
*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 :
**Si ces deux numéros sont égaux, le personnage est alors sélectionné, et son numéro d'identification Wikidata est inséré sur Wikipast. Le bot passe alors au personnage suivant sur Wikipast.
**Si ces deux numéros ne sont pas égaux, le bot va regarder si les dates de naissance et de décès du personnage sur Wikidata correspondent à celles sur Wikipast. Si tel est le cas, ce personnage sera sauvegardé comme "guess", et le bot va continuer à comparer le personnage Wikipast avec les entrées suivantes sur Wikidata, afin de voir si un BNF ID correspondant peut être trouvé (ce qui serait un meilleur match, car un BNF ID est censé être unique et ne devrait pas être erroné).
 
*Si le BNF id n'est pas disponible sur Wikipast ou sur Wikidata, les personnages sont alors comparés à l'aide de leur date de naissance et date de décès.
** Si les deux dates correspondent sur Wikipast et Wikidata, le personnage sera sélectionné et son identifiant Wikidata sera inséré sur Wikipast. Le Wikidataficator passe alors au personnage Wikipast suivant.
** Si une seule des deux dates correspond, le personnage sera sélectionné et son identifiant Wikidata sera inséré sur Wikipast, avec en plus la mention "Uncertain identification". Le Wikidataficator passe alors au personnage Wikipast suivant.
** Si aucune des deux dates ne correspond, le Wikidataficator continue les recherches pour ce personnage Wikipast en le comparant avec l'entrée Wikidata suivante.
 
*Si aucun match n'a été trouvé une fois toutes les entrées Wikidata traitées pour un personnage Wikipast donné, il y a trois possibilités :
**Le bot avait enregistré un "guess" lors d'une comparaison de BNF ID différents. Il va donc choisir ce personnage, et insérer son numéro Wikidata sur Wikipast, ainsi que la mention "Uncertain identification".
**Le bot n'avait enregistré aucun guess, et le personnage avait des dates de naissance et décès sur Wikipast. "Match not found" sera indiqué sur Wikipast avec un lien vers la création d'une nouvelle page sur Wikidata.
**Le personnage sur Wikipast ne possédait ni date de naissance, ni date de décès, et aucun guess n'a été enregistré. Alors le bot va prendre le premier personnage de la liste sur Wikidata, et l'insérer sur Wikipast avec la mention "Uncertain identification".
 
 
Voir la section "Exemple de résultats" pour une illustration de ces différents résultats.
 
=== Choix de stratégie ===
 
*Le BNF ID plus fort que les dates de naissance et décès :
Comme dit précédemment, un BNF ID est un numéro unique. Cela signifie que deux personnages ayant le même numéro BNF ID sont identiques, et que deux entrées ayant des BNF ID différents devraient être deux personnages différents. Cela n'étant pas le cas pour les dates de naissances, il nous a semblé naturel de d'abord se baser sur le numéro de la BNF. De plus, lors de la comparaison des dates, nous avons choisi de nous limiter à une comparaison des années, les erreurs sur les mois et les jours étant trop fréquentes.
 
*La méthode du guess :
Pourquoi avoir donc instauré cette technique de guess, en cas de BNF ID différent, mais de dates de naissance et de décès similaire ? L'erreur étant humaine, il nous est arrivé à quelques reprises d'observer une erreur dans le BNF ID sur Wikidata. Ne voulant pas que ce genre d'erreurs aient trop d'impacts sur nos résultats, nous avons décidé de faire "deviner" au bot une erreur de BNF ID.
 
*Uncertain identification :
 
Lorsque trop peu d'informations sont vérifiées entre Wikipast et Wikidata, le Wikidataficator va tout de même tenter sa chance et sélectionner l'entrée la plus probable (ayant au moins une date en commun, ou la première de la liste si aucune information concernant les dates de naissance et décès ne sont disponibles sur Wikipast). L'indication "Uncertain identification" indiquera cependant aux utilisateurs qu'il serait judicieux d'approfondir un peu la recherche pour être sûr qu'il s'agisse bien de la bonne personne. Nous pensons que cela n'est pas une trop grosse contrainte, et que ce résultat est plus intéressant qu'un simple "Match not found".
 
*Match not found :
 
Les 500'000 entrées insérées par le GallicaSPARQLbot sont composées d'auteurs connus, et moins connus. C'est pourquoi plus ou moins la moitié de ces auteurs ne sont pas présents du tout sur Wikidata. Sans aucune personne du même nom sur Wikidata, nous n'avions donc pas le choix. C'est pour cela que le Wikidataficator insère sur Wikipast un lien direct vers la création de la page du personnage sur Wikidata.


== Évaluation des performances ==
== Évaluation des performances ==
Ligne 20 : Ligne 58 :
=== Première version ===
=== Première version ===


Dans un premier temps, le bot a été testé sur les entrées issues de la page [http://wikipast.epfl.ch/wikipast/index.php/Biographies "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 [http://wikipast.epfl.ch/wikipast/index.php/Test_bot_mionscalisi 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.
Dans un premier temps, le bot a été testé sur les entrées issues de la page [http://wikipast.epfl.ch/wikipast/index.php/Biographies "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 naissance et de décès. Les résultats sont encore visibles sur [http://wikipast.epfl.ch/wikipast/index.php/Test_bot_mionscalisi 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 était 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 ===
=== 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.
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.
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.
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 voulu lancer le bot sur l'ensemble des données. Cependant, nous avons rencontré un problème indépendant de notre volonté : le serveur Wikipast était vite surchargé et ne répondait plus après quelques milliers d'entrées. Nous avons donc dû nous résoudre à les lancer petit à petit. Les statistiques que vous trouverez ci-dessous portent donc sur 400'000 entrées environ.


Voici les statistiques calculées :
Voici les statistiques calculées :


*% de match not found
*Pourcentage de "Match not found"
*% de uncertain identification
*Pourcentage de "Uncertain identification"
*BNF id manquant sur wikidata
*Nombre de BNF ID ne correspondant pas entre Wikipast et Wikidata
*Numero BNF qui ne match pas
 
==== Précision ====
 
*Match not found
Les "Match not found" représentent environ 80% (317'000 / 400'000) des entrées sur Wikipast. Ce pourcentage très élevé s'explique par le fait que les auteurs insérés par le [[GallicaSPARQLBot]] sont souvent peu connus.
 
*Uncertain identification
 
Concernant les "Uncertain identification", cela représente environ 16% (64'321 / 400'000) des données. Si nous avions voulu obtenir un pourcentage plus bas, cela aurait eu comme conséquence d'augmenter le nombre de "Match not found".
 
*BNF ID incohérent


Finalement, lorsque le BNF ID qui ne correspond pas entre Wikidata et Wikipast, mais qu'un personnage est quand même sélectionné à cause de ses dates de naissance et de décès (méthode du guess), il nous a semblé que le résultat était correct (nous en avons vérifié une dizaine, voir [[Joseph Leidy]] par exemple). Ces cas là sont comptés comme "Uncertain identification", et représentent 721 personnes sur 400'000 environ.


[[Fichier:stats_wikidataficator.png]]


==== Précision ====
*Contrôle manuel


En plus des statistiques précédemment décrites, nous avons analysé environ une centaine d'entrées traitées par le Wikidataficator afin de voir si les résultats trouvés étaient bien ceux attendus, et ce fût le cas pour les identifications certaines. En effet, les entrées ne possédant pas le label "Uncertain identification" ont soit un BNF ID correct, soit pas de BNF ID, mais des dates de naissance et de décès cohérentes. Les "Match not found" sont bel et bien introuvables sur Wikidata. Concernant les "Uncertain identification", sur une vingtaine d'entrées, il nous a semblé que l'identification était quand même correcte pour environ la moitié des cas. Pour l'autre moitié, le bot n'ayant aucune information sur les dates de naissance et de décès, avait simplement sélectionné le premier personnage de la liste.


Il est possible que certaines entrées possèdent le label "Match not found" et "Uncertain identification". Cela est dû à la première version de notre bot, qui insérait "Uncertain identification" pour tous les "Match not found". Ceci a été corrigé par la suite, et n'a pas été pris en compte dans le calcul des statistiques.


==== Vitesse ====
==== 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.
La phase la plus lente du bot est celle qui génère la liste des entrées à traiter (la liste de tous les personnages Wikipast). 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.
 
Ensuite, le traitement des entrées est relativement rapide, tant que le serveur répond. Nous avons introduit du multiprocessing afin que le bot soit effectué sur plusieurs threads. Avec 4 threads, environ 300 personnages par minute sont traités. Nous n'avons pas pu augmenter le nombre de threads en raison des problèmes liés au serveur saturé sur Wikipast.
 
==== Analyse critique ====
 
Bien que les résultats observés aient toujours été ceux attendus, il est intéressant de discuter de la pertinence des choix que nous avons fait. En effet, nos critères sont assez souples. Par exemple, si les BNF ID ne sont pas disponibles, une seule des deux dates (naissance ou décès) nous suffit pour sélectionner un personnage, avec la mention "Uncertain identification". Si les dates de naissance et de décès ne sont pas disponibles sur Wikipast, et qu'aucun BNF ID similaire n'est trouvé sur Wikidata, le premier résultat de la liste est sélectionné. Nous aurions pu décider de durcir ces critères, car cela améliorerait probablement la précision des résultats. Il y aurait en effet beaucoup moins d'erreurs car nous n'aurions aucun "Uncertain identification". En revanche, cela augmenterait encore le nombre de "Match not found", alors que plusieurs personnages n'avaient simplement pas le bon BNF ID, ou trop peu d'informations. Avec la méthode que nous avons choisi, un "Match not found" assure l'utilisateur que le personnage n'est pas présent sur Wikidata, et un "Uncertain identification" indique qu'aucun meilleur match n'existe. Il suffit alors d'un seul clic pour vérifier si le résultat est cohérent.


== Exemple de résultats ==
== Exemple de résultats ==
Ligne 60 : Ligne 118 :
=== Entrée non-existante sur Wikidata : ===
=== Entrée non-existante sur Wikidata : ===


[[Fichier:wikidataficator_exemple3.png]]
[[Fichier:wikidataficator_exemple5.png]]


== Code ==
== Code ==
Create the job list with the script bellow:
  <nowiki>
  <nowiki>
import requests
import requests
import json
import json
Ligne 69 : Ligne 132 :
from urllib.request import urlopen
from urllib.request import urlopen
from dateutil.parser import parse
from dateutil.parser import parse
import urllib
import progressbar
import uuid
import pickle
import multiprocessing as mp
import sys
sys.setrecursionlimit(1000000)
user='Wikidataficator'
passw='dhbot2019'
baseurl='http://wikipast.epfl.ch/wikipast/'
summary='Wikidataficator update'
name='Wikidataficator'
# 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 = 30
#!/usr/bin/python3
import json
import os.path
from urllib.request import urlopen
if os.path.isfile('./nbPeople.dat'):
    nbJobs = open('nbPeople.dat', 'r')
    print(nbJobs.read())


def getWikidataBirthdayDeathday(number_wiki):
#get all of the entries that have Q5. We need to while loop since there is a max querry size
wikidata_limit = 500
wikidate_offset = 0
numberOfItemsToTreat = 1
joblist = []
while(numberOfItemsToTreat != 0):
    print(wikidate_offset)
    url = "http://wikipast.epfl.ch/wikipast/api.php?action=query&list=search&srwhat=text&srsearch=Q5&format=json"
    url = url + "&language=en&srlimit="+str(wikidata_limit)+"&sroffset="+str(wikidate_offset)
    url = urlopen(url)
 
    source = json.load(url)
 
    numberOfItemsToTreat = len(source['query']['search'])
 
    for i in range(numberOfItemsToTreat):
        if str(source['query']['search'][i]['snippet']).find('Wikidata: ([https://www.wikidata.org/wiki/<span class=\'searchmatch\'>Q5</span> <span class=\'searchmatch\'>Q5</span>])') != -1 \
        or str(source['query']['search'][i]['snippet']).find('Wikidata: <span class=\'searchmatch\'>Q5</span>') != -1 \
        or str(source['query']['search'][i]['snippet']).find('Wikidata: ([https://www.wikidata.org/wiki/<span class=\'searchmatch\'>Q5</span> <span class=\'searchmatch\'>Q5</span>)]') != -1 :
            title = str(source['query']['search'][i]['title'])
            #print(str(source['query']['search'][i]['title'])+"\n\n")
            joblist.append(title.replace(" ","_"))
    wikidate_offset += wikidata_limit
 
 
print(str(len(joblist))+' people found')
 
nbJobs = open('nbPeople.dat', 'w')
nbJobs.write(str(len(joblist)))
nbJobs.close()
#print(joblist)
 
pickle.dump(joblist, open('joblist.b', 'wb'))
</nowiki>
 
Run [[Wikidataficator]] on joblist.
 
<nowiki>
import requests
import json
from bs4 import BeautifulSoup
from urllib.request import urlopen
from dateutil.parser import parse
import urllib
import progressbar
import uuid
import getpass
import time
from passlib.hash import pbkdf2_sha256
import getpass
from pathvalidate import sanitize_filename
 
 
import multiprocessing as mp
import sys
sys.setrecursionlimit(1000000)
 
 
def getWikidataBirthdayDeathdayBnfID(number_wiki):
     response = urlopen('https://www.wikidata.org/wiki/Special:EntityData/'+number_wiki+'.json')
     response = urlopen('https://www.wikidata.org/wiki/Special:EntityData/'+number_wiki+'.json')
     page_source=json.loads(response.read())
     page_source=json.loads(response.read())
Ligne 78 : Ligne 249 :
     except:
     except:
         birthdate_wikidata = " N/A"
         birthdate_wikidata = " N/A"
     try:
     try:
         deathdate_wikidata = claims['P570'][0]['mainsnak']['datavalue']['value']['time']
         deathdate_wikidata = claims['P570'][0]['mainsnak']['datavalue']['value']['time']
     except:
     except:
         deathdate_wikidata = " N/A"
         deathdate_wikidata = " N/A"
    return (birthdate_wikidata, deathdate_wikidata)


user='testbot'
    bnfid_wikidata = []
passw='******'
    try: 
        count = 0
        while True:
            bnfid_wikidata.append(claims['P268'][count]['mainsnak']['datavalue']['value']['time'])
            count += 1
    except:
        try:
            count = 0
            while True:
                bnfid_wikidata.append(claims['P268'][count]['mainsnak']['datavalue']['value'])
                count += 1
        except:
            bnfid_wikidata.append(" N/A")
    return (birthdate_wikidata, deathdate_wikidata, bnfid_wikidata)
 
 
def getWikidataNumberFromBnFID(bnf_id):
    try:
        url = urlopen("https://www.wikidata.org/w/api.php?action=query&list=search&srwhat=text&srsearch="+bnf_id+"&format=json")   
        source = json.load(url)
        return source['query']['search'][0]['title']
    except:
        return "-1"
#Author Robin Mamie
baseurl = 'http://wikipast.epfl.ch/wikipast/'
import lxml
content=''
 
 
def import_page(content, title):
    params = { "format":"xml", "action":"query", "prop":"revisions", "rvprop":"timestamp|user|comment|content" }
    params["titles"] = "API|%s" % urllib.parse.quote(title.replace(' ', '_'))
    qs = "&".join("%s=%s" % (k, v)  for k, v in params.items())
    url = baseurl + 'api.php?%s' % qs
    try:
        tree = lxml.etree.parse(urlopen(url))
    except:
        print('Error importing page. Trying again. \t'+title)
        return import_page(content,title)
    revs = tree.xpath('//rev')
    if revs:
        old_content = revs[-1].text.split('\n')
        listed_old_content = [x for x in old_content if x]
        content.extend(listed_old_content)
 
 
def insertWikidataLink(link,content,uncertainVerification):
    human_ver_string = ''
    if uncertainVerification:
        human_ver_string = ' \'\'Uncertain identification\'\''
    for i in range(len(content)):
        l = content[i]
        if(l.find('Wikidata: [') == -1):
            if l[10:12] == 'Q5':
                insertPoint = l.find(':') + 1
                Q5URL = ' ([https://www.wikidata.org/wiki/Q5'
                content[i] = str(l[0:insertPoint]) + Q5URL + str(l[insertPoint:]) +  str(']) ')
                l = content[i]
            if l[0:8] == 'Wikidata':
                insertionPoint = l.find(':')+1
                content[i] = str(str(l[0:insertionPoint])+str(link)+str(l[insertionPoint:])+human_ver_string)
 
def cleanNameOfUUID(name):
    idx = name.find("(")
    if idx != -1:
        name = name[:idx-1]
    return name
 
 
user='Wikidataficator'
passw=getpass.getpass('Password: ')
f = open('config/passwordhash.dat','r')
hash = f.read()
if(not pbkdf2_sha256.verify(passw, hash)):
    print('Invalid password!')
    exit()
baseurl='http://wikipast.epfl.ch/wikipast/'
baseurl='http://wikipast.epfl.ch/wikipast/'
summary='Wikipastbot update'
summary='Wikidataficator update'
name='test_bot_mionscalisi'
name='Wikidataficator'


# Login request
# Login request
Ligne 114 : Ligne 360 :


wikidata_addr = 'https://www.wikidata.org/wiki/'
wikidata_addr = 'https://www.wikidata.org/wiki/'
wikidata_limit = 15
wikidata_limit = 50


def processEntry3(a):
def processPageToAddWikidataNumber(a):
     wikidata_addr = 'https://www.wikidata.org/wiki/'
     try:
    wikidata_limit = 15
        number_wiki = ''
    content = ''
        print(a)
    print(a.string)
        wikidata_addr = 'https://www.wikidata.org/wiki/'
       
        wikidata_limit = 30
       
    ## Looking for the date on wikipast
      
      
    u_d = 'http://wikipast.epfl.ch' + a.get('href')
        content=[]
    url_date = urlopen(u_d)
        import_page(content,a)
    source_date = url_date.read()
 
    soup_date=BeautifulSoup(source_date,'html.parser')
        sleepTime = 1
          
        if len(content) == 0:
    birth_found = False
            while len(content) == 0:
          
                time.sleep(sleepTime)
    for t2 in soup_date.findAll('li'):
                sleepTime += 1
        for n in t2.findAll(title = 'Naissance'):
                import_page(content,a)
            birth_found = True
                print('!!! Attempt at fetching again. Will sleep for :'+str(sleepTime)+' Person name :'+a+'\n')
             break;
 
        if(birth_found):
 
            break;
         birth_found = False
    if(birth_found):
        death_found = False
        birthdate_wikipast = n.parent.a.text
        BnFID_found = False
       
         BnF_id = ''
     death_found = False
        for c in content:
    for t2 in soup_date.findAll('li'):
            if (c.find('Wikidata: [') != -1):
        for n in t2.findAll(title = 'Décès'):
                print('Already processsed : '+a)
            for n2 in t2.findAll(class_ = 'selflink'):
                return 0
            if (c.find('BnF ID:') != -1):
                #print('BnF ID : '+ c.split()[-1][0:-1])
                BnF_id = c.split()[-1][0:-1]
                BnFID_found = True
   
             if(c.find('[[Naissance]] de [['+a.replace('_',' ')) != -1 or c.find('[[Naissance]] d\'[['+a.replace('_',' ')) != -1):
                #print('Naisance le : '+c.split('[')[2].split(']')[0])
                birthdate_wikipast = c.split('[')[2].split(']')[0]
                birth_found = True
      
            if((c.find('[[Décès]] de [['+a.replace('_',' ')) != -1) or (c.find('[[Mort]] de [['+a.replace('_',' ')) != -1)or (c.find('[[Décès]] d\'[['+a.replace('_',' ')) != -1) or (c.find('[[Mort]] d\'[['+a.replace('_',' ')) != -1)):
                #print('Décès le : '+c.split('[')[2].split(']')[0])
                 death_found = True
                 death_found = True
            break;
                deathdate_wikipast = c.split('[')[2].split(']')[0]               
        if(death_found):
   
            break;
      
     if(death_found):
        # WIKIDATA
         deathdate_wikipast = n.parent.a.text
        url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&search="
     else:
        #url = url + str(a.replace(' ', '_').replace('ü', 'u').replace('é', 'e').replace('è', 'e').replace('ö', 'o').replace('í', 'i'))
         for t2 in soup_date.findAll('li'):
         url = url + urllib.parse.quote(cleanNameOfUUID(a).replace(' ', '_'))
            for n in t2.findAll(title = 'Mort'):
   
                 for n2 in t2.findAll(class_ = 'selflink'):
        url = url + "&language=en&limit="+str(wikidata_limit)+"&format=json"
                    death_found = True
        url = urlopen(url)
      
         source = json.load(url)
 
        matchFound = False
        resultFound = False # indicates that at least one page has been found
        uncertainIdentification = False
        guess_number_wiki = -1
   
        for itemsQuerryNumber in range(wikidata_limit):
   
            try:
                 number_wiki = source['search'][itemsQuerryNumber]['id']
            except:
                 break;
                 break;
             if(death_found):
             resultFound = True
                break;
            ## Looking for the date on wikidata
        if(death_found):
            (birthdate_wikidata, deathday_wikidata, bnfid_wikidata) = getWikidataBirthdayDeathdayBnfID(number_wiki)
             deathdate_wikipast = n.parent.a.text
   
                   
            birthdate_wikidata_found = (birthdate_wikidata != ' N/A')
       
             deathday_wikidata_found = (deathday_wikidata != ' N/A')
    # WIKIDATA
            bnfid_wikidata_found = (len(bnfid_wikidata) != 1)
    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'))
            if(BnFID_found and bnfid_wikidata_found):
                for bnfid_wikidata_element in bnfid_wikidata:
                    if(BnF_id == bnfid_wikidata_element):
                        matchFound = True
                        break
                    else:
                        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])):
                            if(guess_number_wiki == -1):
                                guess_number_wiki = number_wiki
                if matchFound:
                    break
            else:
                if((birth_found and birthdate_wikidata[1:min(5,len(birthdate_wikipast)+1)].replace('-', '.') == birthdate_wikipast[0:4]) and (death_found and deathday_wikidata[1:min(5,len(deathday_wikidata)+1)].replace('-', '.') == deathdate_wikipast[0:4])):
                    matchFound = True
                    break
                elif((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])):
                    uncertainIdentification = True
                    matchFound = True
                    break
   
      
      
    url = url + "&language=en&limit="+str(wikidata_limit)+"&format=json"
        if(not matchFound and resultFound):
    url = urlopen(url)
            if(guess_number_wiki != -1):
       
                f = open('./botoutput/guess/'+a+'.log', 'w')
    source = json.load(url)
                f.write('1')
                f.close()
                number_wiki = guess_number_wiki
                uncertainIdentification = True
            elif(not birth_found and not death_found):
                number_wiki = source['search'][0]['id']
                uncertainIdentification = True
            else:
                resultFound = False


    matchFound = False
        url_name = number_wiki
    resultFound = False # indicates that at least one page has been found
      
      
    for itemsQuerryNumber in range(wikidata_limit):
        if(not resultFound):
          
            url_name = "Match not found"
            number_wiki = a
 
 
 
        '''f = open('./botoutput/error/'+a+'.txt', 'w')
        f.write(str(content))
        f.close()
        print('ERROR with '+a+'\n\n')
        return 0'''
 
        insertWikidataLink(' [' + wikidata_addr + number_wiki + ' ' + url_name+']',content,uncertainIdentification)
        page=''
        for c in content:
            page += str(c)
            page += '\n\n'
        payload={'action':'edit','assert':'user','format':'json','utf8':'','text':page,'summary':summary,'title':a.replace(' ','_'),'token':edit_token}
         if(len(content) != 0):
            r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie)
 
 
         try:
         try:
             number_wiki = source['search'][itemsQuerryNumber]['id']
             f = open('./botoutput/uncertainId/'+sanitize_filename(a)+'.log', 'a')
         except:
         except:
             break;
             f = open('./botoutput/error/uncertainId.log','w')
        resultFound = True
         if uncertainIdentification:
        ## Looking for the date on wikidata
            f.write('1\n'+a+'\n')
        (birthdate_wikidata, deathday_wikidata) = getWikidataBirthdayDeathday(number_wiki)
         else:
         #try with parse(birthdate_wikidata[1:11], "yyyy.dd.mm")
            f.write('0\n'+a+'\n')
         #print(str(itemsQuerryNumber)+" "+birthdate_wikipast[0:4] + " - "+birthdate_wikidata[1:min(5,len(birthdate_wikipast)+1)].replace('-', '.'))
        f.close()
           
 
         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])):
         try:
             matchFound = True
            f = open('./botoutput/matchFound/'+sanitize_filename(a)+'.txt', 'w')
            break
        except:
   
             f = open('./botoutput/error/matchfound.log', 'a')
    if(not matchFound and resultFound):
         if matchFound:
        number_wiki = source['search'][0]['id']
            f.write('1\n'+a+'\n')
         (birthdate_wikidata,deathday_wikidata) = getWikidataBirthdayDeathday(number_wiki)
            f.close()
    if(not resultFound):
            return 1
        wikidata_addr = ''
         f.write('0\n'+a+'\n')
        number_wiki = ''
        f.close()
        birthdate_wikidata = ''
        return 0
        birthdate_wikipast = ''
     except:
         deathdate_wikipast = ''
        f = open('./botoutput/error/UnkonowError.log','a')
        deathday_wikidata = ''
        f.write(a+'\n')
    content+= '|-'+ '\n' + '| [[' + a.string + ']]\n'
        f.close()
     content+= '|[' + wikidata_addr + number_wiki + ' ' + number_wiki+']\n'  
         print('FATAL Error !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
    if(birth_found):
        return 0
         content+= '|[[' + birthdate_wikipast + ']]\n'
 
    else:
import pickle
        content+= '| -' + '\n'
 
    content+= '|[[' + str((birthdate_wikidata[1:11])).replace('-', '.') + ']]\n'
joblist = pickle.load(open('joblist.b', 'rb'))
   
 
    if(death_found):
print('Number of jobs in joblist.b : '+str(len(joblist)))
         content+= '|[['+deathdate_wikipast+']]\n'
 
    else:
continueProcessing = True
         content+= '| -' + '\n'
while continueProcessing:
    content+= '|[[' + str((deathday_wikidata[1:11])).replace('-', '.') + ']]\n'
 
    content+= '|'+str(not matchFound)+'\n'
 
    return content
        f = open('config/startPoint.dat', 'r')
        startOfFile = int(f.read())
         f.close()
 
         f = open('config/endPoint.dat', 'r')
        maxFileIdx = int(f.read())
        f.close()
 
        endOfFile = min(startOfFile+50, maxFileIdx, len(joblist))
        if startOfFile == maxFileIdx:
            exit()
 
 
 
        print('Processing from :'+str(startOfFile) + '\t\tTo : '+ str(endOfFile))
 
        joblist1 = joblist[startOfFile:endOfFile]


import multiprocessing as mp
        #processPageToAddWikidataNumber(joblist[2])
import sys
sys.setrecursionlimit(10000)


%%time
        print('Running')
        pool = mp.Pool(2)
        res = pool.map(processPageToAddWikidataNumber,joblist1)
        pool.close()
        stats = open('stats.txt', 'a')
        size = len(joblist)
        hits = sum(res)
        stats.write(str(size)+" entries, " + str(hits) + " matches. " + str(hits/size*100) + "% accuracy.\n")
        stats.close()


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'
        f = open('config/startPoint.dat', 'w')
        f.write(str(endOfFile))
        f.close()
        print('Finish match found res: ' + str(sum(res)))


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)
  </nowiki>
  </nowiki>

Dernière version du 21 mai 2019 à 11:02

Langue Français English

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 Wikidataficator 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 deux premiers scénarios sont alors possibles :

  • 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.
  • Des résultats de recherche sont disponibles sur Wikidata.

Si tel est le cas, voici l'algorithme de recherche du Wikidataficator :

  • 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 :
    • Si ces deux numéros sont égaux, le personnage est alors sélectionné, et son numéro d'identification Wikidata est inséré sur Wikipast. Le bot passe alors au personnage suivant sur Wikipast.
    • Si ces deux numéros ne sont pas égaux, le bot va regarder si les dates de naissance et de décès du personnage sur Wikidata correspondent à celles sur Wikipast. Si tel est le cas, ce personnage sera sauvegardé comme "guess", et le bot va continuer à comparer le personnage Wikipast avec les entrées suivantes sur Wikidata, afin de voir si un BNF ID correspondant peut être trouvé (ce qui serait un meilleur match, car un BNF ID est censé être unique et ne devrait pas être erroné).
  • Si le BNF id n'est pas disponible sur Wikipast ou sur Wikidata, les personnages sont alors comparés à l'aide de leur date de naissance et date de décès.
    • Si les deux dates correspondent sur Wikipast et Wikidata, le personnage sera sélectionné et son identifiant Wikidata sera inséré sur Wikipast. Le Wikidataficator passe alors au personnage Wikipast suivant.
    • Si une seule des deux dates correspond, le personnage sera sélectionné et son identifiant Wikidata sera inséré sur Wikipast, avec en plus la mention "Uncertain identification". Le Wikidataficator passe alors au personnage Wikipast suivant.
    • Si aucune des deux dates ne correspond, le Wikidataficator continue les recherches pour ce personnage Wikipast en le comparant avec l'entrée Wikidata suivante.
  • Si aucun match n'a été trouvé une fois toutes les entrées Wikidata traitées pour un personnage Wikipast donné, il y a trois possibilités :
    • Le bot avait enregistré un "guess" lors d'une comparaison de BNF ID différents. Il va donc choisir ce personnage, et insérer son numéro Wikidata sur Wikipast, ainsi que la mention "Uncertain identification".
    • Le bot n'avait enregistré aucun guess, et le personnage avait des dates de naissance et décès sur Wikipast. "Match not found" sera indiqué sur Wikipast avec un lien vers la création d'une nouvelle page sur Wikidata.
    • Le personnage sur Wikipast ne possédait ni date de naissance, ni date de décès, et aucun guess n'a été enregistré. Alors le bot va prendre le premier personnage de la liste sur Wikidata, et l'insérer sur Wikipast avec la mention "Uncertain identification".


Voir la section "Exemple de résultats" pour une illustration de ces différents résultats.

Choix de stratégie

  • Le BNF ID plus fort que les dates de naissance et décès :

Comme dit précédemment, un BNF ID est un numéro unique. Cela signifie que deux personnages ayant le même numéro BNF ID sont identiques, et que deux entrées ayant des BNF ID différents devraient être deux personnages différents. Cela n'étant pas le cas pour les dates de naissances, il nous a semblé naturel de d'abord se baser sur le numéro de la BNF. De plus, lors de la comparaison des dates, nous avons choisi de nous limiter à une comparaison des années, les erreurs sur les mois et les jours étant trop fréquentes.

  • La méthode du guess :

Pourquoi avoir donc instauré cette technique de guess, en cas de BNF ID différent, mais de dates de naissance et de décès similaire ? L'erreur étant humaine, il nous est arrivé à quelques reprises d'observer une erreur dans le BNF ID sur Wikidata. Ne voulant pas que ce genre d'erreurs aient trop d'impacts sur nos résultats, nous avons décidé de faire "deviner" au bot une erreur de BNF ID.

  • Uncertain identification :

Lorsque trop peu d'informations sont vérifiées entre Wikipast et Wikidata, le Wikidataficator va tout de même tenter sa chance et sélectionner l'entrée la plus probable (ayant au moins une date en commun, ou la première de la liste si aucune information concernant les dates de naissance et décès ne sont disponibles sur Wikipast). L'indication "Uncertain identification" indiquera cependant aux utilisateurs qu'il serait judicieux d'approfondir un peu la recherche pour être sûr qu'il s'agisse bien de la bonne personne. Nous pensons que cela n'est pas une trop grosse contrainte, et que ce résultat est plus intéressant qu'un simple "Match not found".

  • Match not found :

Les 500'000 entrées insérées par le GallicaSPARQLbot sont composées d'auteurs connus, et moins connus. C'est pourquoi plus ou moins la moitié de ces auteurs ne sont pas présents du tout sur Wikidata. Sans aucune personne du même nom sur Wikidata, nous n'avions donc pas le choix. C'est pour cela que le Wikidataficator insère sur Wikipast un lien direct vers la création de la page du personnage sur Wikidata.

É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 naissance 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 était 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 voulu lancer le bot sur l'ensemble des données. Cependant, nous avons rencontré un problème indépendant de notre volonté : le serveur Wikipast était vite surchargé et ne répondait plus après quelques milliers d'entrées. Nous avons donc dû nous résoudre à les lancer petit à petit. Les statistiques que vous trouverez ci-dessous portent donc sur 400'000 entrées environ.

Voici les statistiques calculées :

  • Pourcentage de "Match not found"
  • Pourcentage de "Uncertain identification"
  • Nombre de BNF ID ne correspondant pas entre Wikipast et Wikidata

Précision

  • Match not found

Les "Match not found" représentent environ 80% (317'000 / 400'000) des entrées sur Wikipast. Ce pourcentage très élevé s'explique par le fait que les auteurs insérés par le GallicaSPARQLBot sont souvent peu connus.

  • Uncertain identification

Concernant les "Uncertain identification", cela représente environ 16% (64'321 / 400'000) des données. Si nous avions voulu obtenir un pourcentage plus bas, cela aurait eu comme conséquence d'augmenter le nombre de "Match not found".

  • BNF ID incohérent

Finalement, lorsque le BNF ID qui ne correspond pas entre Wikidata et Wikipast, mais qu'un personnage est quand même sélectionné à cause de ses dates de naissance et de décès (méthode du guess), il nous a semblé que le résultat était correct (nous en avons vérifié une dizaine, voir Joseph Leidy par exemple). Ces cas là sont comptés comme "Uncertain identification", et représentent 721 personnes sur 400'000 environ.

Stats wikidataficator.png

  • Contrôle manuel

En plus des statistiques précédemment décrites, nous avons analysé environ une centaine d'entrées traitées par le Wikidataficator afin de voir si les résultats trouvés étaient bien ceux attendus, et ce fût le cas pour les identifications certaines. En effet, les entrées ne possédant pas le label "Uncertain identification" ont soit un BNF ID correct, soit pas de BNF ID, mais des dates de naissance et de décès cohérentes. Les "Match not found" sont bel et bien introuvables sur Wikidata. Concernant les "Uncertain identification", sur une vingtaine d'entrées, il nous a semblé que l'identification était quand même correcte pour environ la moitié des cas. Pour l'autre moitié, le bot n'ayant aucune information sur les dates de naissance et de décès, avait simplement sélectionné le premier personnage de la liste.

Il est possible que certaines entrées possèdent le label "Match not found" et "Uncertain identification". Cela est dû à la première version de notre bot, qui insérait "Uncertain identification" pour tous les "Match not found". Ceci a été corrigé par la suite, et n'a pas été pris en compte dans le calcul des statistiques.

Vitesse

La phase la plus lente du bot est celle qui génère la liste des entrées à traiter (la liste de tous les personnages Wikipast). 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.

Ensuite, le traitement des entrées est relativement rapide, tant que le serveur répond. Nous avons introduit du multiprocessing afin que le bot soit effectué sur plusieurs threads. Avec 4 threads, environ 300 personnages par minute sont traités. Nous n'avons pas pu augmenter le nombre de threads en raison des problèmes liés au serveur saturé sur Wikipast.

Analyse critique

Bien que les résultats observés aient toujours été ceux attendus, il est intéressant de discuter de la pertinence des choix que nous avons fait. En effet, nos critères sont assez souples. Par exemple, si les BNF ID ne sont pas disponibles, une seule des deux dates (naissance ou décès) nous suffit pour sélectionner un personnage, avec la mention "Uncertain identification". Si les dates de naissance et de décès ne sont pas disponibles sur Wikipast, et qu'aucun BNF ID similaire n'est trouvé sur Wikidata, le premier résultat de la liste est sélectionné. Nous aurions pu décider de durcir ces critères, car cela améliorerait probablement la précision des résultats. Il y aurait en effet beaucoup moins d'erreurs car nous n'aurions aucun "Uncertain identification". En revanche, cela augmenterait encore le nombre de "Match not found", alors que plusieurs personnages n'avaient simplement pas le bon BNF ID, ou trop peu d'informations. Avec la méthode que nous avons choisi, un "Match not found" assure l'utilisateur que le personnage n'est pas présent sur Wikidata, et un "Uncertain identification" indique qu'aucun meilleur match n'existe. Il suffit alors d'un seul clic pour vérifier si le résultat est cohérent.

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 exemple5.png

Code

Create the job list with the script bellow:


import requests
import json
from bs4 import BeautifulSoup
from urllib.request import urlopen
from dateutil.parser import parse
import urllib
import progressbar
import uuid
import pickle

import multiprocessing as mp
import sys
sys.setrecursionlimit(1000000)

user='Wikidataficator'
passw='dhbot2019'
baseurl='http://wikipast.epfl.ch/wikipast/'
summary='Wikidataficator update'
name='Wikidataficator'

# 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 = 30

#!/usr/bin/python3

import json
import os.path
from urllib.request import urlopen

if os.path.isfile('./nbPeople.dat'):
    nbJobs = open('nbPeople.dat', 'r')
    print(nbJobs.read())

#get all of the entries that have Q5. We need to while loop since there is a max querry size
wikidata_limit = 500
wikidate_offset = 0
numberOfItemsToTreat = 1
joblist = []
while(numberOfItemsToTreat != 0):
    print(wikidate_offset)
    url = "http://wikipast.epfl.ch/wikipast/api.php?action=query&list=search&srwhat=text&srsearch=Q5&format=json"
    url = url + "&language=en&srlimit="+str(wikidata_limit)+"&sroffset="+str(wikidate_offset)
    url = urlopen(url)

    source = json.load(url)

    numberOfItemsToTreat = len(source['query']['search'])

    for i in range(numberOfItemsToTreat):
        if str(source['query']['search'][i]['snippet']).find('Wikidata: ([https://www.wikidata.org/wiki/<span class=\'searchmatch\'>Q5</span> <span class=\'searchmatch\'>Q5</span>])') != -1 \
        or str(source['query']['search'][i]['snippet']).find('Wikidata: <span class=\'searchmatch\'>Q5</span>') != -1 \
        or str(source['query']['search'][i]['snippet']).find('Wikidata: ([https://www.wikidata.org/wiki/<span class=\'searchmatch\'>Q5</span> <span class=\'searchmatch\'>Q5</span>)]') != -1 :
            title = str(source['query']['search'][i]['title'])
            #print(str(source['query']['search'][i]['title'])+"\n\n")
            joblist.append(title.replace(" ","_"))
    wikidate_offset += wikidata_limit


print(str(len(joblist))+' people found')

nbJobs = open('nbPeople.dat', 'w')
nbJobs.write(str(len(joblist)))
nbJobs.close()
#print(joblist)

pickle.dump(joblist, open('joblist.b', 'wb'))
 

Run Wikidataficator on joblist.

import requests
import json
from bs4 import BeautifulSoup
from urllib.request import urlopen
from dateutil.parser import parse
import urllib
import progressbar
import uuid
import getpass
import time
from passlib.hash import pbkdf2_sha256 
import getpass
from pathvalidate import sanitize_filename


import multiprocessing as mp
import sys
sys.setrecursionlimit(1000000)


def getWikidataBirthdayDeathdayBnfID(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"

    bnfid_wikidata = []
    try:  
        count = 0
        while True:
            bnfid_wikidata.append(claims['P268'][count]['mainsnak']['datavalue']['value']['time'])
            count += 1
    except:
        try:
            count = 0
            while True:
                bnfid_wikidata.append(claims['P268'][count]['mainsnak']['datavalue']['value'])
                count += 1
        except:
            bnfid_wikidata.append(" N/A")
    return (birthdate_wikidata, deathdate_wikidata, bnfid_wikidata)


def getWikidataNumberFromBnFID(bnf_id):
    try:
        url = urlopen("https://www.wikidata.org/w/api.php?action=query&list=search&srwhat=text&srsearch="+bnf_id+"&format=json")    
        source = json.load(url)
        return source['query']['search'][0]['title']
    except:
        return "-1"
#Author Robin Mamie
baseurl = 'http://wikipast.epfl.ch/wikipast/'
import lxml
content=''


def import_page(content, title):
    params = { "format":"xml", "action":"query", "prop":"revisions", "rvprop":"timestamp|user|comment|content" }
    params["titles"] = "API|%s" % urllib.parse.quote(title.replace(' ', '_'))
    qs = "&".join("%s=%s" % (k, v)  for k, v in params.items())
    url = baseurl + 'api.php?%s' % qs
    try:
        tree = lxml.etree.parse(urlopen(url))
    except:
        print('Error importing page. Trying again. \t'+title)
        return import_page(content,title)
    revs = tree.xpath('//rev')
    if revs:
        old_content = revs[-1].text.split('\n')
        listed_old_content = [x for x in old_content if x]
        content.extend(listed_old_content)


def insertWikidataLink(link,content,uncertainVerification):
    human_ver_string = ''
    if uncertainVerification:
        human_ver_string = ' \'\'Uncertain identification\'\''
    for i in range(len(content)):
        l = content[i]
        if(l.find('Wikidata: [') == -1):
            if l[10:12] == 'Q5':
                insertPoint = l.find(':') + 1
                Q5URL = ' ([https://www.wikidata.org/wiki/Q5'
                content[i] = str(l[0:insertPoint]) + Q5URL + str(l[insertPoint:]) +  str(']) ')
                l = content[i]
            if l[0:8] == 'Wikidata':
                insertionPoint = l.find(':')+1
                content[i] = str(str(l[0:insertionPoint])+str(link)+str(l[insertionPoint:])+human_ver_string)

def cleanNameOfUUID(name):
    idx = name.find("(")
    if idx != -1:
        name = name[:idx-1]
    return name


user='Wikidataficator'
passw=getpass.getpass('Password: ')
f = open('config/passwordhash.dat','r')
hash = f.read()
if(not pbkdf2_sha256.verify(passw, hash)):
    print('Invalid password!')
    exit()
baseurl='http://wikipast.epfl.ch/wikipast/'
summary='Wikidataficator update'
name='Wikidataficator'

# 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 = 50

def processPageToAddWikidataNumber(a):
    try:
        number_wiki = ''
        print(a)
        wikidata_addr = 'https://www.wikidata.org/wiki/'
        wikidata_limit = 30
    
        content=[]
        import_page(content,a)

        sleepTime = 1
        if len(content) == 0:
            while len(content) == 0:
                time.sleep(sleepTime)
                sleepTime += 1
                import_page(content,a)
                print('!!! Attempt at fetching again. Will sleep for :'+str(sleepTime)+' Person name :'+a+'\n')


        birth_found = False
        death_found = False
        BnFID_found = False
        BnF_id = ''
        for c in content:
            if (c.find('Wikidata: [') != -1):
                print('Already processsed : '+a)
                return 0
            if (c.find('BnF ID:') != -1):
                #print('BnF ID : '+ c.split()[-1][0:-1])
                BnF_id = c.split()[-1][0:-1]
                BnFID_found = True
    
            if(c.find('[[Naissance]] de [['+a.replace('_',' ')) != -1 or c.find('[[Naissance]] d\'[['+a.replace('_',' ')) != -1):
                #print('Naisance le : '+c.split('[')[2].split(']')[0])
                birthdate_wikipast = c.split('[')[2].split(']')[0]
                birth_found = True
    
            if((c.find('[[Décès]] de [['+a.replace('_',' ')) != -1) or (c.find('[[Mort]] de [['+a.replace('_',' ')) != -1)or (c.find('[[Décès]] d\'[['+a.replace('_',' ')) != -1) or (c.find('[[Mort]] d\'[['+a.replace('_',' ')) != -1)):
                #print('Décès le : '+c.split('[')[2].split(']')[0])
                death_found = True
                deathdate_wikipast = c.split('[')[2].split(']')[0]                
    
    
        # WIKIDATA
        url = "https://www.wikidata.org/w/api.php?action=wbsearchentities&search="
        #url = url + str(a.replace(' ', '_').replace('ü', 'u').replace('é', 'e').replace('è', 'e').replace('ö', 'o').replace('í', 'i'))
        url = url + urllib.parse.quote(cleanNameOfUUID(a).replace(' ', '_'))
    
        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
        uncertainIdentification = False
        guess_number_wiki = -1
    
        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, bnfid_wikidata) = getWikidataBirthdayDeathdayBnfID(number_wiki)
    
            birthdate_wikidata_found = (birthdate_wikidata != ' N/A')
            deathday_wikidata_found = (deathday_wikidata != ' N/A')
            bnfid_wikidata_found = (len(bnfid_wikidata) != 1)
    
            if(BnFID_found and bnfid_wikidata_found):
                for bnfid_wikidata_element in bnfid_wikidata:
                    if(BnF_id == bnfid_wikidata_element):
                        matchFound = True
                        break
                    else:
                        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])):
                            if(guess_number_wiki == -1):
                                guess_number_wiki = number_wiki
                if matchFound:
                    break
            else:
                if((birth_found and birthdate_wikidata[1:min(5,len(birthdate_wikipast)+1)].replace('-', '.') == birthdate_wikipast[0:4]) and (death_found and deathday_wikidata[1:min(5,len(deathday_wikidata)+1)].replace('-', '.') == deathdate_wikipast[0:4])):
                    matchFound = True
                    break
                elif((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])):
                    uncertainIdentification = True
                    matchFound = True
                    break
    
    
        if(not matchFound and resultFound):
            if(guess_number_wiki != -1):
                f = open('./botoutput/guess/'+a+'.log', 'w')
                f.write('1')
                f.close()
                number_wiki = guess_number_wiki
                uncertainIdentification = True
            elif(not birth_found and not death_found):
                number_wiki = source['search'][0]['id']
                uncertainIdentification = True
            else:
                resultFound = False

        url_name = number_wiki
    
        if(not resultFound):
            url_name = "Match not found"
            number_wiki = a



        '''f = open('./botoutput/error/'+a+'.txt', 'w')
        f.write(str(content))
        f.close()
        print('ERROR with '+a+'\n\n')
        return 0'''

        insertWikidataLink(' [' + wikidata_addr + number_wiki + ' ' + url_name+']',content,uncertainIdentification)
        page=''
        for c in content:
            page += str(c)
            page += '\n\n'
        payload={'action':'edit','assert':'user','format':'json','utf8':'','text':page,'summary':summary,'title':a.replace(' ','_'),'token':edit_token}
        if(len(content) != 0):
            r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie)


        try:
            f = open('./botoutput/uncertainId/'+sanitize_filename(a)+'.log', 'a')
        except:
            f = open('./botoutput/error/uncertainId.log','w')
        if uncertainIdentification:
            f.write('1\n'+a+'\n')
        else:
            f.write('0\n'+a+'\n')
        f.close()

        try:
            f = open('./botoutput/matchFound/'+sanitize_filename(a)+'.txt', 'w')
        except:
            f = open('./botoutput/error/matchfound.log', 'a')
        if matchFound:
            f.write('1\n'+a+'\n')
            f.close()
            return 1
        f.write('0\n'+a+'\n')
        f.close()
        return 0
    except:
        f = open('./botoutput/error/UnkonowError.log','a')
        f.write(a+'\n')
        f.close()
        print('FATAL Error !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
        return 0

import pickle

joblist = pickle.load(open('joblist.b', 'rb'))

print('Number of jobs in joblist.b : '+str(len(joblist)))

continueProcessing = True
while continueProcessing:


        f = open('config/startPoint.dat', 'r')
        startOfFile = int(f.read())
        f.close()

        f = open('config/endPoint.dat', 'r')
        maxFileIdx = int(f.read())
        f.close()

        endOfFile = min(startOfFile+50, maxFileIdx, len(joblist))
        if startOfFile == maxFileIdx:
            exit()



        print('Processing from :'+str(startOfFile) + '\t\tTo : '+ str(endOfFile))

        joblist1 = joblist[startOfFile:endOfFile]

        #processPageToAddWikidataNumber(joblist[2])

        print('Running')
        pool = mp.Pool(2)
        res = pool.map(processPageToAddWikidataNumber,joblist1)
        pool.close()
        stats = open('stats.txt', 'a')
        size = len(joblist)
        hits = sum(res)
        stats.write(str(size)+" entries, " + str(hits) + " matches. " + str(hits/size*100) + "% accuracy.\n")
        stats.close()


        f = open('config/startPoint.dat', 'w')
        f.write(str(endOfFile))
        f.close()
        print('Finish match found res: ' + str(sum(res)))