Chargement de fichiers GPX

Le format GPX est couramment utilisé pour stocker des données GPS, générées par une montre connectée 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 qui permet de structurer des informations à l'aide d'éléments délimités par des balises (tags en anglais) ouvrante et fermante. 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>

Télécharger

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 attribut peut aussi être associé à un élément : il apparaît dans la balise ouvrante sous la forme d'un couple (clé,valeur). Dans notre exemple, chaque élément character dispose d'un attribut id.

Le module beautifulsoup4

La structure d'un fichier XML étant plus complexe que celle d'un fichier CSV, nous allons nous appuyer sur le module beautifulsoup4 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 :

pip.exe install beautifulsoup4 lxml

Et sous macOS ou Linux

pip3 install beautifulsoup4 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".

Si vous ne disposez pas des droits administrateur, vous pouvez ajouter l'option --user à la fin de la commande pour installer le module dans votre compte utilisateur.

Un fois le module importé dans le script, on charge le contenu du fichier XML en mémoire à l'aide de la fonction open() et on construit l'arborescence des données avec la fonction BeautifulSoup().

from bs4 import BeautifulSoup as bs

# Chargement du fichier XML
content = open('character.xml')

# Construction de l'arborescence
root = bs(content, features='xml')

Extraction d'informations

la méthode find_all() permet de rechercher un élément dans l'arborescence des données XML afin d'extraire une information particulière. Par exemple, le code ci-dessous permet d'afficher les prénoms de tous les personnages.

from bs4 import BeautifulSoup as bs

# Chargement du fichier XML
content = open('character.xml')

# Construction de l'arborescence
root = bs(content, features='xml')

for tag in root.find_all('firstname'):
    print(tag.string)

Info

La structure for tag in root.find_all('firstname'): permet de parcourir toutes les valeurs d'une liste en initialisant la variable tag avec la valeur de la case courante.

La recherche est effectuée à partir de la racine de l'arborescence 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 d'accès à l'élément firstname

L'accès à la valeur d'un attribut associé à un élément se fait via l'opérateur [ ]. Le script suivant affiche les identifiants de tous les personnages.

from bs4 import BeautifulSoup as bs

# Chargement du fichier XML
content = open('character.xml')

# Construction de l'arborescence
root = bs(content, features='xml')

for tag in root.find_all('character'):
    print(tag['id'])

Enfin, lorsque l'on a besoin d'accéder à plusieurs valeurs au sein d'un élément, il est possible d'appeler la méthode find() à partir de l'élément courant afin d'effectuer une recherche locale parmi ses enfants. Le script ci-dessous affiche l'identifiant ainsi que le nom et le prénom de chaque personnage :

from bs4 import BeautifulSoup as bs

content = open('character.xml')
root = bs(content, features='xml')

print('\n\n+ Liste des personnages principaux :')
for tag in root.find_all('character'):
    id = tag['id']
    # Recherches à partir de l'élément character
    # afin d'extraire les valeurs des enfants
    lastname = tag.find('lastname').string
    firstname = tag.find('firstname').string

    print('(', id, ')', lastname, firstname)

Info

La propriété name permet d'accéder au nom d'un élément et la propriété string 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 bs4 import BeautifulSoup as bs

content = open('character.xml')
root = bs(content, features='xml')

female_count = 0
male_count = 0

print('\n\n+ Liste des personnages principaux :')
for tag in root.find_all('character'):
    id = tag['id']
    lastname = tag.find('lastname').string
    firstname = tag.find('firstname').string
    gender = tag.find('gender').string
    if gender == 'F':
        female_count = female_count + 1
    else:
        male_count = male_count + 1

    print('(', id, ')', lastname, firstname)

character_count = female_count + male_count

print('\n\n+ Proportion d\'hommes et de femmes :')
print('Femmes :', round(female_count*100/character_count), '%')
print('Hommes :', round(male_count*100/character_count), '%\n\n')

Le format GPX

Maintenant que nous nous sommes familiarisés avec le format XML et les méthodes du module beautifulsoup4, 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>

Télécharger

Exercice

Après avoir étudié minutieusement la structure du fichier d'exemple, écrivez un script gpxplorer.py qui remplit un DataFrame composé des colonnes ['datetime', 'latitude', 'longitude', 'elevation'] à partir des données contenues dans l'arborescence XML.

Correction
from bs4 import BeautifulSoup as bs
import pandas as pd

df = pd.DataFrame([], columns = [ 'datetime', 'latitude', 'longitude', 'elevation' ])

# Chargement du fichier GPX
content = open('RATJ2012-21km-herve.schely.gpx')

# Construction de l'arborescnce
root = bs(content, features='xml')

k = 0
for point in root.find_all('trkpt'):
    df.loc[k, 'datetime'] = point.find('time').string
    df.loc[k, 'latitude'] = float(point['lat'])
    df.loc[k, 'longitude'] = float(point['lon'])
    df.loc[k, 'elevation'] = float(point.find('ele').string)
    k = k + 1

df.to_excel('RATJ2012-21km-herve.schely.xlsx', index=False)

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

Exercice

Modifiez le scriptgpxplorer.py afin de générer les deux graphiques ci-dessous.

relevés d'altitudes
trace GPS
Correction
from bs4 import BeautifulSoup as bs
import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame([], columns = [ 'datetime', 'latitude', 'longitude', 'elevation' ])

# Chargement du fichier GPX
content = open('RATJ2012-21km-herve.schely.gpx')

# Construction de l'arborescnce
root = bs(content, features='xml')

k = 0
for point in root.find_all('trkpt'):
    df.loc[k, 'datetime'] = point.find('time').string
    df.loc[k, 'latitude'] = float(point['lat'])
    df.loc[k, 'longitude'] = float(point['lon'])
    df.loc[k, 'elevation'] = float(point.find('ele').string)
    k = k + 1

df.to_excel('RATJ2012-21km-herve.schely.xlsx', index=False)
          
plt.plot(df['elevation'])
plt.ylim(0, 200)
plt.title('Relevé d\'altitudes')
plt.ylabel('hauteur (m)')
plt.show()

plt.plot(df['longitude'], df['latitude'])
plt.title('Trace GPS')
plt.xlabel('longitude (degrés)')
plt.ylabel('latitude (degrés)')
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.