« CurvyBot » : différence entre les versions
Aucun résumé des modifications |
(Wikipastbot update) |
||
(40 versions intermédiaires par 2 utilisateurs non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
{| class="wikitable" | |||
|Langue | |||
|'''Français''' | |||
|[[(en)_CurvyBot|English]] | |||
|} | |||
== Résumés des fonctionnalités == | == Résumés des fonctionnalités == | ||
Ligne 16 : | Ligne 21 : | ||
L'usage de mots remarquables permet de générer plusieurs courbes en effectuant une requête plus précise. La comparaison de ces courbes permet d'évaluer leur fiabilité. On effectue notamment une recherche avec le nom complet du personnage, une recherche avec uniquement son nom de famille, et des recherches avec des mots remarquables. Plusieurs techniques d'extraction de mots remarquables sont proposées: | L'usage de mots remarquables permet de générer plusieurs courbes en effectuant une requête plus précise. La comparaison de ces courbes permet d'évaluer leur fiabilité. On effectue notamment une recherche avec le nom complet du personnage, une recherche avec uniquement son nom de famille, et des recherches avec des mots remarquables. Plusieurs techniques d'extraction de mots remarquables sont proposées: | ||
* Extraction des mots à occurrences multiples dans les pages. | * Extraction des mots à occurrences multiples dans les pages. | ||
* Extraction des fonctions générées par le [SPARQLBot] dans les mentions. | * Extraction des fonctions générées par le [[SPARQLBot]] dans les mentions. | ||
Initialement, 3 courbes sont calculées. | Initialement, 3 courbes sont calculées. | ||
Ligne 25 : | Ligne 30 : | ||
Généralement, la correspondance entre la courbe de confiance élevée et la courbe de confiance moyenne est excellente. La courbe de confiance basse est souvent visiblement altérée par des homonymes. | Généralement, la correspondance entre la courbe de confiance élevée et la courbe de confiance moyenne est excellente. La courbe de confiance basse est souvent visiblement altérée par des homonymes. | ||
Certains cas | Certains cas spéciaux donnent de mauvais résultats car le nom sélectionné par l'algorithme n'est pas pertinent pour les recherches, comme [[Otto von Bismarck]] ou [[Nicolas II]]. Ces cas ne sont malheureusement pas rares. | ||
Ce bot est partiellement dépendant du [SPARQLBot], qui lui permet d'affiner la sélection de mots remarquables. | Ce bot est partiellement dépendant du [[SPARQLBot]], qui lui permet d'affiner la sélection de mots remarquables. Toutefois, la présence ou non de mention ne semble pas affecter significativement la performance du bot. | ||
=== Vitesse === | === Vitesse === | ||
En moyenne, le bot nécessite entre 5 et 10 secondes afin de créer un histogramme et de le téléverser sur wikipast. Un des plus gros problèmes de performance est celui du réseau qui provoque des erreurs après un trop gros nombre de connexions, le transfert des images demandant beaucoup de ressources. | |||
=== Améliorations possibles === | === Améliorations possibles === | ||
Ligne 41 : | Ligne 46 : | ||
* Le nom de famille a un homographe (e.g. Breton, Lumière) | * Le nom de famille a un homographe (e.g. Breton, Lumière) | ||
Le critère de recherche par le nom est donc parfois une borne peu, voir pas du tout, précise. On soulignera toutefois qu'elle prouve son efficacité dans une majorité de cas. On | Le critère de définition d'un mot remarquable pourrait être revu. Actuellement, il s'agit juste d'une borne sur son nombre d'apparitions dans la biographie. Pour permettre une meilleure comparaison entre diverses personnalités il serait peut-être plus juste de choisir un nombre fixe de mots parmi les plus fréquents dans la biographie. | ||
Le critère de recherche par le nom est donc parfois une borne peu, voir pas du tout, précise. On soulignera toutefois qu'elle prouve son efficacité dans une majorité de cas. On pourrait imaginer un premier affinage du critère de confiance bas en regardant, par exemple, si le nom se trouve dans un dictionnaire. | |||
Certains personnages sont très peu mentionnés dans les médias et le graphe pourrait être reconstitué à partir des évènements wikipast (e.g. Yuri Gagarin) | |||
Le format des pages est problématique. On citera | Le format des pages est problématique. On citera | ||
Ligne 50 : | Ligne 59 : | ||
== Exemple de résultats == | == Exemple de résultats == | ||
<gallery widths=800px heights=400px> | |||
Fichier:Histogramme Friedrich Dürrenmatt.png| Le résultat pour [[Friedrich Dürrenmatt]] permet de bien voir le retour en force de son oeuvre au début du 21ème siècle | |||
Fichier:Histogramme Otto von Bismarck.png|[[Otto von Bismarck]] étant souvent mentionné en tant que général von Bismarck la courbe on trouve beaucoup plus de résultats pour "von Bismarck". La courbe low semble toutefois donner un bon résultat par rapport à la vie de Bismarck et sa popularité décroît après sa mort. | |||
</gallery> | |||
== Code == | == Code == | ||
Nous utilisons les imports et constantes suivantes | |||
<nowiki> | |||
import requests | |||
import matplotlib.pyplot as pp | |||
import numpy as np | |||
from urllib.request import urlopen as urlopen | |||
from bs4 import BeautifulSoup | |||
from matplotlib.pyplot import figure | |||
from matplotlib.dates import (YEARLY, DateFormatter, rrulewrapper, RRuleLocator, drange) | |||
from datetime import datetime as dt | |||
import nbimporter | |||
from in_progress import * | |||
from contextlib import closing | |||
import time | |||
FIRST_EVENT_SHIFT = 20 #if no birthday is found in wikipast's events then the originating event for impresso is the first one FIRST_EVENT_SHIFT years earlier | |||
PAUSE_TIME = 180 #on timeout wait PAUSE_TIME seconds | |||
GENERATED_FILENAME = 'CurvyBot_output.png' | |||
user = #Bot credentials | |||
passw = #Bot credentials | |||
baseurl='http://wikipast.epfl.ch/wikipast/' | |||
impresso_token = #Impresso connection token | |||
payload={'action':'query','format':'json','utf8':'','meta':'tokens','type':'login'} | |||
r1=requests.post(baseurl + 'api.php', data=payload) | |||
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) | |||
</nowiki> | |||
Une boucle principale itère sur toute les biographies | |||
<nowiki> | |||
with closing(urlopen("http://wikipast.epfl.ch/wikipast/index.php/Biographies")) as html_biographies: | |||
soup = BeautifulSoup(html_biographies, 'html.parser') | |||
hrefs = [] | |||
for table in soup.findAll('table'): | |||
for a in table.findAll('a'): | |||
if a.has_attr('href'): | |||
hrefs.append('http://wikipast.epfl.ch' + a.get('href')) | |||
"""for each biography creates a new section with an histogram | |||
if the section doesn't exist and reuploads the histogram otherwise""" | |||
times = {} | |||
for i, bio in enumerate(hrefs): | |||
print(bio) | |||
timeout = True | |||
while timeout: | |||
try: | |||
start = dt.now() | |||
end = start | |||
bns = bio_needs_section(bio) | |||
fname, title = None, None | |||
if(bns): | |||
fname, title = create_histogram(bio) | |||
end = dt.now() | |||
add_histogram(fname, title, bns) | |||
timeout = False | |||
print('graph realisation and insertion took', end - start) | |||
times[bio] = end - start | |||
except TimeoutError: | |||
print("Timeout") | |||
time.sleep(PAUSE_TIME) | |||
print(times) | |||
</nowiki> | |||
Pour chaque biographie un graphe est crée et mis sur la biographie wikipast. | |||
La création du graphe est faire avec le code suivant | |||
<nowiki> | |||
def generate_plot(event_dates, birthday, bio): | |||
pp.figure(figsize=(8.0,4.0),dpi=100) | |||
plots = ask_dates(birthday, bio) | |||
labels = ['high', 'medium', 'low'] | |||
events = pp.plot_date(event_dates, np.zeros(len(event_dates)), fmt='ro', label='events') | |||
for i, p in enumerate(plots): | |||
if i < len(labels): | |||
pp.plot_date(p[0], p[1], fmt='-', label=labels[i]) | |||
else: | |||
pp.plot_date(p[0], p[1], fmt='-') | |||
ax = pp.gca() | |||
ax.set_xlabel('années') | |||
ax.set_ylabel('ratio [%]') | |||
pp.legend() | |||
pp.savefig(GENERATED_FILENAME, facecolor='w', edgecolor='k', | |||
orientation='portrait', format='PNG') | |||
pp.show() | |||
return GENERATED_FILENAME | |||
def create_histogram(bio): | |||
event_dates, birthday, title = wikipast_events(bio) | |||
fname = generate_plot(event_dates, birthday, bio) | |||
return fname, title | |||
</nowiki> | |||
Afin de récupèrer les différentes valeurs à dessiner nous utilisons | |||
<nowiki> | |||
def ask_dates(birthday, bio): | |||
dict_list = bioToDictionnaries(bio, birthday, impresso_token) | |||
plots = [dictionnary_to_plot(d) for d in dict_list] | |||
return plots | |||
def wikipast_events(bio): | |||
event_dates = [] | |||
birthday = None | |||
title = "" | |||
with closing(urlopen(bio)) as html_character: | |||
character = BeautifulSoup(html_character, 'html.parser') | |||
title = character.find("h1").find(text=True) | |||
for megaEvent in character.find_all(["h2", "ul"]): | |||
if megaEvent.name == "h2" and megaEvent.text != "Biographie": | |||
break | |||
elif megaEvent.name == "ul": | |||
for event in megaEvent.findAll('li'): | |||
event_date = event.find('a').get('title') | |||
formatted = format_date(event_date) | |||
if formatted: | |||
event_dates.append(formatted) | |||
for text in event.findAll(text=True): | |||
if 'naissance' in text.lower() and not birthday: | |||
birthday = formatted | |||
if(not birthday): | |||
if event_dates: | |||
first_event = event_dates[0] | |||
birthday = dt(first_event.year - FIRST_EVENT_SHIFT, first_event.month, first_event.day) | |||
else: | |||
birthday = dt.min | |||
return event_dates, birthday, title | |||
</nowiki> | |||
Afin de formatter les dates de wikipast et de convertir les résultats d'Impresso vers une version facilement dessinable nous utilisons les fonctions utilitaires suivantes | |||
<nowiki> | |||
def format_date(date): | |||
date_parsed = date.split(' ')[0].split('.') | |||
if(not date_parsed[0].isdigit()): | |||
return None | |||
completion_date = ['01','01'] | |||
date_corrected = [date_parsed[0]] | |||
for i, parsed in enumerate(date_parsed[1:]): | |||
if parsed.isdigit(): | |||
date_corrected.append(parsed) | |||
else: | |||
date_corrected.append(completion_date[i-1]) | |||
date_corrected = date_corrected + completion_date[len(date_parsed)-1:2] | |||
formatted = None | |||
try: | |||
formatted = dt(int(date_corrected[0]), int(date_corrected[1]), int(date_corrected[2])) | |||
except ValueError as err: | |||
print("DATE FORMAT ERROR") | |||
return formatted | |||
def dictionnary_to_plot(d): | |||
d = d.items() | |||
d = sorted(d, key=lambda x: x[0]) | |||
keys_dt = [dt(y[0],1,1) for y in d] | |||
vals = [r[1] for r in d] | |||
return (keys_dt, vals) | |||
</nowiki> | |||
Le téléversement des images sur wikipast et la création de la section "Histogramme" se fait avec ce code | |||
<nowiki> | |||
#inspired by InferenceBot | |||
def get_edit_token(): | |||
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) | |||
return (edit_token, edit_cookie) | |||
#inspired by InferenceBot | |||
def upload_file(fname, title): | |||
(edit_token, edit_cookie) = get_edit_token() | |||
with open(fname, 'rb') as pic: | |||
payload = {'action': 'upload', 'filename': 'Histogramme ' + title, 'token': edit_token, 'ignorewarnings': 1} | |||
files = {'file': pic.read()} | |||
r4 = requests.post(baseurl + 'api.php', data=payload, files=files, cookies=edit_cookie) | |||
return 'Histogramme ' + title + ".png" | |||
def create_content(fname): | |||
content ='\n==Histogramme==\n' | |||
content+='[[Fichier:'+fname+'|frame|left|' | |||
content+='Histogramme de l\'influence médiatique du personnage. ' | |||
content+='Events indique les événements présents dans la biographie. ' | |||
content+='Les 3 courbes décrivent la variation du nombre d\'occurences du personnage dans les journaux année après année, ' | |||
content+='avec une sélection respectivement laxiste (low), modérée (medium) et sévère (high) des articles traitant probablement du personnage. ' | |||
content+='Pour plus d\'informations, consultez [[CurvyBot]].' | |||
content+=']]\n' | |||
return content | |||
"""Inserts a new section named Histogram at the end of the page named title and uploads the histogram""" | |||
def add_histogram(fname, title, create_section=True): | |||
if(create_section): | |||
wiki_fname = upload_file(fname, title) | |||
content = create_content(wiki_fname) | |||
(edit_token, edit_cookie) = get_edit_token() | |||
payload={'action':'edit', | |||
'assert':'user', | |||
'format':'json', | |||
'utf8':'', | |||
'appendtext':content, | |||
'summary':'Histogram insertion', | |||
'title':title, | |||
'token':edit_token} | |||
r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie) | |||
"""Used to check if a given biography already contains a header with the histogram title""" | |||
def bio_needs_section(bio): | |||
with closing(urlopen(bio)) as html_character: | |||
character = BeautifulSoup(html_character, 'html.parser') | |||
for header in character.findAll("h2"): | |||
if 'Histogramme' in header.text: | |||
return False | |||
return True | |||
</nowiki> | |||
Le code qui sut sert à faire les requêtes à Impresso et calculer les ratios. | |||
On y utilise les imports suivant | |||
<nowiki> | <nowiki> | ||
import requests | |||
from urllib.parse import urlencode | |||
from collections import OrderedDict | |||
import os | |||
from urllib.request import urlopen as urlopen | |||
from bs4 import BeautifulSoup | |||
import re | |||
from contextlib import closing | |||
</nowiki> | |||
La fonction principale de ce module et utilisée pour récupèrer les trois courbes est | |||
<nowiki> | |||
# Return [highConfidence, mediumConfidence, lowConfidence] in the right format | |||
def bioToDictionnaries(bio, birthday, impresso_token): | |||
NORMALISATION_FACTOR = 100 | |||
totalArticlesPerYear = articlePerYear(impresso_token,[]) | |||
completeName = getName(bio) | |||
familyName = completeName.split(" ") | |||
if len(familyName)>1: | |||
familyName = " ".join(familyName[1:]) | |||
else: | |||
familyName = "" | |||
print(familyName) | |||
completeNameArticles = articlePerYear(impresso_token,[completeName]) | |||
familyNameArticles = articlePerYear(impresso_token,[familyName]) | |||
mediumConfidenceData = yearsFormatter(completeNameArticles, totalArticlesPerYear, birthday.year, NORMALISATION_FACTOR) | |||
lowConfidenceData = yearsFormatter(familyNameArticles, totalArticlesPerYear, birthday.year, NORMALISATION_FACTOR) | |||
### | |||
remarquableWords = [k for k in occurenceWords(bio, completeName)] | |||
for word in mentionsWords(bio): | |||
if word not in remarquableWords: | |||
remarquableWords.append(word) | |||
#Crée un seul dictionnaire avec la moyenne des ratio de toutes les recherches à haute confiance. | |||
highConfidenceData = {} | |||
numberOfRemarquableWords = len(remarquableWords) | |||
for word in remarquableWords: | |||
sampleArticles = articlePerYear(impresso_token,[completeName,word]) | |||
sampleHighConfidenceData = yearsFormatter(sampleArticles, totalArticlesPerYear, birthday.year, NORMALISATION_FACTOR) | |||
for year in sampleHighConfidenceData: | |||
if year in highConfidenceData: | |||
highConfidenceData[year] += sampleHighConfidenceData[year] | |||
else: | |||
highConfidenceData[year] = sampleHighConfidenceData[year] | |||
#for year in highConfidenceData: | |||
#highConfidenceData[year] = highConfidenceData[year]/numberOfRemarquableWords | |||
### | |||
return [highConfidenceData, mediumConfidenceData, lowConfidenceData] | |||
</nowiki> | |||
On y retrouve le NORMALISATION_FACTOR qui sert à rendre les valeurs plus lisibles et à les convertir en pourcentages. | |||
On consulte les mentions de la biographie avec | |||
<nowiki> | |||
def mentionsWords(bio): | |||
with closing(urlopen(bio)) as html_character: | |||
character = BeautifulSoup(html_character, 'html.parser') | |||
mentionsFlag = False | |||
result = [] | |||
for megaEvent in character.find_all(["h2", "ul"]): | |||
if megaEvent.name == "h2" and megaEvent.text == "Mentions": | |||
print(megaEvent) | |||
print("ACTIVATE SEARCHING") | |||
mentionsFlag = True | |||
elif megaEvent.name == "ul" and mentionsFlag: | |||
fonction = megaEvent.text | |||
if "Mention" in fonction: | |||
fonction = fonction.split("en tant que") | |||
fonction = fonction[1] | |||
fonction = fonction.split(", dans") | |||
fonction = fonction[0] | |||
if fonction not in result: | |||
result.append(fonction) | |||
return result | |||
</nowiki> | |||
Les mots remarquables sont récupèrés de la manière suivante | |||
<nowiki> | |||
# Code qui tente d'extraire des "mots remarquables" des biographies pour affiner les recherches | |||
# Le but sera d'effectuer des recherches avec le nom de la personne + un de ces mots. | |||
# Problème: certaines biographies n'ont aucun mots remarquables avec ce critère -> Il faudra affiner. | |||
# pistes: Extraire la profession/qualificatif de la personne. Extraire des oeuvres de lui. | |||
def occurenceWords(bio, name): | |||
MIN_INTERESTING_LENGTH = 3 | |||
TRASH_WORDS = ['contre','entre','avec','pour','dans','leur','vous','des','par','une','sur','est','son','ses','que'] | |||
with closing(urlopen(bio)) as html_character: | |||
character = BeautifulSoup(html_character, 'html.parser') | |||
wordFrequency = {} | |||
#Creation d'un dictionnaire avec tous les mots et leurs nombres d'apparitions pour chaque bio | |||
for megaEvent in character.find_all(["h2", "ul"]): # Possibilité d'utiliser des listes ! | |||
if megaEvent.name == "h2" and megaEvent.text != "Biographie": | |||
print(megaEvent) | |||
print(megaEvent.text) | |||
print("BREAKING") | |||
break | |||
elif megaEvent.name == "ul": | |||
for event in megaEvent.findAll('li'): | |||
for expression in event.findAll(text=True): | |||
for quasiWord in expression.split(' '): | |||
for word in quasiWord.split("""'"""): | |||
word = word.lower() | |||
if(re.search('^[a-zA-Zàâéèêîûô]*$',word) and len(word) >= MIN_INTERESTING_LENGTH): | |||
if word in wordFrequency: | |||
wordFrequency[word] += 1 | |||
else: | |||
wordFrequency[word] = 1 | |||
#Traitement du dictionnaire | |||
TOLERANCE = 2 | |||
trash = [] | |||
for word in wordFrequency: | |||
#Ne conserve pas le nom dans les mots remarquables | |||
if word in name.lower(): | |||
trash.append(word) | |||
#Ne conserve pas les mots qui n'apparaissent qu'une seule fois dans les mots remarquables | |||
if wordFrequency[word] <= TOLERANCE: | |||
trash.append(word) | |||
#Elimine des mots communs | |||
if word in TRASH_WORDS: | |||
trash.append(word) | |||
for word in trash: | |||
if word in wordFrequency: | |||
del wordFrequency[word] | |||
return wordFrequency | |||
</nowiki> | |||
Le nom de la personne étant une donnée importante on le récupère à travers | |||
<nowiki> | |||
#Donne le nom de la personne sujet de la biographie | |||
def getName(bio): | |||
with closing(urlopen(bio)) as html_character: | |||
character = BeautifulSoup(html_character, 'html.parser') | |||
# Trouve le nom de la personne dont on traite la biographie | |||
name = character.find('title') | |||
name = str(name) | |||
name = name.split('<title>') | |||
name = name[1] | |||
name = name.split(" — Wikipast</title>") | |||
name = name[0] | |||
return name | |||
</nowiki> | |||
Finalement, la fonction chargée de faire les requêtes à Impresso | |||
<nowiki> | |||
#Retourne le nombre d'articles total par année pour les mots clefs données | |||
#Appeler la fonction avec une liste de mots-clef vide retourne le nombre d'article par année. | |||
def articlePerYear(impresso_token, listOfKeyWords): | |||
header = {'Authorization': 'Bearer ' + impresso_token} | |||
# Cette partie de la requête ne change jamais, elle groupe les résultats par article | |||
url_base = "https://impresso-project.ch/api/search?group_by=articles" | |||
query = url_base | |||
for i, keyWord in enumerate(listOfKeyWords): | |||
query += "&filters["+str(i)+"][type]=string&filters["+str(i)+"][q]="+keyWord | |||
query += "&facets=year" | |||
# Fait la requête avec les bons headers | |||
res = requests.get(query, headers=header) | |||
# Retourne le json de la requête | |||
result = res.json() | |||
if 'facets' in result['info']: | |||
return result['info']['facets']['year']['buckets'] | |||
else: | |||
return {} | |||
</nowiki> | </nowiki> |
Dernière version du 20 mai 2019 à 20:36
Langue | Français | English |
Résumés des fonctionnalités
L'objectif de ce bot est de créer pour chaque biographie un graphe avec en abscisse le temps (en années) et en ordonnée le nombre d'articles par année qui traitent du personnage de la biographie. Les événements mentionnés dans la biographie sont clairement indiqués. Le graphe doit donner une idée de la popularité médiatique du personnage au fil du temps, en fonction des événements marquants le concernant.
Description technique
Le bot se base sur les biographies existantes. La page de chaque biographie est analysée pour en extraire le nom du personnage, des mots remarquables qui seront utilisés pour l'évaluation de la performance et les événements biographiques qui seront affichés sur le graphe.
Le nom du personnage est utilisé pour effectuer une requête à la base de données Impresso, qui retourne pour chaque année le nombre d'article comprenant les mots cherchés. Ces données permettent directement de créer un graphe relatif. Chaque point indique le ratio normalisé entre le nombre d'articles total sur l'année et le nombre d'articles concernant les mots-clef. En effet, le nombre d'article total varie fortement selon les années, et un compte absolu ne serait pas représentatif de l'évolution.
Évaluation des performances
Fiabilité
L'usage de mots remarquables permet de générer plusieurs courbes en effectuant une requête plus précise. La comparaison de ces courbes permet d'évaluer leur fiabilité. On effectue notamment une recherche avec le nom complet du personnage, une recherche avec uniquement son nom de famille, et des recherches avec des mots remarquables. Plusieurs techniques d'extraction de mots remarquables sont proposées:
- Extraction des mots à occurrences multiples dans les pages.
- Extraction des fonctions générées par le SPARQLBot dans les mentions.
Initialement, 3 courbes sont calculées.
- La courbe de confiance moyenne est calculée en recherchant les articles où le nom complet du personnage apparaît. C'est la courbe de référence.
- La courbe de confiance basse est calculée en recherchant les articles où seul le nom de famille du personnage apparaît. On s'attend à trouver des pics parasites dans cette courbe. Son but est de représenter au mieux l'amplitude des courbes de confiances plus élevées en minimisant le nombre de faux négatifs.
- La courbe de confiance haute est calculée en recherchant les articles où le nom complet ainsi qu'un mot remarquable apparaissent. On s'attend à ce que certains pics avérés manquent à cette courbe. Son but est de confirmer la présence des pics dans les courbes de confiance plus basses en minimisant le nombre de faux positifs. Cette courbe est la somme des résultats pour le nom complet + un mot remarquable, et son échelle n'est donc pas la même que les deux autres courbes, car certains articles sont comptés plusieurs fois.
Généralement, la correspondance entre la courbe de confiance élevée et la courbe de confiance moyenne est excellente. La courbe de confiance basse est souvent visiblement altérée par des homonymes.
Certains cas spéciaux donnent de mauvais résultats car le nom sélectionné par l'algorithme n'est pas pertinent pour les recherches, comme Otto von Bismarck ou Nicolas II. Ces cas ne sont malheureusement pas rares.
Ce bot est partiellement dépendant du SPARQLBot, qui lui permet d'affiner la sélection de mots remarquables. Toutefois, la présence ou non de mention ne semble pas affecter significativement la performance du bot.
Vitesse
En moyenne, le bot nécessite entre 5 et 10 secondes afin de créer un histogramme et de le téléverser sur wikipast. Un des plus gros problèmes de performance est celui du réseau qui provoque des erreurs après un trop gros nombre de connexions, le transfert des images demandant beaucoup de ressources.
Améliorations possibles
Les critères actuels de recherche occasionnent quelques difficultés. On citera
- Le nom de famille est aussi un prénom courant (e.g. Louise Michel)
- Le personnage est évoqué selon un titre accompagné de son nom de famille (e.g. général von Bismarck)
- Le nom de famille est utilisé pour nommer une invention ou une marque (e.g. Ferrari)
- Le nom de famille a un homographe (e.g. Breton, Lumière)
Le critère de définition d'un mot remarquable pourrait être revu. Actuellement, il s'agit juste d'une borne sur son nombre d'apparitions dans la biographie. Pour permettre une meilleure comparaison entre diverses personnalités il serait peut-être plus juste de choisir un nombre fixe de mots parmi les plus fréquents dans la biographie.
Le critère de recherche par le nom est donc parfois une borne peu, voir pas du tout, précise. On soulignera toutefois qu'elle prouve son efficacité dans une majorité de cas. On pourrait imaginer un premier affinage du critère de confiance bas en regardant, par exemple, si le nom se trouve dans un dictionnaire.
Certains personnages sont très peu mentionnés dans les médias et le graphe pourrait être reconstitué à partir des évènements wikipast (e.g. Yuri Gagarin)
Le format des pages est problématique. On citera
- Le personnage n'a pas de nom de famille (e.g. Nicolas II)
De plus, le bot utilise considère le nom de famille comme étant tout les mots qui suivent le prénom. Il considèrera donc les autres prénoms comme une partie du nom de famille (e.g. Juan Manuel Fangio sera recherché par Manuel Fangio)
Exemple de résultats
Le résultat pour Friedrich Dürrenmatt permet de bien voir le retour en force de son oeuvre au début du 21ème siècle
Otto von Bismarck étant souvent mentionné en tant que général von Bismarck la courbe on trouve beaucoup plus de résultats pour "von Bismarck". La courbe low semble toutefois donner un bon résultat par rapport à la vie de Bismarck et sa popularité décroît après sa mort.
Code
Nous utilisons les imports et constantes suivantes
import requests import matplotlib.pyplot as pp import numpy as np from urllib.request import urlopen as urlopen from bs4 import BeautifulSoup from matplotlib.pyplot import figure from matplotlib.dates import (YEARLY, DateFormatter, rrulewrapper, RRuleLocator, drange) from datetime import datetime as dt import nbimporter from in_progress import * from contextlib import closing import time FIRST_EVENT_SHIFT = 20 #if no birthday is found in wikipast's events then the originating event for impresso is the first one FIRST_EVENT_SHIFT years earlier PAUSE_TIME = 180 #on timeout wait PAUSE_TIME seconds GENERATED_FILENAME = 'CurvyBot_output.png' user = #Bot credentials passw = #Bot credentials baseurl='http://wikipast.epfl.ch/wikipast/' impresso_token = #Impresso connection token payload={'action':'query','format':'json','utf8':'','meta':'tokens','type':'login'} r1=requests.post(baseurl + 'api.php', data=payload) 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)
Une boucle principale itère sur toute les biographies
with closing(urlopen("http://wikipast.epfl.ch/wikipast/index.php/Biographies")) as html_biographies: soup = BeautifulSoup(html_biographies, 'html.parser') hrefs = [] for table in soup.findAll('table'): for a in table.findAll('a'): if a.has_attr('href'): hrefs.append('http://wikipast.epfl.ch' + a.get('href')) """for each biography creates a new section with an histogram if the section doesn't exist and reuploads the histogram otherwise""" times = {} for i, bio in enumerate(hrefs): print(bio) timeout = True while timeout: try: start = dt.now() end = start bns = bio_needs_section(bio) fname, title = None, None if(bns): fname, title = create_histogram(bio) end = dt.now() add_histogram(fname, title, bns) timeout = False print('graph realisation and insertion took', end - start) times[bio] = end - start except TimeoutError: print("Timeout") time.sleep(PAUSE_TIME) print(times)
Pour chaque biographie un graphe est crée et mis sur la biographie wikipast. La création du graphe est faire avec le code suivant
def generate_plot(event_dates, birthday, bio): pp.figure(figsize=(8.0,4.0),dpi=100) plots = ask_dates(birthday, bio) labels = ['high', 'medium', 'low'] events = pp.plot_date(event_dates, np.zeros(len(event_dates)), fmt='ro', label='events') for i, p in enumerate(plots): if i < len(labels): pp.plot_date(p[0], p[1], fmt='-', label=labels[i]) else: pp.plot_date(p[0], p[1], fmt='-') ax = pp.gca() ax.set_xlabel('années') ax.set_ylabel('ratio [%]') pp.legend() pp.savefig(GENERATED_FILENAME, facecolor='w', edgecolor='k', orientation='portrait', format='PNG') pp.show() return GENERATED_FILENAME def create_histogram(bio): event_dates, birthday, title = wikipast_events(bio) fname = generate_plot(event_dates, birthday, bio) return fname, title
Afin de récupèrer les différentes valeurs à dessiner nous utilisons
def ask_dates(birthday, bio): dict_list = bioToDictionnaries(bio, birthday, impresso_token) plots = [dictionnary_to_plot(d) for d in dict_list] return plots def wikipast_events(bio): event_dates = [] birthday = None title = "" with closing(urlopen(bio)) as html_character: character = BeautifulSoup(html_character, 'html.parser') title = character.find("h1").find(text=True) for megaEvent in character.find_all(["h2", "ul"]): if megaEvent.name == "h2" and megaEvent.text != "Biographie": break elif megaEvent.name == "ul": for event in megaEvent.findAll('li'): event_date = event.find('a').get('title') formatted = format_date(event_date) if formatted: event_dates.append(formatted) for text in event.findAll(text=True): if 'naissance' in text.lower() and not birthday: birthday = formatted if(not birthday): if event_dates: first_event = event_dates[0] birthday = dt(first_event.year - FIRST_EVENT_SHIFT, first_event.month, first_event.day) else: birthday = dt.min return event_dates, birthday, title
Afin de formatter les dates de wikipast et de convertir les résultats d'Impresso vers une version facilement dessinable nous utilisons les fonctions utilitaires suivantes
def format_date(date): date_parsed = date.split(' ')[0].split('.') if(not date_parsed[0].isdigit()): return None completion_date = ['01','01'] date_corrected = [date_parsed[0]] for i, parsed in enumerate(date_parsed[1:]): if parsed.isdigit(): date_corrected.append(parsed) else: date_corrected.append(completion_date[i-1]) date_corrected = date_corrected + completion_date[len(date_parsed)-1:2] formatted = None try: formatted = dt(int(date_corrected[0]), int(date_corrected[1]), int(date_corrected[2])) except ValueError as err: print("DATE FORMAT ERROR") return formatted def dictionnary_to_plot(d): d = d.items() d = sorted(d, key=lambda x: x[0]) keys_dt = [dt(y[0],1,1) for y in d] vals = [r[1] for r in d] return (keys_dt, vals)
Le téléversement des images sur wikipast et la création de la section "Histogramme" se fait avec ce code
#inspired by InferenceBot def get_edit_token(): 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) return (edit_token, edit_cookie) #inspired by InferenceBot def upload_file(fname, title): (edit_token, edit_cookie) = get_edit_token() with open(fname, 'rb') as pic: payload = {'action': 'upload', 'filename': 'Histogramme ' + title, 'token': edit_token, 'ignorewarnings': 1} files = {'file': pic.read()} r4 = requests.post(baseurl + 'api.php', data=payload, files=files, cookies=edit_cookie) return 'Histogramme ' + title + ".png" def create_content(fname): content ='\n==Histogramme==\n' content+='[[Fichier:'+fname+'|frame|left|' content+='Histogramme de l\'influence médiatique du personnage. ' content+='Events indique les événements présents dans la biographie. ' content+='Les 3 courbes décrivent la variation du nombre d\'occurences du personnage dans les journaux année après année, ' content+='avec une sélection respectivement laxiste (low), modérée (medium) et sévère (high) des articles traitant probablement du personnage. ' content+='Pour plus d\'informations, consultez [[CurvyBot]].' content+=']]\n' return content """Inserts a new section named Histogram at the end of the page named title and uploads the histogram""" def add_histogram(fname, title, create_section=True): if(create_section): wiki_fname = upload_file(fname, title) content = create_content(wiki_fname) (edit_token, edit_cookie) = get_edit_token() payload={'action':'edit', 'assert':'user', 'format':'json', 'utf8':'', 'appendtext':content, 'summary':'Histogram insertion', 'title':title, 'token':edit_token} r4=requests.post(baseurl+'api.php',data=payload,cookies=edit_cookie) """Used to check if a given biography already contains a header with the histogram title""" def bio_needs_section(bio): with closing(urlopen(bio)) as html_character: character = BeautifulSoup(html_character, 'html.parser') for header in character.findAll("h2"): if 'Histogramme' in header.text: return False return True
Le code qui sut sert à faire les requêtes à Impresso et calculer les ratios. On y utilise les imports suivant
import requests from urllib.parse import urlencode from collections import OrderedDict import os from urllib.request import urlopen as urlopen from bs4 import BeautifulSoup import re from contextlib import closing
La fonction principale de ce module et utilisée pour récupèrer les trois courbes est
# Return [highConfidence, mediumConfidence, lowConfidence] in the right format def bioToDictionnaries(bio, birthday, impresso_token): NORMALISATION_FACTOR = 100 totalArticlesPerYear = articlePerYear(impresso_token,[]) completeName = getName(bio) familyName = completeName.split(" ") if len(familyName)>1: familyName = " ".join(familyName[1:]) else: familyName = "" print(familyName) completeNameArticles = articlePerYear(impresso_token,[completeName]) familyNameArticles = articlePerYear(impresso_token,[familyName]) mediumConfidenceData = yearsFormatter(completeNameArticles, totalArticlesPerYear, birthday.year, NORMALISATION_FACTOR) lowConfidenceData = yearsFormatter(familyNameArticles, totalArticlesPerYear, birthday.year, NORMALISATION_FACTOR) ### remarquableWords = [k for k in occurenceWords(bio, completeName)] for word in mentionsWords(bio): if word not in remarquableWords: remarquableWords.append(word) #Crée un seul dictionnaire avec la moyenne des ratio de toutes les recherches à haute confiance. highConfidenceData = {} numberOfRemarquableWords = len(remarquableWords) for word in remarquableWords: sampleArticles = articlePerYear(impresso_token,[completeName,word]) sampleHighConfidenceData = yearsFormatter(sampleArticles, totalArticlesPerYear, birthday.year, NORMALISATION_FACTOR) for year in sampleHighConfidenceData: if year in highConfidenceData: highConfidenceData[year] += sampleHighConfidenceData[year] else: highConfidenceData[year] = sampleHighConfidenceData[year] #for year in highConfidenceData: #highConfidenceData[year] = highConfidenceData[year]/numberOfRemarquableWords ### return [highConfidenceData, mediumConfidenceData, lowConfidenceData]
On y retrouve le NORMALISATION_FACTOR qui sert à rendre les valeurs plus lisibles et à les convertir en pourcentages. On consulte les mentions de la biographie avec
def mentionsWords(bio): with closing(urlopen(bio)) as html_character: character = BeautifulSoup(html_character, 'html.parser') mentionsFlag = False result = [] for megaEvent in character.find_all(["h2", "ul"]): if megaEvent.name == "h2" and megaEvent.text == "Mentions": print(megaEvent) print("ACTIVATE SEARCHING") mentionsFlag = True elif megaEvent.name == "ul" and mentionsFlag: fonction = megaEvent.text if "Mention" in fonction: fonction = fonction.split("en tant que") fonction = fonction[1] fonction = fonction.split(", dans") fonction = fonction[0] if fonction not in result: result.append(fonction) return result
Les mots remarquables sont récupèrés de la manière suivante
# Code qui tente d'extraire des "mots remarquables" des biographies pour affiner les recherches # Le but sera d'effectuer des recherches avec le nom de la personne + un de ces mots. # Problème: certaines biographies n'ont aucun mots remarquables avec ce critère -> Il faudra affiner. # pistes: Extraire la profession/qualificatif de la personne. Extraire des oeuvres de lui. def occurenceWords(bio, name): MIN_INTERESTING_LENGTH = 3 TRASH_WORDS = ['contre','entre','avec','pour','dans','leur','vous','des','par','une','sur','est','son','ses','que'] with closing(urlopen(bio)) as html_character: character = BeautifulSoup(html_character, 'html.parser') wordFrequency = {} #Creation d'un dictionnaire avec tous les mots et leurs nombres d'apparitions pour chaque bio for megaEvent in character.find_all(["h2", "ul"]): # Possibilité d'utiliser des listes ! if megaEvent.name == "h2" and megaEvent.text != "Biographie": print(megaEvent) print(megaEvent.text) print("BREAKING") break elif megaEvent.name == "ul": for event in megaEvent.findAll('li'): for expression in event.findAll(text=True): for quasiWord in expression.split(' '): for word in quasiWord.split("""'"""): word = word.lower() if(re.search('^[a-zA-Zàâéèêîûô]*$',word) and len(word) >= MIN_INTERESTING_LENGTH): if word in wordFrequency: wordFrequency[word] += 1 else: wordFrequency[word] = 1 #Traitement du dictionnaire TOLERANCE = 2 trash = [] for word in wordFrequency: #Ne conserve pas le nom dans les mots remarquables if word in name.lower(): trash.append(word) #Ne conserve pas les mots qui n'apparaissent qu'une seule fois dans les mots remarquables if wordFrequency[word] <= TOLERANCE: trash.append(word) #Elimine des mots communs if word in TRASH_WORDS: trash.append(word) for word in trash: if word in wordFrequency: del wordFrequency[word] return wordFrequency
Le nom de la personne étant une donnée importante on le récupère à travers
#Donne le nom de la personne sujet de la biographie def getName(bio): with closing(urlopen(bio)) as html_character: character = BeautifulSoup(html_character, 'html.parser') # Trouve le nom de la personne dont on traite la biographie name = character.find('title') name = str(name) name = name.split('<title>') name = name[1] name = name.split(" — Wikipast</title>") name = name[0] return name
Finalement, la fonction chargée de faire les requêtes à Impresso
#Retourne le nombre d'articles total par année pour les mots clefs données #Appeler la fonction avec une liste de mots-clef vide retourne le nombre d'article par année. def articlePerYear(impresso_token, listOfKeyWords): header = {'Authorization': 'Bearer ' + impresso_token} # Cette partie de la requête ne change jamais, elle groupe les résultats par article url_base = "https://impresso-project.ch/api/search?group_by=articles" query = url_base for i, keyWord in enumerate(listOfKeyWords): query += "&filters["+str(i)+"][type]=string&filters["+str(i)+"][q]="+keyWord query += "&facets=year" # Fait la requête avec les bons headers res = requests.get(query, headers=header) # Retourne le json de la requête result = res.json() if 'facets' in result['info']: return result['info']['facets']['year']['buckets'] else: return {}