Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 919 minDébutant

La bibliothèque standard pour l'admin : os, shutil, subprocess, datetime

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • lire une configuration dans les variables d'environnement ;
  • copier, archiver et mesurer l'espace disque avec shutil ;
  • lancer une commande avec subprocess sans risque d'injection ;
  • calculer des dates et des durées, vérifier une empreinte, archiver en zip.

3 exercices corrigés1 schéma

La bibliothèque standard de Python contient, sans rien installer, les modules dont un administrateur a besoin chaque jour : os pour l'environnement, shutil pour copier, déplacer et archiver, subprocess pour lancer des commandes du système, datetime pour les dates et les durées, hashlib pour les empreintes et zipfile pour les archives. Ce chapitre présente chacun avec un exemple d'administration réel : configuration par variables d'environnement, commande externe avec code de retour et délai, nom horodaté d'une sauvegarde, vérification d'un fichier téléchargé, archivage de journaux.

os et les variables d'environnement

Le module os donne accès au système d'exploitation. Son usage le plus courant dans un script : lire la configuration dans les variables d'environnement, plutôt que de l'écrire en dur dans le code.

python
# env_demo.py : lire la configuration dans les variables d'environnement
import os

seuil = int(os.environ.get("SEUIL_ALERTE", "80"))     # 80 si la variable n'existe pas
destinataire = os.environ.get("ALERTE_MAIL")

print(f"Seuil d'alerte : {seuil} %")
if destinataire is None:
    print("ALERTE_MAIL non définie : pas d'envoi de courriel")
else:
    print(f"Alertes envoyées à {destinataire}")
bash
python env_demo.py
SEUIL_ALERTE=85 ALERTE_MAIL=supervision@sorbier.example python env_demo.py
text
Seuil d'alerte : 80 %
ALERTE_MAIL non définie : pas d'envoi de courriel
Seuil d'alerte : 85 %
Alertes envoyées à supervision@sorbier.example

os.environ se lit comme un dictionnaire, et get() fournit une valeur par défaut. Une variable d'environnement est toujours une chaîne : convertis-la. La syntaxe VARIABLE=valeur commande est celle de Bash ; sous PowerShell, on écrit $env:SEUIL_ALERTE = "85" avant de lancer le script. C'est aussi par là qu'arrivent les secrets, comme un jeton d'API (chapitre 10).

shutil : copier, déplacer, archiver

pathlib manipule un fichier à la fois ; shutil (shell utilities) fait les opérations de plus haut niveau :

python
# shutil_demo.py : copier, déplacer, archiver, mesurer l'espace disque
import shutil
from pathlib import Path

source = Path("modeles")
source.mkdir(exist_ok=True)
(source / "accueil.txt").write_text("Bienvenue chez Sorbier Logistique\n", encoding="utf-8")

shutil.copy2(source / "accueil.txt", "accueil-copie.txt")     # copie avec les dates
shutil.copytree(source, "modeles-2026", dirs_exist_ok=True)   # copie d'un dossier entier
shutil.move("accueil-copie.txt", "modeles-2026/accueil-ancien.txt")
print(sorted(p.name for p in Path("modeles-2026").iterdir()))

archive = shutil.make_archive("modeles-2026", "zip", root_dir="modeles-2026")
print(Path(archive).name)

usage = shutil.disk_usage(".")
print(f"Disque : {usage.used / usage.total:.0%} utilisé, {usage.free / 1024 ** 3:.0f} Gio libres")
print(shutil.which("python") is not None)
text
['accueil-ancien.txt', 'accueil.txt']
modeles-2026.zip
Disque : 80% utilisé, 47 Gio libres
True

Les deux dernières lignes dépendent de la machine : l'occupation du disque de la tienne sera différente.

FonctionRôle
shutil.copy2(src, dst)Copie un fichier avec ses métadonnées (dates)
shutil.copytree(src, dst)Copie un dossier et tout son contenu
shutil.move(src, dst)Déplace ou renomme
shutil.rmtree(dossier)Supprime un dossier et tout son contenu, sans corbeille
shutil.make_archive(nom, "zip", root_dir=…)Crée une archive zip ou tar d'un dossier
shutil.disk_usage(chemin)Total, utilisé et libre du volume, en octets
shutil.which("ping")Chemin de la commande, ou None si elle n'est pas installée

subprocess : lancer une commande du système

subprocess.run() lance un programme, attend sa fin et renvoie un objet qui contient son code de retour et, si on le demande, ses sorties.

python
# commande.py : lancer une commande et récupérer son résultat
import subprocess
import sys

# sys.executable : le chemin de l'interpréteur Python qui exécute ce script
resultat = subprocess.run([sys.executable, "--version"], capture_output=True, text=True)
print("Code de retour :", resultat.returncode)
print("Sortie :", resultat.stdout.strip())

