Expresiones regulares para números, teléfonos y direcciones IP

Patrones listos para copiar para entradas numéricas, números de teléfono y direcciones IP, todos probados, con notas sobre cuándo un parser o una biblioteca hace mejor el trabajo.

Los números, los teléfonos y las direcciones IP parecen objetivos fáciles para una regex porque son casi todo dígitos. Cada uno esconde una trampa: \d significa cosas distintas según el lenguaje, los teléfonos siguen reglas nacionales que ningún patrón puede recoger y una dirección IP solo es válida si cada una de sus partes está dentro de rango. Los patrones siguientes se probaron en Node.js salvo que se indique otro motor, y cada ejemplo aceptado o rechazado es uno que ejecutamos.

Solo números

Lo que buscaPatrónAceptaRechaza
Solo dígitos^[0-9]+$123, 007-42, 3.14, un espacio inicial, una entrada vacía
Entero con signo opcional^[+-]?[0-9]+$-42, +42, 0073.14, 1e3
Entero sin ceros a la izquierda^-?(?:0|[1-9][0-9]*)$0, -42007, +42
Decimal^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$3.14, -0.5, .55., 1e3, 1,000
Número al estilo JSON^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$1.5E-7, -0.5, 01.5, +42
Separador de miles con coma y punto decimal^[0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]+)?$1,000, 12,345,678.90, 1001000, 1,00

Una regex comprueba la forma, no el valor. Para un rango como de 1 a 100, compruebe los dígitos con la regex y después compare el número en código. Tampoco se salte la regex confiando solo en la conversión: en JavaScript, Number('') es 0, Number(' 42 ') es 42, Number('0x1A') es 26 y parseInt('12abc') es 12.

¿\d solo coincide con los dígitos del 0 al 9?

No en todas partes. Probamos ^\d+$ con los dígitos arábigo-índicos ٣٤:

Motor¿Coincidió con dígitos no ASCII?Para limitarlo a 0–9
JavaScriptNo, ni siquiera con la bandera uNo hace falta nada; \p{Nd} con u coincide con todos los dígitos decimales
Python 3 (patrones str)Sí, también con los de ancho completo 12[0-9] o la bandera re.ASCII
PHP (PCRE)No; sí con el modificador u[0-9]
.NETSí[0-9] o RegexOptions.ECMAScript
Perl 5Sí[0-9] o el modificador /a

Esto importa cuando un valor pasa la regex y llega a un código que lo interpreta de otra manera. En Python, int('٣٤') devuelve 34, mientras que una columna de base de datos u otro servicio pueden rechazarlo. Escriba [0-9] cuando quiera decir dígitos ASCII: significa lo mismo en todos los motores.

Números de teléfono

El único formato con una definición internacional sencilla es E.164: un signo más, un código de país que no empieza por 0 y, como máximo, 15 dígitos en total.

^\+[1-9]\d{1,14}$     E.164, signo más obligatorio (lo mejor para almacenar)
^\+?[1-9]\d{1,14}$    signo más opcional

Con el signo más obligatorio, +14155552671 y +442071838750 pasan, mientras que +0123456789, un número de 16 dígitos y 004914155552671 (escrito con el prefijo internacional 00) fallan. La gente escribe espacios, puntos, guiones y paréntesis, así que elimínelos primero con value.replace(/[\s().-]/g, ''); así, +1 415 555 2671 se convierte en +14155552671.

Los límites aparecen enseguida. El patrón solo comprueba la forma, así que +12 pasa aunque ningún número real sea tan corto. Peor aún: +49 (0)30 1234567 queda como +490301234567 tras la limpieza y pasa, pero es un número equivocado, porque el 0 entre paréntesis es un prefijo de marcación nacional que hay que quitar al llamar desde el extranjero.

