urllib.parse no Python: manipule URLs

Publicado em: 18/08/2026
Tempo de leitura: 6 minutos
Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.

O módulo urllib.parse no Python separa URLs em componentes, reconstrói endereços, combina caminhos relativos e codifica dados para query strings. Ele é essencial quando uma aplicação precisa manipular URLs sem depender de concatenação manual de strings.

A biblioteca é prática e compatível com código antigo, mas não funciona como um validador completo. urlsplit() e urlparse() podem aceitar entradas incomuns e devolver componentes vazios em vez de lançar erro. Quando a URL tem implicações de segurança, valide esquema, host, porta, credenciais, caminho e destino após o parsing.

Separe uma URL com urlsplit

from urllib.parse import urlsplit

resultado = urlsplit(
    "https://usuario:senha@api.exemplo.com:8443/v1/itens"
    "?pagina=2&ordem=nome#detalhes"
)

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

urlsplit() retorna scheme, netloc, path, query e fragment. O resultado também expõe username, password, hostname e port.

Prefira urlsplit para URLs modernas

urlparse() devolve um campo extra chamado params, baseado em uma sintaxe histórica. Para a maioria das URLs HTTP atuais, urlsplit() é mais simples. Use urlparse() apenas quando a separação de parâmetros de caminho realmente fizer parte do formato que você precisa tratar.

A presença de // importa

from urllib.parse import urlsplit

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

Sem //, a primeira entrada é interpretada como caminho, não como hostname. Não adicione esquema automaticamente sem uma regra clara, porque strings como javascript:..., caminhos locais ou identificadores internos podem ser transformados de forma insegura.

Valide esquema e hostname

from urllib.parse import urlsplit

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

def validar_url(texto: str):
    partes = urlsplit(texto)
    if partes.scheme.lower() not in ESQUEMAS:
        raise ValueError("esquema não permitido")
    if partes.hostname not in HOSTS:
        raise ValueError("host não permitido")
    if partes.username is not None or partes.password is not None:
        raise ValueError("credenciais na URL não são aceitas")
    try:
        porta = partes.port
    except ValueError as erro:
        raise ValueError("porta inválida") from erro
    if porta not in (None, 443):
        raise ValueError("porta não permitida")
    return partes

O atributo hostname é normalizado para minúsculas, enquanto netloc mantém a forma textual. Leia port dentro de tratamento de erro, porque portas fora da faixa geram ValueError.

Parsing não é validação

A documentação oficial deixa explícito que as funções priorizam funcionalidade prática. Elas não garantem conformidade total com RFC 3986 nem com a especificação WHATWG dos navegadores. Defina o que sua aplicação considera válido.

Uma política pode exigir HTTPS, host conhecido, porta padrão, ausência de usuário e senha, caminho absoluto, quantidade máxima de caracteres e ausência de controles. Outra aplicação pode aceitar URLs relativas. A validação deve refletir esse contrato.

URLs e endereços IP

Quando o hostname pode ser um IP, valide-o com ipaddress no Python. Essa verificação ajuda a identificar loopback, link-local e redes privadas, mas não resolve SSRF sozinha. Hostnames precisam ser resolvidos, todos os IPs retornados precisam ser verificados e redirecionamentos devem passar pela mesma política.

Remova fragmentos

from urllib.parse import urldefrag

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

O fragmento não é enviado ao servidor em uma requisição HTTP normal. Removê-lo ajuda a criar chaves de cache ou comparar URLs, mas URLs ainda podem ser equivalentes mesmo com diferenças em porta padrão, percent-encoding ou path.

Reconstrua com geturl ou urlunsplit

from urllib.parse import urlsplit

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

_replace() cria um novo resultado. geturl() pode normalizar o esquema para minúsculas e remover delimitadores vazios. Não dependa da reconstrução para preservar exatamente a string original.

Combine caminhos relativos com urljoin

from urllib.parse import urljoin

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

urljoin() segue regras de resolução de URLs relativas. Ele é útil em crawlers, leitores de feeds e links de documentação.

Cuidado com urljoin e entrada externa

from urllib.parse import urljoin

base = "https://site.exemplo.com/usuarios/"
print(urljoin(base, "https://atacante.exemplo/roubo"))

Uma segunda URL absoluta substitui esquema e host da base. Portanto, não use urljoin(base, valor_do_usuario) esperando que o resultado permaneça no domínio. Antes, use urlsplit() para rejeitar scheme e netloc na entrada relativa, e valide novamente o resultado final.

from urllib.parse import urljoin, urlsplit

def juntar_interno(base: str, relativo: str) -> str:
    entrada = urlsplit(relativo)
    if entrada.scheme or entrada.netloc:
        raise ValueError("URL absoluta não permitida")
    final = urljoin(base, relativo)
    base_host = urlsplit(base).hostname
    if urlsplit(final).hostname != base_host:
        raise ValueError("destino fora do host")
    return final

Codifique componentes com quote

from urllib.parse import quote

nome = "Relatório de vendas/2026"
print(quote(nome))
print(quote(nome, safe=""))

quote() usa percent-encoding. Por padrão, a barra é considerada segura, pois a função foi pensada para caminhos. Para codificar um único segmento, use safe="" para que a barra não seja interpretada como separador.

Não codifique a URL inteira de uma vez

Esquema, hostname, path, query e fragmento possuem regras diferentes. Codificar uma URL completa com quote() pode transformar :, / e ? em dados e quebrar sua estrutura. Monte e codifique cada componente separadamente.

quote versus quote_plus

from urllib.parse import quote, quote_plus

texto = "python avançado"
print(quote(texto))       # python%20avan%C3%A7ado
print(quote_plus(texto))  # python+avan%C3%A7ado