# Une commande qui échoue : le code de retour le dit
echec = subprocess.run([sys.executable, "-c", "import sys; sys.exit(3)"])
print("Code de retour :", echec.returncode)

# check=True transforme un code non nul en exception
try:
    subprocess.run([sys.executable, "-c", "import sys; sys.exit(3)"], check=True)
except subprocess.CalledProcessError as erreur:
    print("Échec, code de retour", erreur.returncode)

# timeout : on n'attend pas indéfiniment
try:
    subprocess.run([sys.executable, "-c", "import time; time.sleep(5)"], timeout=1)
except subprocess.TimeoutExpired:
    print("Délai dépassé : commande interrompue")

# Une commande qui n'existe pas
try:
    subprocess.run(["commande-inconnue"])
except FileNotFoundError as erreur:
    print("Introuvable :", erreur)
text
Code de retour : 0
Sortie : Python 3.15.2
Code de retour : 3
Échec, code de retour 3
Délai dépassé : commande interrompue
Introuvable : [Errno 2] No such file or directory: 'commande-inconnue'
ton script Pythonsubprocess.run(["ping", "-c", "1","192.168.10.177"],capture_output=True)systèmelance le programmepingnouveau processussans shell intermédiaireà la fin du processusCompletedProcessreturncode : 0 si la commande a réussistdout : la sortie standard capturéestderr : la sortie d'erreur capturéetext=True : des chaînestimeout=10 : délai maximum
subprocess.run lance un programme, attend sa fin et renvoie son code de retour et ses sorties
ParamètreEffet
["ping", "-c", "1", ip]La commande et ses arguments, un élément par argument
capture_output=TrueRécupère la sortie dans resultat.stdout et resultat.stderr au lieu de l'afficher
text=TrueRenvoie des chaînes plutôt que des octets
check=TrueLève CalledProcessError si le code de retour n'est pas 0
timeout=10Interrompt la commande après 10 secondes (TimeoutExpired)

Deux particularités de Windows. Les commandes internes de cmd.exe (dir, copy) ne sont pas des programmes : utilise leurs équivalents Python (pathlib, shutil). Et les outils en ligne de commande d'un Windows français (ping, ipconfig) écrivent dans l'encodage de la console, pas en UTF-8 : pour lire leur sortie en texte, précise encoding="oem", ou travaille en octets (sans text=True). Une commande PowerShell se lance avec ["powershell", "-NoProfile", "-Command", "Get-Service Spooler"].

Avant d'appeler une commande, demande-toi si Python sait le faire seul : copier un fichier, lister un dossier, résoudre un nom DNS, tester un port, appeler une API se font sans subprocess, de façon portable et sans analyse de texte fragile. Le chapitre 12 utilise subprocess là où il est irremplaçable : lancer ping.

datetime : dates, durées et horodatage

python
# dates.py : dates, durées et horodatage avec datetime
from datetime import date, datetime, timedelta

maintenant = datetime(2026, 11, 8, 2, 30)      # dans un vrai script : datetime.now()
print(maintenant)
print(maintenant.isoformat())
print(maintenant.strftime("%d/%m/%Y à %H:%M"))
print(maintenant.strftime("sauvegarde-%Y-%m-%d_%H%M.zip"))

fin_garantie = date.fromisoformat("2026-01-10")
aujourd_hui = maintenant.date()
ecart = aujourd_hui - fin_garantie
print(f"Garantie expirée depuis {ecart.days} jours")

retention = timedelta(days=30)
print("Supprimer les sauvegardes d'avant le", (aujourd_hui - retention).isoformat())

trace = datetime.strptime("08/Nov/2026:02:31:07", "%d/%b/%Y:%H:%M:%S")
print(trace, trace.weekday())                   # 0 = lundi ... 6 = dimanche
text
2026-11-08 02:30:00
2026-11-08T02:30:00
08/11/2026 à 02:30
sauvegarde-2026-11-08_0230.zip
Garantie expirée depuis 302 jours
Supprimer les sauvegardes d'avant le 2026-10-09
2026-11-08 02:31:07 6

Le module datetime fournit date (un jour), datetime (un jour et une heure) et timedelta (une durée). La différence de deux dates est un timedelta, dont .days donne le nombre de jours ; une date plus ou moins un timedelta donne une autre date. strftime formate une date en texte, strptime lit un texte selon un format, fromisoformat lit directement le format ISO.

Code de formatSensExemple
%Y %m %dAnnée, mois, jour (sur 2 chiffres)2026 11 08
%H %M %SHeure, minute, seconde02 30 00
%bMois abrégé, en anglais par défautNov
%AJour de la semaine, en anglais par défautSunday

