urllib.parse en Python: maneja URLs

Publicado el: 18/08/2026
Tempo de leitura: 5 minutos
Detailed image of a Burmese Python being held. Captured in Toluca, Mexico.

El módulo urllib.parse en Python separa URLs en componentes, reconstruye direcciones, resuelve referencias relativas y codifica datos para query strings. Sustituye concatenaciones frágiles por funciones específicas para esquema, autoridad, path, query y fragmento.

La biblioteca es práctica y compatible con código antiguo, pero no es un validador completo. urlsplit() y urlparse() pueden aceptar entradas extrañas y devolver componentes vacíos en lugar de lanzar errores. Cuando una URL tiene impacto de seguridad, valida esquema, host, puerto, credenciales, path y destino final después del parsing.

Divide una URL

from urllib.parse import urlsplit

resultado = urlsplit(
    "https://usuario:secreto@api.ejemplo.com:8443/v1/items"
    "?pagina=2&orden=nombre#detalles"
)

print(resultado.scheme)
print(resultado.hostname)
print(resultado.port)
print(resultado.path)
print(resultado.query)
print(resultado.fragment)

urlsplit() devuelve scheme, netloc, path, query y fragment. El resultado estructurado también expone username, password, hostname y port.

Prefiere urlsplit para URLs modernas

urlparse() añade un campo histórico llamado params. Para la mayoría de URLs HTTP actuales, urlsplit() es más sencillo. Usa urlparse() solo cuando separar parámetros de path sea una necesidad real.

Las dobles barras cambian la interpretación

from urllib.parse import urlsplit

print(urlsplit("ejemplo.com/pagina"))
print(urlsplit("//ejemplo.com/pagina"))
print(urlsplit("https://ejemplo.com/pagina"))

Sin //, la primera cadena se interpreta como path, no como hostname. No añadas un esquema automáticamente sin una regla clara, porque podrías transformar rutas locales, identificadores o esquemas peligrosos.

Valida esquema, hostname y puerto

from urllib.parse import urlsplit

ESQUEMAS = {"https"}
HOSTS = {"api.ejemplo.com", "cdn.ejemplo.com"}

def validar_url(texto: str):
    partes = urlsplit(texto)
    if partes.scheme.lower() not in ESQUEMAS:
        raise ValueError("esquema no permitido")
    if partes.hostname not in HOSTS:
        raise ValueError("host no permitido")
    if partes.username is not None or partes.password is not None:
        raise ValueError("no se aceptan credenciales en la URL")
    try:
        puerto = partes.port
    except ValueError as error:
        raise ValueError("puerto inválido") from error
    if puerto not in (None, 443):
        raise ValueError("puerto no permitido")
    return partes

hostname se normaliza a minúsculas, mientras netloc conserva más del texto original. Leer port puede lanzar ValueError si está fuera de rango.

Parsing no es validación

La documentación oficial advierte que estas funciones priorizan comportamiento práctico. No son validadores estrictos de RFC 3986 ni del estándar WHATWG. Tu aplicación debe definir qué considera válido.

Una política puede exigir HTTPS, host conocido, puerto estándar, ausencia de credenciales, path absoluto, longitud máxima y ausencia de caracteres de control. Otra aplicación puede admitir URLs relativas. La validación debe seguir ese contrato.

URLs con direcciones IP

Cuando el hostname pueda ser un IP literal, valídalo con ipaddress en Python. Esto ayuda a detectar loopback, privados y link-local, pero no resuelve SSRF por sí solo. Un hostname puede devolver varios IPs, cambiar después de la validación o redirigir.

Elimina fragmentos

from urllib.parse import urldefrag

resultado = urldefrag("https://ejemplo.com/manual#instalacion")
print(resultado.url)
print(resultado.fragment)

El fragmento normalmente no se envía al servidor HTTP. Eliminarlo puede ayudar en claves de caché y comparaciones, aunque URLs equivalentes todavía pueden diferir en puerto, percent-encoding o normalización del path.

Reconstruye resultados

from urllib.parse import urlsplit

partes = urlsplit("HTTP://Ejemplo.com/pagina?#")
limpia = partes._replace(fragment="").geturl()
print(limpia)

