15 questions d'entretien sur les regex, avec solutions détaillées
Quinze exercices de regex du type de ceux posés en entretien, de la validation d'un nom d'utilisateur à l'explication d'un motif qui bloque un serveur, chacun avec sa solution, les cas limites sur lesquels on vous poussera et des remarques sur les performances.

· 15 min de lecture
/^(?!.*__)[a-z][a-z0-9_]{1,14}[a-z0-9]$/
à attraper
- night_owl
- dev42
- x_ray
à épargner
- _admin
- 9lives
- owl_
Les bonnes questions d'entretien sur les regex vérifient si vous savez écrire un motif correct, lisible et sûr face aux entrées invalides : valider un nom d'utilisateur, reformater des dates, extraire des champs d'une ligne de log, masquer un numéro de carte et expliquer pourquoi un motif bloque un serveur. Voici 15 exercices de ce type avec leurs solutions détaillées, les cas limites sur lesquels un recruteur insistera et des remarques sur les performances quand elles comptent.
Tous les motifs sont en JavaScript, et tous les exemples sont interactifs : les correspondances sont surlignées pendant votre lecture. Quand Python ou Java se comportent différemment, la solution le précise.
01Valider un nom d'utilisateur
La consigne : de 3 à 16 caractères ; lettres minuscules, chiffres et tirets bas ; il commence par une lettre, ne se termine pas par un tiret bas et ne contient jamais deux tirets bas d'affilée.
/^(?!.*__)[a-z][a-z0-9_]{1,14}[a-z0-9]$/Doit correspondre3/3
- night_owl
- dev42
- x_ray
Ne doit pas6/6
- _admin
- 9lives
- night__owl
- owl_
- ab
- Night_owl
Pourquoi ça marche : le premier et le dernier caractère ont leurs propres classes, et le milieu en prend de 1 à 14, soit 3 à 16 au total. Le lookahead (?!.*__) vérifie une seule fois toute la chaîne à la recherche d'un double tiret bas, avant que quoi que ce soit ne soit consommé.
Cas limites : sans ^ et $, !!night_owl!! passe. Évitez \w ici : il accepte les majuscules et, en Python 3, il reconnaît les lettres de n'importe quelle écriture.
02Reformater les dates de JJ/MM/AAAA en AAAA-MM-JJ
La consigne : réécrire toutes les dates d'un bloc de texte sans toucher au reste.
/\b(\d{2})\/(\d{2})\/(\d{4})\b/Doit correspondre2/2
- Due 04/11/2026
- From 05/01/2027 to 12/01/2027
Ne doit pas3/3
- Already ISO: 2026-11-04
- 4/11/2026
- Order 104/11/20261
Le remplacement remet les groupes dans un nouvel ordre :
text.replace(/\b(\d{2})\/(\d{2})\/(\d{4})\b/g, '$3-$2-$1')
// "Due 04/11/2026" → "Due 2026-11-04"
Avec des groupes nommés, le remplacement se lit tout seul : (?<d>\d{2})\/(?<m>\d{2})\/(?<y>\d{4}) et '$<y>-$<m>-$<d>'. En Python, l'équivalent est \g<y>-\g<m>-\g<d> avec des groupes (?P<y>…).
Cas limites : le \b de chaque côté empêche une correspondance au milieu d'un nombre plus long. Une regex vérifie la forme, pas le calendrier : 31/02/2026 passe, donc vérifiez ensuite les vraies dates avec une bibliothèque de dates. Plus de détails dans Regex pour les dates.
03Extraire les champs d'une ligne de log d'accès
La consigne : à partir d'une ligne de log d'un serveur web, récupérer l'IP, l'heure, la méthode, le chemin, le statut et la taille.
/^(?<ip>\S+) \S+ \S+ \[(?<time>[^\]]+)\] "(?<method>[A-Z]+) (?<path>\S+) [^"]*" (?<status>\d{3}) (?<bytes>\d+|-)$/Doit correspondre2/2
- 203.0.113.9 - - [04/Nov/2026:10:15:32 +0000] "GET /blog/regex-games HTTP/1.1" 200 5123
- 198.51.100.4 - - [04/Nov/2026:10:16:01 +0000] "POST /api/login HTTP/2.0" 401 -
Ne doit pas1/1
- 203.0.113.9 - - [04/Nov/2026:10:15:32 +0000] "GET / HTTP/1.1"
Pourquoi ça marche : chaque champ est reconnu par ce qu'il ne peut pas contenir : \S+ pour un champ sans espaces, [^\]]+ jusqu'au crochet fermant, [^"]* jusqu'au guillemet fermant. Les groupes nommés rendent le résultat explicite : line.match(re).groups.status vaut "200".
Performance : les classes négatives ne débordent jamais, donc une ligne invalide échoue vite. La version tentante avec .* entre les champs peut essayer de nombreuses façons de découper une longue ligne avant d'abandonner.
Cas limites : une requête mal formée est journalisée comme "-", que ce motif rejette ; décidez s'il faut ignorer ces lignes ou ajouter |- pour elles.
04Garder uniquement les lignes qui ne mentionnent pas DEBUG
La consigne : filtrer un log pour ne garder que les lignes sans le mot DEBUG.
/^(?!.*\bDEBUG\b).*$/mDoit correspondre2/2
- INFO server started
- DEBUGGER attached
Ne doit pas2/2
- DEBUG cache miss
- warn: DEBUG off
Pourquoi ça marche : le lookahead négatif cherche d'abord le mot dans la ligne ; c'est seulement s'il est absent que .* prend la ligne. Le \b de chaque côté permet de garder DEBUGGER.
Ce qu'il faut dire ensuite : dans du vrai code, lines.filter(l => !/\bDEBUG\b/.test(l)) ou grep -v -w DEBUG est plus clair. Connaître les deux, c'est tout l'enjeu de la question.
05Récupérer le texte entre crochets
La consigne : à partir de [error] disk full [code 28], obtenir error et code 28.
/\[[^\]]*\]/Doit correspondre2/2
- [error] disk full [code 28]
- []
Ne doit pas2/2
- no brackets here
- [unclosed
Le piège : \[(.*)\] est gourmand. Il va jusqu'au dernier ] et renvoie error] disk full [code 28. La version paresseuse \[(.*?)\] fonctionne, mais [^\]]* dit exactement ce que vous voulez et ne revient jamais en arrière. Plus de détails dans Quantificateurs gourmands ou paresseux.
06Trouver les commentaires TODO qui nomment un responsable
La consigne : lister les commentaires // TODO(name): note, avec le responsable et la note.
///\s*TODO\((\w+)\):\s*(.+)/Doit correspondre2/2
- // TODO(ola): retry on timeout
- x = 1 //TODO(dev_ops): remove
Ne doit pas3/3
- // TODO: no owner
- // todo(ola): lowercase
- # TODO(ola): python comment
Cas limites : ajoutez le marqueur i si votre équipe écrit todo. Pour couvrir aussi les commentaires #, commencez par (?://|#). Et dites-le clairement : une regex ne distingue pas un commentaire d'une chaîne qui contient //, donc une règle de linter est la réponse robuste.
07Masquer un numéro de carte sauf les quatre derniers chiffres
La consigne : transformer 4111111111111111 en ************1111.
/\d(?=\d{4})/Doit correspondre2/2
- 4111111111111111
- card 5500000000000004
Ne doit pas2/2
- 1234
- no digits
'4111111111111111'.replace(/\d(?=\d{4})/g, '*') // "************1111"
Pourquoi ça marche : le lookahead (?=\d{4}) ne reconnaît un chiffre que s'il est suivi de quatre autres chiffres, sans les consommer, donc les quatre derniers ne sont jamais remplacés.
Cas limites : avec des espaces, 4111 1111 1111 1111, rien n'est masqué, car aucun chiffre n'est directement suivi de quatre chiffres. \d(?=(?:\D*\d){4}) accepte des séparateurs entre les chiffres et donne **** **** **** 1111.
08Convertir du camelCase en snake_case
La consigne : getUserId → get_user_id, et parseHTTPResponse → parse_http_response.
/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/s.replace(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g, '_').toLowerCase()
Pourquoi ça marche : le motif ne reconnaît aucun caractère, seulement des positions : après une minuscule ou un chiffre et avant une majuscule, ou au sein d'une suite de majuscules juste avant la dernière (le P de HTTPResponse). Avec la première moitié seule, on obtient parse_httpresponse, une mauvaise réponse fréquente.
Cas limites : le lookbehind est pris en charge par tous les navigateurs actuels, Node et Python (le module re de Python exige une longueur fixe, comme ici). Sans lookbehind, capturez plutôt la lettre : ([a-z0-9])([A-Z]) → '$1_$2'.
09Valider une durée comme 1h30m
La consigne : accepter 1h30m, 45m et 2h, mais pas une chaîne vide.
/^(?=\d)(?:(\d+)h)?(?:(\d+)m)?$/Doit correspondre3/3
- 1h30m
- 45m
- 2h
Ne doit pas4/4
- h
- 30m1h
- 1h 30m
- 90
Pourquoi ça marche : les deux parties sont facultatives, donc sans le lookahead le motif accepte aussi la chaîne vide. (?=\d) signifie « il doit y avoir quelque chose ici, et cela commence par un chiffre ». C'est une astuce courante dès que toutes les parties d'un motif sont facultatives.
Cas limites : 1h90m passe. Si les minutes doivent être inférieures à 60, utilisez ([0-5]?\d)m, ou vérifiez le nombre dans le code, ce qui est plus lisible.
10Trouver les titres Markdown de niveau 1 à 3
La consigne : dans un fichier Markdown, trouver les titres #, ## et ### et capturer leur texte.
/^(#{1,3})\s+(\S.*)$/mDoit correspondre2/2
- # Title
- ### Install
Ne doit pas3/3
- #### Too deep
- #hashtag
- Not a # heading
Pourquoi ça marche : #{1,3} ne peut pas s'étendre à quatre, car \s+ doit suivre immédiatement. Le marqueur m fait fonctionner ^ et $ ligne par ligne dans tout un document (voir Les ancres en regex).
Cas limites : une ligne # dans un bloc de code n'est pas un titre, et une regex ne voit pas les blocs de code. C'est le moment de parler d'un parseur Markdown.
11Découper une ligne CSV qui contient des champs entre guillemets
La consigne : découper widget,"Sizes 2, 4 and 6",9.99 en trois champs, en gardant la virgule entre les guillemets.
/(?:^|,)("(?:[^"]|"")*"|[^,]*)/Avec le marqueur g, le groupe 1 de chaque correspondance est un champ : widget, "Sizes 2, 4 and 6" et 9.99. Retirez ensuite les guillemets extérieurs et transformez "" en ".
Ce qu'il faut dire ensuite : les champs entre guillemets peuvent contenir des sauts de ligne, ce qu'une regex appliquée ligne par ligne ne peut pas gérer. Pour du vrai CSV, utilisez un parseur (le module csv de Python, Papa Parse en JavaScript). Une bonne réponse d'entretien donne la regex et le dit.
12Trouver les caractères hors ASCII
La consigne : signaler un texte qui contient des lettres accentuées, des symboles comme € ou des emoji, par exemple avant de l'écrire dans un système qui n'accepte que l'ASCII.
/[^\x00-\x7F]/Doit correspondre3/3
- café
- naïve résumé
- price: 5 €
Ne doit pas2/2
- plain ASCII text
- 100% ok!
Cas limites : sans le marqueur u, un emoji compte pour deux unités UTF-16 et les deux correspondent ; avec u, c'est un seul caractère. Pour n'autoriser que l'ASCII imprimable, inversez la logique : ^[\x20-\x7E]*$.
13Valider une étiquette de nom d'hôte
La consigne : une étiquette (la partie entre les points) compte de 1 à 63 lettres, chiffres ou tirets, et ne peut ni commencer ni se terminer par un tiret.
/^(?!-)[a-z0-9-]{1,63}(?<!-)$/iDoit correspondre3/3
- regexhunter
- my-site-2
- a
Ne doit pas4/4
- -start
- end-
- under_score
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Pourquoi ça marche : le lookahead et le lookbehind protègent les deux extrémités, donc la limite de longueur reste à un seul endroit, {1,63}. La dernière ligne fait 64 caractères, un de trop.
Cas limites : sans lookarounds, on obtient ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$, ce qui est correct mais plus difficile à vérifier. Un nom d'hôte complet est une suite d'étiquettes séparées par des points, de 253 caractères au plus, et cette limite est plus simple à vérifier dans le code.
14Expliquer pourquoi un motif bloque le serveur
La consigne : cette vérification de « mots séparés par des espaces simples » fonctionne dans les tests, mais certaines entrées bloquent le serveur. Pourquoi, et comment la corriger ?
^(\w+\s?)+$
La réponse : l'espace est facultatif, donc une suite de lettres comme aaaaaaaa peut être répartie entre les répétitions du groupe d'un nombre énorme de façons (a|aaaaaaa, aa|aaaaaa, a|a|aaaaaa…). Quand la chaîne se termine par quelque chose qui ne peut pas correspondre, comme !, le moteur essaie chaque découpage avant d'abandonner. Le travail double à chaque lettre supplémentaire : sous Node, 24 lettres suivies d'un ! ont pris 8 secondes. C'est le backtracking catastrophique.
La correction : faites en sorte que chaque caractère ne puisse être reconnu que d'une seule façon. Rendez le séparateur obligatoire et autorisez un espace facultatif à la fin :
/^\w+(?:\s\w+)*\s?$/Doit correspondre2/2
- hello world
- one two three
Ne doit pas2/2
- hello world
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
Le motif corrigé rejette 30 000 lettres suivies d'un ! en une milliseconde environ. Autres bonnes réponses : un groupe atomique ou un quantificateur possessif quand le moteur en dispose (Java, PCRE, Python 3.11+), ou un moteur en temps linéaire comme RE2 pour les entrées non fiables. Toute l'histoire dans Le backtracking catastrophique.
15Supprimer les balises HTML, et savoir s'arrêter
La consigne : transformer <p>Hello <b>world</b></p> en Hello world.
text.replace(/<[^>]+>/g, '') fonctionne sur du balisage simple. La troisième ligne montre où cela casse : un texte brut contenant < et > perd < 5 and 7 >. Un attribut comme title="x > y" le casse aussi.
La réponse attendue : le HTML s'imbrique, et l'imbrication est justement ce que les expressions régulières ne savent pas compter. Pour extraire le texte du HTML, utilisez le parseur du navigateur (new DOMParser() et textContent), et pour sécuriser du HTML, utilisez une bibliothèque de nettoyage. Une regex convient pour un coup d'œil rapide sur du balisage de confiance, jamais pour la sécurité.
16S'entraîner pour l'entretien
Lire des solutions aide ; écrire des motifs sous un peu de pression aide encore plus.
- Les packs d'entraînement travaillent une technique par pack, et la plupart des niveaux sont des motifs que vous écrivez au travail : formats, lignes de log et identifiants dans du code.
- La chasse du jour prend cinq minutes et entretient vos réflexes regex entre deux entretiens.
- Si l'entretien se fait en Python, lisez Regex en JavaScript et en Python pour les différences qui piègent souvent.