Pour faire parler les données ADS-B, il ne suffit pas d’interroger une station ou une API, encore faut-il disposer de repères locaux fiables. Ce quatorzième volet pose donc les fondations de notre environnement Python en reconstruisant plusieurs bases locales — GeoNames, Plane-Alert, OurAirports et Callsign — afin de mieux localiser, identifier et relier les appareils observés. Moins spectaculaire qu’un affichage en direct, ce travail rendra pourtant nos prochains scripts plus clairs, plus rapides et plus solides.
Introduction – Optimisation des programmes
Nous avions conclu notre précédent tutoriel en mettant en place un affichage complet, sur trois lignes, avec, pour chaque appareil, son code ICAO, son immatriculation, son indicatif, son type, sa position, son altitude et sa distance vis-à-vis de notre station.

Exemple d’affichage après interrogation de ma station locale

Exemple d’affichage après interrogation de l’API Airplanes.live
Ce nouvel affichage constituait une étape importante, mais il faisait aussi apparaître une limite : plus nous enrichissions les informations affichées, plus nos programmes dépendaient de fichiers locaux de référence. Il ne suffisait plus d’interroger aircraft.json ou l’API Airplanes.live. Il fallait aussi disposer de bases locales fiables pour localiser les appareils, reconnaître certains aéronefs, rattacher une immatriculation à un pays, à un organisme ou à une catégorie particulière, interpréter un code aéroport, ou encore rapprocher un callsign militaire d’un code ICAO connu.
Jusqu’ici, plusieurs de ces opérations de préparation ou de reconstruction étaient encore dispersées dans différents scripts. Un programme conçu pour afficher les avions visibles pouvait aussi vérifier ou reconstruire une partie du cache géographique. Un autre pouvait interroger Plane-Alert mais aussi mettre à jour nos index enrichis. Cette organisation fonctionnait, mais elle alourdissait progressivement les scripts et les éloignait parfois de leur raison d’être initiale.
J’ai donc cherché à recentrer les programmes fonctionnels sur leur mission principale : interroger la station locale, comparer les données avec une API, rechercher un appareil ou suivre une liste d’aéronefs. Il fallait donc créer des scripts dédiés à la production des fichiers locaux directement réutilisables par les autres scripts.
Ce tutoriel sera donc consacré à la construction ou à la reconstruction de nos bases locales : GeoNames pour les références géographiques, Plane-Alert pour les index enrichis, OurAirports pour les références d’aéroports, et Callsign pour établir un lien entre certains indicatifs militaires et les codes ICAO récemment observés. Les références d’aéroports permettront notamment de transformer des codes IATA ou OACI en informations plus lisibles dans les traitements liés aux routes de vol. De son côté, la base Callsign préparera un index local utile lorsqu’un appareil est plus facilement repéré par son indicatif opérationnel que par son immatriculation ou son code Hex ICAO.
Comme préalable, j’adopte aussi une nouvelle organisation destinée à rendre nos scripts plus faciles à maintenir et à limiter les risques de confusion entre sources officielles, fichiers locaux et fichiers produits par les scripts. Nos listes Plane-Alert officielles, références locales, fichiers PRIO, références GeoNames, données OurAirports, base Callsign et clés API seront désormais rangés dans des espaces distincts.
DOSSIER_PLANE_ALERT = « Fichiers_Plane_Alert » DOSSIER_PLANE_ALERT_LFCZ = « Fichiers_F80543 » DOSSIER_CALLSIGN = « Fichiers_Callsign » DOSSIER_CSV = « Fichiers_CSV »
DOSSIER_PRIO = « Fichiers_F80543 » DOSSIER_GEONAMES = « Fichiers_GeoNames » DOSSIER_OURAIRPORTS = « Fichiers_Airports »
À la suite de ce tutoriel, nos principales bases locales seront prêtes. Le prochain pourra donc s’appuyer sur ces fondations pour revenir aux programmes d’exploitation proprement dits : recherche d’appareils, création ou interrogation de listes prioritaires.
Pour ne pas alourdir le texte, les six scripts présentés dans ce tutoriel ne sont pas reproduits intégralement dans les pages qui suivent. Le lecteur pourra cependant se les procurer en téléchargeant le fichier ZIP Test_Api_Part2.zip, dont le lien figure en fin de document. Les noms employés dans le corps du tutoriel correspondent aux versions de travail, par exemple Test_Api_31g.py ou Test_Api_34m.py ; dans l’archive, les versions destinées à la diffusion portent le suffixe _HamAnalyst.py. Elles concernent respectivement la gestion des références GeoNames, la recherche d’aéronefs atypiques, la préparation des index Plane-Alert, le traitement des messages ADS-B Exchange, la création de la base OurAirports et la gestion de l’index Callsign.
Ces scripts sont fournis comme supports pédagogiques. Ils ont été préparés pour éviter un lancement accidentel : avant leur exécution, un avertissement décrit les accès réseau et les opérations locales prévues, puis demande une confirmation volontaire par la saisie exacte de OUI ; l’option –executer permet également un lancement explicite après lecture et adaptation du code. Certains programmes téléchargent des sources publiques, créent ou remplacent des fichiers CSV et JSON, conservent des sauvegardes, déplacent des messages .msg ou effectuent des consultations pouvant être longues. Le lecteur devra donc vérifier les chemins et sous-dossiers utilisés, installer les éventuelles bibliothèques indiquées, préparer les fichiers locaux nécessaires et respecter les conditions d’utilisation des services interrogés. Aucune adresse IP personnelle, clé privée ou donnée d’accès directement exploitable n’est fournie. Les scripts peuvent être lus, modifiés et adaptés pour un usage personnel, associatif ou pédagogique, sous réserve de conserver une mention de l’auteur d’origine.
Les bases GeoNames constituent une source ouverte de références géographiques couvrant l’ensemble du monde. Elles permettent de rattacher une latitude et une longitude à des points de repère lisibles, notamment des villes, des pays et des subdivisions administratives. Pour notre usage, nous avons retenu le fichier cities15000.zip. Il offre un compromis satisfaisant entre richesse des données et taille raisonnable du fichier : il contient un fichier txt d’environ 34.000 lignes dédié aux coordonnées de l’ensemble des villes importantes de la planète.
1. Bases GeoNames
Les bases GeoNames constituent une source ouverte de références géographiques couvrant l’ensemble du monde. Elles permettent de rattacher une latitude et une longitude à des points de repère lisibles, notamment des villes, des pays et des subdivisions administratives. Pour notre usage, nous avons retenu le fichier cities15000.zip
. Il offre un compromis satisfaisant entre richesse des données et taille raisonnable du fichier : il contient un fichier txt d’environ 34.000 lignes dédié aux coordonnées de l’ensemble des villes importantes de la planète.

