Scraper des XML avec Python

Ca va paraître bizarre dit comme ça, mais je suis sur une très bonne lancée côté Python et j'ai décidé de remettre le couvert.

Du coup, après un premier tuto consacré à l'extraction de tableaux Wikipedia dans un csv, place à un autre morceau de choix : interroger un xml pour réenregistrer ses informations dans un fichier json.

Cet exemple ne sort pas de nulle part. A l'occasion des dernières régionales, je m'étais penché avec d'excellents camarades sur des cartes "entonnoir".

A la base, nous étions censés récupérer des tableurs auprès des dix préfectures départementales du Grand-Est, mais à cause de délais très tardifs de certaines d'entre elles il a fallu changer d'angle d'attaque.

Bref, on s'est rabattus avec Joël Matriche, Ettore Rizza et Tom Wersinger sur le jonglage avec des xml.

Je ne vais pas faire la version longue, mais il a fallu en gros :

  • préparer un script en json lisible par Google Refine (merci Ettore)
  • faire la manip pour chaque xml départemental
  • bosser sur les csv générés (merci Jo')
  • les mettre dans un tableur Google
  • utiliser un parser en PHP pour transformer ça en json (merci Tom)

Alors évidemment c'était à la base pas prévu de devoir traiter avec des xml. Mais à la question est-ce qu'un script Python aurait pu faire la même chose en deux coups de cuillère à pot ?, je répondrai sans ambage oui, et c'est parti pour la démonstration !

Les modules utilisés pour ce tutoriel

On va faire simple ici avec seulement deux modules à utiliser pour transformer nos xml en json. Les heureux élus sont :

  • lxml, un module qui permet de parcourir un xml comme si c'était un arbre de noeuds
  • json, pour évidemment manipuler un fichier json

Ce que va faire le script

On va donc parcourir notre arbre de noeuds pour extraire les infos qui nous intéressent. Le principal problème est la manière de mettre à jour le fichier json.

Pour rappel, le fichier json utilisé pour les régionales était structuré de la sorte :

[{objet1}, {objet2}, {objet3}, ..., {objet n}]

Chaque objet contenait des données comme :

  • le code INSEE, indispensable pour les jonctions avec des données géographiques
  • le nom de la commune
  • le contexte électoral (inscrits, abstentions, exprimés)
  • les voix de chaque liste

La structure d'un objet Javascript est strictement la même qu'un dictionnaire Python : de la même manière qu'un mot est rattaché à sa définition dans un dictionnaire, une clé est rattachée à une valeur.

On peut donc imaginer se servir de boucles for pour :

  • aspirer les données intéressantes pour chaque ville
  • les ranger dans un dictionnaire
  • ajouter chaque dictionnaire dans une liste

Une fois ceci fait, on n'aura plus qu'à se servir du module json pour transformer la liste de dictionnaires dans le bon format !

Le script étape par étape

Commençons par tester notre script sur le seul département des Ardennes (08). On commence, une fois n'est pas coutume, pas importer nos modules :

from lxml import etree
import json

Dans lxml, etree est la fonction qui va nous permettre de transformer un xml en arbre de noeuds. On paramètre alors en variables :

  • notre arbre de noeuds correspondant au fichier XML de tous les résultats des Ardennes
  • le code départemental (objectif : reconstituer le code INSEE complet de chacune de ses communes)
  • une liste vide dans laquelle on regroupera chacun de nos dictionnaires (ou objets si on se projette en JS)

Et c'est parti :

arbre = etree.parse("http://www.interieur.gouv.fr/avotreservice/elections/telechargements/RG2015/resultatsT2/44/4408/4408000.xml")
ardennes = "08"
liste_objets = []

De là, on sélectionner un premier noeud à l'intérieur d'arbre, qui va être le point de départ pour extraire nos différentes informations :

for noeud in arbre.xpath("//Election/Region/SectionElectorale/Communes/Commune"):
    objet = {}

On définit aussi l'objet qu'on va remplir au fur et à mesure. A l'intérieur du noeud principal, on va à chaque fois interroger un sous-noeud correspondant à une information intéressante. On va commencer typiquement par le code INSEE et le nom de la ville, comme suit :

for insee in noeud.xpath("CodSubCom"):
    objet["code insee"] = ardennes+insee.text
for commune in noeud.xpath("LibSubCom"):
    objet["ville"] = commune.text

Observez la manière dont on implémente le dictionnaire objet avec les commandes "objet[clé] = valeur. On continue ainsi avec les infos contextuelles du vote pour le second tour :

for resultats in noeud.xpath("Tours/Tour[NumTour=2]"): # sans [NumTour=2], on prendrait en compte les deux tours
    for inscrits in resultats.xpath("Mentions/Inscrits/Nombre"):
        objet["inscrits"] = int(inscrits.text) # par défaut la fonction text renvoie une chaîne de cara', donc on convertit !
    for abstentions in resultats.xpath("Mentions/Abstentions/Nombre"):
        objet["abstentions"] = int(abstentions.text)
    for exprimes in resultats.xpath("Mentions/Exprimes/Nombre"):
        objet["exprimes"] = int(exprimes.text)

Lors du passage dans les noeuds concernant chaque liste, j'ai paramétré deux variables locales pour affecter la nuance de liste en clé du dictionnaire, comme ceci :

for liste in resultats.xpath("Listes/Liste"):
    nu = ""
    vox = 0
    for nuance in liste.xpath("CodNuaListe"):
        nu = nuance.text
    for voix in liste.xpath("NbVoix"):
        vox = int(voix.text)
    objet[nu] = vox

Une fois ceci fait, on peut imprimer l'objet généré à chaque noeud "Commune" contenu dans le xml. Théoriquement, ça doit nous renvoyer des dictionnaires comme ceci :

{'ville': 'Vaux-Montreuil', 'LFN': 24, 'LDVG': 15, 'exprimes': 63, 'code insee': '08467', 'LUD': 24, 'abstentions': 36, 'inscrits': 99}

Pour le dire vite, on est bien barré, mais il faut encore mettre à jour la liste de dictionnaires Python pour ensuite la transformer en tableau d'objets JS (joie des conversions...).

Pour ce faire, cette commande bien placée suffit :

liste_objets.append(objet)

Il ne nous reste plus qu'à créer un fichier json et à lui faire digérer notre liste, comme suit :

ficjson = open('reg015_ardennes.json','w+')
ficjson.write(json.dumps(liste_objets))

Un petit coup d'oeil au fichier final montrera que tout est en ordre.

Automatiser le script pour tous les départements du Grand-Est

L'automatisation de ce script à plusieurs départements va être grandement facilitée par les URL des différents fichiers. En gros, il suffit juste de placer les codes départementaux aux bons endroits pour passer de l'un à l'autre.

Du coup, un tableau contenant tous les codes départementaux va permettre de paramétrer ça. Pour le script en entier, voilà ce que ça donne :

from lxml import etree
import json

departements = ["08", "10","51","52","54", "55", "57", "67", "68", "88"]
liste_objets = []

for departement in departements:
    arbre = etree.parse("http://www.interieur.gouv.fr/avotreservice/elections/telechargements/RG2015/resultatsT2/44/44"+departement+"/44"+departement+"000.xml")
    print ("Department numero "+departement)
    for noeud in arbre.xpath("//Election/Region/SectionElectorale/Communes/Commune"):
        objet = {}
        for insee in noeud.xpath("CodSubCom"):
            objet["code insee"] = departement+insee.text
        for commune in noeud.xpath("LibSubCom"):
            objet["ville"] = commune.text
        for resultats in noeud.xpath("Tours/Tour[NumTour=2]"):
            for inscrits in resultats.xpath("Mentions/Inscrits/Nombre"):
                objet["inscrits"] = int(inscrits.text)
            for abstentions in resultats.xpath("Mentions/Abstentions/Nombre"):
                objet["abstentions"] = int(abstentions.text)
            for exprimes in resultats.xpath("Mentions/Exprimes/Nombre"):
                objet["exprimes"] = int(exprimes.text)
            for liste in resultats.xpath("Listes/Liste"):
                nu = ""
                vox = 0
                for nuance in liste.xpath("CodNuaListe"):
                    nu = nuance.text
                for voix in liste.xpath("NbVoix"):
                    vox = int(voix.text)
                objet[nu] = vox
            print(objet)
            liste_objets.append(objet)

ficjson = open('reg015_grandest.json','w+')
ficjson.write(json.dumps(liste_objets))

Quelques remarques

J'ai abusé des boucles for pour ces scripts, peut-être trop. Ce n'est donc pas forcément la syntaxe la plus propre, même si je ne vois pas bien comment j'aurais pu formuler les choses autrement.

Ceci dit, en deux minutes l'ensemble des données a bien été créé pour la totalité des communes du Grand-Est. Plutôt pas mal !

J'ai également mis en ligne ces deux scripts via mon compte Github.

Par Raphaël da Silva dans la catégorie
Tags : #python, #webscraping, #xml,