_replace() crea un resultado nuevo. geturl() puede convertir el esquema a minúsculas y quitar delimitadores vacíos. No lo uses si necesitas preservar exactamente el texto original.

Resuelve URLs relativas con urljoin

from urllib.parse import urljoin

base = "https://docs.ejemplo.com/guias/python/"
print(urljoin(base, "instalacion.html"))
print(urljoin(base, "../referencia/api.html"))

urljoin() implementa las reglas de referencias relativas y resulta útil en crawlers, feeds y documentación.

urljoin puede cambiar el dominio

from urllib.parse import urljoin

base = "https://sitio.ejemplo.com/usuarios/"
print(urljoin(base, "https://atacante.ejemplo/robo"))

Una segunda URL absoluta sustituye esquema y hostname. No uses urljoin(base, entrada_externa) esperando permanecer en el dominio original.

from urllib.parse import urljoin, urlsplit

def unir_interno(base: str, relativo: str) -> str:
    candidato = urlsplit(relativo)
    if candidato.scheme or candidato.netloc:
        raise ValueError("URL absoluta no permitida")
    resultado = urljoin(base, relativo)
    if urlsplit(resultado).hostname != urlsplit(base).hostname:
        raise ValueError("el destino salió del host permitido")
    return resultado

Valida nuevamente la URL final porque la normalización y los redirects añaden decisiones de política.

Codifica componentes con quote

from urllib.parse import quote

nombre = "Informe de ventas/2026"
print(quote(nombre))
print(quote(nombre, safe=""))

quote() utiliza percent-encoding. La barra es segura por defecto porque está pensado para paths. Para codificar un único segmento, usa safe="" para impedir que una barra cree otro segmento.

No codifiques una URL completa a ciegas

Esquema, hostname, path, query y fragmento tienen reglas diferentes. Aplicar quote() a toda la URL puede codificar separadores estructurales como :, / y ?. Construye y codifica cada componente.

quote y quote_plus

from urllib.parse import quote, quote_plus

texto = "python avanzado"
print(quote(texto))
print(quote_plus(texto))

quote_plus() está pensado para formularios y queries, donde el espacio se convierte en +. Para paths, usa quote(). Un signo más literal debe codificarse para no convertirse en espacio al decodificar.

Crea query strings con urlencode

from urllib.parse import urlencode

parametros = {
    "busqueda": "python web",
    "pagina": "2",
    "idioma": "es",
}
query = urlencode(parametros)
url = f"https://api.ejemplo.com/buscar?{query}"
print(url)

urlencode() maneja caracteres especiales. En Python 3.14 está deprecado aceptar algunos objetos falsy distintos de cadenas vacías, bytes y None. Convierte números y valores de dominio explícitamente.

Parámetros repetidos

from urllib.parse import urlencode

query = urlencode(
    {"tag": ["python", "web"], "pagina": "1"},
    doseq=True,
)
print(query)

Sin doseq=True, una lista puede codificarse como representación de Python en lugar de varios pares.

Conserva el orden con pares

from urllib.parse import urlencode

pares = [
    ("orden", "nombre"),
    ("orden", "fecha"),
    ("pagina", "1"),
]
print(urlencode(pares))

Una secuencia de pares mantiene orden y claves duplicadas. Puede importar en firmas de solicitudes o APIs con parámetros ordenados.

Interpreta queries con límites

from urllib.parse import parse_qs, parse_qsl

query = "tag=python&tag=web&vacio="
print(parse_qs(query, keep_blank_values=True))
print(parse_qsl(query, keep_blank_values=True))

parse_qs() agrupa valores; parse_qsl() conserva la secuencia. Define max_num_fields para entradas externas.

valores = parse_qs(
    query_recibida,
    keep_blank_values=True,
    strict_parsing=True,
    max_num_fields=200,
)

Errores de decodificación

unquote() usa UTF-8 y reemplaza secuencias inválidas por defecto. Un parser crítico puede usar errors="strict". unquote_to_bytes() devuelve octetos originales cuando la aplicación debe decidir la codificación.

Evita doble decodificación

