Regex pour les nombres, les numéros de téléphone et les adresses IP

Des motifs prêts à copier pour les saisies numériques, les numéros de téléphone et les adresses IP, tous testés, avec les cas où un analyseur ou une bibliothèque fait mieux le travail.

Les nombres, les numéros de téléphone et les adresses IP ont l'air de cibles faciles pour une regex, puisqu'ils sont surtout faits de chiffres. Chacun cache pourtant un piège : \d n'a pas le même sens d'un langage à l'autre, les numéros de téléphone suivent des règles nationales qu'aucun motif ne peut capturer, et une adresse IP n'est valide que si chacune de ses parties reste dans la bonne plage. Les motifs ci-dessous ont été testés dans Node.js, sauf mention d'un autre moteur, et chaque exemple accepté ou refusé a réellement été exécuté.

Uniquement des nombres

ObjectifMotifAccepteRefuse
Chiffres uniquement^[0-9]+$123, 007-42, 3.14, une espace en tête, une saisie vide
Entier, signe facultatif^[+-]?[0-9]+$-42, +42, 0073.14, 1e3
Entier sans zéros en tête^-?(?:0|[1-9][0-9]*)$0, -42007, +42
Nombre décimal (avec un point)^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$3.14, -0.5, .55., 1e3, 1,000
Nombre au format JSON^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$1.5E-7, -0.5, 01.5, +42
Séparateurs de milliers (virgule)^[0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]+)?$1,000, 12,345,678.90, 1001000, 1,00

Une regex vérifie la forme, pas la valeur. Pour une plage comme 1 à 100, validez les chiffres, puis comparez le nombre dans le code. Ne sautez pas pour autant la regex en comptant sur la seule conversion : en JavaScript, Number('') vaut 0, Number(' 42 ') vaut 42, Number('0x1A') vaut 26 et parseInt('12abc') vaut 12.

\d ne reconnaît-il que les chiffres de 0 à 9 ?

Pas partout. Nous avons testé ^\d+$ sur les chiffres arabo-indiens ٣٤ :

MoteurChiffres non ASCII reconnus ?Pour se limiter à 0–9
JavaScriptNon, même avec le drapeau uRien à faire ; \p{Nd} avec u reconnaît tous les chiffres décimaux
Python 3 (motifs str)Oui, y compris les chiffres pleine chasse 12[0-9] ou le drapeau re.ASCII
PHP (PCRE)Non ; oui avec le modificateur u[0-9]
.NETOui[0-9] ou RegexOptions.ECMAScript
Perl 5Oui[0-9] ou le modificateur /a

Cela compte quand une valeur passe la regex puis arrive dans du code qui l'interprète autrement. En Python, int('٣٤') renvoie 34, alors qu'une colonne de base de données ou un autre service peut la refuser. Écrivez [0-9] quand vous voulez des chiffres ASCII : cette classe a le même sens dans tous les moteurs.

Numéros de téléphone

Le seul format doté d'une définition simple et internationale est E.164 : un signe plus, un indicatif de pays qui ne commence pas par 0, et au plus 15 chiffres au total.

^\+[1-9]\d{1,14}$     E.164, signe plus obligatoire (idéal pour le stockage)
^\+?[1-9]\d{1,14}$    signe plus facultatif

Avec le plus obligatoire, +14155552671 et +442071838750 passent, tandis que +0123456789, un numéro de 16 chiffres et 004914155552671 (écrit avec le préfixe international 00) échouent. Les gens tapent des espaces, des points, des tirets et des parenthèses : retirez-les d'abord avec value.replace(/[\s().-]/g, ''), qui transforme +1 415 555 2671 en +14155552671.

Les limites apparaissent vite. Le motif ne vérifie que la forme : +12 passe, alors qu'aucun vrai numéro n'est aussi court. Pire, +49 (0)30 1234567 devient +490301234567 après nettoyage et passe, mais c'est un mauvais numéro : le 0 entre parenthèses est un préfixe interurbain national qu'il faut supprimer quand on appelle depuis l'étranger.

