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
| Objectif | Motif | Accepte | Refuse |
|---|---|---|---|
| Chiffres uniquement | ^[0-9]+$ | 123, 007 | -42, 3.14, une espace en tête, une saisie vide |
| Entier, signe facultatif | ^[+-]?[0-9]+$ | -42, +42, 007 | 3.14, 1e3 |
| Entier sans zéros en tête | ^-?(?:0|[1-9][0-9]*)$ | 0, -42 | 007, +42 |
| Nombre décimal (avec un point) | ^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$ | 3.14, -0.5, .5 | 5., 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, 100 | 1000, 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 ٣٤ :
| Moteur | Chiffres non ASCII reconnus ? | Pour se limiter à 0–9 |
|---|---|---|
| JavaScript | Non, même avec le drapeau u | Rien à 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] |
| .NET | Oui | [0-9] ou RegexOptions.ECMAScript |
| Perl 5 | Oui | [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), ounet.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èveValueErrorsi 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')vauttrue, 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 danscats and dogs!. Écrivez^(?:cat|dog)$. - Le drapeau
m. Avec lui,^et$correspondent au début et à la fin de chaque ligne :/^\d+$/ma ainsi accepté la chaîne de deux lignes"abc\n123". N'utilisez jamaismpour valider une valeur unique. $avant un saut de ligne final. En Python, en PHP et en .NET,^\d+$a accepté"42\n". Utilisezre.fullmatch()en Python, le modificateurDen PHP, ou\zen PHP et en .NET. JavaScript l'a refusée.- Le drapeau
gavectest(). Une regex dotée degmémoriselastIndex: 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.255avec l'octet écrit(?:\d|1\d\d|2[0-4]\d|25[0-5])n'a trouvé que255.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.