datetime.now() donne l'heure locale de la machine, sans fuseau. Pour comparer des horodatages venus de serveurs différents, travaille en UTC : datetime.now(timezone.utc) (avec from datetime import timezone), et zoneinfo.ZoneInfo("Europe/Paris") pour convertir à l'heure de Paris.

hashlib : l'empreinte d'un fichier

Une fonction de hachage calcule, à partir de n'importe quel contenu, une empreinte de taille fixe : changer un seul bit du contenu change toute l'empreinte (fonction de hachage). On s'en sert pour vérifier qu'un fichier téléchargé est intact, en comparant son empreinte à celle publiée par l'éditeur.

python
# empreinte.py : calculer l'empreinte SHA-256 d'un fichier
import hashlib

print(hashlib.sha256(b"Sorbier").hexdigest())
print(hashlib.sha256(b"sorbier").hexdigest())

with open("image-test.iso", "rb") as f:          # "rb" : lecture binaire
    empreinte = hashlib.file_digest(f, "sha256")  # lit le fichier par morceaux
print(empreinte.hexdigest())
text
3cb48d7e4655fbc6b913fa43468304422863b9c8cc6b8d292e6dd72dd68bc065
efe1a5745ad3cf80dd1dff36453d9655ec05b16a0e9d699a893969ffb79846ed
49838531dffda59db889c7b39ffa6251d30fe0a8ffc017bd68351f8137cda776

Sorbier et sorbier ne diffèrent que d'une majuscule, et leurs empreintes n'ont rien en commun. hashlib.file_digest() (Python 3.11 et plus) lit le fichier par morceaux : une image ISO de plusieurs gigaoctets ne remplit pas la mémoire. MD5 et SHA-1 ne sont plus sûrs face à une falsification volontaire : utilise SHA-256. Et pour stocker un mot de passe, une simple empreinte ne suffit pas (stocker un mot de passe).

zipfile : archiver et vérifier

python
# archive.py : archiver des journaux dans un zip, puis vérifier l'archive
import zipfile
from pathlib import Path

journaux = sorted(Path("journaux").glob("*.log"))

with zipfile.ZipFile("journaux-2026-11.zip", "w", compression=zipfile.ZIP_DEFLATED) as archive:
    for journal in journaux:
        archive.write(journal, arcname=journal.name)    # nom dans l'archive, sans le dossier

with zipfile.ZipFile("journaux-2026-11.zip") as archive:
    for info in archive.infolist():
        print(f"{info.filename:<28}{info.file_size:>6} o -> {info.compress_size:>4} o")
    print("Archive intègre" if archive.testzip() is None else "Archive corrompue")

print(f"{Path('journaux-2026-11.zip').stat().st_size} octets pour {len(journaux)} fichiers")
bash
python archive.py
text
sauvegarde-2026-11-03.log     7600 o ->   84 o
sauvegarde-2026-11-04.log     7600 o ->   84 o
sauvegarde-2026-11-05.log     7600 o ->   84 o
sauvegarde-2026-11-06.log     7600 o ->   84 o
sauvegarde-2026-11-07.log     7600 o ->   84 o
sauvegarde-2026-11-08.log     7600 o ->   84 o
Archive intègre
1282 octets pour 6 fichiers

ZipFile(nom, "w") crée une archive ("a" ajoute à une archive existante) ; ZIP_DEFLATED compresse, sinon les fichiers sont seulement stockés. arcname choisit le nom dans l'archive. testzip() relit tout et renvoie le nom du premier fichier abîmé, ou None. Les journaux, très répétitifs, se compressent de 7 600 à 84 octets chacun. Pour une archive .tar.gz, le module tarfile fonctionne de la même façon.

Exercices

Exercice 1 sur 3 Nommer une sauvegarde Réussi À revoir

Une sauvegarde est lancée le 8 novembre 2026 à 2 h 30. Écris le script qui affiche : son nom au format sauvegarde-AAAA-MM-JJ_HHMM, la date (au format français JJ/MM/AAAA) avant laquelle les sauvegardes sont à supprimer pour une rétention de 30 jours, et le jour de la semaine en français.

Voir la réponseMasquer la réponse de l'exercice 1
python
# ex_dates.py : nom d'une sauvegarde et date limite de rétention
from datetime import datetime, timedelta

lancement = datetime(2026, 11, 8, 2, 30)          # datetime.now() en production
nom = lancement.strftime("sauvegarde-%Y-%m-%d_%H%M")
limite = lancement - timedelta(days=30)

