Chapitre 924 minDébutant
Chaînes de caractères et code de César
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- extraire, chercher, découper et assembler des chaînes ;
- formater un affichage et parcourir une chaîne caractère par caractère ;
- passer d'un caractère à son code avec
ordetchr; - programmer le code de César et le chiffrement affine.
6 exercices corrigés1 schéma
Une chaîne de caractères est une suite de caractères (lettres, chiffres, symboles, espaces) traitée comme une seule valeur, par exemple un nom, une adresse IP ou un message. Ce chapitre présente les opérations du programme sur les chaînes (concaténation, extraction au début, au milieu ou à la fin, recherche d'un motif), leur traduction en Python, les algorithmes classiques sur les mots, puis deux chiffrements : le code de César et le chiffrement affine.
Écrire une chaîne
En Python, une chaîne s'écrit entre guillemets doubles ou simples : "BTS SIO" ou 'BTS SIO'. On choisit les doubles quand le texte contient une apostrophe : "l'adresse IP". Quelques caractères spéciaux s'écrivent avec une barre oblique inverse :
| Écriture | Caractère |
|---|---|
\n | Retour à la ligne |
\t | Tabulation |
\" ou \' | Guillemet dans une chaîne délimitée par le même guillemet |
\\ | Barre oblique inverse (chemins Windows : "C:\\Temp") |
La chaîne vide "" ne contient aucun caractère ; sa longueur est 0. Elle sert à initialiser une chaîne qu'on construit dans une boucle, comme 0 initialise une somme.
Les opérations de base
| Opération | Pseudo-code | Python | Exemple | Résultat |
|---|---|---|---|---|
| Longueur | longueur(s) | len(s) | len("réseau") | 6 |
| Concaténation | s1 & s2 ou s1 + s2 | s1 + s2 | "web" + "01" | "web01" |
| Répétition | s * n | "-" * 5 | "-----" | |
| Caractère d'indice i | s[i] | s[i] | "SIO"[0] | "S" |
| Comparaison | =, < | ==, < | "abc" < "abd" | True |
La concaténation n'accepte que des chaînes : "Poste " + 12 lève TypeError. Il faut convertir : "Poste " + str(12).
Les caractères sont numérotés à partir de 0, comme les éléments d'une liste. Mais une chaîne Python est non modifiable : s[0] = "X" lève TypeError: 'str' object does not support item assignment. Pour « modifier » une chaîne, on en construit une nouvelle. C'est l'une des raisons pour lesquelles le programme recommande de considérer la chaîne comme un type à part entière, plutôt que comme un tableau de caractères.
Extraire le début, le milieu ou la fin
Le programme demande les fonctions d'extraction « en début, milieu ou fin ». En pseudo-code, on les écrit souvent Gauche, Droite et Milieu (comme dans un tableur) ; en Python, on utilise le découpage (slicing) s[début:fin], qui prend les caractères de l'indice début inclus à fin exclu, comme range.
| Besoin | Pseudo-code | Python | Avec s = "Réseaux et systèmes" |
|---|---|---|---|
| Les n premiers caractères | Gauche(s, n) | s[:n] | s[:6] donne "Réseau" |
| Les n derniers | Droite(s, n) | s[-n:] | s[-7:] donne "ystèmes" |
| n caractères à partir de l'indice d | Milieu(s, d, n) | s[d:d + n] | s[3:6] donne "eau" |
| Tout sauf le premier | s[1:] | "éseaux et systèmes" | |
| La chaîne à l'envers | s[::-1] | "semètsys te xuaeséR" |
Un découpage ne lève jamais d'erreur d'indice : s[:100] renvoie toute la chaîne si elle est plus courte. Accéder à un caractère seul, en revanche (s[100]), lève IndexError.
Chercher un motif
| Besoin | Python | Exemple | Résultat |
|---|---|---|---|
| Le motif est-il présent ? | motif in s | "@" in "j.dupont@lycee.fr" | True |
| Position de la première occurrence | s.find(motif) | "admin;user".find(";") | 5 (−1 si absent) |
| Nombre d'occurrences | s.count(motif) | "192.168.1.1".count(".") | 3 |
| Commence par, finit par | s.startswith(m), s.endswith(m) | "rapport.pdf".endswith(".pdf") | True |
| Remplacer | s.replace(ancien, nouveau) | "a-b-c".replace("-", ":") | "a:b:c" |
find renvoie −1 quand le motif est absent, comme la recherche séquentielle du cours. La méthode voisine index lève une ValueError à la place.
Transformer et tester
| Besoin | Python | Exemple | Résultat |
|---|---|---|---|
| Majuscules, minuscules | s.upper(), s.lower() | "Sio".upper() | "SIO" |
| Retirer les espaces au bord | s.strip() | " admin \n".strip() | "admin" |
| Que des chiffres ? | s.isdigit() | "2026".isdigit() | True |
| Que des lettres ? | s.isalpha() | "Léa".isalpha() | True |
| Majuscule, minuscule ? | c.isupper(), c.islower() | "A".isupper() | True |
Ces méthodes renvoient une nouvelle chaîne et ne modifient pas s. Écrire s.upper() seul, sans affecter le résultat, ne sert à rien : il faut s = s.upper().
Découper et assembler
split découpe une chaîne selon un séparateur et renvoie une liste de morceaux ; join fait l'inverse.
ip = "192.168.10.25"
octets = ip.split(".")
print(octets)
print(int(octets[3]) + 1)
ligne = "jdupont;Dupont;Jean;Comptabilité"
champs = ligne.split(";")
print(champs[1], champs[3])
print(" ".join(["Bonjour", "le", "BTS"]))
print("-".join(octets))
['192', '168', '10', '25']
26
Dupont Comptabilité
Bonjour le BTS
192-168-10-25
Sans argument, split() découpe sur les espaces (un ou plusieurs) : " trois mots ici ".split() donne ["trois", "mots", "ici"]. Les morceaux sont des chaînes : convertis-les si ce sont des nombres.
Formater un affichage
Construire un message, c'est assembler du texte et des valeurs. Trois façons, de la plus simple à la plus pratique :
nom = "Léa"
moyenne = 15.756
rang = 2
print("Élève :", nom, "- moyenne :", moyenne) # print avec virgules
print("Élève : " + nom + " - rang : " + str(rang)) # concaténation
print(f"Élève : {nom} - moyenne : {moyenne:.2f} - rang : {rang}") # f-string
print(f"|{nom:<8}|{rang:>4}|") # alignements
Élève : Léa - moyenne : 15.756
Élève : Léa - rang : 2
Élève : Léa - moyenne : 15.76 - rang : 2
|Léa | 2|
Dans une f-string (une chaîne précédée de f), chaque expression entre accolades est remplacée par sa valeur. Après le deux-points, un format : .2f pour deux décimales, <8 pour aligner à gauche sur 8 caractères, >4 à droite sur 4. Le programme n'exige pas cette mise en forme, mais elle rend tes affichages nets en quelques caractères.
Parcourir une chaîne
Comme une liste, une chaîne se parcourt caractère par caractère avec for. Compter les voyelles d'un mot :
Fonction NbVoyelles(mot : chaîne) : entier
nb ← 0
Pour chaque c dans mot Faire
Si c est dans "aeiouy" Alors
nb ← nb + 1
FinSi
FinPour
Retourner nb
FinFonction
def nb_voyelles(mot):
nb = 0
for c in mot.lower():
if c in "aeiouyàâéèêëîïôùûü":
nb = nb + 1
return nb
print(nb_voyelles("Algorithmique"), nb_voyelles("Réseaux"))
6 4
mot.lower() évite d'oublier les majuscules, et la chaîne de voyelles inclut les lettres accentuées : en français, sans elles, « Réseaux » n'aurait que 3 voyelles.
Construire une chaîne suit le motif de l'accumulateur, avec la chaîne vide comme point de départ. Inverser un mot sans [::-1] :
def inverser(mot):
resultat = ""
for c in mot:
resultat = c + resultat # chaque caractère passe devant
return resultat
print(inverser("routeur"))
ruetuor
Codes des caractères
Chaque caractère a un code numérique : c'est ce nombre que l'ordinateur stocke. Python utilise Unicode, dont les 128 premiers codes reprennent l'ancienne table ASCII. ord(c) donne le code d'un caractère, chr(n) le caractère d'un code.
| Caractère | A | B | Z | a | z | 0 | espace | é |
|---|---|---|---|---|---|---|---|---|
| Code | 65 | 66 | 90 | 97 | 122 | 48 | 32 | 233 |
Les lettres majuscules ont des codes consécutifs, de 65 à 90 : la position d'une majuscule dans l'alphabet (A = 0, B = 1… Z = 25) est ord(c) - 65. C'est la base des chiffrements qui suivent. C'est aussi pourquoi "Z" < "a" : 90 est inférieur à 97.
Le code de César
Le code de César remplace chaque lettre par celle qui se trouve k rangs plus loin dans l'alphabet, en revenant au début après Z. La clé k est un entier de 1 à 25. Avec k = 3, A devient D, B devient E, et X devient A.
Si p est la position d'une lettre (A = 0), sa position chiffrée est c = (p + k) MOD 26. Le MOD 26 réalise le retour au début : pour X (23) avec k = 3, (23 + 3) MOD 26 = 0, soit A. Pour déchiffrer, on recule : p = (c - k) MOD 26, ce qui revient à chiffrer avec la clé -k.
Fonction Cesar(texte : chaîne, k : entier) : chaîne
// Chiffre les lettres majuscules ; les autres caractères sont recopiés
resultat ← ""
Pour chaque c dans texte Faire
Si c ≥ "A" ET c ≤ "Z" Alors
p ← code(c) - 65
resultat ← resultat + caractere((p + k) MOD 26 + 65)
Sinon
resultat ← resultat + c
FinSi
FinPour
Retourner resultat
FinFonction
def cesar(texte, k):
"""Chiffre les majuscules de texte avec la clé k ; le reste est recopié."""
resultat = ""
for c in texte:
if "A" <= c <= "Z":
p = ord(c) - 65
resultat = resultat + chr((p + k) % 26 + 65)
else:
resultat = resultat + c
return resultat
chiffre = cesar("BONJOUR", 3)
print(chiffre)
print(cesar(chiffre, -3))
print(cesar("BTS SIO", 13), cesar(cesar("BTS SIO", 13), 13))
ERQMRXU
BONJOUR
OGF FVB BTS SIO
Avec k = 13 (le ROT13), chiffrer deux fois redonne le texte de départ, puisque 13 + 13 = 26. En Python, % renvoie toujours un reste positif quand le diviseur est positif : (1 - 3) % 26 vaut 24, et le déchiffrement avec une clé négative fonctionne sans précaution. En C# ou en JavaScript, il faudrait écrire ((p - k) % 26 + 26) % 26, car leur % peut être négatif (chapitre 2).
Casser un message chiffré par force brute, c'est une boucle sur les 25 clés :
def cesar(texte, k):
resultat = ""
for c in texte:
if "A" <= c <= "Z":
resultat = resultat + chr((ord(c) - 65 + k) % 26 + 65)
else:
resultat = resultat + c
return resultat
message = "WKH SDVVZRUG LV VLR"
for k in range(1, 26):
essai = cesar(message, -k)
if "PASSWORD" in essai:
print("Clé", k, ":", essai)
Clé 3 : THE PASSWORD IS SIO
Ici, on cherche un mot attendu dans le texte déchiffré. Sans indice, on afficherait les 25 essais et on lirait celui qui a un sens.
Le chiffrement affine
Le chiffrement affine généralise César : c = (a × p + b) MOD 26, avec deux clés a et b. César est le cas a = 1. Pour que deux lettres différentes ne donnent jamais la même lettre chiffrée, a doit être premier avec 26, c'est-à-dire PGCD(a, 26) = 1 : a est impair et n'est pas 13. Les valeurs possibles sont 1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23 et 25.
Avec a = 5 et b = 8, chiffrons BTS SIO :
| Lettre | p | 5p + 8 | MOD 26 | Lettre chiffrée |
|---|---|---|---|---|
| B | 1 | 13 | 13 | N |
| T | 19 | 103 | 25 | Z |
| S | 18 | 98 | 20 | U |
| I | 8 | 48 | 22 | W |
| O | 14 | 78 | 0 | A |
BTS SIO devient NZU UWA.
Pour déchiffrer, il faut résoudre c ≡ a × p + b (mod 26), donc p ≡ a' × (c − b) (mod 26), où a' est l'inverse de a modulo 26 : l'entier tel que a × a' MOD 26 = 1. Pour a = 5, a' = 21, car 5 × 21 = 105 = 4 × 26 + 1. On le trouve en essayant les entiers de 1 à 25, ou avec pow(a, -1, 26) en Python. Le calcul de l'inverse modulaire est détaillé au cours d'arithmétique (inverse modulaire), avec pourquoi a doit être premier avec 26 (chiffrement affine).
def affine(texte, a, b):
resultat = ""
for c in texte:
if "A" <= c <= "Z":
p = ord(c) - 65
resultat = resultat + chr((a * p + b) % 26 + 65)
else:
resultat = resultat + c
return resultat
def inverse_mod26(a):
"""Renvoie l'inverse de a modulo 26, ou -1 s'il n'existe pas."""
for x in range(1, 26):
if a * x % 26 == 1:
return x
return -1
def affine_dechiffre(texte, a, b):
a_inv = inverse_mod26(a)
resultat = ""
for c in texte:
if "A" <= c <= "Z":
p = (a_inv * (ord(c) - 65 - b)) % 26
resultat = resultat + chr(p + 65)
else:
resultat = resultat + c
return resultat
chiffre = affine("BTS SIO", 5, 8)
print(chiffre, inverse_mod26(5), affine_dechiffre(chiffre, 5, 8))
NZU UWA 21 BTS SIO
inverse_mod26 est une recherche séquentielle sur les 25 candidats. Si a n'est pas premier avec 26 (par exemple a = 4), aucun x ne convient et la fonction renvoie −1 : le message ne pourrait pas être déchiffré, car deux lettres auraient le même chiffré. Les sujets d'algorithmique et d'arithmétique du BTS utilisent volontiers ces chiffrements : le programme cite « codage, cryptage et décryptage » parmi les thèmes d'exercices.
Exercices
Exercice 1 sur 6 Extractions Réussi À revoir
Avec s = "ticket-2026-00421", écris l'expression Python qui donne : (1) "ticket" ; (2) l'année "2026" ; (3) le numéro "00421" puis l'entier 421 ; (4) la longueur ; (5) la chaîne en majuscules.
Voir la réponseMasquer la réponse de l'exercice 1
s[:6], ous.split("-")[0].s[7:11], ous.split("-")[1].s[-5:]donne"00421", etint(s[-5:])donne 421 : la conversion supprime les zéros de tête.len(s)vaut 17.s.upper()donne"TICKET-2026-00421".
La version avec split est plus robuste : elle reste juste si le numéro passe à six chiffres.
Tu avais trouvé ?
Exercice 2 sur 6 Palindrome Réussi À revoir
Un palindrome se lit de la même façon dans les deux sens : « kayak », « radar ». Écris une fonction est_palindrome(mot) sans utiliser [::-1], en comparant le premier et le dernier caractère, puis le deuxième et l'avant-dernier, etc. Puis adapte-la pour ignorer les espaces et les majuscules : « Engage le jeu que je le gagne » doit être reconnu.
Voir la réponseMasquer la réponse de l'exercice 2
def est_palindrome(mot):
n = len(mot)
for i in range(n // 2):
if mot[i] != mot[n - 1 - i]:
return False
return True
def est_palindrome_phrase(phrase):
lettres = phrase.lower().replace(" ", "")
return est_palindrome(lettres)
print(est_palindrome("kayak"), est_palindrome("réseau"))
print(est_palindrome_phrase("Engage le jeu que je le gagne"))
Résultat : True False puis True. On s'arrête au milieu (n // 2 comparaisons), comme pour l'inversion d'un tableau. La deuxième fonction réutilise la première après avoir normalisé la phrase.
Tu avais trouvé ?
Exercice 3 sur 6 Valider une adresse IPv4 Réussi À revoir
Écris une fonction ip_valide(ip) qui renvoie True si la chaîne a la forme d'une adresse IPv4 : quatre nombres séparés par des points, chacun composé uniquement de chiffres et compris entre 0 et 255. Teste avec "192.168.1.10", "10.0.0", "256.1.1.1", "192.168.a.1" et "172.16.0.1".
Voir la réponseMasquer la réponse de l'exercice 3
def ip_valide(ip):
octets = ip.split(".")
if len(octets) != 4:
return False
for o in octets:
if not o.isdigit():
return False
if int(o) > 255:
return False
return True
for essai in ["192.168.1.10", "10.0.0", "256.1.1.1", "192.168.a.1", "172.16.0.1"]:
print(essai, ip_valide(essai))
192.168.1.10 True
10.0.0 False
256.1.1.1 False
192.168.a.1 False
172.16.0.1 True
Le test isdigit() vient avant int(o) : sinon int("a") lèverait une ValueError. Une chaîne vide (cas "192..1.1") n'est pas composée de chiffres : "".isdigit() vaut False, elle est donc rejetée. isdigit() refuse aussi le signe moins, donc les négatifs.
Tu avais trouvé ?
Exercice 4 sur 6 Générer un identifiant Réussi À revoir
Le service informatique crée les identifiants ainsi : la première lettre du prénom, suivie du nom, le tout en minuscules, sans espace ni tiret, limité à 8 caractères. Écris la fonction identifiant(prenom, nom). Que donnent « Jean », « Dupont » et « Marie », « Le Goff-Martin » ?
Voir la réponseMasquer la réponse de l'exercice 4
def identifiant(prenom, nom):
brut = prenom[0] + nom
brut = brut.lower().replace(" ", "").replace("-", "")
return brut[:8]
print(identifiant("Jean", "Dupont"))
print(identifiant("Marie", "Le Goff-Martin"))
Résultats : jdupont et mlegoffm. Le découpage [:8] fonctionne même si la chaîne est plus courte. Une vraie fonction devrait aussi retirer les accents et gérer les homonymes (en ajoutant un chiffre), deux améliorations classiques à proposer à l'oral.
Tu avais trouvé ?
Exercice 5 sur 6 Déchiffrer un message de César Réussi À revoir
Le message "VDXYHJDUGH-QRFWXUQH RN D PLQXLW" a été chiffré avec la clé 3. Déchiffre-le à la main pour les premières lettres, puis avec la fonction cesar. Explique pourquoi le tiret et les espaces sont conservés.
Voir la réponseMasquer la réponse de l'exercice 5
V (21) − 3 = 18 : S ; D (3) − 3 = 0 : A ; X (23) − 3 = 20 : U. Avec cesar("VDXYHJDUGH-QRFWXUQH RN D PLQXLW", -3), on obtient SAUVEGARDE-NOCTURNE OK A MINUIT. Le tiret et les espaces ne sont pas des majuscules : la branche Sinon les recopie tels quels. Inconvénient : les espaces révèlent la longueur des mots et facilitent l'attaque.
Tu avais trouvé ?
Exercice 6 sur 6 Compter les mots d'une phrase Réussi À revoir
Écris une fonction qui renvoie le nombre de mots d'une phrase, sans utiliser split, en comptant les passages d'une espace à une lettre. Teste avec " Le BTS SIO ", qui contient 3 mots.
Voir la réponseMasquer la réponse de l'exercice 6
Un mot commence quand on lit un caractère qui n'est pas une espace, alors que le précédent en était une (ou qu'on est au début).
def nb_mots(phrase):
nb = 0
dans_un_mot = False
for c in phrase:
if c != " " and not dans_un_mot:
nb = nb + 1
dans_un_mot = True
elif c == " ":
dans_un_mot = False
return nb
print(nb_mots(" Le BTS SIO "), len(" Le BTS SIO ".split()))
Résultat : 3 3. La variable booléenne dans_un_mot mémorise l'état du caractère précédent : c'est une technique courante pour détecter un changement en parcourant une séquence. Compter les espaces ne marcherait pas : il y en a 8 ici.
Tu avais trouvé ?