Différences entre les versions de « Utilisateur:Thomas »

De {}
Aller à la navigation Aller à la recherche
 
(107 versions intermédiaires par le même utilisateur non affichées)
Ligne 696 : Ligne 696 :
 
=Writing bot=
 
=Writing bot=
  
Créer un bot qui fait un truc avec des noms de ville rigolos
+
==idées==
 +
Créer un bot qui fait un voyage dans la campagne à travers des villes/villages avec des noms rigolos.
 +
Tous les jours, il poste le nom d'un village de moins de XXX habitants, avec une photo de ce village.
 +
Le village suivant est choisi suivant ces paramètres :
 +
- la taille du village (moins de XXX habitants)
 +
- la distance (possibilité d'y aller à moins de 8h de marche depuis le village du jour)
 +
- direction aléatoire en (évitant peut-être les demi-tours complets ?)
 +
- pas 2 fois le même village
  
 
Créer un bot qui fait des mots-valises ((dans quel contexte ?))
 
Créer un bot qui fait des mots-valises ((dans quel contexte ?))
Ligne 720 : Ligne 727 :
 
Le bot serait une sorte de Père Castor qui raconte des "histoires" sans les images, peut-être que ça peut être à l'heure où on couche les enfants, tous les soirs ?
 
Le bot serait une sorte de Père Castor qui raconte des "histoires" sans les images, peut-être que ça peut être à l'heure où on couche les enfants, tous les soirs ?
 
<br/>Ou alors il en poste plusieurs à la suite (5-10 ?) et fait un lien entre tous pour créer une petite histoire du soir ?
 
<br/>Ou alors il en poste plusieurs à la suite (5-10 ?) et fait un lien entre tous pour créer une petite histoire du soir ?
 +
 +
En fait c'est pas assez narratif mais c'est rigolo quand même :/.
 +
 +
==@baladecampagne==
 +
===RÉSUMÉ===
 +
[[Fichier:out1.png | 4000px]]
 +
[[Fichier:out3.png | 4000px]]
 +
[[Fichier:out5.png | 4000px]]
 +
[[Fichier:out7.png | 4000px]]
 +
[[Fichier:out9.png | 4000px]]
 +
 +
[https://twitter.com/baladecampagne Le lien vers le bot]
 +
 +
Ce bot se balade dans la campagne française, allant de villages en villages (communes de moins de 2000 habitants). 
 +
Dans chaque post, le bot annonce le village dans lequel il compte se rendre, accompagné de la distance en km et du temps de trajet estimé, sachant que le bot se balade à pied. Ce temps de trajet correspond au temps qui va s'écouler avant le prochain post, c'est-à-dire le temps que le bot arrive dans le village et choisisse une prochaine destination. 
 +
Le choix de la commune se fait au hasard parmi les villages se trouvant dans un rayon de plus ou moins 25km autour de l'endroit où se trouve le bot actuellement. La première commune a été choisie au hasard parmi les 27666 communes correspondant aux critères en France, et c'est Griscourt qui sert donc de point de départ.
 +
 +
Les informations concernant les noms des communes et leur position proviennent d'un tableau csv du gouvernement français recensant toutes les communes, et les informations sur le nombre d'habitants viennent de Wikipédia.
 +
 +
Le projet a été réalisé en Python 3.7, avec les modules csv, json, mechanize, lxml, cssselect et tweepy.
 +
 +
===Mise au point du fonctionnement===
 +
Le site https://territoires-fr.fr/communes-list1.php peut peut-être être utile.
 +
 +
Le programme part d'un endroit sur la carte (chez moi en France par exemple).
 +
Avec l'API de TomTom et les filtres on peut créer un cercle dans un rayon de XXX km dans lequel on peut chercher des POI (points of interest)
 +
 +
L'API de TomTom recherche le point d'intérêt "mairie" ou "village" dans un certain rayon, et récupère la ville où elle se trouve.
 +
Grâce au site territoires-fr on peut vérifier : que le village est assez petit (pas plus de XXX habitants), car grâce à l'API TomTom on connaît le département et la ville dans laquelle on se trouve.
 +
 +
Le bot Twitter poste le nom de la ville/village en question, puis une photo de l'endroit (possible avec TomTom ? Google Street View ? Sinon avec Google Images).
 +
 +
Si la ville/village en question a un compte Twitter, le bot pourrait poster une photo/retweeter un message de la ville pour dire qu'il est passé par là.
 +
 +
 +
Liens utiles :
 +
* https://developer.tomtom.com/search-api/search-api-documentation
 +
* https://developer.tomtom.com/search-api/search-api-documentation-filters/geometry-filter
 +
* https://developer.tomtom.com/content/search-api-explorer#/Search/get_search__versionNumber__search__query___ext_
 +
 +
 +
J'arrive pas trop à trouver comment trouver des villes sur l'API de TomTom, dans les paramètre de recherche on peut rechercher un point d'intérêt dans un rayon autour du point où on se trouve, mais quand on recherche "ville" ou "mairie" ça marche pas vraiment. C'est plus fait pour rechercher une pizzeria ou une station essence pas loin de nous.
 +
 +
Donc j'ai récupéré un fichier.csv du site data.gouv.fr [https://www.data.gouv.fr/fr/datasets/listes-des-communes-geolocalisees-par-regions-departements-circonscriptions-nd/#_] qui devrait me permettre de créer un rayon juste en utilisant les données géolocalisées.<br/>
 +
Par exemple je peut rechercher les villes/villages qui correspondent à une distance de XXX km autour de là où on se trouve en disant de rechercher les correspondances entre les latitudes x+r et x-r et les longitudes y+r et y-r (ou x et y sont les latitudes/longitudes de la position actuelle et r le rayon d'action).
 +
 +
Ensuite on a une liste de matches qui vont dans une liste en python (avec leur code postal), et on peut alors chercher dans les détails de la commune pour avoir le nombre d'habitants :
 +
 +
https://territoires-fr.fr/communes-detail.php?dep=35&com=001&actual=1 <br/>
 +
Avec dep=(le numéro de département) et  com=(le numéro de la commune), on tombe sur la page de la commune avec dans le tableau le nombre d'habitants. L'idée est de supprimer les communes trop grandes (plus de 5000 habitants ?) pour mettre en valeur les campagnes du terroir.
 +
 +
Ou alors on peut aller sur Wikipédia qui a peut-être des infos mises à jour plus régulièrement<br/>
 +
(exemple : https://fr.wikipedia.org/wiki/Liste_des_communes_de_l'Ain ) <br/>
 +
et qui évite de devoir faire 1000 requêtes puisqu'on a direct le nom de la commune + le nombre d'habitants dans le tableau (sachant qu'on a accès au nom des communes et des départements dans le .csv du gouvernement)
 +
 +
Par contre il y a sûrement plusieurs lieux-dits avec le même code postal, ça va être plus long à régler.<br/>
 +
 +
En fait le plus simple est de supprimer du csv toutes les villes ayant plus de x habitants, comme ça le programme ne les détectera pas de toute façon dans son rayon. </br>
 +
Pour ça je vais utiliser cette page de Wikipédia [https://fr.wikipedia.org/wiki/Listes_des_communes_de_France] qui va me permettre, grâce au numéro des départements, de trouver toutes les pages listant les communes fraçaises par département. Ensuite je n'ai plus qu'à faire un tableau des communes de plus de x habitants et dans mon csv je supprime ces villes.
 +
 +
Peut-être que l'API TomTom peut servir pour le "routing", pour créer le chemin/le temps de trajet d'une commune à l'autre. (qui peut être posté dans le tweet du bot). Ça permettrait même éventuellement de faire une carte qui se remplit au fur et à mesure pour voir le chemin total parcouru
 +
 +
===Schéma approximatif des étapes de fonctionnement===
 +
[[Fichier:SchémaCampagne.png]]
 +
 +
===Préparation du tableau===
 +
 +
J'ai retaillé le tableau pour enlever les colonnes qui ne servaient à rien, et on utilisant le module csv j'ai pu transformer mon fichier en dictionnaire (plus ou moins un tableau de tableaux).
 +
J'ai donc maintenant 6 tableaux qui me permettent de trouver le nom des communes qui correspondent à une certaine longitude/latitude ou à un département par exemple.
 +
 +
<syntaxhighlight lang=python>
 +
#!/usr/bin/env python3
 +
#coding: utf-8
 +
 +
import csv
 +
 +
d = {}
 +
d['numéro_département'] = []
 +
d['nom_département'] = []
 +
d['nom_commune'] = []
 +
d['codes_postaux'] = []
 +
d['latitude'] = []
 +
d['longitude'] = []
 +
 +
with open('tableau_simplifié.csv', 'r+') as csvfile:
 +
    reader = csv.DictReader(csvfile)
 +
    for row in reader:
 +
        for i in range (0,6):
 +
            valeur = list(row.items())[i][0]
 +
            d[valeur].append(row[valeur])
 +
            print(valeur + ' = ' + row[valeur])
 +
        print('')
 +
 +
</syntaxhighlight>
 +
 +
Maintenant on passe au scraping du nombre d'habitants sur Wikipédia. Je part de la page https://fr.wikipedia.org/wiki/Listes_des_communes_de_France et j'utilise les librairies mechanize, lxml et cssselector
 +
 +
<syntaxhighlight lang="python">
 +
#!/usr/bin/env python3
 +
#coding: utf-8
 +
 +
###TWITTER API
 +
from accès import *
 +
import tweepy
 +
 +
###TEMPORAIRE
 +
import lxml.html as parser
 +
import cssselect, mechanize
 +
 +
###avec mechanize
 +
br = mechanize.Browser()
 +
br.addheaders = [('User-agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0')]
 +
br.set_handle_robots(False)
 +
 +
data = br.open('https://fr.wikipedia.org/wiki/Listes_des_communes_de_France')
 +
 +
source = data.read()
 +
tree = parser.fromstring(source)
 +
num_dpt = []
 +
lien_dpt = []
 +
 +
for selector in cssselect.parse('table.wikitable:nth-of-type(2) td:nth-child(3)'):
 +
    ##on convertit l'objet selector en xpath
 +
    xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
 +
    ##pour chaque lien trouvé par ce xpath
 +
    for link in tree.xpath(xpath_selector):
 +
        num_dpt.append(link.text_content().rstrip())
 +
 +
for selector in cssselect.parse('td:nth-child(5)'):
 +
    ##on convertit l'objet selector en xpath
 +
    xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
 +
    ##pour chaque lien trouvé par ce xpath
 +
    for link in tree.xpath(xpath_selector):
 +
        lien_dpt.append(link.text_content().rstrip())
 +
 +
for i in range(len(num_dpt)):
 +
    try:
 +
        print('%s => %s' % (num_dpt[i], lien_dpt[i]))
 +
    except:
 +
        print('zut')
 +
</syntaxhighlight>
 +
 +
Ce bout de code permet de récupérer les 3e et 5e éléments du 2e tableau, c'est-à-dire les numéros de département ainsi que les liens vers les liste des communes par département (à partir de la page Wikipédia citée plus haut).
 +
 +
Mais ça me sert à rien, j'avais juste besoin des liens en fait (5e colonne du 2e tableau).
 +
 +
J'ai réussi à récupérer les liens dans la 5e colonne du 2e tableau de la page "Liste des communes de France" avec la méthode (?) .get('href') qui me permet de récupérer le contenu de l'attribut href des éléments a dans le tableau.
 +
 +
Maintenant pour chacun de ces liens je dois récupérer les valeurs de la colonne "Nom de la commune" et "Nombre d'habitants". Pour cela même principe en changeant les sélecteurs CSS. MAIS subtilités il faut faire attention de sélectionner le bon tableau avec :nth-of-type(), parce que pour certains départements il y a plusieurs tableaux, donc le script peut récupérer les colonnes de tous les tableaux de la page (là c'était un peu relou, il y a plusieurs types de pages donc c'était long de trouver la règle commune qui marche pour tous les départements).
 +
 +
J'ai fait une grosse fonction qui prend plusieurs paramètres et me permet de récupérer soit les liens, soit les habitants/communes :
 +
 +
<syntaxhighlight lang="python">
 +
def getLiens(url, css_select, type):
 +
    data = br.open(url, timeout=50)
 +
    source = data.read()
 +
    tree = parser.fromstring(source)
 +
 +
    liens = []
 +
    habitants = []
 +
    commune = []
 +
 +
    if type == 'liens':
 +
        for selector in cssselect.parse(css_select):
 +
            ##on convertit l'objet selector en xpath
 +
            xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
 +
            ##pour chaque lien trouvé par ce xpath
 +
            for link in tree.xpath(xpath_selector):
 +
                href = link.get('href')
 +
                liens.append(wiki_base+href)
 +
            return liens
 +
 +
    elif type == 'habitants':
 +
        results = []
 +
        for selector in cssselect.parse(css_select):
 +
            ##on convertit l'objet selector en xpath
 +
            xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
 +
            ##pour chaque lien trouvé par ce xpath
 +
            for link in tree.xpath(xpath_selector):
 +
                hab = link.text
 +
                # hab = re.sub(r"\s+", "", hab, flags=re.UNICODE)
 +
                hab = re.sub(r"\s+", "", hab)
 +
                habitants.append(int(hab))
 +
 +
        # for selector in cssselect.parse('table.wikitable.sortable:nth-of-type(1) td:nth-child(1)'):
 +
        for selector in cssselect.parse('table.wikitable.sortable.titre-en-couleur td:nth-child(1)'):
 +
            ##on convertit l'objet selector en xpath
 +
            xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
 +
            ###pour chaque lien trouvé par ce xpath
 +
            for link in tree.xpath(xpath_selector):
 +
                com = link.text_content().rstrip()
 +
                ###suppriemer les parenthèses si besoin
 +
                com = re.sub(r"\(.+\)", "", com)
 +
                commune.append(com)
 +
            return habitants, commune
 +
</syntaxhighlight>
 +
 +
====Debugging====
 +
 +
Permet de tester à quelle(s) ligne(s) ça bugge :
 +
 +
<syntaxhighlight lang="python">
 +
for i in liens_dpt:
 +
## choper les noms des communes et le nombre d'habitants de chaque département
 +
    try:
 +
        alors = getLiens(i, 'td:nth-child(8)', 'habitants')
 +
 +
###problème de longueur des arrays : il prend en compte les mauvais tableaux pour :nth-child(1)
 +
        if len(alors[0]) == len(alors[1]):
 +
            print('yes')
 +
        else:
 +
            print('%d oh non' % count)
 +
    except:
 +
        print('%d bug de liens' % count)
 +
        coucount.append(count)
 +
 +
    count += 1
 +
</syntaxhighlight>
 +
 +
 +
Permet de tester la ligne (cette page département) en particulier pour trouver qu'est-ce que le script a mal sélectionné :
 +
 +
<syntaxhighlight lang="python">
 +
alors = getLiens(liens_dpt[56], 'td:nth-child(8)', 'habitants')
 +
 +
for i in alors[1]:
 +
    print(i)
 +
print('len_arr =  %s' % len(alors[1]))
 +
</syntaxhighlight>
 +
 +
Ça m'a permis de trouver que pour certaines pages les tableaux ne sont pas dans le même ordre, et n'ont pas la même classe. Il doit y avoir 6 pages pour lesquelles je dois faire des conditions et changer le sélecteur précisément pour chaque page (un plaisir).
 +
 +
J'ai réussi à corriger le script pour rajouter des exceptions à chaque page qui posait problème.
 +
 +
<syntaxhighlight lang="python">
 +
    if dpt == 44:
 +
            css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(8)'
 +
        elif dpt == 48:
 +
            css_select = 'table.wikitable.sortable:nth-of-type(2) td:nth-child(8)'
 +
        elif dpt == 70:
 +
            css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(5)'
 +
        elif dpt == 95:
 +
            css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(8)'
 +
        elif dpt == 103:
 +
                css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(5)'
 +
 +
 +
        if dpt == 56:
 +
            css_select_2 = 'table.wikitable.sortable td:nth-child(1)'
 +
        else:
 +
            css_select_2 = 'table.wikitable.sortable.titre-en-couleur td:nth-child(1)'
 +
 +
</syntaxhighlight>
 +
 +
Maintenant il fait que je supprime du tableau les villes de plus de 2000 habitants (c'est la limite entre village et ville [https://www.lemonde.fr/blog/correcteurs/2012/10/12/entre-ville-et-village-ou-passe-la-frontiere/]). </br>
 +
Pour ça je vais créer un fichier texte avec le nom de toutes les communes dépassant le seuil de 2000 habitants.
 +
Apparemment il n'y a que 5413 communes en France qui ont plus de 2000 habitants. Sur 36 000, il y a donc une grande majorité de villages.
 +
Ensuite je n'aurais plus qu'à supprimer dans le tableau les lignes correspondant à ces communes.
 +
 +
====Supression====
 +
 +
Il y a certaines lignes qui n'ont pas les données de latitude/longitude dans le tableau, ou qui n'ont pas de valeur pour la longitude ("-") : on les supprime aussi.
 +
 +
<syntaxhighlight lang="python">
 +
#!/usr/bin/env python3
 +
#coding: utf-8
 +
 +
import csv
 +
 +
###fonction pour supprimer les lignes qu'on veut pas
 +
def supp(arr):
 +
    for didi in d:
 +
        for r in range(len(arr)):
 +
            ###petite astuce : puisque pop() enlève une valeur il change l'index de toutes les autres. Il faut donc compenser en faisant -r, pour retrouver la bonne valeur
 +
            d[didi].pop(arr[r]-r)
 +
 +
d = {}
 +
d['index_commune'] = []
 +
d['nom_commune'] = []
 +
d['latitude'] = []
 +
d['longitude'] = []
 +
 +
rows = []
 +
 +
##########################LIRE LE CSV##########################
 +
with open('tableau_simplifié.csv', 'r+') as csvfile:
 +
    reader = csv.DictReader(csvfile)
 +
    for row in reader:
 +
        for i in range (0,4):
 +
            valeur = list(row.items())[i][0]
 +
            d[valeur].append(row[valeur])
 +
 +
##########################ENLEVER LES LIGNES VIDES (SANS LONG/LAT)###########################
 +
for i in range(len(d['longitude'])):
 +
    if d['longitude'][i] == "" or d['longitude'][i] == "-":
 +
        # print('vide')
 +
        rows.append(i)
 +
 +
###il y a 2875 communes pour lesquelles on a pas la valeur de la latitude/longitude : on les supprime du csv
 +
supp(rows)
 +
 +
##########################ENLEVER LES COMMUNES DE + DE 2000 HAB###########################
 +
with open('villes1999', 'r+', encoding='utf8') as g:
 +
    file = g.read()
 +
    villes = file.splitlines()
 +
 +
liste_villes = []
 +
 +
for r in range(len(d['nom_commune'])):
 +
    # print(d['nom_commune'][r])
 +
    if d['nom_commune'][r] in villes:
 +
        liste_villes.append(r)
 +
###on supprime aussi les 5000 et quelques villes
 +
supp(liste_villes)
 +
 +
</syntaxhighlight>
 +
 +
Maintenant il faut recréer un nouveau fichier avec les valeurs du dictionnaire, pour éviter que cette étape de suppression se fasse à chaque fois. Le plus simple est alors du json, parce que les opérations d'encodage/décodage en python sur du json marchent bien et sont efficaces.
 +
 +
====Encodage/décodage====
 +
 +
Le code pour convertir le dictionnaire "d" en un fichier json (le mode 'w' pour write -> écriture) :
 +
 +
<syntaxhighlight lang="python">
 +
with open('result.json', 'w', encoding="utf8") as fp:
 +
    json.dump(d, fp)
 +
</syntaxhighlight>
 +
(s'éxécute une seule fois, lors de la création du fichier result.json)
 +
 +
et le code pour le décoder (le mode 'r' pour read -> lecture) :
 +
 +
<syntaxhighlight lang="python">
 +
with open('result.json', 'r', encoding="utf8") as f:
 +
    data = f.read()
 +
    d = json.loads(data)
 +
</syntaxhighlight>
 +
(s'exécute chaque lancement de programme, pour récupérer la "base de données")
 +
 +
Ne pas oublier le encoding='utf8', pour ne pas avoir des noms de communes qui font bugger les accents !
 +
 +
===Trouver les communes dans un rayon donné===
 +
 +
J'ai réussi à faire une fonction qui vérifie si il y a des communes dans le rayon en latitude, mais maintenant il faut que je trouve comment garder en mémoire les index pour checker seulement sur ceux-ci la longitude.
 +
Pour ça j'utilise des tuples qui me permettent de garder en mémoire l'index des communes correspondant pour la latitude, pour pouvoir les tester sur la longitude.
 +
 +
Le point de coordonnées lat_base et long_base change chaque jour.
 +
 +
La variable km_lat est une approximation en degrés de la longueur d'1 km. Comme ça on peut facilement changer le rayon r en un nombre de kilomètres différent (ici c'est 25km pour l'instant).
 +
 +
<syntaxhighlight lang="python">
 +
###fonction qui checke si la latitude est comprise dans le rayon voulu
 +
def check_lat(val):
 +
    if lat_base - r <= val <= lat_base + r:
 +
        return True
 +
    return False
 +
 +
###fonction qui checke si la longitude est comprise dans le rayon voulu
 +
def check_long(val):
 +
    if long_base-r <= val <= long_base+r:
 +
        return True
 +
    return False
 +
 +
lat_base = float('48.632954')
 +
long_base = float('-2.874094')
 +
#1 km +- = à km_lat degrés
 +
km_lat = 0.0090437173295
 +
#r correspond environ à un rayon de 25 km
 +
r = 25*km_lat
 +
 +
foirages = 0
 +
 +
# print(lat_base)
 +
# print(long_base)
 +
compteur = 0
 +
 +
###on crée 2 listes pour ranger les valeurs correspondant au rayon souhaité
 +
lat_good = []
 +
long_good = []
 +
 +
###affiche seulement les communes qui correspondent et leur nom
 +
for index in range(len(d['latitude'])):
 +
    try:
 +
        if check_lat(float(d['latitude'][index])):
 +
            # print(d['nom_commune'][index])
 +
            tuplo = (index, d['nom_commune'][index])
 +
            lat_good.append(tuplo)
 +
    except:
 +
        foirages += 1
 +
        print('oupsi')
 +
 +
for i in lat_good:
 +
    try:
 +
        if check_long(float(d['longitude'][i[0]])):
 +
            long_good.append(i)
 +
            print(i[1])
 +
            compteur += 1
 +
    except:
 +
        print('%s oupso -> %s' % (i[0], d['longitude'][i[0]]))
 +
        foirages +=1
 +
 +
##affiche le nombre de communes matchant avec la requête
 +
print('\n%s communes correspondantes' % compteur)
 +
print('foirages-> %s' % foirages)
 +
 +
</syntaxhighlight>
 +
 +
Ce bout de code affiche le nom des communes qui correspondent au rayon donné (et les stocke en même temps dans une liste).
 +
 +
Ensuite il faut juste que je choisisse une commune au hasard.
 +
 +
<syntaxhighlight lang="python">
 +
###fonction pour choisir au hasard un élément de la liste
 +
import random
 +
print(random.choice(long_good)[1])
 +
</syntaxhighlight>
 +
 +
===Ne pas visiter deux fois la même commune===
 +
 +
J'essaie de faire un log qui enregistre les communes déjà visitées, et qu'à chaque lancement de programme il prenne le dernier élément ajouté dans le log et qu'il récupère ses coordonnées dans les variables lot_base et long_base:
 +
 +
<syntaxhighlight lang="python">
 +
with open('communes_visitées', 'r', encoding='utf8') as f:
 +
    comm_visit = f.read()
 +
    comm_visit = comm_visit.splitlines()
 +
    ###pour avoir le dernier élément de la liste
 +
    comm_visit.reverse()
 +
 +
index_dernière_commune = d['nom_commune'].index(comm_visit[0])
 +
 +
###remplace la latitude et la longitude de départ avec les valeurs de la commune précédente
 +
lat_base = float(d['latitude'][index_dernière_commune])
 +
long_base = float(d['longitude'][index_dernière_commune])
 +
</syntaxhighlight>
 +
 +
 +
Et la deuxième partie:
 +
<syntaxhighlight lang="python">
 +
compte = 0
 +
 +
###supprime les valeurs de long_good qui sont déjà dans le log (déjà visitées)
 +
for index, village in long_good:
 +
    if village in comm_visit:
 +
        del long_good[compte]
 +
    compte += 1
 +
 +
for i, k in long_good:
 +
    print('long_good = %s' % k)
 +
 +
for j in comm_visit:
 +
    print('comm_visit = %s' % j)
 +
 +
### ajoute la commune visitée dans un fichier
 +
with open('communes_visitées', 'a', encoding='utf8') as f:
 +
    commune_selec = random.choice(long_good)[1]
 +
    f.write(commune_selec+'\n')
 +
    print('commune_selec = %s' % commune_selec)
 +
</syntaxhighlight>
 +
 +
C'est assez étrange puisque ça à l'air de fonctionner (certaines communes qui sont déjà visitées sont bien supprimées), mais de temps en temps pourtant le programme choisit une commune déjà visitée. Je n'arrive pas à débugger pour l'instant.
 +
 +
Version corrigée :
 +
<syntaxhighlight lang="python">
 +
###checker si le village est déjà dans la liste des communes visitées
 +
compte = 0
 +
long_goodok = []
 +
 +
for index, village in long_good:
 +
    # print("checking if %s is in comm_visit" % (village))
 +
    if village not in comm_visit:
 +
        # print("not in comm_visit. valid.")
 +
        long_goodok.append(long_good[compte])
 +
    else:
 +
        print("commune supprimée")
 +
    compte += 1
 +
 +
# print(long_goodok)
 +
long_good = long_goodok
 +
 +
## ajouter la commune visitée dans un fichier
 +
with open('communes_visitées', 'a', encoding='utf8') as f:
 +
    try:
 +
        commune_selec = random.choice(long_good)[1]
 +
        f.write(commune_selec+'\n')
 +
        print('commune_selec = %s' % commune_selec)
 +
    except:
 +
        print('Pas de village dans le rayon. :(\nRéesayer avec un plus grand rayon ?')
 +
</syntaxhighlight>
 +
 +
En fait la boucle regardait un élément après l'autre, et quand la commune était déjà visitée supprimait l'élément qu'on regardait. L'élément suivant se décalait vers la gauche (son index qui était par exemple 3 devenait 2) et au prochain tour de boucle on le passait sans le prendre en compte. C'est pour ça que certaines communes passaient dans les mailles du filet.
 +
 +
===Enjoliver le post===
 +
====Télécharger une image de la commune====
 +
J'ai essayé d'utiliser une librairie qui s'appelle google_images_download pour télécharger le premier résultat de Google Images, mais ça ne fonctionne pas :
 +
 +
<syntaxhighlight lang="python">
 +
###(code trouvé dans la doc de la librairie)
 +
#instantiate the class
 +
response = google_images_download.googleimagesdownload()
 +
arguments = {"keywords":"tressignaux","limit":1,"print_urls":True, "language":"French"}
 +
paths = response.download(arguments)
 +
#print complete paths to the downloaded images
 +
print(paths)
 +
</syntaxhighlight>
 +
 +
Et le résultat:
 +
<syntaxhighlight lang="python">
 +
Item no.: 1 --> Item name = tressignaux
 +
Evaluating...
 +
Starting Download...
 +
 +
Errors: 0
 +
 +
 +
({'tressignaux': []}, 0)
 +
</syntaxhighlight>
 +
La liste des résultats est vide, même avec des mot en anglais comme dans la doc ça ne donne rien. Peut-être que la librairie n'est peut-être plus mise à jour ?
 +
 +
J'essaie avec mechanize de récupérer les images de Google Image, mais les images en bonne qualité ne sont pas disponibles dans des balises html. Elles sont cachées en bas du code source, alors j'ai essayé avec une Regex de la récupérer.
 +
<syntaxhighlight lang="python">
 +
#!/usr/bin/env python3
 +
#coding: utf-8
 +
 +
###############################SCRAPING WIKIPEDIA####################
 +
import lxml.html as parser
 +
import cssselect, mechanize, re
 +
import urllib.parse as urlP
 +
 +
###avec mechanize
 +
br = mechanize.Browser()
 +
# br.addheaders = [('User-agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0')]
 +
br.addheaders = [('User-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36')]
 +
br.set_handle_robots(False)
 +
 +
commune_selec = 'Ploëzec'.lower()
 +
commune_selec = commune_selec.encode('utf-8')
 +
 +
url = 'https://www.google.com/search?hl=en&tbm=isch&q=' + urlP.quote(commune_selec) + '&source=lnms'
 +
 +
print(url)
 +
data = br.open(url, timeout=50)
 +
source = data.read()
 +
 +
# with open('g_img_source.txt', 'w', encoding='utf8') as f:
 +
#    f.write(str(data))
 +
 +
###regex pour choper l'URL de l'image dans le code source, parce qu'il est pas dans une balise donc on peut pas le trouver avec css_select
 +
img = re.search(r',\["(.+?\.jpg)",\d+?,\d+?\]', str(data))
 +
###afficher le premier lien d'image trouvé
 +
 +
print(img[1])
 +
</syntaxhighlight>
 +
 +
Le problème est que la Regex fonctionne bien quand je la teste dans Atom, mais avec Python ça ne fonctionne pas.
 +
Pour l'instant je ne trouve pas de solution.
 +
 +
En remplaçant
 +
<syntaxhighlight lang="python">
 +
img = re.search(r',\["(.+?\.jpg)",\d+?,\d+?\]', str(data))
 +
</syntaxhighlight>
 +
par
 +
<syntaxhighlight lang="python">
 +
###(code de la page de Brigitte Coric, en BAC2 (sur ce site))
 +
img = re.findall(r'\["([^"]+\.jpg)",[0-9]+,[0-9]+\]', str(source))
 +
</syntaxhighlight>
 +
 +
ça marche, j'arrive à récupérer la première image de Google Image.
 +
 +
Maintenant, il faut savoir si je peux poster une image sur Twitter à partir de son URL, ou bien si je dois la télécharger avant.
 +
J'ai trouvé [https://iq.opengenus.org/post-image-twitter-api/ ce site] qui explique comment uploader une image sur le serveur de Twitter mais ça me semble plus simple de télécharger et remplacer chaque jour l'image sur le serveur.
 +
 +
Avec mechanize c'est simple de télécharger une image :
 +
<syntaxhighlight lang="python">
 +
###téléchargement de la première image
 +
###si la première ne marche pas on essaie la suivante jusqu'à ce que ça marche
 +
n = 0
 +
while True:
 +
    try:
 +
        br.retrieve(img[n], 'balade_dépendences/image.jpg')
 +
        break
 +
    except:
 +
        n += 1
 +
</syntaxhighlight>
 +
Ça fonctionne bien, mais Twitter n'accepte pas les images de plus de 3 Mo. Il faut que je rajoute une condition pour ne pas télécharger les images trop grandes, ou bien pour réduire la taille des images téléchargées.
 +
<syntaxhighlight lang="text">
 +
Traceback (most recent call last):
 +
  File "balade_campagnarde.py", line 201, in <module>
 +
    api.update_with_media(photo_path, status=phrase+'\n'+'Arrivée dans %s' % temps_trajet)
 +
  File "/home/tumtum/.local/lib/python3.7/site-packages/tweepy/api.py", line 218, in update_with_media
 +
    headers, post_data = API._pack_image(filename, 3072, form_field='media[]', f=f)
 +
  File "/home/tumtum/.local/lib/python3.7/site-packages/tweepy/api.py", line 1303, in _pack_image
 +
    raise TweepError('File is too big, must be less than %skb.' % max_size)
 +
tweepy.error.TweepError: File is too big, must be less than 3072kb.
 +
</syntaxhighlight>
 +
 +
J'ai trouvé un script qui permet de récupérer la taille de l'image en octets avant même de la télécharger.
 +
Si l'image est trop grande, on télécharge la suivante dans la liste.
 +
 +
La fonction:
 +
<syntaxhighlight lang="python">
 +
import urllib
 +
from PIL import ImageFile
 +
 +
def getsizes(uri):
 +
    # get file size *and* image size (None if not known)
 +
    file = urllib.request.urlopen(uri)
 +
    size = file.headers.get("content-length")
 +
    if size:
 +
        size = int(size)
 +
    p = ImageFile.Parser()
 +
    while True:
 +
        data = file.read(1024)
 +
        if not data:
 +
            break
 +
        p.feed(data)
 +
        if p.image:
 +
            return size, p.image.size
 +
            break
 +
    file.close()
 +
    return(size, None)
 +
 +
</syntaxhighlight>
 +
Et le code qui l'intègre :
 +
<syntaxhighlight lang="python">
 +
##téléchargement de la première image
 +
##si la première ne marche pas on essaie la suivante jusqu'à ce que ça marche
 +
n = 0
 +
while True:
 +
    try:
 +
        ###pour ne pas avoir d'images trop grandes pour Twitter
 +
        print(n+1)
 +
        size = getsizes(img[n])[0]
 +
        print(size)
 +
        if size >= 3000072:
 +
            print('trop grand')
 +
            n += 1
 +
            continue
 +
 +
        file_img = 'balade_dépendences/image.jpg'
 +
        br.retrieve(img[n], file_img)
 +
        print('image n°%s téléchargée' % (n+1))
 +
        break
 +
    except:
 +
        print('image corrompue')
 +
        n += 1
 +
</syntaxhighlight>
 +
 +
====Rajouter le temps de trajet====
 +
Pour ça je vais utiliser l'API de TomTom, pour calculer le temps entre les coordonnées de départ et celles d'arrivée.
 +
Il faut que j'utilise [https://developer.tomtom.com/routing-api/routing-api-documentation l'API de Routing], qui calcule le trajet entre deux points (et donc aussi le temps de trajet). en plus je peux calculer ce temps pour un piéton, ce qui est ce que je recherche. Le résultat est un fichier json.
 +
 +
[http://curlybraces.be/wiki/Utilisateur:LenaS#code_BOT_: Léna] utilise déjà l'API de TomTom pour son bot, je reprends son code pour comprendre comment fonctionne la requête à l'API :
 +
<syntaxhighlight lang="python">
 +
import json
 +
import datetime
 +
from urllib.request import urlopen
 +
 +
api_key = 'GYNAQ6sx8c98oqXqGGOmvqvgOYn0FARQ'
 +
 +
url = 'https://api.tomtom.com/routing/1/calculateRoute/%d%2C%d%3A%d%2C%d/json?avoid=unpavedRoads&travelMode=pedestrian&key=%s' % (lat_base, long_base, lat_fin, long_fin, api_key)
 +
</syntaxhighlight>
 +
Je remplace l'url avec ce que me donne [https://developer.tomtom.com/content/routing-api-explorer#/Routing/get_routing__versionNumber__calculateRoute__locations___contentType_ l'exemple d'utilisation de l'API], et je met des placeholders pour pouvoir utiliser les variables de latitude et de longitude de mes points de départ et d'arrivée.
 +
 +
Le placeholder %d est uti lisé pour les nombres entiers, donc ça bugge avec les latitudes/longitudes qui sont des nombres décimaux.
 +
En plus il y à des % dans l'URL, ce qui doit faire tout planter. Il faut utiliser .format, une autre méthode de placeholder :
 +
<syntaxhighlight lang="python">
 +
tomtom_url = 'https://api.tomtom.com/routing/1/calculateRoute/{0}%2C{1}%3A{2}%2C{3}/json?avoid=unpavedRoads&travelMode=pedestrian&key={4}'.format(lat_base, long_base, lat_fin, long_fin, api_key)
 +
</syntaxhighlight>
 +
 +
Il faut maintenant que je lance la requête HTTP pour récupérer le fichier JSON.
 +
<syntaxhighlight lang="python">
 +
tomtom_url = 'https://api.tomtom.com/routing/1/calculateRoute/{0}%2C{1}%3A{2}%2C{3}/json?avoid=unpavedRoads&travelMode=pedestrian&key={4}'.format(lat_base, long_base, lat_fin, long_fin, api_key)
 +
 +
getData = urlopen(tomtom_url).read()
 +
result = json.loads(getData)
 +
 +
for i in result['routes'][0]['summary']:
 +
    print(i, result['routes'][0]['summary'][i])
 +
</syntaxhighlight>
 +
 +
On obtient ce résultat :
 +
<syntaxhighlight lang="text">
 +
lengthInMeters 13124
 +
travelTimeInSeconds 9449
 +
trafficDelayInSeconds 0
 +
departureTime 2020-04-16T12:54:27+02:00
 +
arrivalTime 2020-04-16T15:31:56+02:00
 +
</syntaxhighlight>
 +
 +
Comme ça je peux afficher la distance et le temps de trajet prévu dans le post Twitter.
 +
 +
Avec le module datetime je peux afficher l'heure de manière plus lisible :
 +
<syntaxhighlight lang="python">
 +
temps_trajet = str(datetime.timedelta(seconds=temps_trajet))
 +
###affiche par exemple 3:25:09
 +
</syntaxhighlight>
 +
 +
====Rajouter des petites phrases pour que ce soit moins froid====
 +
Je fais une liste de tournures de phrases qui contiennent le nom de la commune, du style :
 +
*COMMUNE, me voilà !
 +
*Direction COMMUNE
 +
*C'est parti pour COMMUNE !
 +
*Aujourd'hui je vais à COMMUNE
 +
*Objectif : COMMUNE !
 +
*Cap sur COMMUNE
 +
*Allons à COMMUNE
 +
*En route pour COMMUNE
 +
*Départ pour COMMUNE
 +
*Prochaine étape : COMMUNE
 +
*Et la prochaine commune est COMMUNE
 +
*Maintenant je me dirige vers COMMUNE
 +
*Aujourd'hui j'ai décidé d'aller à COMMUNE
 +
*En déplacement à COMMUNE
 +
*COMMUNE est la prochaine étape sur ma route
 +
*Bonjour COMMUNE !
 +
*Au revoir COMMUNE PRÉCÉDENTE, bonjour COMMUNE !
 +
*COMMUNE en vue !
 +
dans lesquelles le programme choisit au hasard chaque jour.
 +
 +
<syntaxhighlight lang="python">
 +
###on choisit une tournure de phrase au hasard et on inclut le nom de la commune sélectionnée
 +
phr = ["Direction %s","C'est parti pour %s !","Aujourd'hui je vais à %s","Objectif : %s !","Cap sur %s","Allons à %s","En route pour %s","Départ pour %s","Prochaine étape : %s","Et la prochaine commune est %s","Maintenant je me dirige vers %s","Aujourd'hui j'ai décidé d'aller à %s","En déplacement à %s","Bonjour %s !","%s, me voilà !","%s est la prochaine étape sur ma route","%s en vue !"]
 +
phrase = random.choice(phr) % commune_selec.decode('utf-8')
 +
print(phrase)
 +
</syntaxhighlight>
 +
 +
===Poster le tout sur Twitter===
 +
C'est très simple de poster seulement le nom de la commune + une image :
 +
<syntaxhighlight lang="python">
 +
###authentification à l'API de Twitter
 +
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
 +
auth.set_access_token(access_token, access_token_secret)
 +
 +
###post en lui-même, en 2 parties (texte et image)
 +
api = tweepy.API(auth)
 +
photo_path = '/directory/image.jpg'
 +
api.update_with_media(photo_path, status=commune_selec)
 +
</syntaxhighlight>
 +
 +
Pour poster plusieurs lignes, on combine les chaînes de caractères avec un retour chariot entre chaque ligne.
 +
<syntaxhighlight lang="python">
 +
api.update_with_media(photo_path, status=phrase+'\n'+'Arrivée dans %s' % temps_trajet)
 +
print('Post envoyé !')
 +
 +
</syntaxhighlight>
 +
 +
===Régler la fréquence de post du programme===
 +
L'idée est que le bot poste sur Twitter le village où il se rend + une photo + l'heure d'arrivée prévue (il se déplace à pied), puis ne poste le village suivant qu'une fois arrivé.
 +
Le programme va être lancé par cron, toutes les 5 minutes. Il faut donc créer une condition pour qu'il n'exécute tout le code
 +
 +
À la fin du programme, on écrit la date et l'heure d'arrivée dans un fichier texte :
 +
<syntaxhighlight lang="python">
 +
with open('balade_dépendences/h_arriv.txt', 'w', encoding='utf8') as h:
 +
    h.write(heure_arrivée)
 +
</syntaxhighlight>
 +
 +
Au début du programme, on récupère cette heure d'arrivée, et si le moment d'exécution du programme (la variable "maintenant", datetime.datetime.now()) est trop tôt par rapport à l'heure d'arrivée prévue (le bot n'est pas encore arrivé dans le village), alors le programme s'arrête prématurément.
 +
 +
<syntaxhighlight lang="python">
 +
with open('balade_dépendences/h_arriv.txt', 'r', encoding='utf8') as h:
 +
    h_arriv = h.read()
 +
    h_arriv = datetime.datetime.strptime(h_arriv, '%Y-%m-%dT%H:%M:%S%z')
 +
    maintenant = datetime.datetime.now(h_arriv.tzinfo)
 +
 +
    print('Heure d\'arrivée prévue : %s ' % h_arriv)
 +
    print('Heure d\'exécution du programme : %s' % maintenant)
 +
 
 +
    if h_arriv > maintenant:
 +
        print('quitter le programme')
 +
        quit()
 +
</syntaxhighlight>
 +
Il faut utiliser la classe tzinfo pour rajouter cette information dans l'objet datetime.now(), car par défaut cet objet est "naïf" (il ne prend pas en compte les fuseaux horaires).
 +
De cette façon on peut comparer les deux dates.
 +
 +
Output:
 +
<syntaxhighlight lang="text">
 +
Heure d'arrivée prévue : 2020-04-23 15:57:07+02:00
 +
Heure d'exécution du programme : 2020-04-23 12:34:41.312797+02:00
 +
quitter le programme
 +
</syntaxhighlight>
 +
 +
Sinon, la suite du programme s'exécute et on obtient un nouveau post qui définit la prochaine destination, ainsi que le temps de trajet estimé par TomTom.
 +
 +
===Définir un point de départ ?===
 +
Je vais choisir une ville au hasard pour le point de départ du programme.
 +
Pour cela je reprends le tableau avec seulement les villages :
 +
<syntaxhighlight lang="python">
 +
#!/usr/bin/env python3
 +
#coding: utf-8
 +
 +
import random, json
 +
 +
with open('balade_dépendences/result.json', 'r', encoding="utf8") as f:
 +
    data = f.read()
 +
    d = json.loads(data)
 +
 +
print(random.choice(d['nom_commune']))
 +
</syntaxhighlight>
 +
 +
Le résultat est : Griscourt. Ce sera donc la commune de départ.
 +
 +
===Corrections===
 +
====Communes en double====
 +
Après avoir tourné un moment, le bot est bloqué pendant 5 jours : il se dirige vers une commune à plus de 600km !
 +
Apparemment c'est dû à un doublon : il y a 2 communes qui s'appellent Morville, et c'est celle en-dehors du rayon qui a été sélectionné par le programme. Je dois corriger ça dans mon fichier result.json.
 +
 +
[[Fichier:pb_morville.png]]
 +
 +
Dans mon programme qui supprime dans le tableau les communes avec trop d'habitants, ou celles sans information de longitude/latitude, je rajoute ça :
 +
<syntaxhighlight lang="python">
 +
##########################ENLEVER LES COMMUNES EN DOUBLE###########################
 +
###trouvé sur stackoverflow, crée une array de noms de communes en double dans 'dupes'
 +
seen = {}
 +
dupes = []
 +
for x in d['nom_commune']:
 +
    if x not in seen:
 +
        seen[x] = 1
 +
    else:
 +
        if seen[x] == 1:
 +
            dupes.append(x)
 +
        seen[x] += 1
 +
 +
###crée une liste d'indexs à supprimer dans le dictionnaire d
 +
a_sup = []
 +
for doublon in dupes:
 +
    ###ajoute le premier index trouvé à la liste a_sup
 +
    a_sup.append(d['nom_commune'].index(doublon))
 +
 +
###on supprime tout ça
 +
supp(a_sup)
 +
</syntaxhighlight>
 +
Je re-génère un fichier result.json, que je remplace dans le dossier balade_dépendences/ du programme.
 +
 +
===Interactions ?===
 +
Pour l'instant le bot fait sa vie tout seul, il n'as pas d'interaction avec les utilisateurs de Twitter, ce qui est dommage car le bot voyage et devrait donc avoir des relations avec les personnes/villages qu'il rencontre sur sa route. 
 +
 +
À un moment j'avais pensé à twitter un message à la commune quand le bot arrive, pour cela je vais essayer de faire en sorte que le bot trouve le compte Twitter de la ville, s'il y en a un, pour twitter un petit message de "Bien arrivé !"
 +
 +
En cherchant un peu, puisque c'est des petits villages, il y a peu de chance qu'ils aient un compte Twitter officiel (par exemple, je suis tombé sur un architecte, David Morville, en cherchant le Twitter de la commune de Morville).
 +
 +
Je me suis dit que le bot allait répondre au premier tweet qui contient le nom de la commune, en disant "Je suis passé dans ce village !", ou quelque chose dans le style.
 +
 +
En utilisant api.search() pour chercher les tweets parlant de la commune d'Eply, je suis tombé sur ce tweet :
 +
<syntaxhighlight lang="text">
 +
@RTommison @Mufc20Hus @DavidJonesMufc @BRJ259 Great banter, dirdct eply to you Robby
 +
</syntaxhighlight>
 +
 +
Pour éviter ce genre de quiproquo, je vais essayer de chercher par hashtag. Avec #Eply je suis presque sûr de tomber sur un tweet qui parle effectivement de la commune en question.
 +
 +
Le problème c'est que la recherche n'est pas sensible à la casse ni aux accents, donc même en cherchant "Clémery" exemple on peut trouver clemery, qui peut peut-être vouloir dire quelque chose dans une autre langue par exemple.
 +
 +
Apparemment en changeant la requête en '"Éply"' au lieu de 'Éply', on a des résultats plus précis. On peut aussi ajouter un paramètre lang='fr' pour avoir normalement plus de chance d'avoir des textes en français.
 +
 +
Mais finalement, comme pour les images qui peuvent ne pas être vraiment pertinentes par rapport à la commune en question, ces tweets peuvent être un peu à côté de la plaque, ça peut rajouter de l'humour au projet.
 +
 +
Je rajoute un bloc if pour trier les tweets qui ne contiennent pas du tout le mot recherché (il arrive que l'API en trouve):
 +
<syntaxhighlight lang="python">
 +
if commune.lower() in tweetText.lower():
 +
            print(tweetText)
 +
</syntaxhighlight>
 +
 +
Il faut ensuite que je trouve comment répondre au tweet trouvé (s'il y en a un, puisque  d'après [http://docs.tweepy.org/en/latest/api.html la documentation de tweepy] il y a une limite de 7 jours pour la recherche de tweets).
 +
 +
Pour répondre à un tweet il faut utiliser la fonction :
 +
<syntaxhighlight lang="python">
 +
api.update_status('@<username> contenu', tweet_id)
 +
</syntaxhighlight>
 +
 +
J'ai donc fait un bout de code qui me permet de répondre à un message mentionnant la commune en question, en espérant que les gens vont réagir et que le bot va sortir un peu de son isolement.
 +
Je vais peut-être reprendre la liste des communes déjà visitées pour relancer ce programme, histoire d'inclure dans cette extension du projet tout ce qui a déjà été posté.
 +
 +
<syntaxhighlight lang="python">
 +
try:
 +
        for status in tweepy.Cursor(api.search, q=commune, lang='fr', tweet_mode='extended').items(0):
 +
            tweet_id = status.id
 +
            tweet_username = status.user.screen_name
 +
            #si le tweet est retweeté, afficher le texte du retweet
 +
            if hasattr(status, 'retweeted_status'):
 +
                tweetText = status.retweeted_status.full_text
 +
            else:
 +
                tweetText = status.full_text
 +
        if commune.lower() in tweetText.lower():
 +
            tweet = tweetText
 +
            # print(tweet)
 +
        else:
 +
            print('Pas de tweet trouvé avec ce nom de commune, on continue')
 +
    except NameError:
 +
        print('Pas de tweet trouvé avec ce nom de commune, on continue')
 +
 +
phrases = ['Je suis déjà passé par %s !', 'Je suis déjà allé à %s :)', 'Je la trouve sympa la commune de %s.']
 +
 +
api_update_status('@{0} {1}'.format(tweet_username, random.choice(phrases)) % commune, tweet_id)
 +
</syntaxhighlight>
 +
 +
J'ai pour l'instant lancé ce programme manuellement en utilisant la liste des communes déjà visitées, pour que les premières semaines d'activité donnent lieu à quelques interactions.
 +
 +
J'ai rajouté une petite condition pour ne pas que le bot réponde sous ses propres posts :
 +
<syntaxhighlight lang="python">
 +
if tweet_username == 'baladecampagne':
 +
    print('on ne se répond pas à soi enfin')
 +
    break
 +
</syntaxhighlight>
 +
Avec break qui interrompt la boucle si le nom d'utilisateur est celui du bot. Il faut aussi rajouter <code>sleep(30)</code> dans la boucle pour ne pas que Twitter considère cette activité comme du spam par des réponses trop rapprocheées.
 +
 +
Il ne me reste plus qu'à trouver comment l'insérer dans mon programme principal. En effet, la question du timing va se poser, puisque si je fait cette recherche juste après avoir posté le premier message, il y a de grandes chances que le tweet qui contient le nom de la commune soit mon propre tweet.
 +
 +
Pour cela je fais en sorte qu'à chaque fois que le bot poste il envoie aussi un message à une commune qu'il a vu plus tôt (80 communes me semble bien, ça représente entre 6 et 10 jours en moyenne je pense). Ce laps de temps permet que d'autres personnes réelles (et pas le bot) aient twitté quelque chose en rapport avec la commune après le post du bot sur cette commune. Apparemment l'API de recherche de Twitter ne permet pas de trouver les tweets qui sont vieux de plus de 2 semaines, il faut peut-être que je base les posts là-dessus pour éviter de retomber systématiquement sur mes propres posts ?
 +
 +
Apparemment, en prenant la 120e dernière commune de la liste on arrive à dépasser ce délai de deux semaines, et donc le bot ne retombe pas sur ses propres posts.
 +
J'ai aussi augmenté le nombre de résultats de l'API de recherche à 2, comme ça si jamais on tombe sur un tweet non valide il y a une deuxième chance que le l'autre le soit : <code>for status in tweepy.Cursor(api.search, q=ancienne_commune, lang='fr', tweet_mode='extended').items(2):</code>
 +
 +
Je rajoute donc cette option dans le programme principal.
 +
 +
===Carte===
 +
J'ai crée [http://curlybraces.be/erg/2019-2020/bots/balade_carte/FrancePointMap.html une carte de France] qui retrace le parcours du bot (pas mise à jour en temps réel pour l'instant). Les points bleus sont le début du parcours et les points rouges les plus récents.
 +
 +
J'ai utilisé le module folium pour gérer la carte. Avec folium on part d'une carte du monde, mais on peut choisir à quel endroit on centre la carte et à quel niveau de zoom on se trouve au départ. Pour centrer sur la France j'ai utilisé cette ligne :
 +
<syntaxhighlight lang="python">
 +
franceMap = folium.Map(location=[46.9, 2], tiles='Stamen Toner', zoom_start=6)
 +
</syntaxhighlight>
 +
Le paramètre "tiles" permet de définir le style du fond de carte, plusieurs styles préconçus existent avec plus ou moins dé détails.
 +
 +
On peut ensuite rajouter différentes couches d'informations sur ces cartes, j'ai donc ajouté les points correspondant aux villages déjà visités ainsi que les lignes les reliant.
 +
<syntaxhighlight lang="python">
 +
########################## COMMUNES DÉJÀ VISITÉES #######################
 +
with open('communes_visitées', 'r', encoding='utf-8') as f:
 +
    data = f.read()
 +
    communes = data.splitlines()
 +
 +
liste_points = []
 +
###créer une liste de points avec les coordonnées des communes visitées
 +
for commune in communes:
 +
    index = d['nom_commune'].index(commune)
 +
    coordonnées = tuple([float(d['latitude'][index]), float(d['longitude'][index])])
 +
    liste_points.append(coordonnées)
 +
 +
for i, commune in enumerate(communes):
 +
    color = colorFader('blue', 'red', i/len(communes))
 +
    if i != 0:
 +
        ###créer des lignes pour chaque point (chaque village parcouru)
 +
        folium.PolyLine([liste_points[i-1], liste_points[i]], color=color, weight=2.5, opacity=1).add_to(franceMap)
 +
        ###créer des points pour afficher le nom de chaque commune
 +
        folium.CircleMarker((liste_points[i][0], liste_points[i][1]), radius=2, weight=1, color=color, fill_color=color, fill_opacity=1, tooltip=commune).add_to(franceMap)
 +
 +
###départ
 +
folium.CircleMarker((liste_points[0][0], liste_points[0][1]), radius=4, weight=3, color='black', fill_color='blue', fill_opacity=1, tooltip="Griscourt (DÉPART)").add_to(franceMap)
 +
###arrivée
 +
folium.CircleMarker((liste_points[len(liste_points)-1][0], liste_points[len(liste_points)-1][1]), radius=4, weight=3, color='black', fill_color='red', fill_opacity=1, tooltip=commune+' (ARRIVÉE)').add_to(franceMap)
 +
 +
###save the map as an html file
 +
franceMap.save('FrancePointMap.html')
 +
print('C\'est la carte')
 +
<syntaxhighlight>
 +
 +
Ce module nous donne un fichier HTML en sortie, sur lequel se trouve la carte dans laquelle on peut zoomer, se déplacer… En passant la souris sur les points on obtient le nom des communes visitées.
 +
 +
J'ai ajouté grace aux modules numpy et matplotlib une variation de couleurs : bleu pour le départ et rouge pour la position actuelle. Je trouve ça un peu plus clair, mais c'est pas encore super lisible.</br>
 +
Le problème vient aussi des "singularités" qui sont assez régulières quand même, des moments où il y a un bug et où la prochaine commune se trouve à 5 jours de marche au lieu de quelques heures. Il faudrait que j'observe les logs en détail et que j'essaie de reproduire les conditions dans lesquelles le programme a buggé pour résoudre ce problème.
 +
 +
==Liens==
 +
[https://gitlab.com/123450/balade_campagnarde Le GitLab]
 +
 +
[https://twitter.com/baladecampagne Le Twitter]

Version actuelle datée du 23 juin 2020 à 21:51

Poésie binaire

Poème de 6 vers, de 4 pieds chacun (en binaire ASCII):

100110011001011110011010000011001011100100110100111101001101001110111111011101110011000110101000101001100110010101000001000011110000111011101100001111000011001010100010000110111011010100111110000111101101100001110100111001011101110111010001000001100100110111111011101101110110010100011011110101110111011001010100000110110111010011110011111001111010011101111110111001000000111010000110111101011101110010000011001111110010110000111011101100100010000011100101101111110110111000011101110000110111101001110010110000111011101110011110011011011111110010110110111010011110011111010011001010101110

La traduction :

Les éditions

"Le Canapé"

m'avaient donné

une mission :

un grand roman

transformiste

Bonjour Python

Opérations sur les chaînes de caractères

Créer un programme qui agit sur le poème écrit au premier cours.

Je voudrais créer un programme qui transforme les "a" en "aaaaah",

(la référence en vidéo -> [1])

et qui transforme une lettre sur 2 en Capitale, pour donner un style ado en train de muer.


Ou alors un programme qui rajoute "xx" à la fin des mots qui finissent par "a", comme dans "chanmax" et qui rajoute "ss" à la fin des mots qui terminent par "s" comme dans "cooloss". Si le mot modifié se trouve en fin de vers, alors on rajoute une ligne intercalaire qui fait "Cooloss !" ou "Chanmax !" selon la fin du vers précédent (et qui fait "Stylé !" à la fin des vers qui se terminent par "é".

OU ALORS le programme ultime qui rajoute les chanmax, cooloss et stylé et ensuite demande à l'utilisateur s'il.elle veut prendre un lysopaïne, qui transforme les a en aaaah. Si oui, alors exécuter la première partie (changer les a en aaaah et alterner capitales et bas de casse).

Le programme lysopaïne.py :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

print("Tu veux un lysopaine ?")
reponse = raw_input()
reponse = str(reponse.lower())

if reponse == "oui" :
    import re

    f= open("poeme.txt", "r")

    poeme = f.read()

    poeme = poeme.lower()

    #transforme les a en aaaah
    lyso = re.sub("a", "aaaah", poeme)

    # boucle qui imprime 1 caractère sur 2 en capitale
    i = 1
    poeme = ""
    while i < len(lyso):
        poeme = poeme + lyso[i-1].upper() + lyso[i].lower()
        i += 2

    print(poeme)
    f.close()

else:
    print("Ben tant pis pour toi.")
Tu veux un lysopaine ?
oui
LeS EdItIoNs
"Le cAaAaHnAaAaHpE"
m'aAaAhVaAaAhIeNt dOnNe
uNe mIsSiOn :
uN GrAaAaHnD RoMaAaAhN
TrAaAaHnSfOrMiStE.
cEsT VrAaAaHi oU FaAaAhUx
uN Ou zErO
CeSt nOiR Ou bLaAaAhNc
lAaAaHrMe aAaAhMmE BiEn
lE CoDe bInAaAaHiRe
tOuTs eSt tReS ClAaAaHiRe
l oRdInAaAaHtEuR
CiRcUiTs iMmEnSeS
ZeRoS Et uNs
rYtHmEnT Le cOd(e).
rEpEtItIoN,
zErOs eT UnS
SaAaAhUt aAaAh lAaAaH LiGnE,
c'eSt uN SeCrEt.
ÉCrIrE à l'oRdInAaAaHtEuR,
oU ├á lAaAaH MaAaAhIn ?
├ëcRiRe sUr lE PaAaAhSs├®,
Ou sUr dEmAaAaHiN ?
ÉCrIrE DeS MoTs,
Ou uN Po├¿Me ?
ÉcRiRe sUr l'aAaAhMoUr,
Ou sUr lAaAaH HaAaAhInE ?
PlUs dE PoInTs
dÔÇÖiNtErRoGaAaAhTiOn,
├®CrIvEz jUsTe
uNe aAaAhSsErTiOn !

Le programme de Violette (loup-garou.py):

#!/usr/bin/env python
#coding: utf-8
'''Un programme qui
- demande le nom de l'utilisateur qui sera enregistré comme nom de poète
- rajoute aouuuu quand il y a a lettre a
- rajoute grrrrrrrrrr quand il y a la lettre g '''

print("Quel est ton nom, poète.esse ?")
nom = raw_input()

import re

f= open("poeme.txt", "r+")

poeme = f.read()

#poeme = re.sub('a', "aouuuu", poeme)
poeme = re.sub('([aA])', r"\1ouuuu", poeme)
poeme = re.sub('([gG])', r"\1rrrrrrrrrr", poeme)

nom = re.sub('([aA])', r"\1ouuuu", nom)
nom = re.sub('([gG])', r"\1rrrrrrrrrr", nom)
print(poeme)
print("Signé, " + nom + " le loup grrrrrrrrrraouuuurou")

f.close()
Quel est ton nom, po├¿te.esse ?
Gaspard
Les editions
"Le Caouuuunaouuuupe"
m'aouuuuvaouuuuient donne
une mission :
un grrrrrrrrrrraouuuund romaouuuun
traouuuunsformiste.
Cest vraouuuui ou faouuuuux
un ou zero
Cest noir ou blaouuuunc
laouuuurme aouuuumme bien
le code binaouuuuire
touts est tres claouuuuire
L ORDINAouuuuTEUR
Circuits Immenses
Zeros et uns
Rythment le cod(e).
Repetition,
Zeros et uns
Saouuuuut aouuuu laouuuu ligrrrrrrrrrrne,
C'est un secret.
Écrire à l'ordinaouuuuteur,
ou ├á laouuuu maouuuuin ?
├ëcrire sur le paouuuuss├®,
ou sur demaouuuuin ?
Écrire des mots,
ou un po├¿me ?
Écrire sur l'aouuuumour,
ou sur laouuuu haouuuuine ?
Plus de points
dÔÇÖinterrogrrrrrrrrrraouuuution,
├®crivez juste
une aouuuussertion !
Sign├®, Grrrrrrrrrraouuuuspaouuuurd le loup grrrrrrrrrraouuuurou

Le programme de Stijn (pluie.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''demander est ce qu’il pleut? oui: changer tous les a e i o u -> ! non: print chance '''

print("Est-ce qu'il pleut ?")
print("(oui/non)")
pluie = str(raw_input())
pluie = pluie.lower()

import re

f= open("poeme.txt", "r+")
poeme = f.read()

if pluie == "oui":
    oui = re.sub('a|e|i|o|u', "!", poeme, flags=re.IGNORECASE)
    print(oui)
    print("Quel temps pourri...")
else:
    print("Chouette alors !")

f.close()
Est-ce qu'il pleut ?
(oui/non)
oui
L!s !d!t!!ns
"L! C!n!p!"
m'!v!!!nt d!nn!
!n! m!ss!!n :
!n gr!nd r!m!n
tr!nsf!rm!st!.
C!st vr!! !! f!!x
!n !! z!r!
C!st n!!r !! bl!nc
l!rm! !mm! b!!n
l! c!d! b!n!!r!
t!!ts !st tr!s cl!!r!
L !RD!N!T!!R
C!rc!!ts !mm!ns!s
Z!r!s !t !ns
Rythm!nt l! c!d(!).
R!p!t!t!!n,
Z!r!s !t !ns
S!!t ! l! l!gn!,
C'!st !n s!cr!t.
Écr!r! à l'!rd!n!t!!r,
!! ├á l! m!!n ?
├ëcr!r! s!r l! p!ss├®,
!! s!r d!m!!n ?
Écr!r! d!s m!ts,
!! !n p!├¿m! ?
Écr!r! s!r l'!m!!r,
!! s!r l! h!!n! ?
Pl!s d! p!!nts
dÔÇÖ!nt!rr!g!t!!n,
├®cr!v!z j!st!
!n! !ss!rt!!n !
Quel temps pourri...

Le programme de Léna (inverse.py)

#!/usr/bin/env python
#coding: utf-8

'''inverser 1er et dernier mot de chaque vers + inverser la première et la dernière lettre du premier mot'''

#ouvrir le fichier
f = open("poeme.txt", "r")

poeme = f.read()

#décomposer le tableau en lignes
lines = poeme.splitlines()
for line in lines:
    mots = line.split(" ")

#inverser la dernière lettre et la première lettre du dernier mot (qui deviendra le premier ensuite)
    last_mot = mots[len(mots)-1]
    last_lettre = last_mot[0]
    last_mot = last_mot.replace(last_mot[0], last_mot[len(last_mot)-1], 1)
    #petite technique : utiliser [:x] pour afficher toute la chaîne/le tableau jusqu'à l'indice donné
    #fonctionne aussi avec [x:] pour afficher tout après l'indice donné
    last_mot = last_mot[:len(last_mot)-1] + last_lettre

#inverser le premier et le dernier mot de chaque ligne
    momo = mots[0]
    mots[0] = last_mot
    mots[len(mots)-1] = momo

#petite correction pour que les lignes de 1 mot soient modifiées (sinon, vu que le programme remplace le dernier mot par le premier il annule le changement de lettres effectué avant et seulement sur le dernier mot de chauqe ligne)
    if len(mots)-1 == 0:
        mots[len(mots)-1] = last_mot

    new_line = str()

#reconstituer la ligne youpi
    for mot in mots:
        new_line = new_line + " " + mot

    print(new_line)

f.close()
 sditione Les
 "anapeC "Le
 eonnd m'avaient
 : mission une
 nomar grand un
 .ransformistet
 xauf vrai ou Cest
 oerz ou un
 clanb noir ou Cest
 nieb amme larme
 einairb code le
 elairc est tres touts
 RRDINATEUO L
 smmenseI Circuits
 snu et Zeros
 .od(e)c le Rythment
 ,epetitionR
 snu et Zeros
 ,ignel a la Saut
 .ecrets un C'est
 ,'ordinateurl à Écrire
 ? à la main ou
 ,ass├®p sur le ├ëcrire
 ? sur demain ou
 ,otsm des Écrire
 ? un poème ou
 ,'amourl sur Écrire
 ? sur la haine ou
 sointp de Plus
 ,ÔÇÖinterrogationd
 eustj ├®crivez
 ! assertion une

Dernier énoncé

Le programme ci-dessous marche

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''Le programme demande à l'utilisateur le nombre de vers à generer.
Le programme genère autant de vers aleatoires que le nombre entre par l'utilisateur, à partir des mots du poème source et en utilisant la formule syntaxique "article + nom + complement + verbe"
BONUS
Une fois que ça marche deux ameliorations possibles:
* accorder en genre et en nombre les phrases (ortographe inclusive ou non)
* avoir plusieurs formules syntaxiques et les choisir aleatoirement (ou selon une certaine logique) à chaque vers
'''
article = ['les', 'le', 'la', 'un', 'une', 'des', 'de', 'du', 'l\'', 'd\'']
nom = ['ordinateur', 'editions', 'canape', 'mission', 'zero', 'repetition', 'circuits', 'tout', 'code', 'larme' 'saut', 'ligne', 'secret', 'main', 'passe', 'demain', 'mots', 'poeme', 'amour', 'haine', 'interrogation', 'points', 'plus', 'assertion']
complement = ['grand', 'transformiste', 'vrai', 'juste', 'faux', 'noir', 'blanc', 'immenses', 'binaire', 'clair', 'bien']
verbe = ['donne', 'est', 'm\'avaient', 'rythment', 'ecrire', 'ecrivez']
ponctuation = ['…', '!', '?', '.', ',', ':']

import random
import re
from time import sleep
print("Combien j't'en sers ?")
nbre = raw_input()
nbre = int(nbre)

with open('poeme.txt', 'r') as teteLecture:
    poeme = teteLecture.read()

for i in range(0, nbre):
    vers = print(random.choice(article) + " " + random.choice(complement) + " " + random.choice(nom) + " " + random.choice(verbe) + " " + random.choice(ponctuation))

    pt_s_espace = ['.', ',', '…']

    if ponctuation == '.'|','|'…':
        vers = re.sub(' (.|,|…)', r'\1', vers)

    #sleep(0.1)
teteLecture.close()

Le programme marche à peu près en l'état, mais il faut corriger les espaces avant les points et les virgules ainsi que les articles avec apostrophes qui se mettent devant les mots qui commencent par une consonne. Pour régler l'accord en nombre on peut mettre tous les mots au singulier et n'accorder que quand les pronoms pluriels (les, des) sont choisis au hasard.

Combien j't'en sers ?
19
la immenses secret ecrivez !
un noir haine ecrivez ,
une vrai larmesaut ecrivez 
la transformiste zero ecrire !
les transformiste passe ecrivez !
une faux code donne ?
l' transformiste interrogation rythment ?
de clair amour donne 
de blanc plus ecrivez .
le grand amour ecrivez !
l' binaire ordinateur m'avaient ,
les vrai tout est 
du grand canape donne ,
l' binaire ligne donne ?
les clair mots est 
les clair plus m'avaient :
un faux code ecrire !
les grand haine m'avaient ?
un vrai code ecrire !

Version plus améliorée du programme :

(exemples ci-dessous)

Transformiste poeme noir
L'immense plus
Le secret ecrit
Les binaires demains rythment
Les mains,
Une haine binaire et une juste haine
L'immense interrogation
Claire repetition juste
Une repetition transformiste :
Des mains, les haines, un plus !
Les secrets blancs et le faux demain
Les missions blanches,
L'assertion vraie ?
Claire edition transformiste
Un saut juste
Claire mission claire
L'ordinateur juste ou une noire assertion
Blancs ordinateurs clairs
Une grande mission ecrit !
Une noire assertion ecrit,
Les points immenses et blancs
Le poeme ecrit
Noir passe faux
L'edition.
La vraie edition m'avait donne ?
Un mot binaire ou immense
Les blancs codes
Vraies interrogations immenses
Une ligne, des demains, les zeros.
Des sauts vrais ou des fausses assertions
Les transformistes codes ecrivent,
Une transformiste edition...
Les fausses assertions sont
Le plus rythme ?
Les assertions claires ou la vraie edition !
La binaire edition donne
Un code, le circuit, les interrogations
Les binaires secrets m'avaient donne...
L'interrogation ?
Justes mots immenses
Le secret grand :
Des points immenses ou les blancs ordinateurs...
Une haine fausse...
Les justes interrogations sont ?
Le juste secret rythme
Les larmes noires ou immenses
Une haine grande ou une fausse larme
Les mains

Le programme amélioré :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''Le programme demande à l'utilisateur le nombre de vers à generer.
Le programme genère autant de vers aleatoires que le nombre entre par l'utilisateur, à partir des mots du poème source et en utilisant la formule syntaxique "article + nom + complement + verbe"
BONUS
Une fois que ça marche deux ameliorations possibles:
* accorder en genre et en nombre les phrases (ortographe inclusive ou non)
* avoir plusieurs formules syntaxiques et les choisir aleatoirement (ou selon une certaine logique) à chaque vers
'''
##définition des listes de mots à utiliser ensuite

#essayer de faire des listes à 2 dimensions (tableaux de tableaux)
singulier = ['le', 'la', 'un', 'une']
pluriel = ['les', 'des']
voyelle = ['l\'', 'd\'']
article = [singulier, singulier, pluriel]
#article = [singulier]

masculin = ['ordinateur', 'canape', 'zero', 'circuit', 'code', 'saut', 'secret', 'passe', 'demain', 'mot', 'poeme', 'amour', 'point', 'plus']
feminin = ['edition', 'mission', 'repetition', 'larme', 'ligne', 'main', 'haine', 'interrogation', 'assertion']
gender = [masculin, feminin]

complement = ['grand', 'transformiste', 'vrai', 'juste', 'faux', 'noir', 'blanc', 'immense', 'binaire', 'clair']
complement_f = ['grande', 'transformiste', 'vraie', 'juste', 'fausse', 'noire', 'blanche', 'immense', 'binaire', 'claire']

verbe = ['donne', 'est', 'm\'avait donne', 'rythme', 'ecrit']
verbe_pl = ['donnent', 'sont', 'm\'avaient donne', 'rythment', 'ecrivent']

ponctuation = [',', ':', '', '', '', '', '', '', '...', '!', '?', '.']
liaison = ['et', 'ou']

autre = ['tout', 'de', 'bien']

import random
import re
from time import sleep

print("Combien j't'en sers ?")
nbre = raw_input()
nbre = int(nbre)

with open('poeme.txt', 'r') as teteLecture:
    poeme = teteLecture.read()

vers = ""
vers_list = []

#générer les composants des vers
for i in range(0, nbre):
    #première fournée
    genre = random.choice(gender)

    if genre == feminin:
        singulier = ['une', 'la']
        s_nom = random.choice(feminin)
        s_complement = random.choice(complement_f)
        s_complementBis = random.choice(complement_f)
        s_article = random.choice(article)

    else:
        singulier = ['le', 'un']
        s_nom = random.choice(masculin)
        s_complement = random.choice(complement)
        s_complementBis = random.choice(complement)
        s_article = random.choice(article)

    if s_article == pluriel:
        s_nom = s_nom +'s'

        if s_complement == 'immense':
            s_articleBis = 'd\''
        else:
            s_articleBis = random.choice(pluriel)

        s_article = random.choice(pluriel)

        s_complement = s_complement + 's'
        s_complementBis = s_complementBis + 's'
        s_verbe = random.choice(verbe_pl)
        s_ponctuation = random.choice(ponctuation)

    else:
        s_verbe = random.choice(verbe)
        if s_complement == 'immense':
            s_articleBis = 'l\''
        else:
            s_articleBis = random.choice(singulier)

        if re.match('^[aeiouy]', s_nom) is not None:
            s_article = 'l\''
        else :
            s_article = random.choice(singulier)

    s_ponctuation = random.choice(ponctuation)

    #deuxième fournée
    genre2 = random.choice(gender)

    if genre2 == feminin:
        singulier = ['une', 'la']
        s_nom2 = random.choice(feminin)
        s_complement2 = random.choice(complement_f)
        s_article2 = random.choice(article)

    else:
        singulier = ['le', 'un']
        s_nom2 = random.choice(masculin)
        s_complement2 = random.choice(complement)
        s_article2 = random.choice(article)

    if s_article2 == pluriel:
        s_nom2 = s_nom2 +'s'
        s_article2 = random.choice(pluriel)
        s_complement2 = s_complement2 + 's'

        if s_complement2 == 'immense':
            s_articleBis2 = 'd\''
        else:
            s_articleBis2 = random.choice(pluriel)
    else:
        if s_complement2 == 'immense':
            s_articleBis2 = 'l\''
        else:
            s_articleBis2 = random.choice(singulier)

        if re.match('^[aeiouy]', s_nom) is not None:
            s_article2 = 'l\''
        else :
            s_article2 = random.choice(singulier)

    #troisième fournée
    genre3 = random.choice(gender)

    if genre3 == feminin:
        singulier = ['une', 'la']
        s_nom3 = random.choice(feminin)
        s_complement3 = random.choice(complement_f)
        s_article3 = random.choice(article)

    else:
        singulier = ['le', 'un']
        s_nom3 = random.choice(masculin)
        s_complement3 = random.choice(complement)
        s_article3 = random.choice(article)

    if s_article3 == pluriel:
        s_nom3 = s_nom3 +'s'
        s_complement3 = s_complement3 + 's'
        s_article3 = random.choice(pluriel)

    else:
        if re.match('^[aeiouy]', s_nom) is not None:
            s_article3 = 'l\''
        else :
            s_article3 = random.choice(singulier)

#composition des vers
    choix = random.randint(1, 3)
    if choix == 1:
        vers = s_articleBis + " " + s_complement + " " + s_nom + s_ponctuation
    else:
        vers = s_articleBis + " " + s_complement + " " + s_nom + " " + s_verbe + s_ponctuation

    if choix == 1:
        vers2 = s_article + " " + s_nom + s_ponctuation
    else:
        vers2 = s_article + " " + s_nom + " " + s_verbe + s_ponctuation

    if choix == 1:
        vers3 = s_article + " " + s_nom + s_ponctuation
    else:
        vers3 = s_article + " " + s_nom + " " + s_complement + s_ponctuation

    vers4 = s_complement + " " + s_nom + " " + s_complementBis
    vers5 = s_article + " " + s_nom + ", " + s_article2 + " " + s_nom2 + ", " + s_article3 + " " + s_nom3 + s_ponctuation
    vers6 = s_article + " " + s_nom + " " + s_complement + " " + random.choice(liaison) + " " + s_articleBis2 + " " + s_complement2 + " " + s_nom2 + s_ponctuation
    vers7 = s_article + " " + s_nom + " " + s_complement + " " + random.choice(liaison) + " " + s_complementBis

    #crée une liste de vers possible et choisit un vers. (il y a plusieurs fois vers pour influencer la probabilité que vers apparaissent plus souvent que les autres)
    vers_list = [vers, vers, vers2, vers3, vers4, vers5, vers6, vers7]
    #vers_list = [vers]

    '''print('s_nom :')
    print(type(s_nom))
    print('s_article :')
    print(type(s_article))
    print('s_complement :')
    print(type(s_complement))
    print('s_nom2 :')
    print(type(s_nom2))
    print('s_article2 :')
    print(type(s_article2))
    print('s_complement2 :')
    print(type(s_complement2))
    print('s_nom3 :')
    print(type(s_nom3))
    print('s_article3 :')
    print(type(s_article3))
    print('s_complement3 :')
    print(type(s_complement3))
    print('s_ponctuation :')
    print(type(s_ponctuation))
    print('s_verbe :')
    print(type(s_verbe))'''

    oui = random.choice(vers_list)

    #met la première lettre du vers en capitale
    oui = oui[0].upper() + oui[1:]
    #met une espace avant les caractères qui en nécessitent une typographiquement
    oui = re.sub(r'(:|\?|!)$', r' \1', oui)
    #supprime les erreurs
    oui = re.sub('fauxs', 'faux', oui)
    oui = re.sub('pluss', 'plus', oui)
    oui = re.sub('(.\') ', r'\1', oui)

    print(oui)
    #sleep(0.1)

#verbes ne se mettent pas au pluriel à la fin de vers
#pas de point sur la dernière ligne

teteLecture.close()

HTML2PRINT

Créer un recueil de poèmes mis en page grâce à HTML/CSS (et possiblement js ?)

Idées

Je pourrais faire un programme qui génère (avec le programme du dernier énoncé aléat.py) à chaque page le nombre de vers correspondant au nombre de pages souhaitées (sur la page 1 il y aurait 1 vers et sur la page 36 il y aurait 36 vers par exemple). Le corps de texte s'adapterait automatiquement pour que tout rentre sur une seule page. On pourrait choisir de générer autant de pages que l'on veut


Writing bot

idées

Créer un bot qui fait un voyage dans la campagne à travers des villes/villages avec des noms rigolos. Tous les jours, il poste le nom d'un village de moins de XXX habitants, avec une photo de ce village. Le village suivant est choisi suivant ces paramètres : - la taille du village (moins de XXX habitants) - la distance (possibilité d'y aller à moins de 8h de marche depuis le village du jour) - direction aléatoire en (évitant peut-être les demi-tours complets ?) - pas 2 fois le même village

Créer un bot qui fait des mots-valises ((dans quel contexte ?))

Créer un bot qui donne des avant-goûts gratuits d'images de sites de stock photos ou vidéos :

  • (pour "pomme" sur Shutterstock) -> Pommes isolées. Fruit de pomme rouge entier avec tranche (coupée) isolé sur blanc avec chemin de détourage
  • (pour "roux" sur Shutterstock) -> Capture d'écran d'un homme caucasien séduisant avec une barbe de gingembre épaisse et une coupe de cheveux branchée, habillé en t-shirt blanc décontracté, regardant directement la caméra, portant des lunettes rondes, isolé sur fond blanc
  • (pour "connexion" sur Shutterstock) -> Deux mains en aidant une autre. Des gens s'aidant les uns les autres.


L'idée serait de poster la description des images de sites de stock photos qui est souvent très détaillée pour avoir plein de mots clés, sans que l'on puisse voir l'image. La description rajoute des interprétations du contexte de l'image, ce qui est intéressant car ces images sont utilisées souvent car elles n'ont pas de contexte et peuvent être accolées à n'importe quoi pour être une simple illustration. Exemple :

(pour "énervé" sur Shutterstock) -> Une jeune femme énervée et furieuse, furieuse et folle de spams, téléphone bloqué, regardant un smartphone isolé sur fond studio vierge, adolescente furieuse ayant des problèmes avec son téléphone portable, irritée par un téléphone portable cassé

Permet de stimuler l'imagination sans débourser un rond.
Le choix du mot pourrait être au hasard, ou bien partir d'un mot existant dans la définition précédente (risque de tourner en rond ?). Il y aurait sûrement besoin d'un peu de NLP (Natural Language Processing) pour ne pas choisir des stopwords.

Pour le choix de l'image dans les résultats de recherche je ne sais pas comment procéder. Sur le site des descriptions en français et anglais cohabitent, mais j'aimerais ne garder que celles en français car elles ont des formulations particulières (peut-être des traductions automatiques ? Mais alors pourquoi certaines restent en anglais ?).

Il y a souvent plusieurs phrases, qui vont du contexte général de ce qui est représenté (une personne fait telle action pour telle raison) à des détails plus techniques (isolé sur fond blanc, détourage, illustration vectorielle...). J'aimerais ne pas garder ces descriptions qui sortent du contexte d'une narration et renvoie vers le type d'image.

Pour le réseau utilisé je ne sais pas lequel serait le plus judicieux. Peut être que la limitation de Twitter en terme de nombre de caractères est intéressante ? Ça peut couper une description à un moment et créer une sorte de cliffhanger ?

Le bot serait une sorte de Père Castor qui raconte des "histoires" sans les images, peut-être que ça peut être à l'heure où on couche les enfants, tous les soirs ?
Ou alors il en poste plusieurs à la suite (5-10 ?) et fait un lien entre tous pour créer une petite histoire du soir ?

En fait c'est pas assez narratif mais c'est rigolo quand même :/.

@baladecampagne

RÉSUMÉ

 4000px Out3.png Out5.png Out7.png Out9.png

Le lien vers le bot

Ce bot se balade dans la campagne française, allant de villages en villages (communes de moins de 2000 habitants). Dans chaque post, le bot annonce le village dans lequel il compte se rendre, accompagné de la distance en km et du temps de trajet estimé, sachant que le bot se balade à pied. Ce temps de trajet correspond au temps qui va s'écouler avant le prochain post, c'est-à-dire le temps que le bot arrive dans le village et choisisse une prochaine destination. Le choix de la commune se fait au hasard parmi les villages se trouvant dans un rayon de plus ou moins 25km autour de l'endroit où se trouve le bot actuellement. La première commune a été choisie au hasard parmi les 27666 communes correspondant aux critères en France, et c'est Griscourt qui sert donc de point de départ.

Les informations concernant les noms des communes et leur position proviennent d'un tableau csv du gouvernement français recensant toutes les communes, et les informations sur le nombre d'habitants viennent de Wikipédia.

Le projet a été réalisé en Python 3.7, avec les modules csv, json, mechanize, lxml, cssselect et tweepy.

Mise au point du fonctionnement

Le site https://territoires-fr.fr/communes-list1.php peut peut-être être utile.

Le programme part d'un endroit sur la carte (chez moi en France par exemple). Avec l'API de TomTom et les filtres on peut créer un cercle dans un rayon de XXX km dans lequel on peut chercher des POI (points of interest)

L'API de TomTom recherche le point d'intérêt "mairie" ou "village" dans un certain rayon, et récupère la ville où elle se trouve. Grâce au site territoires-fr on peut vérifier : que le village est assez petit (pas plus de XXX habitants), car grâce à l'API TomTom on connaît le département et la ville dans laquelle on se trouve.

Le bot Twitter poste le nom de la ville/village en question, puis une photo de l'endroit (possible avec TomTom ? Google Street View ? Sinon avec Google Images).

Si la ville/village en question a un compte Twitter, le bot pourrait poster une photo/retweeter un message de la ville pour dire qu'il est passé par là.


Liens utiles :


J'arrive pas trop à trouver comment trouver des villes sur l'API de TomTom, dans les paramètre de recherche on peut rechercher un point d'intérêt dans un rayon autour du point où on se trouve, mais quand on recherche "ville" ou "mairie" ça marche pas vraiment. C'est plus fait pour rechercher une pizzeria ou une station essence pas loin de nous.

Donc j'ai récupéré un fichier.csv du site data.gouv.fr [2] qui devrait me permettre de créer un rayon juste en utilisant les données géolocalisées.
Par exemple je peut rechercher les villes/villages qui correspondent à une distance de XXX km autour de là où on se trouve en disant de rechercher les correspondances entre les latitudes x+r et x-r et les longitudes y+r et y-r (ou x et y sont les latitudes/longitudes de la position actuelle et r le rayon d'action).

Ensuite on a une liste de matches qui vont dans une liste en python (avec leur code postal), et on peut alors chercher dans les détails de la commune pour avoir le nombre d'habitants :

https://territoires-fr.fr/communes-detail.php?dep=35&com=001&actual=1
Avec dep=(le numéro de département) et com=(le numéro de la commune), on tombe sur la page de la commune avec dans le tableau le nombre d'habitants. L'idée est de supprimer les communes trop grandes (plus de 5000 habitants ?) pour mettre en valeur les campagnes du terroir.

Ou alors on peut aller sur Wikipédia qui a peut-être des infos mises à jour plus régulièrement
(exemple : https://fr.wikipedia.org/wiki/Liste_des_communes_de_l'Ain )
et qui évite de devoir faire 1000 requêtes puisqu'on a direct le nom de la commune + le nombre d'habitants dans le tableau (sachant qu'on a accès au nom des communes et des départements dans le .csv du gouvernement)

Par contre il y a sûrement plusieurs lieux-dits avec le même code postal, ça va être plus long à régler.

En fait le plus simple est de supprimer du csv toutes les villes ayant plus de x habitants, comme ça le programme ne les détectera pas de toute façon dans son rayon.
Pour ça je vais utiliser cette page de Wikipédia [3] qui va me permettre, grâce au numéro des départements, de trouver toutes les pages listant les communes fraçaises par département. Ensuite je n'ai plus qu'à faire un tableau des communes de plus de x habitants et dans mon csv je supprime ces villes.

Peut-être que l'API TomTom peut servir pour le "routing", pour créer le chemin/le temps de trajet d'une commune à l'autre. (qui peut être posté dans le tweet du bot). Ça permettrait même éventuellement de faire une carte qui se remplit au fur et à mesure pour voir le chemin total parcouru

Schéma approximatif des étapes de fonctionnement

SchémaCampagne.png

Préparation du tableau

J'ai retaillé le tableau pour enlever les colonnes qui ne servaient à rien, et on utilisant le module csv j'ai pu transformer mon fichier en dictionnaire (plus ou moins un tableau de tableaux). J'ai donc maintenant 6 tableaux qui me permettent de trouver le nom des communes qui correspondent à une certaine longitude/latitude ou à un département par exemple.

#!/usr/bin/env python3
#coding: utf-8

import csv

d = {}
d['numéro_département'] = []
d['nom_département'] = []
d['nom_commune'] = []
d['codes_postaux'] = []
d['latitude'] = []
d['longitude'] = []

with open('tableau_simplifié.csv', 'r+') as csvfile:
    reader = csv.DictReader(csvfile)
    for row in reader:
        for i in range (0,6):
            valeur = list(row.items())[i][0]
            d[valeur].append(row[valeur])
            print(valeur + ' = ' + row[valeur])
        print('')

Maintenant on passe au scraping du nombre d'habitants sur Wikipédia. Je part de la page https://fr.wikipedia.org/wiki/Listes_des_communes_de_France et j'utilise les librairies mechanize, lxml et cssselector

#!/usr/bin/env python3
#coding: utf-8

###TWITTER API
from accès import *
import tweepy

###TEMPORAIRE
import lxml.html as parser
import cssselect, mechanize

###avec mechanize
br = mechanize.Browser()
br.addheaders = [('User-agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0')]
br.set_handle_robots(False)

data = br.open('https://fr.wikipedia.org/wiki/Listes_des_communes_de_France')

source = data.read()
tree = parser.fromstring(source)
num_dpt = []
lien_dpt = []

for selector in cssselect.parse('table.wikitable:nth-of-type(2) td:nth-child(3)'):
    ##on convertit l'objet selector en xpath
    xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
    ##pour chaque lien trouvé par ce xpath
    for link in tree.xpath(xpath_selector):
        num_dpt.append(link.text_content().rstrip())

for selector in cssselect.parse('td:nth-child(5)'):
    ##on convertit l'objet selector en xpath
    xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
    ##pour chaque lien trouvé par ce xpath
    for link in tree.xpath(xpath_selector):
        lien_dpt.append(link.text_content().rstrip())

for i in range(len(num_dpt)):
    try:
        print('%s => %s' % (num_dpt[i], lien_dpt[i]))
    except:
        print('zut')

Ce bout de code permet de récupérer les 3e et 5e éléments du 2e tableau, c'est-à-dire les numéros de département ainsi que les liens vers les liste des communes par département (à partir de la page Wikipédia citée plus haut).

Mais ça me sert à rien, j'avais juste besoin des liens en fait (5e colonne du 2e tableau).

J'ai réussi à récupérer les liens dans la 5e colonne du 2e tableau de la page "Liste des communes de France" avec la méthode (?) .get('href') qui me permet de récupérer le contenu de l'attribut href des éléments a dans le tableau.

Maintenant pour chacun de ces liens je dois récupérer les valeurs de la colonne "Nom de la commune" et "Nombre d'habitants". Pour cela même principe en changeant les sélecteurs CSS. MAIS subtilités il faut faire attention de sélectionner le bon tableau avec :nth-of-type(), parce que pour certains départements il y a plusieurs tableaux, donc le script peut récupérer les colonnes de tous les tableaux de la page (là c'était un peu relou, il y a plusieurs types de pages donc c'était long de trouver la règle commune qui marche pour tous les départements).

J'ai fait une grosse fonction qui prend plusieurs paramètres et me permet de récupérer soit les liens, soit les habitants/communes :

def getLiens(url, css_select, type):
    data = br.open(url, timeout=50)
    source = data.read()
    tree = parser.fromstring(source)

    liens = []
    habitants = []
    commune = []

    if type == 'liens':
        for selector in cssselect.parse(css_select):
            ##on convertit l'objet selector en xpath
            xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
            ##pour chaque lien trouvé par ce xpath
            for link in tree.xpath(xpath_selector):
                href = link.get('href')
                liens.append(wiki_base+href)
            return liens

    elif type == 'habitants':
        results = []
        for selector in cssselect.parse(css_select):
            ##on convertit l'objet selector en xpath
            xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
            ##pour chaque lien trouvé par ce xpath
            for link in tree.xpath(xpath_selector):
                hab = link.text
                # hab = re.sub(r"\s+", "", hab, flags=re.UNICODE)
                hab = re.sub(r"\s+", "", hab)
                habitants.append(int(hab))

        # for selector in cssselect.parse('table.wikitable.sortable:nth-of-type(1) td:nth-child(1)'):
        for selector in cssselect.parse('table.wikitable.sortable.titre-en-couleur td:nth-child(1)'):
            ##on convertit l'objet selector en xpath
            xpath_selector = cssselect.HTMLTranslator().selector_to_xpath(selector)
            ###pour chaque lien trouvé par ce xpath
            for link in tree.xpath(xpath_selector):
                com = link.text_content().rstrip()
                ###suppriemer les parenthèses si besoin
                com = re.sub(r"\(.+\)", "", com)
                commune.append(com)
            return habitants, commune

Debugging

Permet de tester à quelle(s) ligne(s) ça bugge :

for i in liens_dpt:
## choper les noms des communes et le nombre d'habitants de chaque département
    try:
        alors = getLiens(i, 'td:nth-child(8)', 'habitants')

###problème de longueur des arrays : il prend en compte les mauvais tableaux pour :nth-child(1)
        if len(alors[0]) == len(alors[1]):
            print('yes')
        else:
            print('%d oh non' % count)
    except:
        print('%d bug de liens' % count)
        coucount.append(count)

    count += 1


Permet de tester la ligne (cette page département) en particulier pour trouver qu'est-ce que le script a mal sélectionné :

alors = getLiens(liens_dpt[56], 'td:nth-child(8)', 'habitants')

for i in alors[1]:
    print(i)
print('len_arr =  %s' % len(alors[1]))

Ça m'a permis de trouver que pour certaines pages les tableaux ne sont pas dans le même ordre, et n'ont pas la même classe. Il doit y avoir 6 pages pour lesquelles je dois faire des conditions et changer le sélecteur précisément pour chaque page (un plaisir).

J'ai réussi à corriger le script pour rajouter des exceptions à chaque page qui posait problème.

    if dpt == 44:
            css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(8)'
        elif dpt == 48:
            css_select = 'table.wikitable.sortable:nth-of-type(2) td:nth-child(8)'
        elif dpt == 70:
            css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(5)'
        elif dpt == 95:
            css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(8)'
        elif dpt == 103:
                css_select = 'table.wikitable.sortable.titre-en-couleur td:nth-child(5)'


        if dpt == 56:
            css_select_2 = 'table.wikitable.sortable td:nth-child(1)'
        else:
            css_select_2 = 'table.wikitable.sortable.titre-en-couleur td:nth-child(1)'

Maintenant il fait que je supprime du tableau les villes de plus de 2000 habitants (c'est la limite entre village et ville [4]).
Pour ça je vais créer un fichier texte avec le nom de toutes les communes dépassant le seuil de 2000 habitants. Apparemment il n'y a que 5413 communes en France qui ont plus de 2000 habitants. Sur 36 000, il y a donc une grande majorité de villages. Ensuite je n'aurais plus qu'à supprimer dans le tableau les lignes correspondant à ces communes.

Supression

Il y a certaines lignes qui n'ont pas les données de latitude/longitude dans le tableau, ou qui n'ont pas de valeur pour la longitude ("-") : on les supprime aussi.

#!/usr/bin/env python3
#coding: utf-8

import csv

###fonction pour supprimer les lignes qu'on veut pas
def supp(arr):
    for didi in d:
        for r in range(len(arr)):
            ###petite astuce : puisque pop() enlève une valeur il change l'index de toutes les autres. Il faut donc compenser en faisant -r, pour retrouver la bonne valeur
            d[didi].pop(arr[r]-r)

d = {}
d['index_commune'] = []
d['nom_commune'] = []
d['latitude'] = []
d['longitude'] = []

rows = []

##########################LIRE LE CSV##########################
with open('tableau_simplifié.csv', 'r+') as csvfile:
    reader = csv.DictReader(csvfile)
    for row in reader:
        for i in range (0,4):
            valeur = list(row.items())[i][0]
            d[valeur].append(row[valeur])

##########################ENLEVER LES LIGNES VIDES (SANS LONG/LAT)###########################
for i in range(len(d['longitude'])):
    if d['longitude'][i] == "" or d['longitude'][i] == "-":
        # print('vide')
        rows.append(i)
 
###il y a 2875 communes pour lesquelles on a pas la valeur de la latitude/longitude : on les supprime du csv
supp(rows)

##########################ENLEVER LES COMMUNES DE + DE 2000 HAB###########################
with open('villes1999', 'r+', encoding='utf8') as g:
    file = g.read()
    villes = file.splitlines()

liste_villes = []

for r in range(len(d['nom_commune'])):
    # print(d['nom_commune'][r])
    if d['nom_commune'][r] in villes:
        liste_villes.append(r)
###on supprime aussi les 5000 et quelques villes
supp(liste_villes)

Maintenant il faut recréer un nouveau fichier avec les valeurs du dictionnaire, pour éviter que cette étape de suppression se fasse à chaque fois. Le plus simple est alors du json, parce que les opérations d'encodage/décodage en python sur du json marchent bien et sont efficaces.

Encodage/décodage

Le code pour convertir le dictionnaire "d" en un fichier json (le mode 'w' pour write -> écriture) :

with open('result.json', 'w', encoding="utf8") as fp:
    json.dump(d, fp)

(s'éxécute une seule fois, lors de la création du fichier result.json)

et le code pour le décoder (le mode 'r' pour read -> lecture) :

with open('result.json', 'r', encoding="utf8") as f:
    data = f.read()
    d = json.loads(data)

(s'exécute chaque lancement de programme, pour récupérer la "base de données")

Ne pas oublier le encoding='utf8', pour ne pas avoir des noms de communes qui font bugger les accents !

Trouver les communes dans un rayon donné

J'ai réussi à faire une fonction qui vérifie si il y a des communes dans le rayon en latitude, mais maintenant il faut que je trouve comment garder en mémoire les index pour checker seulement sur ceux-ci la longitude. Pour ça j'utilise des tuples qui me permettent de garder en mémoire l'index des communes correspondant pour la latitude, pour pouvoir les tester sur la longitude.

Le point de coordonnées lat_base et long_base change chaque jour.

La variable km_lat est une approximation en degrés de la longueur d'1 km. Comme ça on peut facilement changer le rayon r en un nombre de kilomètres différent (ici c'est 25km pour l'instant).

###fonction qui checke si la latitude est comprise dans le rayon voulu
def check_lat(val):
    if lat_base - r <= val <= lat_base + r:
        return True
    return False

###fonction qui checke si la longitude est comprise dans le rayon voulu
def check_long(val):
    if long_base-r <= val <= long_base+r:
        return True
    return False

lat_base = float('48.632954')
long_base = float('-2.874094')
#1 km +- = à km_lat degrés
km_lat = 0.0090437173295
#r correspond environ à un rayon de 25 km
r = 25*km_lat

foirages = 0

# print(lat_base)
# print(long_base)
compteur = 0

###on crée 2 listes pour ranger les valeurs correspondant au rayon souhaité
lat_good = []
long_good = []

###affiche seulement les communes qui correspondent et leur nom
for index in range(len(d['latitude'])):
    try:
        if check_lat(float(d['latitude'][index])):
            # print(d['nom_commune'][index])
            tuplo = (index, d['nom_commune'][index])
            lat_good.append(tuplo)
    except:
        foirages += 1
        print('oupsi')

for i in lat_good:
    try:
        if check_long(float(d['longitude'][i[0]])):
            long_good.append(i)
            print(i[1])
            compteur += 1
    except:
        print('%s oupso -> %s' % (i[0], d['longitude'][i[0]]))
        foirages +=1

##affiche le nombre de communes matchant avec la requête
print('\n%s communes correspondantes' % compteur)
print('foirages-> %s' % foirages)

Ce bout de code affiche le nom des communes qui correspondent au rayon donné (et les stocke en même temps dans une liste).

Ensuite il faut juste que je choisisse une commune au hasard.

###fonction pour choisir au hasard un élément de la liste
import random
print(random.choice(long_good)[1])

Ne pas visiter deux fois la même commune

J'essaie de faire un log qui enregistre les communes déjà visitées, et qu'à chaque lancement de programme il prenne le dernier élément ajouté dans le log et qu'il récupère ses coordonnées dans les variables lot_base et long_base:

with open('communes_visitées', 'r', encoding='utf8') as f:
    comm_visit = f.read()
    comm_visit = comm_visit.splitlines()
    ###pour avoir le dernier élément de la liste
    comm_visit.reverse()

index_dernière_commune = d['nom_commune'].index(comm_visit[0])

###remplace la latitude et la longitude de départ avec les valeurs de la commune précédente
lat_base = float(d['latitude'][index_dernière_commune])
long_base = float(d['longitude'][index_dernière_commune])


Et la deuxième partie:

compte = 0

###supprime les valeurs de long_good qui sont déjà dans le log (déjà visitées)
for index, village in long_good:
    if village in comm_visit:
        del long_good[compte]
    compte += 1

for i, k in long_good:
    print('long_good = %s' % k)

for j in comm_visit:
    print('comm_visit = %s' % j)

### ajoute la commune visitée dans un fichier
with open('communes_visitées', 'a', encoding='utf8') as f:
    commune_selec = random.choice(long_good)[1]
    f.write(commune_selec+'\n')
    print('commune_selec = %s' % commune_selec)

C'est assez étrange puisque ça à l'air de fonctionner (certaines communes qui sont déjà visitées sont bien supprimées), mais de temps en temps pourtant le programme choisit une commune déjà visitée. Je n'arrive pas à débugger pour l'instant.

Version corrigée :

###checker si le village est déjà dans la liste des communes visitées
compte = 0
long_goodok = []

for index, village in long_good:
    # print("checking if %s is in comm_visit" % (village))
    if village not in comm_visit:
        # print("not in comm_visit. valid.")
        long_goodok.append(long_good[compte])
    else:
        print("commune supprimée")
    compte += 1

# print(long_goodok)
long_good = long_goodok

## ajouter la commune visitée dans un fichier
with open('communes_visitées', 'a', encoding='utf8') as f:
    try:
        commune_selec = random.choice(long_good)[1]
        f.write(commune_selec+'\n')
        print('commune_selec = %s' % commune_selec)
    except:
        print('Pas de village dans le rayon. :(\nRéesayer avec un plus grand rayon ?')

En fait la boucle regardait un élément après l'autre, et quand la commune était déjà visitée supprimait l'élément qu'on regardait. L'élément suivant se décalait vers la gauche (son index qui était par exemple 3 devenait 2) et au prochain tour de boucle on le passait sans le prendre en compte. C'est pour ça que certaines communes passaient dans les mailles du filet.

Enjoliver le post

Télécharger une image de la commune

J'ai essayé d'utiliser une librairie qui s'appelle google_images_download pour télécharger le premier résultat de Google Images, mais ça ne fonctionne pas :

###(code trouvé dans la doc de la librairie)
#instantiate the class
response = google_images_download.googleimagesdownload()
arguments = {"keywords":"tressignaux","limit":1,"print_urls":True, "language":"French"}
paths = response.download(arguments)
#print complete paths to the downloaded images
print(paths)

Et le résultat:

Item no.: 1 --> Item name = tressignaux
Evaluating...
Starting Download...

Errors: 0


({'tressignaux': []}, 0)

La liste des résultats est vide, même avec des mot en anglais comme dans la doc ça ne donne rien. Peut-être que la librairie n'est peut-être plus mise à jour ?

J'essaie avec mechanize de récupérer les images de Google Image, mais les images en bonne qualité ne sont pas disponibles dans des balises html. Elles sont cachées en bas du code source, alors j'ai essayé avec une Regex de la récupérer.

#!/usr/bin/env python3
#coding: utf-8

###############################SCRAPING WIKIPEDIA####################
import lxml.html as parser
import cssselect, mechanize, re
import urllib.parse as urlP

###avec mechanize
br = mechanize.Browser()
# br.addheaders = [('User-agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0')]
br.addheaders = [('User-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36')]
br.set_handle_robots(False)

commune_selec = 'Ploëzec'.lower()
commune_selec = commune_selec.encode('utf-8')

url = 'https://www.google.com/search?hl=en&tbm=isch&q=' + urlP.quote(commune_selec) + '&source=lnms'

print(url)
data = br.open(url, timeout=50)
source = data.read()

# with open('g_img_source.txt', 'w', encoding='utf8') as f:
#     f.write(str(data))

###regex pour choper l'URL de l'image dans le code source, parce qu'il est pas dans une balise donc on peut pas le trouver avec css_select
img = re.search(r',\["(.+?\.jpg)",\d+?,\d+?\]', str(data))
###afficher le premier lien d'image trouvé

print(img[1])

Le problème est que la Regex fonctionne bien quand je la teste dans Atom, mais avec Python ça ne fonctionne pas. Pour l'instant je ne trouve pas de solution.

En remplaçant

img = re.search(r',\["(.+?\.jpg)",\d+?,\d+?\]', str(data))

par

###(code de la page de Brigitte Coric, en BAC2 (sur ce site))
img = re.findall(r'\["([^"]+\.jpg)",[0-9]+,[0-9]+\]', str(source))

ça marche, j'arrive à récupérer la première image de Google Image.

Maintenant, il faut savoir si je peux poster une image sur Twitter à partir de son URL, ou bien si je dois la télécharger avant. J'ai trouvé ce site qui explique comment uploader une image sur le serveur de Twitter mais ça me semble plus simple de télécharger et remplacer chaque jour l'image sur le serveur.

Avec mechanize c'est simple de télécharger une image :

###téléchargement de la première image
###si la première ne marche pas on essaie la suivante jusqu'à ce que ça marche
n = 0
while True:
    try:
        br.retrieve(img[n], 'balade_dépendences/image.jpg')
        break
    except:
        n += 1

Ça fonctionne bien, mais Twitter n'accepte pas les images de plus de 3 Mo. Il faut que je rajoute une condition pour ne pas télécharger les images trop grandes, ou bien pour réduire la taille des images téléchargées.

Traceback (most recent call last):
  File "balade_campagnarde.py", line 201, in <module>
    api.update_with_media(photo_path, status=phrase+'\n'+'Arrivée dans %s' % temps_trajet)
  File "/home/tumtum/.local/lib/python3.7/site-packages/tweepy/api.py", line 218, in update_with_media
    headers, post_data = API._pack_image(filename, 3072, form_field='media[]', f=f)
  File "/home/tumtum/.local/lib/python3.7/site-packages/tweepy/api.py", line 1303, in _pack_image
    raise TweepError('File is too big, must be less than %skb.' % max_size)
tweepy.error.TweepError: File is too big, must be less than 3072kb.

J'ai trouvé un script qui permet de récupérer la taille de l'image en octets avant même de la télécharger. Si l'image est trop grande, on télécharge la suivante dans la liste.

La fonction:

import urllib
from PIL import ImageFile

def getsizes(uri):
    # get file size *and* image size (None if not known)
    file = urllib.request.urlopen(uri)
    size = file.headers.get("content-length")
    if size:
        size = int(size)
    p = ImageFile.Parser()
    while True:
        data = file.read(1024)
        if not data:
            break
        p.feed(data)
        if p.image:
            return size, p.image.size
            break
    file.close()
    return(size, None)

Et le code qui l'intègre :

##téléchargement de la première image
##si la première ne marche pas on essaie la suivante jusqu'à ce que ça marche
n = 0
while True:
    try:
        ###pour ne pas avoir d'images trop grandes pour Twitter
        print(n+1)
        size = getsizes(img[n])[0]
        print(size)
        if size >= 3000072:
            print('trop grand')
            n += 1
            continue

        file_img = 'balade_dépendences/image.jpg'
        br.retrieve(img[n], file_img)
        print('image n°%s téléchargée' % (n+1))
        break
    except:
        print('image corrompue')
        n += 1

Rajouter le temps de trajet

Pour ça je vais utiliser l'API de TomTom, pour calculer le temps entre les coordonnées de départ et celles d'arrivée. Il faut que j'utilise l'API de Routing, qui calcule le trajet entre deux points (et donc aussi le temps de trajet). en plus je peux calculer ce temps pour un piéton, ce qui est ce que je recherche. Le résultat est un fichier json.

Léna utilise déjà l'API de TomTom pour son bot, je reprends son code pour comprendre comment fonctionne la requête à l'API :

import json
import datetime
from urllib.request import urlopen

api_key = 'GYNAQ6sx8c98oqXqGGOmvqvgOYn0FARQ'

url = 'https://api.tomtom.com/routing/1/calculateRoute/%d%2C%d%3A%d%2C%d/json?avoid=unpavedRoads&travelMode=pedestrian&key=%s' % (lat_base, long_base, lat_fin, long_fin, api_key)

Je remplace l'url avec ce que me donne l'exemple d'utilisation de l'API, et je met des placeholders pour pouvoir utiliser les variables de latitude et de longitude de mes points de départ et d'arrivée.

Le placeholder %d est uti lisé pour les nombres entiers, donc ça bugge avec les latitudes/longitudes qui sont des nombres décimaux. En plus il y à des % dans l'URL, ce qui doit faire tout planter. Il faut utiliser .format, une autre méthode de placeholder :

tomtom_url = 'https://api.tomtom.com/routing/1/calculateRoute/{0}%2C{1}%3A{2}%2C{3}/json?avoid=unpavedRoads&travelMode=pedestrian&key={4}'.format(lat_base, long_base, lat_fin, long_fin, api_key)

Il faut maintenant que je lance la requête HTTP pour récupérer le fichier JSON.

tomtom_url = 'https://api.tomtom.com/routing/1/calculateRoute/{0}%2C{1}%3A{2}%2C{3}/json?avoid=unpavedRoads&travelMode=pedestrian&key={4}'.format(lat_base, long_base, lat_fin, long_fin, api_key)

getData = urlopen(tomtom_url).read()
result = json.loads(getData)

for i in result['routes'][0]['summary']:
    print(i, result['routes'][0]['summary'][i])

On obtient ce résultat :

lengthInMeters 13124
travelTimeInSeconds 9449
trafficDelayInSeconds 0
departureTime 2020-04-16T12:54:27+02:00
arrivalTime 2020-04-16T15:31:56+02:00

Comme ça je peux afficher la distance et le temps de trajet prévu dans le post Twitter.

Avec le module datetime je peux afficher l'heure de manière plus lisible :

temps_trajet = str(datetime.timedelta(seconds=temps_trajet))
###affiche par exemple 3:25:09

Rajouter des petites phrases pour que ce soit moins froid

Je fais une liste de tournures de phrases qui contiennent le nom de la commune, du style :

  • COMMUNE, me voilà !
  • Direction COMMUNE
  • C'est parti pour COMMUNE !
  • Aujourd'hui je vais à COMMUNE
  • Objectif : COMMUNE !
  • Cap sur COMMUNE
  • Allons à COMMUNE
  • En route pour COMMUNE
  • Départ pour COMMUNE
  • Prochaine étape : COMMUNE
  • Et la prochaine commune est COMMUNE
  • Maintenant je me dirige vers COMMUNE
  • Aujourd'hui j'ai décidé d'aller à COMMUNE
  • En déplacement à COMMUNE
  • COMMUNE est la prochaine étape sur ma route
  • Bonjour COMMUNE !
  • Au revoir COMMUNE PRÉCÉDENTE, bonjour COMMUNE !
  • COMMUNE en vue !

dans lesquelles le programme choisit au hasard chaque jour.

###on choisit une tournure de phrase au hasard et on inclut le nom de la commune sélectionnée
phr = ["Direction %s","C'est parti pour %s !","Aujourd'hui je vais à %s","Objectif : %s !","Cap sur %s","Allons à %s","En route pour %s","Départ pour %s","Prochaine étape : %s","Et la prochaine commune est %s","Maintenant je me dirige vers %s","Aujourd'hui j'ai décidé d'aller à %s","En déplacement à %s","Bonjour %s !","%s, me voilà !","%s est la prochaine étape sur ma route","%s en vue !"]
phrase = random.choice(phr) % commune_selec.decode('utf-8')
print(phrase)

Poster le tout sur Twitter

C'est très simple de poster seulement le nom de la commune + une image :

###authentification à l'API de Twitter
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)

###post en lui-même, en 2 parties (texte et image)
api = tweepy.API(auth)
photo_path = '/directory/image.jpg'
api.update_with_media(photo_path, status=commune_selec)

Pour poster plusieurs lignes, on combine les chaînes de caractères avec un retour chariot entre chaque ligne.

api.update_with_media(photo_path, status=phrase+'\n'+'Arrivée dans %s' % temps_trajet)
print('Post envoyé !')

Régler la fréquence de post du programme

L'idée est que le bot poste sur Twitter le village où il se rend + une photo + l'heure d'arrivée prévue (il se déplace à pied), puis ne poste le village suivant qu'une fois arrivé. Le programme va être lancé par cron, toutes les 5 minutes. Il faut donc créer une condition pour qu'il n'exécute tout le code

À la fin du programme, on écrit la date et l'heure d'arrivée dans un fichier texte :

with open('balade_dépendences/h_arriv.txt', 'w', encoding='utf8') as h:
    h.write(heure_arrivée)

Au début du programme, on récupère cette heure d'arrivée, et si le moment d'exécution du programme (la variable "maintenant", datetime.datetime.now()) est trop tôt par rapport à l'heure d'arrivée prévue (le bot n'est pas encore arrivé dans le village), alors le programme s'arrête prématurément.

with open('balade_dépendences/h_arriv.txt', 'r', encoding='utf8') as h:
    h_arriv = h.read()
    h_arriv = datetime.datetime.strptime(h_arriv, '%Y-%m-%dT%H:%M:%S%z')
    maintenant = datetime.datetime.now(h_arriv.tzinfo)

    print('Heure d\'arrivée prévue : %s ' % h_arriv)
    print('Heure d\'exécution du programme : %s' % maintenant)
   
    if h_arriv > maintenant:
        print('quitter le programme')
        quit()

Il faut utiliser la classe tzinfo pour rajouter cette information dans l'objet datetime.now(), car par défaut cet objet est "naïf" (il ne prend pas en compte les fuseaux horaires). De cette façon on peut comparer les deux dates.

Output:

Heure d'arrivée prévue : 2020-04-23 15:57:07+02:00
Heure d'exécution du programme : 2020-04-23 12:34:41.312797+02:00
quitter le programme

Sinon, la suite du programme s'exécute et on obtient un nouveau post qui définit la prochaine destination, ainsi que le temps de trajet estimé par TomTom.

Définir un point de départ ?

Je vais choisir une ville au hasard pour le point de départ du programme. Pour cela je reprends le tableau avec seulement les villages :

#!/usr/bin/env python3
#coding: utf-8

import random, json

with open('balade_dépendences/result.json', 'r', encoding="utf8") as f:
    data = f.read()
    d = json.loads(data)

print(random.choice(d['nom_commune']))

Le résultat est : Griscourt. Ce sera donc la commune de départ.

Corrections

Communes en double

Après avoir tourné un moment, le bot est bloqué pendant 5 jours : il se dirige vers une commune à plus de 600km ! Apparemment c'est dû à un doublon : il y a 2 communes qui s'appellent Morville, et c'est celle en-dehors du rayon qui a été sélectionné par le programme. Je dois corriger ça dans mon fichier result.json.

Pb morville.png

Dans mon programme qui supprime dans le tableau les communes avec trop d'habitants, ou celles sans information de longitude/latitude, je rajoute ça :

##########################ENLEVER LES COMMUNES EN DOUBLE###########################
###trouvé sur stackoverflow, crée une array de noms de communes en double dans 'dupes'
seen = {}
dupes = []
for x in d['nom_commune']:
    if x not in seen:
        seen[x] = 1
    else:
        if seen[x] == 1:
            dupes.append(x)
        seen[x] += 1

###crée une liste d'indexs à supprimer dans le dictionnaire d
a_sup = []
for doublon in dupes:
    ###ajoute le premier index trouvé à la liste a_sup
    a_sup.append(d['nom_commune'].index(doublon))

###on supprime tout ça
supp(a_sup)

Je re-génère un fichier result.json, que je remplace dans le dossier balade_dépendences/ du programme.

Interactions ?

Pour l'instant le bot fait sa vie tout seul, il n'as pas d'interaction avec les utilisateurs de Twitter, ce qui est dommage car le bot voyage et devrait donc avoir des relations avec les personnes/villages qu'il rencontre sur sa route.

À un moment j'avais pensé à twitter un message à la commune quand le bot arrive, pour cela je vais essayer de faire en sorte que le bot trouve le compte Twitter de la ville, s'il y en a un, pour twitter un petit message de "Bien arrivé !"

En cherchant un peu, puisque c'est des petits villages, il y a peu de chance qu'ils aient un compte Twitter officiel (par exemple, je suis tombé sur un architecte, David Morville, en cherchant le Twitter de la commune de Morville).

Je me suis dit que le bot allait répondre au premier tweet qui contient le nom de la commune, en disant "Je suis passé dans ce village !", ou quelque chose dans le style.

En utilisant api.search() pour chercher les tweets parlant de la commune d'Eply, je suis tombé sur ce tweet :

@RTommison @Mufc20Hus @DavidJonesMufc @BRJ259 Great banter, dirdct eply to you Robby

Pour éviter ce genre de quiproquo, je vais essayer de chercher par hashtag. Avec #Eply je suis presque sûr de tomber sur un tweet qui parle effectivement de la commune en question.

Le problème c'est que la recherche n'est pas sensible à la casse ni aux accents, donc même en cherchant "Clémery" exemple on peut trouver clemery, qui peut peut-être vouloir dire quelque chose dans une autre langue par exemple.

Apparemment en changeant la requête en '"Éply"' au lieu de 'Éply', on a des résultats plus précis. On peut aussi ajouter un paramètre lang='fr' pour avoir normalement plus de chance d'avoir des textes en français.

Mais finalement, comme pour les images qui peuvent ne pas être vraiment pertinentes par rapport à la commune en question, ces tweets peuvent être un peu à côté de la plaque, ça peut rajouter de l'humour au projet.

Je rajoute un bloc if pour trier les tweets qui ne contiennent pas du tout le mot recherché (il arrive que l'API en trouve):

if commune.lower() in tweetText.lower():
            print(tweetText)

Il faut ensuite que je trouve comment répondre au tweet trouvé (s'il y en a un, puisque d'après la documentation de tweepy il y a une limite de 7 jours pour la recherche de tweets).

Pour répondre à un tweet il faut utiliser la fonction :

api.update_status('@<username> contenu', tweet_id)

J'ai donc fait un bout de code qui me permet de répondre à un message mentionnant la commune en question, en espérant que les gens vont réagir et que le bot va sortir un peu de son isolement. Je vais peut-être reprendre la liste des communes déjà visitées pour relancer ce programme, histoire d'inclure dans cette extension du projet tout ce qui a déjà été posté.

try:
        for status in tweepy.Cursor(api.search, q=commune, lang='fr', tweet_mode='extended').items(0):
            tweet_id = status.id
            tweet_username = status.user.screen_name
            #si le tweet est retweeté, afficher le texte du retweet
            if hasattr(status, 'retweeted_status'):
                tweetText = status.retweeted_status.full_text
            else:
                tweetText = status.full_text
        if commune.lower() in tweetText.lower():
            tweet = tweetText
            # print(tweet)
        else:
            print('Pas de tweet trouvé avec ce nom de commune, on continue')
    except NameError:
        print('Pas de tweet trouvé avec ce nom de commune, on continue')

phrases = ['Je suis déjà passé par %s !', 'Je suis déjà allé à %s :)', 'Je la trouve sympa la commune de %s.']

api_update_status('@{0} {1}'.format(tweet_username, random.choice(phrases)) % commune, tweet_id)

J'ai pour l'instant lancé ce programme manuellement en utilisant la liste des communes déjà visitées, pour que les premières semaines d'activité donnent lieu à quelques interactions.

J'ai rajouté une petite condition pour ne pas que le bot réponde sous ses propres posts :

if tweet_username == 'baladecampagne':
    print('on ne se répond pas à soi enfin')
    break

Avec break qui interrompt la boucle si le nom d'utilisateur est celui du bot. Il faut aussi rajouter sleep(30) dans la boucle pour ne pas que Twitter considère cette activité comme du spam par des réponses trop rapprocheées.

Il ne me reste plus qu'à trouver comment l'insérer dans mon programme principal. En effet, la question du timing va se poser, puisque si je fait cette recherche juste après avoir posté le premier message, il y a de grandes chances que le tweet qui contient le nom de la commune soit mon propre tweet.

Pour cela je fais en sorte qu'à chaque fois que le bot poste il envoie aussi un message à une commune qu'il a vu plus tôt (80 communes me semble bien, ça représente entre 6 et 10 jours en moyenne je pense). Ce laps de temps permet que d'autres personnes réelles (et pas le bot) aient twitté quelque chose en rapport avec la commune après le post du bot sur cette commune. Apparemment l'API de recherche de Twitter ne permet pas de trouver les tweets qui sont vieux de plus de 2 semaines, il faut peut-être que je base les posts là-dessus pour éviter de retomber systématiquement sur mes propres posts ?

Apparemment, en prenant la 120e dernière commune de la liste on arrive à dépasser ce délai de deux semaines, et donc le bot ne retombe pas sur ses propres posts. J'ai aussi augmenté le nombre de résultats de l'API de recherche à 2, comme ça si jamais on tombe sur un tweet non valide il y a une deuxième chance que le l'autre le soit : for status in tweepy.Cursor(api.search, q=ancienne_commune, lang='fr', tweet_mode='extended').items(2):

Je rajoute donc cette option dans le programme principal.

Carte

J'ai crée une carte de France qui retrace le parcours du bot (pas mise à jour en temps réel pour l'instant). Les points bleus sont le début du parcours et les points rouges les plus récents.

J'ai utilisé le module folium pour gérer la carte. Avec folium on part d'une carte du monde, mais on peut choisir à quel endroit on centre la carte et à quel niveau de zoom on se trouve au départ. Pour centrer sur la France j'ai utilisé cette ligne :

franceMap = folium.Map(location=[46.9, 2], tiles='Stamen Toner', zoom_start=6)

Le paramètre "tiles" permet de définir le style du fond de carte, plusieurs styles préconçus existent avec plus ou moins dé détails.

On peut ensuite rajouter différentes couches d'informations sur ces cartes, j'ai donc ajouté les points correspondant aux villages déjà visités ainsi que les lignes les reliant. <syntaxhighlight lang="python">

                                                    1. COMMUNES DÉJÀ VISITÉES #######################

with open('communes_visitées', 'r', encoding='utf-8') as f:

   data = f.read()
   communes = data.splitlines()

liste_points = []

      1. créer une liste de points avec les coordonnées des communes visitées

for commune in communes:

   index = d['nom_commune'].index(commune)
   coordonnées = tuple([float(d['latitude'][index]), float(d['longitude'][index])])
   liste_points.append(coordonnées)

for i, commune in enumerate(communes):

   color = colorFader('blue', 'red', i/len(communes))
   if i != 0:
       ###créer des lignes pour chaque point (chaque village parcouru)
       folium.PolyLine([liste_points[i-1], liste_points[i]], color=color, weight=2.5, opacity=1).add_to(franceMap)
       ###créer des points pour afficher le nom de chaque commune
       folium.CircleMarker((liste_points[i][0], liste_points[i][1]), radius=2, weight=1, color=color, fill_color=color, fill_opacity=1, tooltip=commune).add_to(franceMap)
      1. départ

folium.CircleMarker((liste_points[0][0], liste_points[0][1]), radius=4, weight=3, color='black', fill_color='blue', fill_opacity=1, tooltip="Griscourt (DÉPART)").add_to(franceMap)

      1. arrivée

folium.CircleMarker((liste_points[len(liste_points)-1][0], liste_points[len(liste_points)-1][1]), radius=4, weight=3, color='black', fill_color='red', fill_opacity=1, tooltip=commune+' (ARRIVÉE)').add_to(franceMap)

      1. save the map as an html file

franceMap.save('FrancePointMap.html') print('C\'est la carte') <syntaxhighlight>

Ce module nous donne un fichier HTML en sortie, sur lequel se trouve la carte dans laquelle on peut zoomer, se déplacer… En passant la souris sur les points on obtient le nom des communes visitées.

J'ai ajouté grace aux modules numpy et matplotlib une variation de couleurs : bleu pour le départ et rouge pour la position actuelle. Je trouve ça un peu plus clair, mais c'est pas encore super lisible.
Le problème vient aussi des "singularités" qui sont assez régulières quand même, des moments où il y a un bug et où la prochaine commune se trouve à 5 jours de marche au lieu de quelques heures. Il faudrait que j'observe les logs en détail et que j'essaie de reproduire les conditions dans lesquelles le programme a buggé pour résoudre ce problème.

Liens

Le GitLab

Le Twitter