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

    Desenvolvedor configurando servidor HTTPS e certificado TLS com Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crie servidor HTTPS local no Python

    Aprenda HTTPSServer no Python para servir HTTPS localmente, configurar certificados, usar threads e entender limites de segurança.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Arquivos protegidos representando extração segura de TAR com Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extraia TAR com segurança

    Aprenda tarfile extraction_filter no Python para extrair arquivos TAR com validação, segurança e controle de caminhos.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026
    Código Python exibindo avisos controlados com catch_warnings
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: capture warnings em testes Python

    Aprenda catch_warnings no Python para capturar, testar e controlar avisos com filtros específicos e segurança.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código Python representando referências persistentes do pickle
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: serialize referências externas

    Aprenda pickle persistent_id no Python para serializar referências externas com IDs estáveis, validação, segurança e compatibilidade.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código binário representando buffers e memória no Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: conte valores sem copiar buffers

    Aprenda memoryview.count no Python para contar bytes e valores em buffers sem cópias, com formatos, limites e boas práticas.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026
    Código Python com anotações de tipos
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: evite imports circulares em anotações

    Aprenda annotationlib no Python para ler anotações adiadas, evitar imports circulares e criar ferramentas de introspecção seguras.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026