Les formats nationaux ne font pas mieux. Le motif américain courant ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$ a accepté (415-555-2671, avec une parenthèse jamais fermée, et 000-000-0000, qui ne peut pas être un vrai numéro. Chaque pays a ses propres longueurs, préfixes et plages de numéros mobiles, et tout cela évolue. Pour valider et formater pour de bon, utilisez libphonenumber de Google ou l'un de ses portages : libphonenumber-js pour JavaScript, phonenumbers pour Python, giggsey/libphonenumber-for-php pour PHP. Stockez le résultat au format E.164 et, si c'est important, confirmez que le numéro appartient bien à l'utilisateur avec un code envoyé par SMS.

Adresses IPv4

Le raccourci populaire ^\d{1,3}(?:\.\d{1,3}){3}$ a accepté 256.1.1.1 et 999.999.999.999. Pour limiter chaque partie à 0–255, il faut détailler les plages :

^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$

Nous avons vérifié la partie octet sur chaque nombre de 0 à 999 écrit avec un, deux et trois chiffres, puis le motif complet sur 200 000 adresses aléatoires : il a rendu le même verdict que net.isIPv4() de Node à chaque fois. Il accepte 0.0.0.0, 192.168.1.1 et 255.255.255.255, et refuse 192.168.1, 192.168.1.1.1 ainsi que les zéros en tête, comme dans 192.168.01.1.

Refuser les zéros en tête est volontaire. Certains analyseurs les lisent en octal : en Python, socket.inet_aton('010.0.0.1') nous a renvoyé l'adresse 8.0.0.1. net.isIPv4() de Node, ipaddress.ip_address() de Python et filter_var($ip, FILTER_VALIDATE_IP) de PHP ont eux aussi tous refusé 192.168.01.1.

Pour trouver des adresses dans une ligne de journal, retirez ^ et $, ajoutez \b des deux côtés et utilisez le drapeau g. Sur from 10.0.0.5 to 192.168.1.300 via 8.8.8.8, on obtient 10.0.0.5 et 8.8.8.8. Sans \b, le texte 1192.168.1.300x a produit une fausse correspondance, 192.168.1.30. Une limite demeure : dans 1.2.3.4.5, la version avec \b trouve encore 1.2.3.4.

Adresses IPv6 : utilisez un analyseur

IPv6 autorise :: pour remplacer une suite de groupes nuls, des zéros en tête facultatifs, une IPv4 intégrée comme ::ffff:192.0.2.1 et des identifiants de zone comme fe80::1%eth0. Une regex qui couvre tout cela est longue et facile à rendre subtilement fausse. Un analyseur est plus court et mieux testé :

  • Node.js : net.isIPv6(value), ou net.isIP(value), qui renvoie 4, 6 ou 0. Les deux ont accepté l'IPv4 intégrée et les identifiants de zone.
  • Python : ipaddress.ip_address(value) lève ValueError si la valeur est invalide. La fonction a accepté l'IPv4 intégrée et les identifiants de zone.
  • PHP : filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6). La fonction a accepté l'IPv4 intégrée mais refusé fe80::1%eth0.
  • Navigateurs : il n'existe aucun validateur d'IP intégré ; utilisez une bibliothèque comme ipaddr.js.

Si vous avez malgré tout besoin d'une regex, par exemple dans un schéma ou un fichier de configuration qui n'accepte rien d'autre, ce motif de 408 caractères couvre les formes hexadécimales et refuse volontairement l'IPv4 intégrée et les identifiants de zone. Sur 500 000 chaînes générées puis mutées, sans points ni signes pourcent, il a rendu le même verdict que net.isIPv6() pour chacune.

^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$

Les erreurs qui laissent passer de mauvaises saisies

  • Des ancres manquantes. /\d+/.test('abc123') vaut true, car le motif trouve des chiffres n'importe où. Une validation a besoin de ^ et de $.
  • Une alternative hors d'un groupe. ^cat|dog$ signifie « commence par cat, ou se termine par dog » : le motif a donc trouvé une correspondance dans cats and dogs!. Écrivez ^(?:cat|dog)$.
  • Le drapeau m. Avec lui, ^ et $ correspondent au début et à la fin de chaque ligne : /^\d+$/m a ainsi accepté la chaîne de deux lignes "abc\n123". N'utilisez jamais m pour valider une valeur unique.
  • $ avant un saut de ligne final. En Python, en PHP et en .NET, ^\d+$ a accepté "42\n". Utilisez re.fullmatch() en Python, le modificateur D en PHP, ou \z en PHP et en .NET. JavaScript l'a refusée.
  • Le drapeau g avec test(). Une regex dotée de g mémorise lastIndex : appeler trois fois /^\d+$/g.test('42') sur le même objet a renvoyé true, false, true.
  • Des alternatives non ancrées dans le mauvais ordre. Chercher dans 255.255.255.255 avec l'octet écrit (?:\d|1\d\d|2[0-4]\d|25[0-5]) n'a trouvé que 255.255.255.2, car le moteur retient la première alternative qui fonctionne. Placez les alternatives les plus longues en premier, ou ancrez le motif.

Testez votre motif avant de le mettre en production

Collez un motif dans le testeur d'expressions régulières sans les barres obliques, réglez les drapeaux sur gm et saisissez vos exemples, un par ligne : des valeurs valides, des valeurs invalides et les cas délicats de cette page. Chaque ligne qui correspond est surlignée. Le testeur utilise le moteur JavaScript de votre navigateur : revérifiez les différences de \d et de $ décrites plus haut si le motif doit tourner en Python, en PHP ou en .NET. Vous validez plutôt des adresses e-mail ? Consultez le guide Regex pour valider une adresse e-mail.

Questions Fréquentes

Quelle regex n'accepte que des chiffres ?

Utilisez ^[0-9]+$ pour des chiffres uniquement, ou ^[+-]?[0-9]+$ pour autoriser un signe. Préférez [0-9] à \d, car en Python, en .NET et en PHP avec le modificateur u, \d reconnaît aussi les chiffres d'autres écritures.

Quelle regex valide les numéros de téléphone internationaux ?

Pour le format E.164, ^\+[1-9]\d{1,14}$, après suppression des espaces, points, tirets et parenthèses. Elle ne vérifie que la forme : utilisez libphonenumber ou l'un de ses portages pour vérifier qu'un numéro est réellement valide dans son pays.

Quelle est la regex d'une adresse IPv4 valide ?

^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$ limite chaque partie à 0-255 et refuse les zéros en tête. Pour IPv6, utilisez un analyseur comme net.isIPv6 dans Node.js ou ipaddress en Python.

Pourquoi ma regex accepte-t-elle une saisie terminée par un saut de ligne ?

En Python, en PHP et en .NET, $ correspond aussi juste avant un saut de ligne final. Utilisez re.fullmatch en Python, le modificateur D en PHP, ou \z à la place de $.