Guides

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.

Logo de Regex Hunter

· 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.

Correspondances9/9 comme prévu
/^(?!.*__)[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
Essayez-le dans le bac à sable

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.

Correspondances5/5 comme prévu
/\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
Essayez-le dans le bac à sable

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.

Correspondances3/3 comme prévu
/^(?<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"
Essayez-le dans le bac à sable

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.

Correspondances4/4 comme prévu
/^(?!.*\bDEBUG\b).*$/m

Doit correspondre2/2

  • INFO server started
  • DEBUGGER attached

Ne doit pas2/2

  • DEBUG cache miss
  • warn: DEBUG off
Essayez-le dans le bac à sable

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.

Correspondances4/4 comme prévu
/\[[^\]]*\]/

Doit correspondre2/2

  • [error] disk full [code 28]
  • []

Ne doit pas2/2

  • no brackets here
  • [unclosed
Essayez-le dans le bac à sable

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.

Correspondances5/5 comme prévu
///\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
Essayez-le dans le bac à sable

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.

Correspondances4/4 comme prévu
/\d(?=\d{4})/

Doit correspondre2/2

  • 4111111111111111
  • card 5500000000000004

Ne doit pas2/2

  • 1234
  • no digits
Essayez-le dans le bac à sable
'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.

Motif47 car.
/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/
Essayez-le dans le bac à sable
Reconnaît la position vide entre deux mots, donc le remplacement insère « _ » sans rien supprimer.
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.

Correspondances7/7 comme prévu
/^(?=\d)(?:(\d+)h)?(?:(\d+)m)?$/

Doit correspondre3/3

  • 1h30m
  • 45m
  • 2h

Ne doit pas4/4

  • h
  • 30m1h
  • 1h 30m
  • 90
Essayez-le dans le bac à sable

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.

Correspondances5/5 comme prévu
/^(#{1,3})\s+(\S.*)$/m

Doit correspondre2/2

  • # Title
  • ### Install

Ne doit pas3/3

  • #### Too deep
  • #hashtag
  • Not a # heading
Essayez-le dans le bac à sable

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.

Motif29 car.
/(?:^|,)("(?:[^"]|"")*"|[^,]*)/
Essayez-le dans le bac à sable
Chaque correspondance est un champ : un champ entre guillemets (où "" est un guillemet échappé) ou du texte simple jusqu'à la virgule suivante.

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.

Correspondances5/5 comme prévu
/[^\x00-\x7F]/

Doit correspondre3/3

  • café
  • naïve résumé
  • price: 5 €

Ne doit pas2/2

  • plain ASCII text
  • 100% ok!
Essayez-le dans le bac à sable

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.

Correspondances7/7 comme prévu
/^(?!-)[a-z0-9-]{1,63}(?<!-)$/i

Doit correspondre3/3

  • regexhunter
  • my-site-2
  • a

Ne doit pas4/4

  • -start
  • end-
  • under_score
  • aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Essayez-le dans le bac à sable

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 :

Correspondances4/4 comme prévu
/^\w+(?:\s\w+)*\s?$/

Doit correspondre2/2

  • hello world
  • one two three

Ne doit pas2/2

  • hello world
  • aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
Essayez-le dans le bac à sable

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.

Correspondances3/3 comme prévu
/<[^>]+>/
  • <p>Hello <b>world</b></p>
  • <br/>
  • 3 < 5 and 7 > 2
  • plain text
Essayez-le dans le bac à sable

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.