Rapports datés, os.scandir et shutil
Créer un dossier de rapports, nommer chaque rapport avec sa date pour qu'il se trie tout seul, lister un dossier avec os.scandir, calculer l'âge d'un fichier, puis copier, déplacer, archiver et supprimer avec shutil.
Mis à jour le
À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.
Pourquoi on voit ça
Ton outil vérifie le parc tous les jours et écrit un rapport à chaque passage. Très vite, tu as des dizaines de fichiers. Il faut les nommer pour retrouver le plus récent sans réfléchir, les lister, repérer ceux qui sont trop vieux, puis les archiver ou les supprimer.
C’est le quotidien du DevOps : rotation de logs, sauvegardes, nettoyage. Python le fait avec trois modules de base : os, datetime et shutil. La leçon se termine par la fonction la plus dangereuse du chapitre.
Un dossier de rapports : os.makedirs
Avant d’écrire un rapport, le dossier doit exister.
import os
os.makedirs("reports/2026", exist_ok=True)
os.makedirs("reports/2026", exist_ok=True)
print("ok")
ok
Deux choses à savoir :
makedirscrée aussi les dossiers intermédiaires : icireports, puis2026dedans.exist_ok=Trueveut dire « si le dossier existe déjà, ce n’est pas grave ». Sans lui, le deuxième appel plante.
os.makedirs("reports/2026")
FileExistsError: [Errno 17] File exists: 'reports/2026'
Un outil relancé tous les jours trouve le dossier déjà là dès le deuxième jour. Il lui faut donc toujours exist_ok=True.
Dater un rapport : datetime
datetime.now() donne la date et l’heure du moment. Pour les exemples, on fixe une date à la main.
from datetime import datetime
d = datetime(2026, 11, 14, 9, 5)
print(d.strftime("%Y-%m-%d"))
print(d.strftime("%Y-%m-%d_%H-%M"))
print(d.isoformat())
2026-11-14
2026-11-14_09-05
2026-11-14T09:05:00
strftime transforme une date en texte selon un format que tu choisis :
| Code | Sens | Exemple |
|---|---|---|
%Y |
année sur 4 chiffres | 2026 |
%m |
mois sur 2 chiffres | 11 |
%d |
jour sur 2 chiffres | 04 |
%H |
heure sur 2 chiffres | 09 |
%M |
minutes sur 2 chiffres | 05 |
Attention à la casse : %m est le mois, %M les minutes.
isoformat() ne prend pas de format : il écrit toujours AAAA-MM-JJ, un T, puis l’heure. C’est la forme standard pour une date dans un rapport JSON.
Le nom du rapport se construit avec une f-string :
nom = f"reports/report-{d.strftime('%Y-%m-%d')}.json"
print(nom)
reports/report-2026-11-14.json
Pourquoi AAAA-MM-JJ se trie tout seul
sorted et ls trient les noms comme du texte : caractère par caractère, de gauche à droite. Si le nom commence par l’année, puis le mois, puis le jour, l’ordre alphabétique est aussi l’ordre du temps.
iso = ["report-2026-11-02.json", "report-2026-10-30.json", "report-2025-12-25.json"]
fr = ["report-02-11-2026.json", "report-30-10-2026.json", "report-25-12-2025.json"]
print(sorted(iso))
print(sorted(fr))
['report-2025-12-25.json', 'report-2026-10-30.json', 'report-2026-11-02.json']
['report-02-11-2026.json', 'report-25-12-2025.json', 'report-30-10-2026.json']
Première ligne : du plus ancien au plus récent. Le dernier rapport est simplement sorted(iso)[-1]. Deuxième ligne : le tri regarde d’abord le jour, et l’ordre ne veut plus rien dire.
Les zéros comptent aussi : "2026-9-5" se range après "2026-10-1", car le caractère 9 est plus grand que 1. strftime écrit toujours 09 et 05, ce qui évite le piège.
Lister un dossier : os.scandir
os.scandir(dossier) donne une entrée par élément du dossier. Chaque entrée porte trois informations utiles :
| Écriture | Ce que ça donne |
|---|---|
entry.name |
le nom seul, sans le dossier devant : "a.json" |
entry.is_file() |
True pour un fichier, False pour un dossier |
entry.stat().st_mtime |
la date de dernière modification |
Avec un dossier reports qui contient a.json, b.json, notes.txt et un sous-dossier old :
import os
noms = []
for entry in os.scandir("reports"):
if entry.is_file() and entry.name.endswith(".json"):
noms.append(entry.name)
print(sorted(noms))
['a.json', 'b.json']
old est écarté par is_file(), notes.txt par endswith. L’ordre dans lequel scandir donne les entrées n’est pas garanti : si l’ordre compte, trie avec sorted.
Note bien les parenthèses : name est une valeur, is_file() et stat() sont des appels.
L’âge d’un fichier
st_mtime n’est pas une date lisible. C’est un nombre de secondes écoulées depuis le 1er janvier 1970. time.time() donne l’instant présent dans la même unité. L’âge d’un fichier est donc une simple soustraction :
import time
age_secondes = time.time() - entry.stat().st_mtime
age_jours = age_secondes / 86400
Un jour dure 86 400 secondes (24 × 60 × 60). Avec des nombres fixes, pour voir le calcul :
maintenant = 1_800_000_000
mtime = maintenant - 3 * 86400
print((maintenant - mtime) / 86400)
3.0
La rotation des rapports tient alors en une condition : if age_jours > 7:.
Copier, déplacer, archiver, supprimer : shutil
| Fonction | Ce qu’elle fait | La source existe encore ? |
|---|---|---|
shutil.copy2(src, dst) |
copie le fichier et ses métadonnées (dont la date de modification) | oui |
shutil.move(src, dst) |
déplace ou renomme | non |
shutil.make_archive(base, "zip", dossier) |
compresse un dossier dans base.zip |
oui |
shutil.rmtree(dossier) |
supprime le dossier et tout son contenu | non |
import shutil
shutil.copy2("reports/a.json", "a.bak")
shutil.move("reports/b.json", "archive/b.json")
shutil.make_archive("backup", "zip", "reports")
(Le dossier archive doit déjà exister : crée-le avec os.makedirs.) Trois détails qui tombent en QCM :
copy2garde la date. La copie a le mêmest_mtimeque l’original. Un calcul d’âge sur la copie donne donc le même résultat.make_archiveajoute l’extension. Tu écris"backup", tu obtiensbackup.zip. Avec"gztar"à la place de"zip", tu obtiensbackup.tar.gz.make_archivene supprime rien. Le dossierreportsest toujours là après.
rmtree : le danger
shutil.rmtree("reports") est l’équivalent de rm -rf reports. Il supprime le dossier, ses fichiers et ses sous-dossiers :
- sans confirmation ;
- sans corbeille : rien n’est récupérable ;
- quel que soit le contenu.
Le risque réel, c’est le chemin. Une variable vide ou mal construite, et tu effaces autre chose que prévu. Avant un rmtree, vérifie le chemin (affiche-le, ou journalise-le), et ne le construis jamais à partir d’une valeur que tu ne contrôles pas.
À retenir
os.makedirs(chemin, exist_ok=True)crée le dossier et ses parents, sans planter s’il existe déjà. Sansexist_ok, c’estFileExistsError.strftime("%Y-%m-%d")fabrique un nom daté ;isoformat()donne2026-11-14T09:05:00.- Un nom en AAAA-MM-JJ se trie tout seul : l’ordre alphabétique est l’ordre des dates.
os.scandirdonne des entrées :entry.name,entry.is_file(),entry.stat().st_mtime.- Âge d’un fichier en secondes :
time.time() - st_mtime. Un jour = 86 400 secondes. copy2copie avec les métadonnées,movedéplace,make_archivecompresse,rmtreesupprime tout, sans confirmation ni corbeille.
Entraînement
12 questions, dont 3 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.
Ta note
Cartes · 8
Réviser les cartes ici
Pourquoi