Chargement de fichiers GPX

Le format GPX (GPS Exchange Format) est couramment utilisé pour stocker des données GPS, générées par un téléphone portable lors d'un parcours de course par exemple. Dans ce chapitre, nous allons étudier la structure d'un tel fichier afin d'en extraire différentes informations comme la distance totale parcourue, la vitesse moyenne, jusqu'à la représentation du parcours sur une carte.

Le format XML

Le format GPX utlise la syntaxe XML (Extensible Markup Language) qui permet de structurer des informations à l'aide d'éléments (tag en anglais) délimités par les caractères < et >.

Ci-dessous, un extrait du fichier XML character.xml

<?xml version="1.0" encoding="UTF-8"?>
<bigbangtheory>
    <character id="46567">
        <lastname>Cooper</lastname>
        <firstname>Sheldon</firstname>
        <gender>M</gender>
    </character>
    <character id="12874">
        <lastname>?</lastname>
        <firstname>Penny</firstname>
        <gender>F</gender>
    </character>
    <character id="12355">
        <firstname>Leonard</firstname>
        <lastname>Hofstadter</lastname>
        <gender>M</gender>
    </character>
    <character id="978776">
        <lastname>Wolowitz</lastname>
        <firstname>Howard</firstname>
        <gender>M</gender>
    </character>
    <character id="34345">
        <gender>M</gender>
        <firstname>Raj</firstname>
        <lastname>Koothrappali</lastname>
    </character>
 </bigbangtheory>

Ce fichier recense les personnages principaux de la série américaine Big Bang Theory. Vous remarquerez que les informations sont organisées sous la forme d'une arborescence traduite par l'imbrication des éléments : l'élément racine bigbangtheory contient plusieurs éléments enfants character. Chaque élément enfant peut lui-même contenir des enfants (lastname, firstname et gender dans ce cas précis).

Vue arborescente d'un fichier XML
Vue arborescente d'un fichier XML

Ainsi, le contenu d'un élément délimité par une balise ouvrante <tagname> et une balise fermante </tagname> peut être :

Un élément peut aussi être associé à un attribut qui apparaît dans la balise ouvrante sous la forme d'un couple (nom,valeur). Dans notre exemple, chaque élément character dispose d'un attribut id.

Le module lxml

La structure d'un fichier XML étant un peu plus complexe que celle d'un fichier CSV, nous allons nous appuyer sur le module lxml pour parcourir le contenu du fichier afin d'en extraire certaines informations. Comme tous les modules externes, il doit être installé avant d'être utilisé. La commande sous Windows est la suivante :

python.exe -m pip install lxml

Et sous macOS

python3 -m pip install lxml

Attention !

Cette commande doit être exécutée en mode administrateur. Sous Windows, dans le menu Démarrer, clic droit sur l'application "invite de commandes" et choisir "exécuter en tant qu'administrateur".

Un fois le module importé dans le script, on charge le contenu du fichier XML en mémoire à l'aide de la méthode parse de l'objet etree.

from lxml import etree

root = etree.parse("character.xml")

Extraction d'informations avec XPath

XPath permet de définir un chemin dans l'arborescence des données XML afin d'extraire une information particulière. Par exemple, le code ci-dessous permet de récupérer, sous la forme d'une liste, les prénoms de tous les personnages.

from lxml import etree

root = etree.parse("character.xml")

firstnameList = root.xpath("/bigbangtheory/character/firstname")
for firstname in firstnameList:
  print(firstname.text)

La recherche est effectuée à partir de la racine de l'arborescence (variable root) afin d'extraire tous les éléments firstname, enfant d'un élément character lui-même enfant de l'élément bigbangtheory.

XPath
Mise en évidence du chemin de recherche XPath

Info

La structure for firstname in firstnameList permet de parcourir toutes les valeurs de la liste firstnameList en initialisant la variable firstname avec la valeur de la case courante.

L'accès à la valeur d'un attribut d'un élément donné se fait grâce à la méthode get(). Le script suivant affiche les identifiants de tous les personnages.

from lxml import etree

root = etree.parse("character.xml")