quote_plus() é indicado para dados de formulário e query strings, onde espaço vira +. Em paths, prefira quote(). Um sinal de mais literal precisa ser codificado para não virar espaço ao usar unquote_plus().

Crie query strings com urlencode

from urllib.parse import urlencode

parametros = {
    "busca": "python web",
    "pagina": "2",
    "idioma": "pt-BR",
}
query = urlencode(parametros)
url = f"https://api.exemplo.com/pesquisa?{query}"
print(url)

urlencode() evita erros de caracteres especiais. No Python 3.14, passar certos objetos falsy diferentes de strings vazias, bytes e None está depreciado. Converta números e valores de domínio explicitamente.

Parâmetros repetidos com doseq

from urllib.parse import urlencode

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

Sem doseq=True, a lista pode ser convertida como uma representação Python em vez de gerar vários pares.

Preserve a ordem quando necessário

from urllib.parse import urlencode

pares = [
    ("ordem", "nome"),
    ("ordem", "data"),
    ("pagina", "1"),
]
print(urlencode(pares))

Uma sequência de pares mantém ordem e parâmetros duplicados. Isso é útil em assinaturas de requisição e APIs que atribuem significado à ordem, embora o protocolo deva documentar essa exigência.

Leia queries com parse_qs e parse_qsl

from urllib.parse import parse_qs, parse_qsl

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

parse_qs() agrupa valores por chave; parse_qsl() mantém a sequência de pares. Defina max_num_fields em entradas externas para evitar consumo excessivo.

dados = parse_qs(
    query_recebida,
    keep_blank_values=True,
    strict_parsing=True,
    max_num_fields=200,
)

Decodificação e erros

unquote() usa UTF-8 e, por padrão, substitui sequências inválidas. Em dados críticos, escolha errors="strict" para rejeitar bytes inválidos. Use unquote_to_bytes() quando você precisa dos octetos originais antes de decidir a codificação.

Evite dupla decodificação

Uma entrada como %252e%252e%252f vira %2e%2e%2f após uma decodificação e ../ após outra. Decodifique em uma única camada bem definida. Componentes usados como caminhos locais também precisam de validação contra path traversal; o artigo de tarfile no Python mostra riscos semelhantes em extrações.

str e bytes

As funções de parsing aceitam str ou bytes ASCII. Não misture os tipos na mesma chamada. Bytes não ASCII geram UnicodeDecodeError. Em aplicações web, normalmente é melhor decodificar o transporte de forma explícita e trabalhar com str.

URLs com IPv6

Um IPv6 literal no hostname precisa de colchetes, como https://[2001:db8::1]:8443/. Colchetes não balanceados geram erro. Após o parsing, valide hostname com ipaddress e trate zone IDs conforme a política.

Logs e dados sensíveis

URLs podem conter senhas, tokens e dados pessoais na query. Não registre a URL completa automaticamente. Remova credenciais e filtre chaves como token, key, password e signature.

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

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

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

Configuração e observabilidade

Carregue listas de esquemas, hosts e portas com validação, por exemplo usando configparser no Python. Para investigar decisões de redirecionamento e parsing, use trace no Python. Em crawlers concorrentes, distribua URLs com queue no Python e aplique limites por host.

Testes recomendados

Teste URLs absolutas e relativas, host ausente, credenciais, portas inválidas, IPv6, caracteres de controle, Unicode, fragmentos, queries duplicadas, parâmetros em excesso, percent-encoding inválido, dupla codificação, urljoin() com URL absoluta e redirecionamentos para redes privadas.

Boas práticas

  • Prefira urlsplit() para URLs modernas.
  • Valide cada componente após o parsing.
  • Use allowlists para esquemas e destinos críticos.
  • Não confie em urljoin() com entrada externa.
  • Codifique cada componente separadamente.
  • Limite campos de query.
  • Evite dupla decodificação.
  • Remova segredos de logs.
  • Combine URL, DNS e validação IP contra SSRF.

Conclusão

O urllib.parse no Python elimina manipulações frágeis de URLs e oferece APIs claras para decomposição, resolução, quoting e query strings. Seu uso seguro exige uma camada de validação baseada no contrato da aplicação.

Consulte a documentação oficial do urllib.parse e a RFC 3986. Parsing fornece componentes; confiar no destino é uma decisão separada.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ipaddress no Python: redes IPv4 e IPv6

    Aprenda ipaddress no Python para validar IPv4 e IPv6, calcular redes CIDR, dividir sub-redes e criar políticas de acesso seguras.

    Ler mais

    Tempo de leitura: 6 minutos
    18/08/2026
    A vibrant collection of blue sewing threads arranged with hands on a white background.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    queue no Python: coordene threads

    Aprenda queue no Python para coordenar threads com FIFO, prioridade, backpressure, task_done, join, retries e shutdown seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    A male software engineer working on code in a modern office setting.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    struct no Python: trabalhe com binário

    Aprenda struct no Python para empacotar dados binários, controlar endianness, usar buffers e validar protocolos e arquivos externos.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Close-up of a laptop screen with code and a coffee mug, perfect for tech abstract themes.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tarfile no Python: crie TAR seguro

    Aprenda tarfile no Python para criar TAR comprimido, inspecionar membros e extrair com filtros, limites e proteção contra path traversal.

    Ler mais

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

    gzip no Python: comprima arquivos .gz

    Aprenda gzip no Python para ler e gravar .gz, criar saídas reproduzíveis, trabalhar com streams e limitar a expansão de

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    Color-coded office binders organized neatly in a storage shelf, featuring labels and a striking red binder.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    lzma no Python: comprima arquivos XZ

    Aprenda lzma no Python para criar arquivos XZ, usar streams, checks, filtros e limites de memória ao descompactar dados externos.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026