%252e%252e%252f se convierte en %2e%2e%2f tras una decodificación y en ../ tras dos. Decodifica exactamente una vez en una capa definida. Si el resultado se usa como ruta local, valida path traversal; el mismo riesgo aparece en tarfile en Python.

Texto y bytes

Las funciones aceptan str o bytes ASCII. No mezcles ambos tipos en una llamada. Los bytes no ASCII producen UnicodeDecodeError. Las aplicaciones web suelen beneficiarse de decodificar el transporte explícitamente y trabajar con texto.

IPv6 literal en URLs

Un hostname IPv6 debe usar corchetes, por ejemplo https://[2001:db8::1]:8443/. Los corchetes sin pareja producen error. Valida el hostname con ipaddress y define cómo manejar zone IDs.

Protege los logs

Las URLs pueden contener contraseñas, tokens, datos personales y firmas. No registres el valor completo por defecto. Elimina credenciales y oculta claves sensibles.

from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

SENSIBLES = {"token", "password", "key", "signature"}

def url_para_log(url: str) -> str:
    partes = urlsplit(url)
    query = urlencode([
        (clave, "***" if clave.lower() in SENSIBLES else valor)
        for clave, valor in parse_qsl(partes.query, keep_blank_values=True)
    ])
    host = partes.hostname or ""
    if partes.port:
        host = f"{host}:{partes.port}"
    return urlunsplit((partes.scheme, host, partes.path, query, ""))

Configuración y concurrencia

Carga esquemas, hosts y puertos permitidos mediante una capa validada como configparser en Python. Usa trace en Python para investigar decisiones de parsing y redirects. Los crawlers pueden distribuir URLs con queue en Python y aplicar límites por host.

Pruebas recomendadas

Prueba URLs absolutas y relativas, host ausente, credenciales, puertos inválidos, IPv6, caracteres de control, Unicode, fragmentos, queries duplicadas, demasiados campos, percent-encoding inválido, doble codificación, URL absoluta en urljoin() y redirects hacia redes privadas.

Buenas prácticas

  • Prefiere urlsplit().
  • Valida cada componente.
  • Usa allowlists para destinos críticos.
  • No confíes en urljoin() con entrada externa.
  • Codifica cada componente por separado.
  • Limita campos de query.
  • Evita doble decodificación.
  • Oculta secretos en logs.
  • Combina validación de URL, DNS e IP contra SSRF.

Conclusión

urllib.parse en Python proporciona herramientas fiables para dividir, resolver, codificar y construir URLs. Un uso seguro requiere una capa adicional de validación basada en el contrato de la aplicación.

Consulta la documentación oficial de urllib.parse y la RFC 3986. El parsing devuelve componentes; confiar en el destino es una decisión separada.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ipaddress en Python: redes IPv4 e IPv6

    Aprende ipaddress en Python para validar IPv4 e IPv6, calcular redes CIDR, dividir subredes y crear políticas de acceso más

    Ler mais

    Tempo de leitura: 5 minutos
    18/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    queue en Python: coordina hilos

    Aprende queue en Python para coordinar hilos con FIFO, prioridad, backpressure, tracking, reintentos y shutdown seguro.

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    Chic portrait of a woman wearing trendy sunglasses reflecting numbers, captured in a modern setting.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    struct en Python: datos binarios

    Aprende struct en Python para empaquetar datos binarios, controlar endianness, reutilizar buffers y validar protocolos externos.

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    A laptop screen showing a code editor with visible programming code in a dimly lit environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tarfile en Python: crea TAR seguro

    Aprende tarfile en Python para crear TAR comprimido, inspeccionar miembros y extraer con filtros, límites y protección de rutas.

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    Row of colorful office binders neatly arranged on a shelf, ideal for organization concepts.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    gzip en Python: comprime archivos .gz

    Aprende gzip en Python para leer y escribir .gz, crear streams reproducibles, procesar datos grandes y limitar la expansión externa.

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    Neatly arranged blue office binders labeled with dates and names for organized storage.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    lzma en Python: comprime archivos XZ

    Aprende lzma en Python para crear archivos XZ, procesar streams, elegir checks y filtros y limitar memoria con datos externos.

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026