Fichier cities15000.txt
Ce fichier texte reprend la structure générale de la table geoname de GeoNames. Il est encodé en UTF-8, avec des champs séparés par des tabulations. Les voici tels qu’on les retrouve pour la ville de Paris :
geonameid | 2988507 : identifiant unique GeoNames de Paris ; |
name | Paris : nom principal ; |
asciiname | Paris : nom ASCII, sans caractères spéciaux si besoin ; |
alternatenames | Paris,Parigi,Parijs,… : variantes ou noms dans d’autres langues ; |
latitude | 48.85341 |
longitude | 2.3488 |
feature class | P : classe de l’objet géographique, ici lieu peuplé ; |
feature code | PPLC : code indiquant une capitale politique ; |
country code | FR : code pays ; |
cc2 | |
admin1 code | 11 : code administratif de niveau 1, ici l’Île-de-France ; |
admin2 code | 75 : code administratif de niveau 2, ici Paris ; |
admin3 code | |
admin4 code | |
population | 2138551 : population ; |
elevation | |
dem | |
timezone | Europe/Paris : fuseau horaire ; |
modification date | 2024-09-05 : date de dernière modification de la fiche. |
Les champs admin1 code et admin2 code ne sont pas directement lisibles. Ils doivent être rapprochés des fichiers admin1CodesASCII.txt et admin2Codes.txt pour obtenir des libellés comme Nouvelle-Aquitaine, Gironde, California, Los Angeles County, etc.
À partir de cette base, le script générera un fichier local villes_reference_GeoNames.csv qui suppléera au fichier villes_references.csv appelé par certains des scripts présentés dans le précédent tutoriel.
1.1. Gestion GeoNames // Test_Api_31.py
Notre fichier villes_references.csv contenait de simples repères locaux pour des villes proches de ma station LFCZ : le nom de la ville, son pays, sa latitude, sa longitude et une distance maximale d’utilisation. Le nouveau fichier villes_reference_GeoNames.csv est plus riche. Sa structure envisagée est :
nom,pays,admin1_nom,admin2_nom,lat,lon,distance_max_km,population,geonameid
En vue de sa création, notre programme commence par vérifier la présence des fichiers GeoNames. Si un fichier est absent ou si les informations fournies par le serveur indiquent qu’il a changé, il télécharge ou met à jour cities15000.zip, admin1CodesASCII.txt, admin2Codes.txt et countryInfo.txt.
Il charge ensuite les tables annexes permettant de transformer les codes pays, régions et subdivisions en libellés lisibles. Le fichier contenu dans cities15000.zip est alors parcouru ligne par ligne : pour chaque ville exploitable, le script récupère le nom, le pays, les coordonnées, les subdivisions administratives, la population et l’identifiant GeoNames.
Avant d’écrire le résultat, le programme applique si nécessaire quelques corrections de libellés administratifs, notamment pour franciser ou normaliser certaines régions et départements français et ainsi éviter les absences d’accent et des noms comme Upper Garonne, Correze, New Aquitaine, Brittany ou Normandy, un petit fichier de corrections modifiable corrections_subdivisions_geographiques.csv a été préalablement créé.

Avant remplacement d’une précédente version, le script conserve deux sauvegardes roulantes, puis affiche un bilan du traitement : nombre de villes lues, exportées, pays distincts et champs administratifs renseignés.


Dans le cas de Paris, on obtient (fichier villes_reference_GeoNames.csv) :
geonameid | 2988507 : identifiant unique GeoNames de Paris ; |
name | Paris : nom principal ; |
asciiname | Paris : nom ASCII, sans caractères spéciaux si besoin ; |
alternatenames | Paris,Parigi,Parijs,… : variantes ou noms dans d’autres langues ; |
latitude | 48.85341 |
longitude | 2.3488 |
feature class | P : classe de l’objet géographique, ici lieu peuplé ; |
feature code | PPLC : code indiquant une capitale politique ; |
country code | FR : code pays ; |
cc2 | |
admin1 code | 11 : code administratif de niveau 1, ici l’Île-de-France ; |
admin2 code | 75 : code administratif de niveau 2, ici Paris ; |
admin3 code | |
admin4 code | |
population | 2138551 : population ; |
elevation | |
dem | |
timezone | Europe/Paris : fuseau horaire ; |
modification date | 2024-09-05 : date de dernière modification de la fiche. |

Le script crée également un cache géographique cache_geographie.json qui rassemble dans une même structure les villes, les repères pays, les zones maritimes et les rattachements pays/zone. Il permet ainsi à nos scripts de transformer plus rapidement une position brute, par exemple une latitude et une longitude reçues depuis aircraft.json ou depuis l’API Airplanes.live, en une indication géographique plus lisible.
Dans sa forme actuelle, le cache contient cinq grandes parties :
{
« _meta »: {},
« villes »: [],
« pays »: [],
« zones_maritimes »: [],
« pays_zones »: []
}
La première section, _meta, ne contient pas de données géographiques directement utilisées pour localiser un avion. Elle sert plutôt à décrire le cache lui-même : version du cache, programme qui l’a créé, date de création, fichiers sources utilisés et empreintes de contrôle. Ces empreintes permettent de savoir si les fichiers d’origine ont changé et si le cache doit être reconstruit.
Les quatre autres sections contiennent les données utiles :
| Section | Rôle |
villes | Liste des villes issues de villes_reference_GeoNames.csv, avec leurs coordonnées et leurs subdivisions administratives. |
pays | Points de repère géographiques par pays, utilisés comme solution de repli lorsqu’aucune ville proche n’est trouvée. |
zones_maritimes | Zones maritimes définies par des rectangles de latitude et longitude. |
pays_zones | Table de rattachement des pays à un continent ou à une zone de menu. |
Prenons à nouveau l’exemple de Paris dans la section villes. Dans le cache, l’entrée correspondante, directement issue du fichier villes_reference_GeoNames.csv, se présente sous cette forme :
{
« nom »: « Paris »,
« pays »: « France »,
« admin1_nom »: « Île-de-France »,
« admin2_nom »: « Paris »,
« lat »: « 48.85341 »,
« lon »: « 2.3488 »,
« distance_max_km »: « 100 »,
« population »: « 2138551 »,
« geonameid »: « 2988507 »
}
Lorsqu’un avion dispose d’une latitude et d’une longitude, le programme peut comparer sa position avec les entrées de la section villes. Il cherche alors la ville la plus proche et vérifie que la distance reste acceptable afin de produire une localisation lisible plutôt qu’un simple affichage de coordonnées.
Le principe d’utilisation du cache peut se résumer ainsi :
</> Python
import json
from pathlib import Path
chemin_cache = Path("Fichiers_GeoNames/cache_geographie.json")
with chemin_cache.open("r", encoding="utf-8") as f:
cache = json.load(f)
villes = cache.get("villes", [])La variable villes contient alors la liste des villes disponibles dans le cache. On peut ensuite rechercher une ville précise, par exemple Paris :
</> Python
paris = None
for ville in villes:
if ville.get("nom") == "Paris" and ville.get("pays") == "France":
paris = ville
break
if paris:
print(paris["lat"], paris["lon"])Ce petit exemple renverrait les coordonnées de Paris :
48.85341 2.3488
La section pays complète ce mécanisme. Elle ne décrit pas les villes, mais quelques points de référence par pays. Pour la France, l’entrée est plus générale :
{
« pays »: « France »,
« lat »: « 46.6 »,
« lon »: « 2.2 »
}
Cette référence n’a pas la précision d’une ville. Elle sert de solution de repli lorsqu’aucune ville proche ou aucune zone maritime ne permet de formuler une localisation satisfaisante.
La section pays_zones apporte une information différente. Elle ne sert pas directement à calculer une distance, mais à rattacher un pays à un continent ou à une zone de menu. Pour la France, l’entrée correspondante est de ce type :
{
« code_pays »: « FR »,
« code_pays_3 »: « FRA »,
« pays »: « France »,
« capitale »: « Paris »,
« continent_code »: « EU »,
« continent_nom »: « Europe »,
« zone_menu »: « Europe »,
« source_zone »: « continent_geonames »,
« population »: « 66987244 »,
« geonameid »: « 3017382 »,
« commentaire »: « »
}
Cette table permet, par exemple, de savoir que la France appartient au continent Europe et qu’elle doit être rangée dans la zone de menu Europe. Pour certains pays, ce rattachement peut être corrigé par le fichier corrections_zones_pays.csv, lorsque le classement opérationnel souhaité ne correspond pas strictement au continent fourni par GeoNames.