characterList = root.xpath("/bigbangtheory/character")
for character in characterList:
  print(character.get("id"))

Enfin, lorsque l'on a besoin d'accéder à plusieurs valeurs au sein d'un élément, il est déconseillé d'appeler plusieurs fois la méthode xpath() qui effectue un parcours complet de la structure XML. Il est plutôt recommandé d'utiliser la méthode getChildren() qui permet d'accéder à liste des enfants d'un élément. Le script ci-dessous affiche l'identifiant ainsi que le nom et le prénom de chaque personnage :

from lxml import etree

root = etree.parse("character.xml")

print("\n\n+ Liste des personnages principaux :")
characterList = root.xpath("/bigbangtheory/character")
for character in characterList:
  id = character.get("id")
  lastname = ""
  firstname = ""
  for child in character.getchildren():
        if child.tag == "lastname":
            lastname = child.text
        if child.tag == "firstname":
            firstname = child.text
  print("(" + id + ") " + lastname + " " + firstname)

Info

La propriété tag permet d'accéder au nom d'un élément et la propriété text permet de récupérer le texte contenu entre la balise ouvrante et fermante de l'élément.

Exercice

Modifiez le script précédent afin de calculer la proportion de personnages féminins et masculins dans Big Bang Theory.

+ Liste des personnages principaux :
(46567) Cooper Sheldon
(12874) ? Penny
(12355) Hofstadter Leonard
(978776) Wolowitz Howard
(34345) Koothrappali Raj
(75543) Bloom Stuart
(65238) Fowler Amy Farrah
(45984) Rostenkowski Bernadette
(17360) Jeffries Arthur
(91727) Kripke Barry
(12757) Winkle Leslie
(761289) Sweeney Emily
(13247) Winkle Leslie
(97852) Koothrappali Priya
(57689) Wheaton Wil
(91557) Gibbs Dave
(836411) Barnett Stephanie


+ Proportion d'hommes et de femmes :
Femmes : 47%
Hommes : 53%
Correction
from lxml import etree

root = etree.parse("user.xml")

femaleCount = 0
maleCount = 0

print("\n\n+ Liste des personnages principaux :")
characterList = root.xpath("/bigbangtheory/character")
for character in characterList:
    id = character.get("id")
    lastname = ""
    firstname = ""
    for child in character.getchildren():
        if child.tag == "lastname":
            lastname = child.text
        if child.tag == "firstname":
            firstname = child.text
        if child.tag == "gender":
            if child.text == "F":
                femaleCount = femaleCount + 1
            else:
                maleCount = maleCount + 1

    print("(" + id + ") " + lastname + " " + firstname)

print("\n\n+ Proportion d'hommes et de femmes :")
print("Femmes : " + str(round(femaleCount*100/len(characterList))) + "%")
print("Hommes : " + str(round(maleCount*100/len(characterList))) + "%\n\n")

Le format GPX

Maintenant que nous nous sommes familiarisés avec le format XML et les méthodes du module lxml, il est temps de disséquer un fichier GPX contenant des relevés GPS. Je vous propose de commencer avec un extrait du fichier RATJ2012-21km-herve.schely.gpx.

<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1"
     creator="runtastic - makes sports funtastic, http://www.runtastic.com"
     xsi:schemaLocation="http://www.topografix.com/GPX/1/1
                         http://www.topografix.com/GPX/1/1/gpx.xsd
                         http://www.garmin.com/xmlschemas/GpxExtensions/v3
                         http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd
                         http://www.garmin.com/xmlschemas/TrackPointExtension/v1
                         http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd"
     xmlns="http://www.topografix.com/GPX/1/1"
     xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
     xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <metadata>
    <copyright author="www.runtastic.com">
      <year>2012</year>
      <license>http://www.runtastic.com</license>
    </copyright>
    <link href="http://www.runtastic.com">
      <text>runtastic</text>
    </link>
    <time>2012-10-21T07:01:38.000Z</time>
  </metadata>
  <trk>
    <link href="http://www.runtastic.com/sport-sessions/29614704">
      <text>Cliquez sur ce lien pour voir cette activité sur runtastic.com</text>
    </link>
    <trkseg>
      <trkpt lon="4.0281324386596697" lat="49.2516021728515980">
        <ele>82.0</ele>
        <time>2012-10-21T07:01:39.000Z</time>
      </trkpt>
      <trkpt lon="4.0278654098510698" lat="49.2515068054199006">
        <ele>82.0</ele>
        <time>2012-10-21T07:01:52.000Z</time>
      </trkpt>
      <trkpt lon="4.0278654098510698" lat="49.2515068054199006">
        <ele>82.0</ele>
        <time>2012-10-21T07:02:12.000Z</time>
      </trkpt>
    </trkseg>
  </trk>
