Patrones1 caza jugable incluida

Regex para URLs: qué reconocer y qué dejar a un parser

Encontrar enlaces en un texto y validar una URL son trabajos distintos. Un patrón para cada uno, las trampas que dejan pasar URLs malas (puntuación final, puntos sin escapar, anclas que faltan) y cuándo pasarle el trabajo a new URL() o a urllib.

Logo de Regex Hunter

· 8 min de lectura · Actualizado el

/^https?://\S+$/

atrapa

  • https://a.io
  • http://x.com/p
  • https://b.dev?q=1

deja libre

  • ftp://x.com
  • www.x.com
  • http:/x.com

“Regex de URL” suele significar una de dos cosas:

  1. Encontrar URLs dentro de un bloque de texto, como una app de chat que vuelve clicables los enlaces.
  2. Validar una entrada concreta, como el campo “Sitio web” de un formulario de perfil.

Necesitan patrones distintos, y el segundo es sobre todo trabajo para un parser de URLs. Este artículo cubre los dos, además de las trampas en las que cae la gente en cada caso. Todos los ejemplos son interactivos, así que edita los patrones y mira qué cambia.

Respuesta rápidaJavaScript
/^(https?)://([^/:?#\s]+)(:\d+)?(/[^?#\s]*)?(\?[^#\s]*)?(#\S*)?$/i

Una URL http o https con un host y, después, puerto, ruta, consulta y fragmento opcionales, sin espacios.

Acepta

  • https://example.com
  • http://localhost:3000/api?x=1
  • https://example.com/blog#top

Rechaza

  • ftp://example.com
  • www.example.com
  • https://exa mple.com

01Encontrar URLs en un texto

La versión rápida es un esquema seguido de cualquier cosa que no sea un espacio:

Coincidencias5/5 como se esperaba
/https?://\S+/

