10 scripts PyQGIS pour automatiser QGIS
SIG

10 scripts PyQGIS prêts à l'emploi pour automatiser QGIS

QGISAutomatisationSIG

C’est une scène qui revient à chaque formation Python. Un stagiaire me montre son écran : 47 couches chargées une par une, à la main, tous les lundis matin. Je lui montre 15 lignes de PyQGIS. Silence dans la salle, puis la question que j’adore : “Mais… on peut automatiser quoi d’autre, comme ça ?”

À peu près tout ce qui vous fait perdre du temps, en fait.

Cette page rassemble les 10 scripts de ma série d’août : 10 automatisations PyQGIS à copier-coller dans la console Python de QGIS. Je les ai tous testés sur un même cas d’étude, réaliste et complet : l’identification de zones d’implantation photovoltaïque autour de Carcassonne. Parcelles candidates, postes sources, zones d’exclusion, la totale.

Gardez cette page dans vos favoris : elle est faite pour être rouverte un lundi matin, le jour où on vous livrera un dossier de 47 fichiers.

Avant de commencer

Tous ces scripts se lancent dans la console Python de QGIS (Ctrl+Alt+P, ou Cmd+Alt+P sur Mac), directement ou via son éditeur de scripts. Les lignes marquées “a adapter” attendent vos chemins et vos noms de champs. Pour les dossiers de sortie, j’utilise os.path.expanduser("~/...") : cette écriture pointe vers votre dossier personnel et fonctionne telle quelle sous Windows, macOS et Linux, sans les soucis de droits d’écriture des dossiers synchronisés (iCloud, OneDrive et compagnie, qui bloquent parfois les écritures des applications). Enfin, certains scripts modifient vos données : travaillez toujours sur une copie la première fois.

Une question qui revient souvent : “Ai-je vraiment besoin de tous ces imports ?” Dans la console Python de QGIS, pas toujours : l’environnement précharge déjà la plupart des objets, et QgsProject passe souvent sans rien importer. Les scripts de cette page gardent pourtant leurs imports, pour trois raisons. Ils fonctionnent partout : console, éditeur de scripts, plugin ou exécution externe. Ils ne coûtent que trois lignes. Et surtout, ils vous montrent d’où vient chaque objet que vous manipulez : quand vous voudrez aller plus loin dans la documentation PyQGIS, vous saurez exactement où chercher. Trois lignes qui transforment un copier-coller en apprentissage.

1. Un dossier entier chargé en un clic

On vous livre un dossier de fichiers en vrac : shapefiles, GeoPackage, GeoJSON, rasters. Le réflexe classique coûte 20 minutes de glisser-déposer. Celui-ci coûte 10 secondes : le script parcourt le dossier, charge tout ce qui est valide et range le résultat dans un groupe dédié.

import os
from qgis.core import QgsProject, QgsVectorLayer, QgsRasterLayer

dossier = r"C:/mes_donnees"  # a adapter
ext_vecteur = ('.shp', '.gpkg', '.geojson')
ext_raster = ('.tif', '.tiff')

racine = QgsProject.instance().layerTreeRoot()
groupe = racine.insertGroup(0, os.path.basename(dossier))

for fichier in sorted(os.listdir(dossier)):
    chemin = os.path.join(dossier, fichier)
    nom = os.path.splitext(fichier)[0]
    if fichier.lower().endswith(ext_vecteur):
        couche = QgsVectorLayer(chemin, nom, "ogr")
    elif fichier.lower().endswith(ext_raster):
        couche = QgsRasterLayer(chemin)
    else:
        continue
    if couche.isValid():
        QgsProject.instance().addMapLayer(couche, False)
        groupe.addLayer(couche)
    else:
        print(f"Ignore (invalide) : {fichier}")

2. Tout reprojeter en Lambert 93

Du Lambert 93, du WGS 84, un vieux Lambert II étendu qui traîne : les projets qui mélangent les SCR produisent des mesures fausses et des géotraitements qui hurlent. Ce script parcourt toutes les couches vecteur du projet, reprojette celles qui ne sont pas en EPSG:2154 et enregistre des GeoPackage propres.

import os
import processing
from qgis.core import QgsProject, QgsVectorLayer

epsg_cible = "EPSG:2154"
dossier_sortie = os.path.expanduser("~/sortie_2154")  # a adapter
os.makedirs(dossier_sortie, exist_ok=True)

for couche in list(QgsProject.instance().mapLayers().values()):
    if not isinstance(couche, QgsVectorLayer):
        continue
    if couche.crs().authid() == epsg_cible:
        continue
    sortie = os.path.join(dossier_sortie, couche.name() + ".gpkg")
    processing.run("native:reprojectlayer", {
        "INPUT": couche,
        "TARGET_CRS": epsg_cible,
        "OUTPUT": sortie
    })
    nouvelle = QgsVectorLayer(sortie, couche.name() + " (2154)", "ogr")
    QgsProject.instance().addMapLayer(nouvelle)
    print(f"Reprojete : {couche.name()}")

3. Le grand ménage attributaire

