« SliderBot » : différence entre les versions
Ligne 274 : | Ligne 274 : | ||
==== Github : serveur déjà existant mais les fichiers ne sont pas mis à jour automatiquement. ==== | ==== Github : serveur déjà existant mais les fichiers ne sont pas mis à jour automatiquement. ==== | ||
Pour que la visualisation des résultats soit possible, les fichiers json doivent être mis à jour sur Github. Cela | Pour que la visualisation des résultats soit possible, les fichiers json doivent être mis à jour sur Github. Cela pour l'instant fait à la main, aucune méthode automatisée n'ayant été trouvée. | ||
=== Analyse des performances === | === Analyse des performances === |
Version du 21 mai 2018 à 15:19
Objectifs
- Création d'une base de données en associant [Personne, Date, Lieu] via un scrapping de page.
- Création d'un ensemble de cartes affichant les positions de toutes les personnes recensées pour chaque date (discretisation par année).
- Création d’un slider dynamique, où l’annee souhaitée est choisie, et affiche la carte correspondante (JavaScript / HTML)
Description
Le bot extrait les données nécessaires de Wikipast, en parcourant chaque page existante d'années à travers le temps. Les informations retenues sont d'abord les mentions de villes et deuxièmement les mentions de personnes respectives pour chaque année. Ce travail est fait par un code python, qui va sauvegarder l'information dans un tableau. Ce tableau est utilisé pour afficher les lieux mentionnées et les personnes respectives sur une carte, pour une année spécifique, en utilisant un code JavaScript.
Le but final est d'appliquer cette méthode pour chaque année existante dans la base de données de Wikipast et ainsi de créer une carte interactive permettant de naviguer à travers le temps et l'espace et ainsi visualiser la distribution d'événements sur le globe au fil des ans.
Fonctionnement
Extraction de données
BeautifulSoup est une bibliothèque de parsage (analyse syntaxique) en code Python pour le langage HTML/XML. Dans ce cas, elle est utilisée pour extraire des données de Wikipast : villes et personnes mentionnées, dans les pages existantes concernant les années. Geopy est une bibliothèque Python, utilisée pour localiser les coordonnées d'endroits spécifiques dans le monde (dans ce cas des villes).
Code Complet: maincode.py [1]
Récupération des lieux et personnes
Le résultat du parsing, avec les services de BeautifulSoup, est une liste de tuples: année, lieux, personne. Ceci est principalement fait par la fonction getYearCityAndName:
def getYearCityAndName(div): tagDate = div[div.index("<a"):div.index("</a>")] date = tagDate[tagDate.index(">") + 1:] year = date.split(".")[0] if not year.isnumeric(): return if div[div.index("</a>") + 7] == '-' or div[div.index("</a>") + 4] == '.' or div[div.index("</a>") + 7] == ' ': return else: skipDate = div[div.index("<a")+ 2:] tagCityStart = skipDate[skipDate.index("<a"):] tagCity = tagCityStart[:tagCityStart.index("</a>")] city = tagCity[tagCity.index(">") + 1:] skipCity = tagCityStart[tagCityStart.index("</a>")+6:tagCityStart.index("</li>")] class MyHTMLParser(HTMLParser): data = [] def handle_starttag(self, tag, attrs): if tag == 'a': for attr in attrs: if str(attr[0]) == 'title': if isName(attr[1]): self.data.append(attr[1]) parser = MyHTMLParser() parser.feed(skipCity) if len(parser.data) != 0: return (year, city, parser.data)
Localisation
Pays vs. Capitale
Si le lieu d'un événement sur une page Wikipast est décrite seulement par son pays, plutôt que de l'afficher au centre géographique du pays, l'événement sera plutôt affiché comme s'étant produit dans la capitale de ce pays. Ceci est fait à l'aide d'un fichier countries.py [2] qui contient une liste avec les pays dans le monde et leur capitale, par la fonction capitalIfCountry:
def capitalIfCountry(location): """ If location is a country, then it changes it to its capital """ dataPays = next((item for item in countries if item["name"] == location), False) if(dataPays): return (dataPays['capital']) else: return location
Conversion de coordonnées
La location est convertie en coordonnées, avec les services de geo-localisation de Geopy, par la fonction finalNameCoordTuple:
def finalNameCoordTuple(tuples): """ Produces an array 'output' containing [Name, latitude, longitude] If location is a country, then it changes it to its capital """ output = [] for tuple in tuples: coord = geolocator.geocode(capitalIfCountry(tuple[1])) output.append([tuple[2], coord.latitude, coord.longitude]) return output
Ici, les différents éléments du tuple proviennent du parsing : tuple[1] correspond au nom du lieu et tuple[2] correspond à la personne.
Répulsion de lieux identiques
L'affichage correct de plusieurs événements se déroulant au même lieu. En effet, geocode renvoie une position (latitude, longitude) précisément identique pour chaque ville. Lorsque deux événements se passent au même lieu, les points se confondent rigoureusement et l'API Google ne permet pas de les séparer (peu importe le niveau de zoom).
Pour parer à cette éventualité, la fonction repulsePoints décale sur la carte de quelques centaines de mètres les événements (par un simple incrément arbitraire de longitude et latitude).
def repulsePoints(tuples): for i in range (len(tuples)): for j in range (len(tuples)): if (i != j and tuples[i][1] == tuples[j][1] and tuples[i][2] == tuples[j][2]): tuples[j][1] = tuples[i][1] + 0.005 tuples[j][2] = tuples[i][2] + 0.005 return tuples
Cela étant dit, une telle méthode peut donner l'impression d'une précision artificielle sur les lieux (qu'il sera impossible d'éviter, les lieux sur Wikipast étant au mieux localisés par ville).
Fichier Json
Ajout du lien Wikipast
La fonction addWikiLink ajoute aux tuples crées plus tôt le lien de la page WikiPast de la personne concernée.
def addWikiLink(input_string): output_string=input_string for i in range (len(input_string)): output_string[i].append('http://wikipast.epfl.ch/wikipast/index.php/'+input_string[i][0].split(', ')[0].replace(" ", "_")) return output_string
Dans le cas où plusieurs personnes sont concernées par un événement, le choix a été fait de lier la page Wiki de la première personne seulement, à la fois pour simplifier le code et rendre la carte plus lisible.
Création du JSON
Finalement le fichier JSON est généré, et en prenant en paramètres des arrays en format [nom, latitude, longitude, lien wiki] et l'année souhaitée (commune à toutes les fonctions du programme), produit un fichier de la forme (par exemple ici 1986.json, où l'on a gardé qu'un événement), qui est enregistré dans un dossier server/maps sur github.
[ { "proprietes" : ["Enzo Ferrari",44.5255217,10.8663607, "http://wikipast.epfl.ch/wikipast/index.php/Enzo_Ferrari"] } ]
La fonction est la suivante:
def createJson(inputData, year): """ Takes array [Name, lat, long] for each year Outputs in .json such that it can be plotted """ indent = ' ' indent2 = ' ' maxLimit = len(inputData) with open(str(year)+'.json', 'w') as outfile: outfile.write('['+'\n') for i in range (maxLimit): if i == (maxLimit-1): outfile.write(indent + '{'+'\n') outfile.write(indent2 + ' "proprietes" : '+'['+'"'+remove_accents(str(inputData[i][0]))+'"'+ ','+ str(inputData[i][1])+ ','+ str(inputData[i][2])+']'+'\n') outfile.write(indent + '}'+'\n') else: outfile.write(indent + '{'+'\n') outfile.write(indent2 + ' "proprietes" : '+'['+'"'+remove_accents(str(inputData[i][0]))+'"'+ ','+ str(inputData[i][1])+ ','+ str(inputData[i][2])+']'+'\n') outfile.write(indent + '}'+','+'\n') outfile.write(']') outfile.close()
Tous les besoins précis de structure d'un fichier Json sont pour l'instant (faute de mieux) gérés manuellement (comme la conversion en str des float de coordonnées, ou bien les retours à la ligne).
Placement sur la carte
Le placement des points (personne, ville) est fait avec un code en Javascript, à partir du fichier JSON créé auparavant. Les points respectifs à chaque année sont affichés sur une carte Google Maps. Un slider est aussi crée pour pouvoir naviguer dans le temps.
Code complet: index.html [3]
Le code est caractérisé par trois fonctions principales:
main() initialise la carte avec les caractéristiques définies: sa position par défaut, l'option zoom et le slider, prédéfini à une certaine année.
function main(){ //Initialise les éléments de la page var slider = document.getElementById("slider_annee"); var output = document.getElementById("annee"); var b_autoplay = document.getElementById("autoplay"); // Affiche la valeur par défaut de l'année output.innerHTML = slider.value; slider.onmouseup = function() { //Met à jour la carte quand on relâche le slider update(); } slider.oninput = function() { //Met à jour la valeur du slider quand on change la valeur output.innerHTML = this.value; } //Options de la carte var options={ zoom:2, center:{lat:30,lng:20} } //Met à jour la carte la première fois update(); // ... }
update() extrait tous les points du fichier JSON pour l'année définie par le slider, ceci fait à travers un liens direct vers github.
function update(){ //Réinitialise tableaux fenetres=[]; markers=[]; //Création de la carte carte=new google.maps.Map(document.getElementById('carte'), options); //Ferme les fenêtres d'infos si on clique en dehors des markers carte.addListener('click', function(){ for(var i=0; i<fenetres.length; i++){ fenetres[i].close(); } }); //Chercher le fichier JSON var requestURL = 'https://raw.githubusercontent.com/EtienneBonvin/SliderBot/master/server/maps/'+String(slider.value)+'.json'; var request = new XMLHttpRequest(); request.open('GET', requestURL); request.responseType = 'json'; request.send(); request.onload = function() { var reponse = request.response; //On appelle la fonction ajoutant les points for(var i=0; i<reponse.length; i++){ ajouterPoint(reponse[i]); } //On regroupe les points var regroupement = new MarkerClusterer(carte, markers,{ imagePath: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m' }); } }
ajouterPoint() affiche les points sur la carte grâce aux coordonnées spécifiées (latitude, longitude).
function ajouterPoint(proprietes){ //On définit l'emplacement du marker var point=new google.maps.Marker({ position:{lat:proprietes["proprietes"][1], lng: proprietes["proprietes"][2]}, map:carte, }); // On ajoute une fenêtre d'info (avec également lien si disponible) if(proprietes["proprietes"][3]){ var description='<a href="'+proprietes["proprietes"][3]+'">'+proprietes["proprietes"][0]+'</a>'; } else { var description=proprietes["proprietes"][0]; } var fenetreInfo=new google.maps.InfoWindow({ content:description }); //Au clic, on ouvre la fenêtre d'info et au ferme toutes les autres... point.addListener('click', function(){ for(var i=0; i<fenetres.length; i++){ fenetres[i].close(); } fenetreInfo.open(carte,point); }); markers.push(point); fenetres.push(fenetreInfo); }
Exemple
À gauche est un exemple de comment la carte du monde est visualisé pour l'année 1809. Dans le bas de la carte il y a le slider pour avancer ou retourner en arrière dans le temps. La carte contient l'option de naviguer dans l'espace et de zoom, ainsi que des boutons de localization.
À droite est un exemple de l'année 1962, dont la carte est zoom-in sur Genève. Quand le zoom est assez grand pour identifier des points individuellement, les noms de personnes et les details de l'événement sont affichés.
Discussion et améliorations
Serveur Node.js vs Github
Node.js : efficace mais requiert un serveur externe
Node.js est une plateforme logicielle libre en JavaScript orientée vers les applications réseau qui doivent pouvoir monter en charge. Parmi les frameworks de Node.js, on retrouve Express qui permet le développement simple de serveurs HTTP. Initialement on avait crée un server externe, avec Node.js, pour transmettre l'information qui a été extraite vers le code html pour l'afficher sur la carte. On s'est rendu compte par contre que la façon plus direct était simplement d'utiliser le serveur de Github. Ceci est fait dans index.html dans la fonction update(), qui va chercher le fichier JSON d'une année spécifié par le slider :
var requestURL = 'https://raw.githubusercontent.com/EtienneBonvin/SliderBot/master/server/maps/'+String(slider.value)+'.json';
Github : serveur déjà existant mais les fichiers ne sont pas mis à jour automatiquement.
Pour que la visualisation des résultats soit possible, les fichiers json doivent être mis à jour sur Github. Cela pour l'instant fait à la main, aucune méthode automatisée n'ayant été trouvée.
Analyse des performances
Temps de Calcul
Temps pour calculer les json correspondants à toutes les années disponibles sur Wikipast (de 1765 à 1999) : 1 minute 51 secondes.
Temps moyen par année : 0.47 secondes.
Limitations
La principale limitation de SliderBot provient de la bonne wikifiction des articles créés : sans une structure efficace Date/Lieu, ou bien une mention explicite de la personne dans la ligne d'événement (et non, e.g. 1947 / Paris. Récompense pour tel livre), le bot n'a pas de moyen de correctement parser les informations.
D'autre part, mis à part les imprécisions de wikifications humaines, le SliderBot nécessite le fonctionnement correct et fréquent du ChronoBot, son fonctionnement étant basé sur les pages annuelles de ChronoBot.
Enfin, notre code nécessite encore pour le parsing et la production de fichiers JSON, la suppression systématique des accents (problèmes d'encodage). Ceci se révèle problématique dans le lien des pages WikiPast (qui nécessitent les accents corrects) : plutôt que de linker vers une page inexistante, le choix a été fait de mettre un lien vers le wiki uniquement pour les personnes sans accents.
Aucune solution pour parer à ce problème de conservation d'accents n'a encore été concluante. Ce serait une amélioration majeure à apporter à ce projet.
Possibles collaborations avec d'autres bot
ImageBot
Une fois que ImageBot sera terminé et aura ajouté des images à un nombre satisfaisant de personnalités, il serait possible et intéressant d'afficher ces images en plus du nom sur la carte.
CreatoBot
Une des limitations de notre bot et qu'un nombre non négligeable d'années et / ou de lieux n'ont pas été correctement mis en hypermot. Le travail de CreatoBot visant à ajouter ces hypermots quand nécessaire augmentera le nombre de données réunies.
Mise à jour du bot
Le bot devrait être mis à jour environ une fois par année, ou après un ajout conséquent de pages. Ceci est mieux fait après que les autres bots aient fait leur travail. Pour mettre à jour il suffit de lancer le bot et de renouveler les fichiers JSON sur Github.
Code Complet
https://github.com/EtienneBonvin/SliderBot
Groupe
Nom | Pseudo |
---|---|
Paul Guhennec | Pguhennec |
Maël Wildi | mwildi |
Etienne Bonvin | AbsInt |
Mathilde Raynal | PizzaWhisperer |
Stefano Politi | spoliti |