Los formatos nacionales no salen mejor parados. El patrón habitual para Estados Unidos, ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$, aceptó (415-555-2671, con un paréntesis sin cerrar, y 000-000-0000, que no puede ser un número real. Cada país tiene sus propias longitudes, prefijos y rangos de móviles, y además cambian. Para validar y dar formato de verdad, use libphonenumber de Google o uno de sus ports: libphonenumber-js para JavaScript, phonenumbers para Python y giggsey/libphonenumber-for-php para PHP. Guarde el resultado en E.164 y, si es importante, confirme la titularidad con un código por SMS.

Direcciones IPv4

El atajo popular ^\d{1,3}(?:\.\d{1,3}){3}$ aceptó 256.1.1.1 y 999.999.999.999. Para limitar cada parte a 0–255, hay que escribir los rangos uno a uno:

^(?:(?: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)$

Comprobamos la parte del octeto con todos los números de 0 a 999 escritos con uno, dos y tres dígitos, y el patrón completo con 200 000 direcciones aleatorias: coincidió siempre con net.isIPv4() de Node. Acepta 0.0.0.0, 192.168.1.1 y 255.255.255.255, y rechaza 192.168.1, 192.168.1.1.1 y los ceros a la izquierda, como en 192.168.01.1.

Rechazar los ceros a la izquierda es deliberado. Algunos parsers los interpretan como octal: socket.inet_aton('010.0.0.1') de Python nos devolvió la dirección 8.0.0.1. net.isIPv4() de Node, ipaddress.ip_address() de Python y filter_var($ip, FILTER_VALIDATE_IP) de PHP también rechazaron 192.168.01.1.

Para encontrar direcciones dentro de una línea de log, quite ^ y $, añada \b a ambos lados y use la bandera g. Con from 10.0.0.5 to 192.168.1.300 via 8.8.8.8 devolvió 10.0.0.5 y 8.8.8.8. Sin \b, el texto 1192.168.1.300x produjo una coincidencia falsa: 192.168.1.30. Queda una limitación: en 1.2.3.4.5, la versión con \b sigue encontrando 1.2.3.4.

Direcciones IPv6: use un parser

IPv6 permite que :: sustituya una serie de grupos de ceros, admite ceros a la izquierda opcionales, IPv4 incrustada como ::ffff:192.0.2.1 e identificadores de zona como fe80::1%eth0. Una regex que cubra todo eso es larga y fácil de equivocar de forma sutil. Un parser es más corto y está mejor probado:

  • Node.js: net.isIPv6(value), o net.isIP(value), que devuelve 4, 6 o 0. Ambos aceptaron IPv4 incrustada e identificadores de zona.
  • Python: ipaddress.ip_address(value) lanza ValueError si la dirección no es válida. Aceptó IPv4 incrustada e identificadores de zona.
  • PHP: filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6). Aceptó IPv4 incrustada, pero rechazó fe80::1%eth0.
  • Navegadores: no hay un validador de IP integrado, así que use una biblioteca como ipaddr.js.

Si aun así necesita una regex, por ejemplo en un esquema o en un archivo de configuración que solo admite eso, este patrón de 408 caracteres cubre las formas hexadecimales y rechaza a propósito la IPv4 incrustada y los identificadores de zona. Con 500 000 cadenas generadas y mutadas, sin puntos ni signos de porcentaje, coincidió con net.isIPv6() en todas ellas.

^(?:(?:[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}|:))$

Errores que dejan pasar entradas incorrectas

  • Faltan las anclas. /\d+/.test('abc123') es true porque encuentra dígitos en cualquier parte. Para validar hacen falta ^ y $.
  • Alternancia fuera de un grupo. ^cat|dog$ significa «empieza por cat o termina en dog», así que coincidió con cats and dogs!. Escriba ^(?:cat|dog)$.
  • La bandera m. Con ella, ^ y $ coinciden en cada línea, así que /^\d+$/m aceptó la cadena de dos líneas "abc\n123". No use nunca m para validar un único valor.
  • $ antes de un salto de línea final. En Python, PHP y .NET, ^\d+$ aceptó "42\n". Use re.fullmatch() en Python, el modificador D en PHP o \z en PHP y .NET. JavaScript lo rechazó.
  • La bandera g con test(). Una regex con g recuerda lastIndex, así que llamar tres veces a /^\d+$/g.test('42') sobre el mismo objeto devolvió true, false, true.
  • Alternativas sin anclar en el orden equivocado. Al buscar en 255.255.255.255 con el octeto escrito como (?:\d|1\d\d|2[0-4]\d|25[0-5]) solo se obtuvo 255.255.255.2, porque el motor se queda con la primera alternativa que funciona. Ponga primero las alternativas más largas o ancle el patrón.

Pruebe su patrón antes de publicarlo

Pegue un patrón en el probador de expresiones regulares sin las barras, ponga las banderas gm y escriba sus ejemplos, uno por línea: los válidos, los inválidos y los casos incómodos de esta página. Cada línea que coincide se resalta. El probador usa el motor JavaScript de su navegador, así que repase las diferencias de \d y $ descritas arriba si el patrón va a ejecutarse en Python, PHP o .NET. ¿Lo que quiere validar son direcciones de correo? Consulte Regex para validar email.

Preguntas Frecuentes

¿Qué regex permite solo números?

Use ^[0-9]+$ para aceptar solo dígitos, o ^[+-]?[0-9]+$ para permitir un signo. Es preferible [0-9] a \d, porque en Python, en .NET y en PHP con el modificador u, \d también coincide con dígitos de otras escrituras.

¿Qué regex valida números de teléfono internacionales?

Para el formato E.164, ^\+[1-9]\d{1,14}$ después de quitar espacios, puntos, guiones y paréntesis. Solo comprueba la forma, así que use libphonenumber o uno de sus ports para verificar que el número es realmente válido en su país.

¿Cuál es la regex de una dirección IPv4 válida?

^(?:(?: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)$ limita cada parte a un valor de 0 a 255 y rechaza los ceros a la izquierda. Para IPv6, use un parser como net.isIPv6 en Node.js o ipaddress en Python.

¿Por qué mi regex acepta una entrada con un salto de línea al final?

En Python, PHP y .NET, $ también coincide justo antes de un salto de línea final. Use re.fullmatch en Python, el modificador D en PHP o \z en lugar de $.