OBJECTID, Shape_Leng, Shape_Area : ces champs vous suivent de couche en couche sans que personne ne les utilise. Le script les supprime sur toutes les couches du projet où ils existent, et vous dit exactement ce qu’il a nettoyé. Attention, c’est le script destructif de la page : il modifie les fichiers sources, testez sur une copie.

from qgis.core import QgsProject, QgsVectorLayer

champs_a_supprimer = ["OBJECTID", "Shape_Leng", "Shape_Area"]  # a adapter

for couche in QgsProject.instance().mapLayers().values():
    if not isinstance(couche, QgsVectorLayer):
        continue
    index = [couche.fields().indexOf(n) for n in champs_a_supprimer]
    index = [i for i in index if i >= 0]
    if index:
        couche.startEditing()
        couche.deleteAttributes(index)
        couche.commitChanges()
        print(f"{couche.name()} : {len(index)} champ(s) supprime(s)")

L’IA est un amplificateur. Un géomaticien qui maîtrise ses scripts devient redoutable. Un géomaticien qui les survole devient dangereux, en plus rapide.

4. Tout le projet dans un GeoPackage

Un projet, 23 shapefiles, 6 dossiers : on a tous hérité de ce projet-là. Ce script empaquette toutes les couches vecteur du projet dans un GeoPackage unique, styles inclus. Un seul fichier à archiver, à envoyer, à versionner. Stylez vos couches avant de le lancer : les styles voyagent avec.

import os
import processing
from qgis.core import QgsProject, QgsVectorLayer

couches = [c for c in QgsProject.instance().mapLayers().values()
           if isinstance(c, QgsVectorLayer)]

processing.run("native:package", {
    "LAYERS": couches,
    "OUTPUT": os.path.expanduser("~/projet_consolide.gpkg"),  # a adapter
    "OVERWRITE": True,
    "SAVE_STYLES": True
})
print(f"{len(couches)} couches consolidees dans un seul GeoPackage")

5. Une couche découpée par attribut

Une couche, 16 communes, une livraison attendue commune par commune : à la main, l’après-midi y passe. Ce script génère un fichier par valeur du champ de votre choix, nommé automatiquement. Même recette pour 101 départements, des clients ou des types d’équipements. Sélectionnez la couche dans le panneau avant de lancer.

import os
import processing
from qgis.utils import iface

couche = iface.activeLayer()
champ = "nom"  # a adapter

processing.run("native:splitvectorlayer", {
    "INPUT": couche,
    "FIELD": champ,
    "FILE_TYPE": 0,  # 0 = GeoPackage
    "OUTPUT": os.path.expanduser("~/decoupage")  # a adapter
})
print("Decoupage termine, un fichier par valeur du champ")

6. Traquer et réparer les géométries invalides

Votre géotraitement plante “sans raison” ? La raison a un nom : géométries invalides. Polygones papillons, auto-intersections, anneaux mal fermés, invisibles à l’œil nu et mortels pour vos analyses. Ce script scanne la couche active, liste les entités fautives et génère une version réparée en mémoire (exportez-la d’un clic droit si elle vous convient). À lancer avant chaque grosse analyse.

import processing
from qgis.core import QgsProject
from qgis.utils import iface

couche = iface.activeLayer()

invalides = []
for ent in couche.getFeatures():
    g = ent.geometry()
    if g.isEmpty() or not g.isGeosValid():
        invalides.append(ent.id())

print(f"{len(invalides)} geometrie(s) invalide(s)")
print(f"Identifiants : {invalides[:20]}")

if invalides:
    resultat = processing.run("native:fixgeometries", {
        "INPUT": couche,
        "OUTPUT": "memory:version_reparee"
    })
    QgsProject.instance().addMapLayer(resultat["OUTPUT"])
    print("Couche reparee ajoutee au projet")

7. Le rapport statistique automatique

Votre chef ne veut pas une carte, il veut un tableau. Ce script transforme la couche active en rapport chiffré : effectifs, totaux, moyennes, mini et maxi par catégorie, exportés en CSV prêt à joindre à un mail. Le point-virgule en séparateur garantit l’ouverture propre dans Excel en français.

import csv
import os
from collections import defaultdict
from qgis.core import NULL
from qgis.utils import iface

couche = iface.activeLayer()
champ_categorie = "type"       # a adapter
champ_valeur = "surface_ha"    # a adapter
fichier_sortie = os.path.expanduser("~/rapport.csv")  # a adapter

stats = defaultdict(list)
for ent in couche.getFeatures():
    val = ent[champ_valeur]
    if val != NULL:
        stats[str(ent[champ_categorie])].append(float(val))

