« SliderBot » : différence entre les versions
Ligne 86 : | Ligne 86 : | ||
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. | 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). | 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). | ||
==== Récupération des lieux et personnes ==== | |||
A faire ? | |||
==== Conversion de coordonnées et création d'un fichier Json ==== | ==== Conversion de coordonnées et création d'un fichier Json ==== |
Version du 8 mai 2018 à 09:31
Objectifs
- Crée une base de données en associant [Personne, Date, Lieu] via un scrapping de page.
- Crée 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 ou 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
Server
Node.js est une plateforme logicielle libre en JavaScript orientée vers les applications réseau qui doivent pouvoir monter en charge. Parmi les modules natifs de Node.js, on retrouve HTTP qui permet le développement de serveurs HTTP.
Username | Password |
---|---|
SliderBot | SliderBot123 |
server.js
Le serveur est initialisé à l'aide du module 'express' de Node.js, pour générer le cadre d'une application. La méthode app.post() indique au serveur qu'il est en attente d'une demande HTTP. La méthode app.listen() définie le chemin sur lequel le serveur est prêt à l'écoute.
const express = require('express'); const app = express(); app.post('/', function(req, res){ console.log('Post request received: '); res.writeHead(200, { 'Content-Type': 'text/plain' }); req.on('data', function (chunk) { var buffer = JSON.stringify(chunk); var data = getData(buffer); console.log('GOT DATA : '+data); }); res.end(JSON.stringify({"data":0})); }); app.listen(3000, () => { console.log('Server listening on port 3000!'); });
communication.js
JSON (JavaScript Object Notation) est un format de données textuelles dérivé de la notation des objets du langage JavaScript. La fonction sendReq() envoie une demande au server pour recevoir l'information sous ce format.
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; var IP_ADDRESSE = "128.179.178.60"; var PORT = "3000"; function sendReq(){ var request = new XMLHttpRequest(); request.open('POST', 'http://'+IP_ADDRESSE+':'+PORT, true); request.setRequestHeader("Content-type", "application/json"); request.onload = () => { if (request.status >= 200 && request.status < 400) { const res = JSON.parse(request.responseText); console.log(res); } else { console.log("Error on server side.") } }; request.onerror = () => { console.log("Error on communication."); }; request.send(JSON.stringify({"year":1996})); } sendReq();
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).
Récupération des lieux et personnes
A faire ?
Conversion de coordonnées et création d'un fichier Json
Le résultat du parsing est une liste de tuples, contenant des années, villes et personnes associés. 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' qui contient une liste avec les pays dans le monde et leur capitale (voir fonction capitalIfCountry). 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 et/ou l'événement. C'est la partie encore problématique du parsing (qui sera discutée plus loin) : tuple[2] contient variablement le nom de la personne (comme souhaité), l'événement (e.g. "Naissance") ou bien les deux.
Finalement le JSON est généré, et en prenant en paramètres des arrays en format [nom, longitude, latitude] 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).
[ { "proprietes" : ["Enzo Ferrari",44.5255217,10.8663607] } ]
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).
Code complet : maincode.py
from urllib.request import urlopen from bs4 import BeautifulSoup import sys import io import json import time from countries import countries from geopy import * from geopy.geocoders import * from html.parser import HTMLParser import unicodedata def masterFunction(year): """ Core function : creates a JSON file with [Name, lat, long] for any year (argument) """ #==========Initialisation=========== sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') response=urlopen("http://wikipast.epfl.ch/wikipast/index.php/"+str(year)) page_source=response.read() soup=BeautifulSoup(page_source,'html.parser') stringSoup = str(soup) geolocator = Nominatim() #=============String=============== """check that every word starts with upperletter and max length of 20 chars""" def isName(string): if len(string) < 20 and string.istitle(): return True return False def remove_accents(input_str): nfkd_form = unicodedata.normalize('NFKD', input_str) return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) #=============Parsing=============== def getNthDiv(stringSoup, n): divStart = stringSoup[stringSoup.index("mw-content-ltr") + len("mw-content-ltr"):] for i in range(n): try: nextIndex = divStart.index("<li") except ValueError: return None divStart = divStart[nextIndex + len("<li"):] return divStart 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: #print(year, city, parser.data) return (year, city, parser.data) #===========Location=========== 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 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 #=============Print============= def printTuples(tuples): for tuple in tuples: print(tuple[0]+" : "+tuple[1]) def tuplesWithMultiplicity(yearCityList): tuples = [] for i in range(0, len(yearCityList)): compte = yearCityList.count(yearCityList[i]) tuples.append((yearCityList[i], compte)) return [list(item) for item in set(tuple(row) for row in tuples)] def printBigTuples(tuples): for tuple in tuples: print(tuple[0][0], tuple[0][1], tuple[1]) def printFinalTuples(tuples): for tuple in tuples: print('Who : ' + tuple[0] + ' Where : ' + tuple[1] + ' ' + tuple[2]) #===========JSON file=========== 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() def createCoreList(): counter = 1 divDate = getNthDiv(stringSoup, counter) yearCityNameList = [] while divDate != None: element = getYearCityAndName(divDate) if element is not None: yearCityNameList.append(element) counter += 1 divDate = getNthDiv(stringSoup, counter) return yearCityNameList createJson(finalNameCoordTuple(createCoreList()),year) masterFunction(1962)
Placement sur la carte
Le placement des points (personne, ville) est fait avec un code en language HTML (HyperText Markup Language), à partir du fichier JSON crée auparavant. Les points respectives à chaque année sont affichés sur une carte Google Maps. Un slider est aussi crée pour pouvoir naviguer dans le temps.
index.html
La fonction initCarte() initialise la carte et le slider avec les caractéristiques définies. La fonction update() extrait tous les points du fichier JSON. La fonction ajoutePoints() affiche les points sur la carte.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>SliderBot</title> <style> html, body{ height:100%; font-family: Arial, Verdana, sans-serif; } #carte{ height:55%; width:100%; border-radius:4px; } #titre, p, div{ text-align:center; } #slider_annee{ width:50%; } </style> </head> <body> <h1 id="titre">SliderBot</h1> <div id="carte"></div> <div> <input type="range" min="1600" max="2018" value="1809" id="slider_annee"> </div> <div> <button onclick="diminuer()">-</button> <button onclick="augmenter()">+</button> <p>Année: <b><span id="annee"></b></span></p> </div> <div> <button onclick="recentrer(2,30,20)">Monde</button> <button onclick="recentrer(3,4,18)">Afrique</button> <button onclick="recentrer(3,48,-100)">Amérique du Nord</button> <button onclick="recentrer(3,-25,-65)">Amérique du Sud</button> <button onclick="recentrer(3,35,96)">Asie</button> <button onclick="recentrer(4,51.5,16.36)">Europe</button> <button onclick="recentrer(4,-30.5,143)">Océanie</button> </div> <script> function initCarte(){ //Initialise slider var slider = document.getElementById("slider_annee"); var output = document.getElementById("annee"); // Display the default slider value output.innerHTML = slider.value; // Chaque fois qu'on bouge le slider : 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(); function update(){ //Réinitialise tableau markers=[]; //Création de la carte var carte=new google.maps.Map(document.getElementById('carte'), options); //Chercher le fichier JSON var requestURL = 'https://raw.githubusercontent.com/EtienneBonvin/SliderBot/master/bot/test3.json?token=AkepwChC82KwQVTpNSOKDwl8FbLc6MEjks5a-YRKwA%3D%3D'; var request = new XMLHttpRequest(); request.open('GET', requestURL); request.responseType = 'json'; request.send(); request.onload = function() { var reponse = request.response; //On ajoute 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' }); } } function ajouterPoint(proprietes){ var point=new google.maps.Marker({ position:{lat:proprietes["proprietes"][1], lng: proprietes["proprietes"][2]}, map:carte, }); var fenetreInfo=new google.maps.InfoWindow({ content:proprietes["proprietes"][0] }); point.addListener('click', function(){ fenetreInfo.open(carte,point); }); markers.push(point); } //Déclarer comme ça la fonction permet qu'elle soit accessible en dehors d'initCarte() recentrer=function (niv_zoom,latitude,longitude){ options={ zoom:niv_zoom, center:{lat:latitude,lng:longitude} } update(); } augmenter=function(){ slider.stepUp(1); output.innerHTML = slider.value; update(); } diminuer=function(){ slider.stepDown(1); output.innerHTML = slider.value; update(); } } </script> <script src="https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/markerclusterer.js"> </script> <script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDUYredUZ1kaPBxNz0ToUQas1MwCF4-ER4&callback=initCarte"> </script> </body> </html>
Exemple
Ceci est un exemple de comment la carte 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. Quand le zoom est assez proche pour distinguer les différentes personnes comme points individuels, ils sont représentés par leurs images (si existantes).
Discussion
Groupe
Nom | Pseudo |
---|---|
Paul Guhennec | Pguhennec |
Maël Wildi | mwildi |
Etienne Bonvin | AbsInt |
Mathilde Raynal | PizzaWhisperer |
Stefano Politi | spoliti |