</gpx>

Espace de nommage

Le format GPX utilise un espace de nommage (xmlns pour XML namespace en anglais). Cette utilisation des espaces de nommage s'avère utile lors de la fusion de plusieurs fichiers XML afin d'éviter des conflits dans les noms des éléments de l'arborescence XML. Concrètement, le nom complet de l'élément trk d'un fichier GPX est : {http://www.topografix.com/GPX/1/1}trk.

L'utilisation des espaces de nommage a aussi des conséquences sur l'utilisation de la méthode xpath() pour l'extraction des éléments d'un fichier GPX. Le script suivant extrait tous les éléments trkpt.

from lxml import etree

root = etree.parse("RATJ2012-21km-herve.schely.gpx")
ns = 'http://www.topografix.com/GPX/1/1'

trackpointlist = root.xpath("/ns:gpx/ns:trk/ns:trkseg/ns:trkpt",
                            namespaces={"ns": ns})

Info

Pour des soucis de concision, j'ai créé une variable Python ns qui contient le nom, assez long, de l'espace de nommage d'un fichier GPX.

Exercice

Après avoir étudié minutieusement la structure du fichier d'exemple, écrivez un script GPXplorer.py qui remplit trois listes avec les données de longitude, latitude et altitude contenues dans l'arborescence.

Correction
from lxml import etree

# Liste des latitudes exprimées en degrés
latitude  = []

# Liste des longitudes exprimées en degrés
longitude = []

# Liste des altitudes exprimées en mètres
elevation = []

root = etree.parse("RATJ2012-21km-herve.schely.gpx")
ns = "http://www.topografix.com/GPX/1/1"

trackpointlist = root.xpath("/ns:gpx/ns:trk/ns:trkseg/ns:trkpt",
                            namespaces={"ns": ns}) 
for point in trackpointlist:
    latitude.append(float(point.get("lat")))
    longitude.append(float(point.get("lon")))
    for param in point.getchildren():
        if param.tag == "{" + ns + "}ele":
            elevation.append(float(param.text))

Dans le chapitre Visualisation de trajectoires, nous avons appris à créer des graphiques à l'aide du module matplotlib.

Exercice

Modifiez le scriptGPXplorer.py afin de générer un graphique à partir des relevés d'altitude.

relevés d'altitudes
Correction
from lxml import etree
import matplotlib.pyplot as plt

# Liste des latitudes exprimées en degrés
latitude  = []

# Liste des longitudes exprimées en degrés
longitude = []

# Liste des altitudes exprimées en mètres
elevation = []

root = etree.parse("RATJ2012-21km-herve.schely.gpx")
ns = "http://www.topografix.com/GPX/1/1"

trackpointlist = root.xpath("/ns:gpx/ns:trk/ns:trkseg/ns:trkpt",
                            namespaces={"ns": ns}) 
for point in trackpointlist:
    latitude.append(float(point.get("lat")))
    longitude.append(float(point.get("lon")))
    for param in point.getchildren():
        if param.tag == "{" + ns + "}ele":
            elevation.append(float(param.text))

plt.plot(range(0,len(elevation)), elevation)
plt.ylim(0, 200)
plt.title("Relevé d'altitudes")
plt.ylabel("hauteur (m)")
plt.show()

Pour évaluer des distances à partir de couples (latitude, longitude), nous allons avoir besoin de quelques rudiments de trigonométrie, c'est justement l'objet du prochain chapitre.