with open(fichier_sortie, "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f, delimiter=";")
    w.writerow(["categorie", "nb", "total", "moyenne", "mini", "maxi"])
    for cat, valeurs in sorted(stats.items()):
        w.writerow([
            cat, len(valeurs),
            round(sum(valeurs), 2),
            round(sum(valeurs) / len(valeurs), 2),
            round(min(valeurs), 2),
            round(max(valeurs), 2)
        ])
print(f"Rapport ecrit : {fichier_sortie}")

8. Tout un schéma PostGIS d’un coup

Charger 30 tables PostGIS une par une, c’est un bizutage. Ce script se connecte à votre base, détecte les tables spatiales d’un schéma et les charge toutes dans le projet. Si vos données sérieuses vivent en base (et elles devraient), il devient votre porte d’entrée quotidienne.

from qgis.core import (QgsDataSourceUri, QgsVectorLayer,
                       QgsProject, QgsProviderRegistry)

uri = QgsDataSourceUri()
uri.setConnection("localhost", "5432", "ma_base", "utilisateur", "mot_de_passe")  # a adapter
schema = "public"  # a adapter

md = QgsProviderRegistry.instance().providerMetadata("postgres")
conn = md.createConnection(uri.uri(False), {})

nb = 0
for table in conn.tables(schema):
    if not table.geometryColumn():
        continue
    uri.setDataSource(schema, table.tableName(), table.geometryColumn())
    couche = QgsVectorLayer(uri.uri(False), table.tableName(), "postgres")
    if couche.isValid():
        QgsProject.instance().addMapLayer(couche)
        nb += 1
print(f"{nb} table(s) spatiale(s) chargee(s) depuis le schema {schema}")

Deux remarques de terrain. D’abord, si psql fonctionne dans votre terminal mais que QGIS refuse la connexion : c’est normal, le terminal passe par un canal local sans mot de passe alors que QGIS passe par le réseau, qui en exige un (un ALTER USER ... WITH PASSWORD règle la question). Ensuite, la sécurité : le mot de passe en clair dans le script, c’est acceptable pour apprendre sur votre machine, jamais en production. Préférez alors un service PostgreSQL (pg_service.conf) ou la connexion enregistrée de QGIS, et ne partagez jamais un script contenant un mot de passe.

9. Des dizaines de cartes via l’atlas

Le duo gagnant de la mise en série : un atlas configuré dans votre mise en page (couche de couverture, expression de nom de fichier) et une quinzaine de lignes de Python qui exportent chaque page en PNG nommé automatiquement. Seize cartes communales pendant la pause café, deux cents demain avec le même script.

import os
from qgis.core import QgsProject, QgsLayoutExporter

nom_mise_en_page = "carte_commune"  # a adapter
dossier = os.path.expanduser("~/cartes")  # a adapter
os.makedirs(dossier, exist_ok=True)

layout = QgsProject.instance().layoutManager().layoutByName(nom_mise_en_page)
atlas = layout.atlas()

atlas.beginRender()
exporteur = QgsLayoutExporter(layout)
for i in range(atlas.count()):
    atlas.seekTo(i)
    layout.refresh()
    chemin = os.path.join(dossier, f"{atlas.currentFilename()}.png")
    exporteur.exportToImage(chemin, QgsLayoutExporter.ImageExportSettings())
    print(f"Exporte : {chemin}")
atlas.endRender()
print("Export termine")

10. Votre premier outil Processing

Le dernier script ne se lance pas dans la console : il s’enregistre via Boîte à outils > Créer un nouveau script, et devient un véritable outil QGIS avec son interface et ses paramètres. Utilisable par vos collègues qui ne liront jamais une ligne de Python, partageable dans toute votre organisation. C’est le vrai passage de niveau : de celui qui bricole à celui qui outille son service.

from qgis.processing import alg
from qgis import processing

@alg(name="tampon_dissous", label="Tampon dissous en un clic",
     group="mes_outils", group_label="Mes outils")
@alg.input(type=alg.SOURCE, name="INPUT", label="Couche d'entree")
@alg.input(type=alg.DISTANCE, name="DISTANCE", label="Distance", default=100)
@alg.input(type=alg.SINK, name="OUTPUT", label="Couche de sortie")
def tampon_dissous(instance, parameters, context, feedback, inputs):
    """Cree un tampon dissous en un clic, pret a partager."""
    resultat = processing.run("native:buffer", {
        "INPUT": parameters["INPUT"],
        "DISTANCE": parameters["DISTANCE"],
        "DISSOLVE": True,
        "SEGMENTS": 25,
        "END_CAP_STYLE": 0,
        "JOIN_STYLE": 0,
        "MITER_LIMIT": 2,
        "OUTPUT": parameters["OUTPUT"]
    }, context=context, feedback=feedback, is_child_algorithm=True)
    return {"OUTPUT": resultat["OUTPUT"]}

Le vrai déclic

Dix scripts, dix corvées éliminées. Mais le vrai déclic n’est pas dans le copier-coller : il est dans le réflexe. La prochaine fois qu’une tâche QGIS vous prend plus de 10 minutes de clics répétitifs, posez-vous LA question : “Un script ferait ça en combien de secondes ?” C’est ce réflexe qui sépare celui qui subit ses données de celui qui outille son service. Et c’est exactement par là que passe la suite… dont on reparle très vite.

Retour au blog
🪄

Prêt à déplacer
votre valeur vers le jugement ?

Rejoignez la communauté des Géomagiciens : la première communauté IA × Géomatique francophone. Formation, workshops en petit groupe, et 100 prompts SIG testés pour commencer dès aujourd'hui.

Rejoindre les Géomagiciens →Lire d'autres articles