Debe coincidir3/3

  • Docs: https://regexhunter.com/guide.
  • (see https://example.com/a)
  • <https://example.com>

No debe2/2

  • see www.example.com
  • ftp://files.example.com
Pruébalo en la Zona de pruebas
Todas las líneas coinciden, pero fíjate en lo que se resalta. El punto, el paréntesis de cierre y el > acaban dentro de la URL.

\S+ se lo lleva todo hasta el siguiente espacio, y en un texto real una URL suele ir seguida de puntuación. Enlaza eso y quien lo lea se encontrará un 404.

02Quitar la puntuación final

La solución es decir cuál puede ser el último carácter. El cuerpo puede ser cualquier cosa salvo espacios, corchetes angulares y comillas. El carácter final, además, no puede ser un signo de puntuación habitual:

Coincidencias4/4 como se esperaba
/https?://[^\s<>"]*[^\s<>".,;:!?)]/
  • Docs: https://regexhunter.com/guide.
  • (see https://example.com/a)
  • <https://example.com>
  • https://example.com/search?q=regex&page=2
Pruébalo en la Zona de pruebas
La URL puede seguir teniendo puntos y dos puntos en medio. Lo que no puede es terminar en uno.

Funciona porque el codicioso [^\s<>"]* llega hasta el final de la URL y luego retrocede de carácter en carácter hasta que encaja la clase final. (Nuestro artículo sobre cuantificadores codiciosos y perezosos explica ese retroceso paso a paso.)

Aquí no hay una regla perfecta. Los enlaces de Wikipedia como https://en.wikipedia.org/wiki/Regex_(disambiguation) terminan de verdad en ), y este patrón lo recorta. Las bibliotecas que convierten texto en enlaces lo resuelven comprobando si los paréntesis están equilibrados, que es más lógica de la que debería cargar una sola regex.

Fíjate en que www.example.com no se encuentra, porque no tiene esquema. Si también quieres los enlaces que empiezan por www. a secas, empieza el patrón con (https?://|www\.).

03Validar una URL: las partes

Una URL web tiene hasta seis partes. Aquí están en https://regexhunter.com:443/blog/regex-urls?ref=home#top:

Parte Ejemplo ¿Obligatoria?
esquema https sí
host regexhunter.com sí
puerto :443 no
ruta /blog/regex-urls no
consulta ?ref=home no
fragmento #top no

Un patrón que comprueba la forma de cada parte queda así:

Coincidencias9/9 como se esperaba
/^(https?)://([^/:?#\s]+)(:\d+)?(/[^?#\s]*)?(\?[^#\s]*)?(#\S*)?$/i

Debe coincidir4/4

  • https://example.com
  • HTTPS://EXAMPLE.COM
  • http://localhost:3000/api?x=1
  • https://regexhunter.com/blog/regex-urls#top

No debe5/5

  • ftp://example.com
  • https://
  • https://exa mple.com
  • https://example.com:port
  • example.com
Pruébalo en la Zona de pruebas
Esquema, host y luego puerto, ruta, consulta y fragmento opcionales, cada uno en su propio grupo. El modificador i admite HTTPS:// en mayúsculas.

Léelo parte por parte:

  1. ^(https?):// el esquema, http o https, y el ://
  2. ([^/:?#\s]+) el host: cualquier cosa hasta el siguiente /, :, ? o #
  3. (:\d+)? un puerto opcional, solo dígitos
  4. (/[^?#\s]*)? una ruta opcional, que empieza por /
  5. (\?[^#\s]*)? una consulta opcional, que empieza por ?
  6. (#\S*)?$ un fragmento opcional y luego el final

Es una buena comprobación de forma. Pero no dice nada sobre si el host tiene sentido. https://999.999.999.999 pasa, y https://- también. Podrías seguir añadiendo reglas para nombres de host, direcciones IP, corchetes de IPv6, usuarios y contraseñas, y dominios internacionales. Hay quien lo ha hecho, y el resultado ocupa cientos de caracteres.

04Deja que un parser haga el parseo

Todos los lenguajes habituales traen un parser de URLs que ya conoce esas reglas. En JavaScript (navegadores y Node), el constructor URL lanza una excepción con cualquier cosa que no pueda analizar:

function isHttpUrl(s) {
  try {
    const u = new URL(s);
    return u.protocol === 'http:' || u.protocol === 'https:';
  } catch {
    return false;
  }
}

new URL("https://exa mple.com"), new URL("https://999.999.999.999") y new URL("example.com") lanzan una excepción. La comprobación del protocolo importa: new URL("javascript:alert(1)") se analiza sin problema, y eso no lo quieres en un enlace.

Además obtienes las piezas gratis: u.hostname, u.port, u.pathname, u.search, u.hash y u.searchParams.get("x") para un valor de la consulta. Y la URL vuelve normalizada: HTTPS://Example.COM:443/a/../b se convierte en https://example.com/b.

En Python, urllib.parse.urlsplit divide una URL en las mismas partes:

from urllib.parse import urlsplit

u = urlsplit("http://localhost:3000/api?x=1#top")
(u.scheme, u.hostname, u.port, u.path, u.query, u.fragment)
# ('http', 'localhost', 3000, '/api', 'x=1', 'top')

Pero urlsplit divide, no valida. Casi nunca lanza un error. urlsplit("example.com") devuelve un esquema y un host vacíos con example.com como ruta, y urlsplit("https://exa mple.com") te dice sin inmutarse que el host es exa mple.com. Así que comprueba u.scheme in ("http", "https") y que u.netloc no esté vacío, y añade tus propias reglas para el host si las necesitas.

05Trampa: el punto sin escapar

En un patrón, . significa “cualquier carácter”. Olvida la barra invertida en un nombre de dominio y habrás admitido dominios que se le parecen:

Coincidencias2/2 como se esperaba
/^https://www.example.com/
  • https://www.example.com
  • https://wwwxexample.com
Pruébalo en la Zona de pruebas
Las dos líneas coinciden. Los puntos después de www y de example aceptan cualquier carácter, así que alguien podría registrar wwwxexample.com.

Escribe www\.example\.com. Cada punto literal de un patrón necesita su barra invertida.

06Trampa: el final que falta

Esta importa en las comprobaciones de redirecciones y listas de permitidos, del tipo “solo devolver a la gente a URLs de nuestro propio sitio”:

Coincidencias2/2 como se esperaba
/^https://example\.com/
  • https://example.com/login
  • https://example.com.evil.io
Pruébalo en la Zona de pruebas
Los puntos están escapados y el inicio anclado, pero nada dice que el host termine ahí. La segunda línea es el dominio de otra persona.

El host tiene que acabar justo después de .com. Exige uno de los caracteres que pueden cerrarlo, o el final de la cadena:

Coincidencias6/6 como se esperaba
/^https://example\.com([/?#:]|$)/

Debe coincidir3/3

  • https://example.com
  • https://example.com/login
  • https://example.com?ref=mail

No debe3/3

  • https://example.com.evil.io
  • https://example.company.io
  • https://evil.io/?next=https://example.com
Pruébalo en la Zona de pruebas
Después de .com tiene que haber un /, ?, #, los dos puntos de un puerto o nada en absoluto.

Mejor aún para comprobaciones de seguridad: analiza con new URL() o urlsplit y luego compara el nombre de host con === o ==. Una comparación exacta de cadenas no tiene casos límite que olvidar.

07Pruébalo

Todos los monstruos son direcciones web. Los inocentes usan el esquema equivocado, un :// roto, una errata o ningún esquema:

Caza jugablepar 10

Atrapa las direcciones http y https y deja libre el resto.

Objetivos · 0/4 atrapados

  • https://regexhunter.comobjetivo
  • http://localhost:3000objetivo
  • https://a.io/x?y=1objetivo
  • http://example.org/objetivo

Inocentes · 0 errores

  • ftp://example.orginocente
  • https:/broken.cominocente
  • example.cominocente
  • mailto:ana@site.cominocente
  • htps://typo.ioinocente

Escribe una regex: el tablero se actualiza mientras escribes.

Pruébalo en la Zona de pruebas
¿Te gustó? Cada día llega una nueva.Jugar la caza de hoy

08Lista de comprobación

  • Para encontrar URLs en un texto: https?://[^\s<>"]*[^\s<>".,;:!?)], así la puntuación final se queda fuera.
  • Para validar una URL: usa new URL() en JavaScript y comprueba el protocolo, o urlsplit en Python y comprueba el esquema y el host.
  • Escapa cada punto literal: example\.com.
  • En las listas de permitidos, ancla el principio y el final del host o, mejor, compara nombres de host ya analizados.
  • Añade el modificador i si pueden aparecer mayúsculas. HTTPS://EXAMPLE.COM es una URL válida.

09Sigue practicando