print(nom)
print("Supprimer avant le", limite.strftime("%d/%m/%Y"))
print("Jour de la semaine :", ["lundi", "mardi", "mercredi", "jeudi", "vendredi",
                               "samedi", "dimanche"][lancement.weekday()])
text
sauvegarde-2026-11-08_0230
Supprimer avant le 09/10/2026
Jour de la semaine : dimanche

8 novembre moins 30 jours : les 8 premiers jours de novembre, puis 22 jours d'octobre, on arrive au 9 octobre. weekday() renvoie 6 pour un dimanche, d'où l'indice dans la liste des jours. En production, datetime.now() remplace la date fixe ; la garder en paramètre permet de tester le script avec n'importe quelle date (chapitre 15).

Tu avais trouvé ?

Exercice 2 sur 3 Vérifier un téléchargement Réussi À revoir

L'éditeur d'un logiciel publie l'empreinte SHA-256 de son image : 49838531dffda59db889c7b39ffa6251d30fe0a8ffc017bd68351f8137cda776. Écris un script qui calcule l'empreinte du fichier image-test.iso téléchargé, affiche Empreinte conforme ou un message d'erreur avec les deux valeurs, et se termine avec le code 1 en cas de différence.

Voir la réponseMasquer la réponse de l'exercice 2
python
# ex_verifier.py : comparer l'empreinte d'un fichier à la valeur publiée
import hashlib
import sys

ATTENDUE = "49838531dffda59db889c7b39ffa6251d30fe0a8ffc017bd68351f8137cda776"


def sha256_fichier(chemin):
    with open(chemin, "rb") as f:
        return hashlib.file_digest(f, "sha256").hexdigest()


calculee = sha256_fichier("image-test.iso")
if calculee == ATTENDUE.lower():
    print("Empreinte conforme")
else:
    print(f"NON CONFORME : attendue {ATTENDUE}, obtenue {calculee}")
    sys.exit(1)
text
Empreinte conforme

.lower() accepte une empreinte publiée en majuscules, ce que font certains éditeurs. Le code de retour 1 permet à un script d'installation de s'arrêter avant d'utiliser un fichier abîmé ou falsifié. Une empreinte prouve l'intégrité si elle est récupérée par un canal de confiance (le site de l'éditeur en HTTPS) ; une signature numérique prouve en plus l'origine (HMAC et signature).

Tu avais trouvé ?

Exercice 3 sur 3 Archiver les vieux journaux Réussi À revoir

Le dossier journaux contient sauvegarde-2026-11-03.log à sauvegarde-2026-11-08.log. Le 8 novembre 2026, archive dans archives-journaux.zip les journaux datés de plus de 2 jours (la date est dans le nom), puis supprime-les, mais seulement s'ils sont bien dans l'archive.

Voir la réponseMasquer la réponse de l'exercice 3
python
# ex_archiver_anciens.py : archiver puis supprimer les journaux de plus de 2 jours
import zipfile
from datetime import date, datetime, timedelta
from pathlib import Path

AUJOURD_HUI = date(2026, 11, 8)                   # date.today() en production
limite = AUJOURD_HUI - timedelta(days=2)

anciens = []
for journal in sorted(Path("journaux").glob("sauvegarde-*.log")):
    jour = datetime.strptime(journal.stem, "sauvegarde-%Y-%m-%d").date()
    if jour < limite:
        anciens.append(journal)

with zipfile.ZipFile("archives-journaux.zip", "a", compression=zipfile.ZIP_DEFLATED) as archive:
    for journal in anciens:
        archive.write(journal, arcname=journal.name)

with zipfile.ZipFile("archives-journaux.zip") as archive:
    archives = set(archive.namelist())
for journal in anciens:
    if journal.name in archives:                  # on ne supprime que ce qui est archivé
        journal.unlink()
        print("Archivé puis supprimé :", journal.name)

print("Restent :", [j.name for j in sorted(Path("journaux").glob("*.log"))])
text
Archivé puis supprimé : sauvegarde-2026-11-03.log
Archivé puis supprimé : sauvegarde-2026-11-04.log
Archivé puis supprimé : sauvegarde-2026-11-05.log
Restent : ['sauvegarde-2026-11-06.log', 'sauvegarde-2026-11-07.log', 'sauvegarde-2026-11-08.log']

La limite est le 6 novembre : les journaux du 3, du 4 et du 5 sont strictement plus anciens. strptime lit la date dans le nom (journal.stem est le nom sans .log), ce qui ne dépend pas de la date de modification du fichier, qu'une copie peut changer. Le mode "a" complète l'archive des jours précédents. La suppression ne vient qu'après la relecture de l'archive : un script qui supprime avant d'avoir vérifié la copie finit toujours par perdre des données.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Un script d'administration propre : argparse, logging et planificationRevoir le sommaire du cours