Le code complet de ce script [31-Gestion_Geonames.py // Test_Api_31g] peut être téléchargé à partir du lien placé en fin de document (fichier Test_Api_Part2.zip).
1.2. Mise en pratique _ Drones, ULM, etc.
Avec ce nouveau script, nous allons interroger l’API Airplanes.live à la recherche d’aéronefs atypiques répondant aux classifications suivantes :
SHIP : dirigeable
LO : drone horizontal
VFHC : drone vertical
BALL : ballon
GLID : planeur
GLIM : motoplaneur
ULAC : ULM
GYRO : autogire
UHEL : hélicoptère léger
PARA : parachute motorisé
MQ9 : drone MALE
ZZZZ : type non attribué (famille rajoutée depuis l’écriture de ces paragraphes)
Ces appareils étant assez peu nombreux, nous ne précisons pas de zone géographique. Le menu ne se contente pas de présenter une liste. Il donne aussi une indication immédiate sur l’intérêt d’une recherche en signalant, pour chaque type, le nombre d’appareils en vol.. Ces résultats sont conservés en mémoire, ce qui évite de relancer inutilement les mêmes appels si l’utilisateur se décide à un second choix.

L’exemple ci-dessus nous a conduits au Qatar. Il nous confirme le bon fonctionnement de notre gestion géographique.

Le choix « BALL – Ballon » abouti à l’affichage d’un appareil non identifié (UNIDENT)
Depuis plusieurs mois, cet appareil apparaît régulièrement au-dessus du Qatar. Il est parfois possible de le suivre pendant plusieurs heures d’affilée. Des observateurs signalent fréquemment des trajectoires répétitives, en cercles ou en boucles, dans cette zone. On ne connaît pas son identité réelle : UNIDENT signifie que l’identification publique est absente, incomplète ou non fiable. ADS-B Exchange rappelle d’ailleurs que certains transpondeurs peuvent être mal configurés et que des appareils militaires peuvent utiliser des codes hexadécimaux faux, invalides ou non attribués. L’hypothèse la plus raisonnable, compte tenu du lieu, de la durée des vols et des trajectoires d’orbite observées, est celle d’un appareil militaire, parapublic ou d’un drone de surveillance, peut-être lié à la zone sensible de Doha / Al Udeid. Mais cela reste une supposition, car les sources publiques ne permettent d’affirmer ni son type, ni son pays, ni son opérateur, et encore moins sa mission exacte.
Mais revenons-en à notre script. Jusqu’ici, nos scripts interrogeaient une zone géographique autour de la station, ou recherchaient un appareil précis à partir de son code ICAO ou de son immatriculation. Ici, la question posée à l’API est autre, le programme n’utilise plus l’appel géographique de type /point/latitude/longitude/rayon, mais l’appel /type/code_type. Chaque code, par exemple SHIP, FFLO, VFHC, BALL, GLID ou ULAC, correspond à une famille d’appareils que l’on souhaite interroger directement.
</> Python
import json
from urllib.request import Request, urlopen
BASE_API = "https://api.airplanes.live/v2"
TYPES_A_INTERROGER = {
"SHIP": "dirigeable",
"FFLO": "drone horizontal",
"VFHC": "drone vertical",
"BALL": "ballon",
"GLID": "planeur",
"ULAC": "ULM",
"GYRO": "autogire",
"PARA": "parachute motorisé",
"MQ9": "drone militaire",
}
def interroger_type(code_type):
"""
Interroge Airplanes.live pour un seul type d'appareil.
La réponse attendue contient une rubrique ac, comme pour les autres
appels de l'API, mais la sélection est ici faite par code type.
"""
url = f"{BASE_API}/type/{code_type}"
requete = Request(
url,
headers={
"User-Agent": "F80543-HamAnalyst-API-Tests/23"
}
)
with urlopen(requete, timeout=20) as reponse:
data = json.load(reponse)
return data.get("ac", [])Ce premier extrait montre le principe de base : un type est choisi, une adresse API est construite, puis la réponse est lue comme dans les autres programmes. La rubrique ac contient alors les appareils retournés par Airplanes.live pour ce type précis. L’intérêt est que la recherche n’est plus limitée aux environs immédiats de notre station. Elle peut faire apparaître des appareils atypiques visibles ailleurs, dès lors qu’ils sont connus de l’API au moment de l’interrogation.
Afin de rendre le programme plus pratique, un menu est proposé au lancement. Il permet de choisir un seul type, plusieurs types, ou tous les types prévus. Avant l’affichage de ce menu, le programme peut interroger rapidement chaque type afin d’indiquer le nombre d’appareils actuellement visibles. Si cette préparation prend quelques secondes, une simple succession de points fait patienter l’utilisateur.
</> Python
def preparer_menu_types():
"""
Prépare le menu de choix des types.
Pour chaque type, le programme interroge l'API une première fois,
compte les appareils retournés et conserve les résultats en mémoire.
"""
resultats_par_type = {}
print("Préparation du menu", end="", flush=True)
for code_type in TYPES_A_INTERROGER:
print(".", end="", flush=True)
try:
appareils = interroger_type(code_type)
except Exception:
appareils = []
resultats_par_type[code_type] = appareils
print()
return resultats_par_type
def afficher_menu_types(resultats_par_type):
"""
Affiche le menu des types avec le nombre d'appareils visibles.
"""
print()
print("Choix des types à interroger sur Airplanes.live")
print("------------------------------------------------------")
for numero, (code_type, description) in enumerate(TYPES_A_INTERROGER.items(), start=1):
nombre = len(resultats_par_type.get(code_type, []))
print(f"{numero:2d} - {code_type:<5} : {description:<22} {nombre:4d} en vol")
print()
print("T - Tous les types")
print("Q - Quitter")Lorsque l’API Airplanes.live fournit une latitude et une longitude pour un appareil trouvé, le programme ne les affiche pas immédiatement sous forme de coordonnées brutes. Il utilise les références géographiques locales déjà préparées, notamment le fichier villes_reference_GeoNames.csv, les références par pays et, lorsque c’est utile, les zones maritimes.
Le principe reste simple. Pour chaque appareil disposant d’une position exploitable, le programme compare sa latitude et sa longitude avec les points de référence connus. Il calcule la distance entre l’appareil et les villes du fichier GeoNames, puis retient la ville la plus pertinente. GeoNames permet ici d’obtenir non seulement le nom de la ville, mais aussi sa subdivision administrative (le département pour la France), sa région et son pays. C’est ce qui permet de produire une localisation lisible, par exemple : près de Mont-de-Marsan — Landes, Nouvelle-Aquitaine (France) ou à environ 42 km de Toulouse — Haute-Garonne, Occitanie (France).
Si aucune ville ne donne un résultat satisfaisant, le programme tente une seconde lecture. Il vérifie si la position correspond à une zone maritime connue, par exemple le golfe de Gascogne ou la Méditerranée occidentale. À défaut, il se rabat sur une référence pays, moins précise mais toujours plus parlante qu’une simple paire de coordonnées. En dernier recours seulement, il affiche la latitude et la longitude brutes.
Avant l’affichage final, une dernière mise en forme rend la localisation plus naturelle, avec des formulations du type près de… ou à environ … km de…. Le résultat est ensuite intégré au tableau du programme, avec l’identification de l’appareil, le type recherché, la nature de la donnée, l’altitude, la distance à ma station locale, la vitesse et la localisation.
En fin de traitement, il reste possible d’ouvrir un ou plusieurs appareils sur Airplanes.live afin de visualiser leur position sur la carte.
Le code complet de ce script [23-Drones_ULM_etc.py // Test_Api_23a] peut être téléchargé à partir du lien placé en fin de document (fichier Test_Api_Part2.zip).
2. Bases Plane-Alert
Au paragraphe 5 du treizième volet de nos tutoriels, nous avons découvert l’intérêt des fichiers Plane-Alert (https://github.com/sdr-enthusiasts/plane-alert-db). Ces bases, qui recensent certains appareils gouvernementaux, militaires, de police ou civils particuliers, nous ont permis de commencer à trier, enrichir et surveiller des listes d’aéronefs présentant un intérêt spécifique.
Le programme [17-Interroger_Plane_Alert.py // Test_Api_17t.py] jouait alors un double rôle. Il exploitait ces références pour nos recherches, mais il assurait aussi le téléchargement des sources et la reconstruction des fichiers enrichis nécessaires à leur utilisation. Cette solution avait l’avantage de fonctionner, mais elle mélangeait dans un même script deux tâches de nature différente : la préparation des bases et leur exploitation.
Nous allons donc maintenant séparer clairement les rôles. Le programme présenté ici sera consacré à la gestion des fichiers Plane-Alert : téléchargement des sources, vérification de leur évolution et reconstruction des index enrichis. La recherche d’un appareil, la création ou la mise à jour de listes prioritaires, ainsi que l’exploitation croisée des bases locales préparées dans ce tutoriel, seront abordées plus tard, dans le tutoriel 15.
2.1. Gestion Plane-Alert (Test_Api_32.py)
Voici [32-Gestion_Plane_Alert_Enrichi.py // Test_Api_32.py], il prépare les données afin que les autres programmes les exploitent. Avant de reconstruire les index, il compare les empreintes des fichiers sources avec celles déjà mémorisées. Si rien n’a changé, il évite une reconstruction inutile. Si un fichier source a été modifié, ajouté ou supprimé, il régénère les index enrichis et leurs métadonnées. Le script lit les fichiers Plane-Alert officiels (plane-alert-gov.csv, plane-alert-mil.csv, plane-alert-pol.csv et éventuellement plane-alert-civ.csv) et crée les fichiers enrichis correspondants en utilisant les fichiers locaux d’aide à l’enrichissement, comme les mots-clés de pays, les préfixes d’immatriculation ou les rattachements de référence.
La principale amélioration touche les images. Le script télécharge aussi les fichiers Plane-Alert « images », les lit comme des sources complémentaires, puis les utilise pour remplir automatiquement les quatre champs d’images (vides mais déjà présents) dans les index enrichis. En conséquence, les fichiers *-images.csv entrent eux aussi dans le contrôle des empreintes, ce qui permet de reconstruire les index en cas de changement.
</> Python
def completer_images_depuis_index(ligne: dict, index_images: dict) -> dict:
"""
Complète image1 à image4 d'une ligne Plane-Alert enrichie
à partir d'un index construit depuis les fichiers
plane-alert-*-images.csv.
"""
icao = (ligne.get("icao") or "").strip().upper()
immat = (ligne.get("registration") or "").strip().upper()
entree_images = None
if icao and icao in index_images:
entree_images = index_images[icao]
elif immat and immat in index_images:
entree_images = index_images[immat]
if not entree_images:
return ligne
for champ in ("image1", "image2", "image3", "image4"):
if not (ligne.get(champ) or "").strip():
ligne[champ] = (entree_images.get(champ) or "").strip()
return ligneLe code complet de ce script [32-Gestion_Plane_Alert_Enrichi.py // Test_Api_32e.py] peut être téléchargé à partir du lien placé en fin de document (fichier Test_Api_Part2.zip).
2.2. Traitement des messages ADS-B Exchange (Test_Api_20.py)
L’un des nombreux intérêts du site ADS-B Exchange (https://globe.adsbexchange.com/) est de proposer un filtre military. Une fois activé, celui-ci permet notamment de recevoir une alerte par courriel lorsqu’un appareil militaire s’approche d’un point géographique défini.

Je l’ai ainsi configuré afin d’être averti dès qu’un appareil de ce type passe à moins de 15 milles nautiques, soit environ 28 kilomètres, de ma station LFCZ.

À l’usage, j’ai constaté qu’un certain nombre de ces appareils n’étaient pas connus des bases Plane-Alert. À l’aide des données reçues, j’ai donc décidé de créer mes propres fichiers locaux, construits dans le même esprit :
plane-alert-LFCZ.csv, qui conserve une structure simple, proche des bases Plane-Alert officielles et compatible avec elles ;
plane-alert-LFCZ-enrichi.csv, qui ajoute des champs plus détaillés, dans la continuité des index enrichis utilisés par mes programmes.
Les messages sont régulièrement sauvegardés dans un répertoire dédié, en leur attribuant un nom incrémenté : MSG_001.msg, MSG_002.msg, etc. Le script [20-Traiter_Messages_ADSB-Ex.py // Test-Api-20s.py] en extrait les informations utiles et les transforme en données locales réutilisables. Celui-ci tente plusieurs méthodes de lecture ; en premier la solution propre avec extract-msg, puis Outlook/COM, et enfin une lecture brute de secours.
</> Python
def lire_message_msg(chemin: Path) -> Tuple[Optional[str], str]:
"""Lit un .msg en essayant plusieurs methodes."""
essais = [
("extract-msg", lire_msg_extract_msg),
("Outlook/COM", lire_msg_outlook_com),
("lecture brute", lire_msg_brut),
]
for nom_methode, fonction in essais:
texte = fonction(chemin)
if texte and len(texte.strip()) > 20:
return texte, nom_methode
return None, "aucune methode utilisable"Ces messages d’alerte peu structurés peuvent ainsi être ramenés à des champs exploitables : immatriculation éventuelle, type ICAO, altitude, distance, latitude et longitude.
</> Python
def extraire_blocs_adsb_exchange(texte: str) -> List[Dict[str, str]]:
"""Extrait tous les blocs standard des alertes ADS-B Exchange."""
motif = re.compile(
r"""
(?:
(?P<registration>(?:[A-Z]{1,3}\.)?[A-Z0-9]{1,5}-[A-Z0-9]{1,6}|[A-Z0-9]{1,10})
\s*,\s*
)?
\s*,?\s*a[n]?\s*
(?P<icao_type>[A-Z0-9]{2,5})?
\s*at\s+
(?P<altitude>-?\d+(?:[\.,]\d+)?)\s*ft
\s+is\s+within\s+a\s+
(?P<distance>\d(?:\s*\d)*)\s*nm
\s+radius\s+of\s+your\s+location\s*
\(\s*(?P<lat>-?\d+(?:[\.,]\d+)?)\s*,\s*(?P<lon>-?\d+(?:[\.,]\d+)?)\s*\)
""",
re.IGNORECASE | re.VERBOSE | re.DOTALL,Reprenons l’extrait ci-dessus. La fonction extraire_blocs_adsb_exchange() reçoit en entrée le texte complet d’un message d’alerte ADS-B Exchange. Son objectif est d’y retrouver automatiquement les lignes correspondant à une alerte standard, puis d’en extraire les informations utiles sous forme de champs nommés : immatriculation éventuelle, type d’appareil, altitude, distance, latitude et longitude.
Le cœur du traitement repose sur une expression régulière, c’est-à-dire un motif de recherche capable de reconnaître une structure de texte. Ici, le programme ne cherche pas seulement un mot isolé : il tente de retrouver une phrase du type :
F-XXXX, an A400 at 12000 ft is within a 15 nm radius of your location (44.0793, -1.2585)
Le motif commence par une partie optionnelle consacrée à l’immatriculation :
(?:
(?P<registration>(?:[A-Z]{1,3}\.)?[A-Z0-9]{1,5}-[A-Z0-9]{1,6}|[A-Z0-9]{1,10})
\s*,\s*
)?
Le point important est la syntaxe (?P<registration>…). Elle crée un groupe nommé registration, ce qui permettra ensuite de récupérer directement l’immatriculation trouvée. Cette immatriculation peut prendre plusieurs formes : avec un tiret, comme F-RABC, éventuellement avec un préfixe suivi d’un point, ou sous une forme plus compacte composée de lettres et de chiffres. Le bloc complet est suivi d’un ?, ce qui signifie qu’il est facultatif : certaines alertes peuvent ne pas fournir d’immatriculation clairement exploitable.
La suite du motif accepte ensuite la formulation anglaise de l’alerte :
\s*,?\s*a[n]?\s*
(?P<icao_type>[A-Z0-9]{2,5})?
Cette partie permet de reconnaître a ou an, suivi du type court de l’appareil, par exemple A400 C130 B350 ou R135. Là encore, le groupe nommé icao_type facilite la récupération de cette information. Le type est rendu facultatif, car tous les messages ne sont pas toujours parfaitement renseignés.
Le programme recherche ensuite l’altitude :
\s*at\s+
(?P<altitude>-?\d+(?:[\.,]\d+)?)\s*ft
Le mot at est attendu, puis une valeur numérique est capturée dans le groupe altitude. Le motif accepte aussi bien un entier qu’un nombre avec virgule ou point décimal. L’unité ft confirme que la valeur correspond bien à une altitude en pieds.
La distance à la station est ensuite extraite avec :
\s+is\s+within\s+a\s+
(?P<distance>\d(?:\s*\d)*)\s*nm
\s+radius\s+of\s+your\s+location\s*
Cette partie reconnaît la phrase standard is within a … nm radius of your location. La distance est récupérée dans le groupe distance. Le motif autorise même des espaces à l’intérieur du nombre, ce qui rend la lecture plus tolérante si le message contient une mise en forme imparfaite.
Enfin, la position géographique de référence est lue entre parenthèses :
\(\s*(?P<lat>-?\d+(?:[\.,]\d+)?)\s*,\s*(?P<lon>-?\d+(?:[\.,]\d+)?)\s*\)
Les groupes lat et lon récupèrent la latitude et la longitude. Le signe moins est accepté, ce qui est indispensable pour les longitudes ou latitudes négatives. Là encore, le motif accepte aussi bien le point que la virgule comme séparateur décimal.
Trois options accompagnent cette expression régulière :
re.IGNORECASE | re.VERBOSE | re.DOTALL
re.IGNORECASE rend la recherche insensible aux majuscules et minuscules. re.VERBOSE permet d’écrire l’expression régulière sur plusieurs lignes, avec des espaces et des commentaires, ce qui la rend beaucoup plus lisible. re.DOTALL autorise le motif à traverser plusieurs lignes si le message d’origine contient des retours à la ligne.
Plus loin, le script vérifie si l’appareil signalé est déjà connu dans les fichiers Plane-Alert officiels. Si c’est le cas, les informations extraites du message sont complétées par les données existantes : opérateur, type d’appareil, catégorie ou rattachement. Si l’appareil n’est pas connu, il est conservé dans la base locale dès lors que l’alerte contient un code ICAO et des informations suffisantes.
Le code complet de ce script [20-Traiter_Messages_ADSB-Ex.py // Test-Api-20s.py] peut être téléchargé à partir du lien placé en fin de document (fichier Test_Api_Part2.zip).
3. Base OurAirports
Certains traitements font apparaître des codes d’aéroports plutôt que des noms directement lisibles. Ces codes, qu’ils soient au format IATA ou OACI, sont très pratiques pour les logiciels, mais moins parlants dans un tableau destiné au lecteur. Le rôle de [33-Gestion_OurAirports.py // Test_Api_33d.py] sera donc de préparer une référence locale capable de transformer rapidement un code comme LFPG, LFBZ ou CDG en informations compréhensibles : nom de l’aéroport, ville, pays, région et coordonnées.
À ne pas confondre : le code OACI d’un aéroport est un indicateur de quatre lettres ; à ne pas confondre avec l’adresse ICAO 24 bits d’un aéronef, généralement affichée sous la forme de six caractères hexadécimaux.
Pour constituer cette référence, nous nous appuierons sur le site OurAirports, qui met à disposition des fichiers ouverts décrivant les aéroports, les pays et les régions du monde. Ces fichiers constituent une source simple à exploiter : ils peuvent être téléchargés, lus comme des fichiers CSV, puis adaptés à nos propres besoins.
Ce programme ne recherche aucun avion et n’interroge aucune API de trafic. Il appartient à la même famille que les scripts de gestion GeoNames et Plane-Alert : il télécharge des sources ouvertes, les contrôle, les simplifie, puis produit des fichiers directement réutilisables par les programmes fonctionnels. Le traitement est ainsi effectué une seule fois, au lieu d’être répété à chaque recherche de vol.
3.1. Gestion OurAirports (Test_Api_33.py)
Le script utilise trois exports CSV distincts. airports.csv décrit les aérodromes ; countries.csv associe les codes pays à leurs libellés ; regions.csv fournit les noms des régions correspondant aux codes administratifs présents dans le fichier principal. Les adresses de téléchargement et toute l’architecture locale sont regroupées au début du programme.
</> Python
DOSSIER_OURAIRPORTS = "Fichiers_Airports"
DOSSIER_SOURCES_AIRPORTS = "Sources_OurAirports"
DOSSIER_OLD = "OLD"
URLS_OURAIRPORTS = {
"airports": "https://davidmegginson.github.io/ourairports-data/airports.csv",
"countries": "https://davidmegginson.github.io/ourairports-data/countries.csv",
"regions": "https://davidmegginson.github.io/ourairports-data/regions.csv",
}Au lancement, le programme ne retélécharge pas systématiquement plusieurs dizaines de milliers de lignes. Il commence par envoyer une requête HTTP de type HEAD demandant l’en-tête du fichier distant, sans récupérer son contenu complet. Ce retour est comparé aux valeurs mémorisées dans aeroports_reference.csv.meta.json. Lorsque les deux versions possèdent un ETag, celui-ci est prioritaire. Si rien n’a changé et que les trois sorties locales existent, le programme s’arrête sans reconstruire la base.


Une fois les trois sources disponibles, countries.csv et regions.csv sont d’abord convertis en petits dictionnaires. Le script peut ainsi remplacer les codes FR ou FR-IDF rencontrés dans airports.csv par des libellés lisibles. Chaque ligne du fichier principal est ensuite normalisée.
Pour déterminer le meilleur code OACI disponible, le programme consulte successivement icao_code, gps_code puis, en dernier recours, ident si celui-ci présente bien la forme attendue sur quatre caractères. Le code IATA est, lui, conservé seulement s’il comporte trois caractères.
</> Python
def extraire_icao(row: dict) -> str:
icao = normaliser_code(row.get("icao_code"), 4)
if icao:
return icao
gps_code = normaliser_code(row.get("gps_code"), 4)
if gps_code:
return gps_code
ident = normaliser_code(row.get("ident"))
if len(ident) == 4 and "-" not in ident:
return ident
return ""
# Une ligne sans aucun code OACI ni IATA est écartée.
if not icao and not iata:
return NoneLe CSV produit contient dix-huit colonnes. Elles peuvent être regroupées ainsi :
Codes : icao, iata, ident_ourairports, gps_code et local_code.
Identification : nom_aeroport, ville, pays, iso_country, region, iso_region et continen
Caractéristiques : type, service_regulier et altitude_ft.
Position et recherche : latitude, longitude et mots_cles.


Le CSV reste utile pour une lecture humaine, un contrôle ou un tri dans un tableur. Il serait en revanche peu efficace de le parcourir intégralement à chaque apparition d’un code dans une route. Le programme crée donc en parallèle aeroports_reference_index.json, dont la structure est pensée pour une recherche directe.
</> Python
index = {
"_meta": {...},
"par_icao": {},
"par_iata": {},
"doublons_icao": {},
"doublons_iata": {},
"statistiques": stats,
}
if icao not in index["par_icao"]:
index["par_icao"][icao] = mini
else:
index["doublons_icao"].setdefault(
icao, [index["par_icao"][icao]]
).append(mini)
if iata not in index["par_iata"]:
index["par_iata"][iata] = miniLes sections par_icao et par_iata jouent le rôle de deux dictionnaires. Une recherche sur un code de quatre lettres ou de trois caractères accède directement à une fiche réduite contenant les éléments utiles : codes, nom, ville, pays, région, type et coordonnées. Les éventuels codes non uniques ne sont pas supprimés : la première fiche reste dans l’index principal et toutes les variantes sont également consignées dans doublons_icao ou doublons_iata.


Cette organisation permettra notamment à un futur script (mon prochain Tutoriel N°15) d’enrichir les informations de route retournées par FlightRadar24. Un code d’origine ou de destination peut ainsi être développé localement sans requête supplémentaire vers un service extérieur.
Le fichier aeroports_reference.csv.meta.json conserve également les adresses sources, leurs dates et tailles, l’empreinte SHA-256 de chaque téléchargement, puis les empreintes et tailles des deux sorties. Il fournit enfin le nombre de lignes lues, le nombre d’aéroports conservés, le nombre de lignes écartées et la répartition des types présents dans la source. Il devient donc à la fois un témoin de mise à jour et un compte rendu technique du traitement.

Le code complet de ce script [33-Gestion_OurAirports.py // Test_Api_33d.py] peut être téléchargé à partir du lien placé en fin de document (fichier Test_Api_Part2.zip).
3.2. Interrogation de la base locale
À des fins de vérification et de démonstration, une entrée de consultation a été ajoutée au script. Elle permet d’interroger directement la base locale qui vient d’être créée, sans passer par un autre programme. L’utilisateur saisit simplement un code d’aéroport, au format OACI ou IATA, et le script recherche la fiche correspondante dans l’index local.
Cette consultation n’a pas vocation à remplacer les futurs traitements automatisés. Elle sert surtout à contrôler que les fichiers ont bien été téléchargés, que l’index JSON a été correctement construit et que les informations attendues peuvent être retrouvées rapidement. Dans l’exemple ci-dessous, la saisie du code LFPG permet ainsi de retrouver la fiche de l’aéroport Paris-Charles-de-Gaulle, avec son nom, sa ville, son pays, sa région, son type, ses coordonnées et quelques informations complémentaires.
Cette fonction de test est également utile sur le plan pédagogique : elle montre concrètement l’intérêt du fichier local. Un code court, peu parlant pour le lecteur, peut être transformé immédiatement en une fiche lisible, sans nouvelle récupération distante et sans traitement complexe au moment de l’affichage.

4. Base Callsign (Test_Api_34.py)
Les indicatifs tactiques, ou callsigns, employés par les forces armées sont très utiles pour suivre un vol, mais ils sont variables : un même sigle peut être réattribué à une autre cellule et un même aéronef peut en utiliser plusieurs. Lorsqu’un correspondant signale observer « ATOS1 » sans donner son adresse Mode-S, nos programmes qui savent exploiter un code Hex ICAO ou une immatriculation ont bien du mal à retourner un résultat. Mieux relier un callsign au dernier code ICAO connu, tel sera le rôle du script [34-Gestion_Callsign.py // Test_Api_34m.py].
Pour créer sa base initiale France sur douze mois, le programme va interroger certaines pages du site Live Military Mode-S au moyen de requêtes HTTP, puis utiliser Beautiful Soup pour analyser les tableaux qu’elles contiennent. Beautiful Soup est une bibliothèque Python spécialisée dans la lecture du code HTML : elle permet de repérer les lignes et les cellules d’un tableau afin d’en extraire automatiquement les données sous une forme exploitable par le programme.
Les éléments propres à ce programme sont regroupés dans un unique répertoire. Trois fichiers principaux y sont utilisés ou produits :
plages_icao_pays_avec_continent.csv associe chaque pays à ses plages d’adresses ICAO et à sa zone géographique afin d’organiser les menus par continent et de vérifier la cohérence des codes Hex collectés ;
index_callsign.csv constitue la base locale principale et conserve, pour chaque couple pays–callsign, le dernier code ICAO connu, ainsi que l’immatriculation fournie par la source et la date de la dernière observation ;
etat_callsign_pays.json mémorise les pays déjà intégrés dans la base, leur période de couverture et la date jusqu’à laquelle leur mise à jour a été menée à bien.
Le sous-répertoire travail_en_cours reçoit les fichiers temporaires et les points de reprise créés pendant les traitements longs. Il permet au programme de reprendre une création ou une mise à jour interrompue sans recommencer l’ensemble des consultations déjà effectuées
Le code complet de ce script [34-Gestion_Callsign.py // Test_Api_34m.py] peut être téléchargé à partir du lien placé en fin de document (fichier Test_Api_Part2.zip).
4.1. Création initiale France sur douze mois
Dans cette version intermédiaire du script, le choix 1 propose la création initiale à la France. La période va de la date du jour moins douze mois jusqu’à la date du lancement, bornes incluses. Pour chaque journée, le script envoie une recherche limitée à France, transforme la réponse HTML en observations structurées, rejette les codes ICAO situés hors des plages françaises, normalise le callsign et la date UTC, puis applique la règle du « dernier ICAO connu ». Dans l’essai présenté ici, 366 journées ont été interrogées.
Avant d’analyser le tableau HTML retourné par le site, le programme doit construire une véritable requête de recherche en associant à l’adresse de base le pays et la date à examiner.
</> Python
def parametres_recherche(
pays: PaysReference,
jour: date,
) -> Dict[str, str]:
return {
"Country": pays.nom_source,
"Location": "",
"Plane": "",
"ZoekCallsign": "",
"ZoekDate": jour.isoformat(),
"ZoekHexCode": "",
"ZoekSerial": "",
"submit": "Search",
}
La fonction prépare ici les paramètres attendus par le moteur de recherche de Live Military Mode-S. Pour la France, Country reçoit la valeur France, tandis que ZoekDate contient la date traitée au format international, par exemple 2026-07-14. Les autres critères restent vides puisque la recherche porte seulement sur un pays et une journée.
Ces paramètres sont ensuite transmis avec la requête HTTP :
return session.get(
URL_RECHERCHE,
params=params,
timeout=TIMEOUT_HTTP_SECONDES,
allow_redirects=True,
)
Le paramètre params=params demande à la bibliothèque requests de compléter automatiquement l’adresse avec les critères de recherche. La requête produite correspond ainsi à une adresse de cette nature :
https://live-mobile-mode-s.eu/zoekresult2.php
?Country=France
&ZoekDate=2026-07-14
<&submit=Search
Ouverte seule dans un navigateur, l’adresse zoekresult2.php affichera You must fill in some data, car elle ne reçoit aucun critère. Il ne s’agit pas une page autonome, mais d’une page de traitement chargée de produire les résultats d’une recherche. Le programme lui transmet donc les informations nécessaires avant de récupérer et d’analyser le tableau HTML obtenu.

Cet autre extrait ci-dessous montre comment le programme repère le tableau HTML pertinent à partir de son contenu, plutôt que de supposer qu’il se trouve toujours à une position déterminée dans la page.
</> Python
soup = BeautifulSoup(html, "html.parser")
for table in soup.find_all("table"):
for position, ligne in enumerate(table.find_all("tr")):
textes = [texte_cellule(c) for c in ligne.find_all(["th", "td"], recursive=False)]
normes = {normaliser_entete(texte) for texte in textes}
if "callsign" in normes and "modes" in normes:
entetes = textes
debut_donnees = position + 1
table_exploitable = True
break
if not table_exploitable:
raise ErreurCollecte(
"Aucun tableau Mode-S / Callsign reconnu."
)
Le parseur ne suppose pas que le tableau utile soit toujours le premier. Il recherche les en-têtes attendus, ignore les lignes inexploitables et refuse de valider la journée si la structure du site a changé, si une page anti-robot est retournée ou si le plafond de résultats semble atteint. Cette validation défensive évite qu’une page vide ou tronquée soit prise pour une journée sans trafic.
Un mécanisme de « battement » affiché dans la console signale que le programme reste actif lorsqu’une réponse du site tarde à parvenir.
</> Python
with ThreadPoolExecutor(max_workers=1) as executor:
futur = executor.submit(appel)
while True:
try:
reponse = futur.result(timeout=INTERVALLE_BATTEMENT_SECONDES)
print("\r" + " " * 150 + "\r", end="", flush=True)
return reponse
except FutureTimeoutError:
attente = int(time.monotonic() - debut)
print(
f"\r{libelle} - attente de la réponse... {attente} s",
end="", flush=True,
)La requête réseau est exécutée dans un thread de travail. Toutes les cinq secondes, la boucle principale reprend la main pour afficher la durée d’attente. L’utilisateur voit ainsi que le programme n’est pas bloqué. En cas d’échec, trois tentatives sont effectuées avec une pause progressive avant que la journée soit déclarée non validée.
Ici, le programme calcule en temps réel l’avancement du traitement et estime le temps restant à partir de la durée moyenne des journées déjà analysées.
</> Python
elapsed = time.monotonic() - debut_chrono
moyenne = elapsed / numero
restant = moyenne * (nombre_total - numero)
pourcentage = numero * 100 / nombre_total
print(
f"{pays.pays:<24} | {numero:>3}/{nombre_total:<3} | "
f"{pourcentage:>5.1f} % | {jour.isoformat()} | "
f"obs {len(observations):>4} | index {nombre_index_pays:>5} | "
f"écoulé {duree_lisible(elapsed)} | reste ~{duree_lisible(restant)}"
)La première estimation du temps restant varie fortement, car elle repose encore sur très peu de journées. Elle se stabilise ensuite à mesure que la moyenne de traitement devient représentative. Tous les sept jours, l’index temporaire et un JSON de progression sont enregistrés dans travail_en_cours ; une interruption volontaire ou une erreur réseau peut donc être reprise sans recommencer les douze mois.
Ce dernier extrait montre comment le programme sécurise l’enregistrement du fichier CSV et du fichier JSON en les écrivant d’abord dans des fichiers temporaires, avant de remplacer les versions existantes seulement lorsque l’opération s’est correctement terminée.
</> Python
def ecrire_texte_atomique(chemin: Path, texte: str) -> None:
temporaire = chemin.with_name(chemin.name + ".tmp")
with temporaire.open("w", encoding="utf-8", newline="") as fichier:
fichier.write(texte)
fichier.flush()
os.fsync(fichier.fileno())
os.replace(temporaire, chemin)Le fichier définitif n’est jamais réécrit directement. Le contenu est d’abord sauvegardé dans un fichier temporaire, forcé sur le disque, puis substitué en une seule opération avec os.replace(). Une coupure pendant l’écriture limite ainsi le risque de laisser un CSV ou un JSON partiellement écrit. Des versions .old1 et .old2 sont en outre conservées dans le sous-dossier OLD.
La capture suivante montre les divers messages retournés par le script à l’occasion de la création de la base initiale.


Chaque ligne correspond à une journée. 1/366 indique la position dans la période, le pourcentage mesure l’avancement, obs donne le nombre d’observations valides trouvées ce jour-là et index le nombre de callsigns français distincts déjà conservés. Le temps restant est encore instable pendant les premières secondes, puis se précise. Après la septième journée, un point de reprise est automatiquement enregistré.
La création s’est terminée après 16 min 13 s. Le site a fourni 76 758 observations exploitables, mais le CSV final ne contient que 5 255 callsigns distincts, car seul le dernier rapprochement pays + callsign → ICAO est conservé. Les deux chemins affichés confirment la création de index_callsign.csv et de etat_callsign_pays.json dans Fichiers_Callsign.
4.2. Mise à jour
Le choix 6 lit etat_callsign_pays.json pour connaître les pays présents et, pour chacun, la dernière journée complètement couverte. Dans le cas français présenté, la base avait été créée jusqu’au 14 juillet 2026. Lancée le 15 juillet, la mise à jour relit volontairement le 14 juillet puis traite le 15 juillet : ce chevauchement d’un jour permet de récupérer une publication tardive, tandis que le dédoublonnage empêche d’ajouter deux fois la même observation.

Lorsqu’un callsign est nouveau, une ligne est ajoutée. Si le callsign existe déjà mais qu’une observation plus récente fournit un autre ICAO, la ligne est remplacée. Une observation plus ancienne est ignorée. Lorsque deux codes ICAO différents sont associés au même callsign au même instant, une seule ligne est conservée dans index_callsign.csv. Les champs ICAO et immatriculation sont laissés vides. On suppose qu’une observation ultérieure, plus récente et non ambiguë, remplacera cette ligne par la nouvelle association valide. Par exemple, si ces deux observations sont reçues au même instant :
FR ; ATOS1 ; 3B7683 ; 2026-07-15T10:30:00Z
FR ; ATOS1 ; 3B7637 ; 2026-07-15T10:30:00Z
le fichier final contient une seule ligne :
FR;ATOS1;;;2026-07-15T10:30:00Z;live_mobile_mode_s;2026-07-15T10:35:00Z;ambigu
Cette capture d’écran montre le lancement d’une mise à jour. La base était couverte jusqu’au 14/07/2026 (partiellement). Nous sommes le 15 juillet.

Le lancement du 15 juillet traite deux journées : le 14 est relu par sécurité, puis le 15 est ajouté. Le dédoublonnage fait passer l’index de 5 255 à 5 259 callsigns sans reconstruire les douze mois. La couverture du JSON est ensuite portée au 15/07/2026, et le menu général réapparaît après seulement trois secondes.
4.3. Interrogation de la base locale
À des fins de vérification et de démonstration, plusieurs entrées de consultation ont été ajoutées au script. Elles permettent d’interroger directement l’index local des callsigns, sans relancer la collecte sur le site source. L’utilisateur saisit un callsign complet ou seulement ses premières lettres, puis le programme recherche les correspondances déjà conservées dans index_callsign.csv.
L’exemple retenu ici est volontairement parlant. Le préfixe ATOS renvoie aux indicatifs associés à la Patrouille de France. L’orthographe officielle couramment rencontrée est plutôt Athos, nom donné aux pilotes de la Patrouille ; le ministère des Armées rappelle d’ailleurs que les pilotes s’interpellent avec leur numéro accolé à Athos, indicatif générique de la PAF, en référence à la devise Un pour tous, tous pour un. Dans les observations ADS-B exploitées ici, cette famille d’indicatifs apparaît sous la forme normalisée ATOS, sans distinction de casse ni d’orthographe typographique. L’exemple est d’autant plus intéressant que la Patrouille de France évolue sur Alpha Jet, appareil que l’on retrouve bien dans les enrichissements Plane-Alert accompagnés du nom du constructeur : Dassault Aviation.
Le premier choix du menu effectue une recherche large. En saisissant ATOS, le programme retourne toutes les lignes dont le callsign commence par ce préfixe. La capture montre ici 32 correspondances, classées d’abord par code ICAO, puis, pour un même ICAO, par date de dernière observation décroissante. Ce mode conserve donc plusieurs variantes lorsque le même appareil a été observé avec plusieurs indicatifs proches : ATOS, ATOS0015, ATOS016, ATOS8, etc. C’est précisément l’intérêt de cette consultation : elle ne se limite pas à une réponse unique, mais permet de visualiser la diversité des callsigns récemment associés à une même famille d’appareils.

Le second choix répond à un besoin différent. Il ne cherche plus à afficher toutes les variantes, mais à ne conserver que le dernier callsign connu pour chaque code ICAO. Dans notre exemple, la liste passe ainsi de 32 correspondances à 15 appareils. Pour chaque ICAO, le programme ne garde que l’observation la plus récente, ce qui donne une lecture beaucoup plus synthétique de la base. Ce mode est particulièrement utile lorsque l’on souhaite retrouver rapidement quel appareil se cache probablement derrière un indicatif, sans être gêné par les anciennes variantes conservées dans l’index. Ce 14 juillet 2026, la Patrouille de France ouvrait le défilé aérien au-dessus des Champs-Élysées. Dans notre base Callsign, l’enregistrement montre trois appareils de cette famille d’indicatifs observés et enregistrés par la source consultée. Il ne faut pas en déduire que seuls ces appareils étaient engagés, mais simplement que ceux-là ont laissé une trace exploitable dans les données collectées.

Cette fonction de consultation n’est pas seulement pratique pour vérifier la base Callsign. Elle prépare aussi les usages des programmes suivants. Lorsqu’un appareil est signalé par un indicatif tactique, mais que son code ICAO n’est pas immédiatement connu, l’index local pourra fournir une piste de recherche rapide nous ramenant au code Hex pour interroger d’autres sources, compléter un fichier prioritaire ou retrouver l’appareil dans les affichages normalisés.
Conclusion
Après ce travail de clarification, nos principales bases locales disposent désormais d’une organisation plus cohérente. Les références géographiques, les index Plane-Alert enrichis, les données d’aéroports et la base Callsign ne sont plus dispersés dans les scripts d’exploitation : chacun de ces ensembles est préparé par un programme dédié, puis rangé dans un dossier clairement identifié.
Ce choix peut sembler moins spectaculaire qu’une recherche en temps réel ou qu’un affichage d’appareils en vol, mais il constitue une étape importante. En séparant la préparation des données de leur utilisation, nous rendons nos programmes plus lisibles, plus faciles à maintenir et moins dépendants de traitements répétés inutilement. Les scripts fonctionnels pourront ainsi se concentrer sur leur rôle principal : interroger, comparer, rechercher, afficher ou surveiller.
À ce stade, les principales bases locales sont en place : villes_reference_GeoNames.csvet cache_geographie.json pour la géographie, plane-alert-index-enrichi.csv pour Plane-Alert, aeroports_reference.csv et aeroports_reference_index.json pour OurAirports, puis index_callsign.csv et etat_callsign_pays.json pour les callsigns militaires. Ces fichiers constituent désormais le socle commun sur lequel les scripts fonctionnels pourront s’appuyer.
Je vous donne donc rendez-vous dans un prochain tutoriel, où nous reviendrons d’abord sur notre script d’interrogation et de création de listes d’avions prioritaires. Nous verrons comment il peut désormais s’appuyer sur ces bases locales mieux structurées pour rechercher plus efficacement un appareil, exploiter les références Plane-Alert, créer ou compléter des fichiers PRIO, et préparer des affichages plus homogènes.
Ce sera aussi l’occasion de poursuivre nos essais autour des sources complémentaires, notamment les API payantes, afin de déterminer ce qu’elles apportent réellement par rapport à l’API gratuite Airplanes.live. Autrement dit, maintenant que les fondations locales sont en place, nous pourrons revenir à ce qui nous intéresse le plus directement : faire parler les données ADS-B de manière plus précise, plus rapide et plus utile.
Fichiers à télécharger
François Paget est aujourd’hui retraité. Membre fondateur du McAfee Labs, il y travailla de 1993 à 2016 comme spécialiste de la sécurité informatique et de la cybercriminalité. Il s’est notamment intéressé aux programmes malveillants, aux nouvelles menaces numériques, à la fraude financière et aux cyber-conflits. Longtemps Secrétaire général du CLUSIF, il anima son Panorama de la Cybercriminalité de 2002 à 2017 et publia en 2005, chez Dunod, l’ouvrage Vers & Virus.
Toujours curieux de techniques nouvelles, il s’est également passionné, à partir de 2021, pour la radio logicielle, ou SDR. Il a depuis publié plusieurs articles de vulgarisation consacrés au décodage de signaux numériques, notamment dans la revue Radioamateurs France et sur les sites ON5VL et HamAnalyst. Ses expérimentations récentes portent en particulier sur la réception ADS-B dans la bande des 1090 MHz.
Avec son épouse Yvette, il partage son temps entre la région parisienne et les Landes. Ils ont trois enfants et six petits-enfants.






