ProxynetProxynet

Erros de codificação no Python: acentos e UnicodeDecodeError

Publicado:

16 min de leitura

Acar Diveroli
Autor: Acar Diveroli
Três camadas isométricas: bytes C4 B1 abaixo, codec UTF-8 azul no meio, U+0131 acima; placa ISO-8859-1 tracejada à esquerda

Um script coleta editais de licitação do site antigo de uma prefeitura na Türkiye. No navegador, um título aparece como Çankırı İhale İlanı, mas o script imprime Çankýrý Ýhale Ýlaný. Depois, a lista que ele salvou em um notebook com Windows não abre em um servidor Linux e dá 'utf-8' codec can't decode byte 0xc7 in position 0: invalid continuation byte. Os dois problemas têm a mesma causa: os bytes foram gravados com uma tabela de códigos e lidos com outra.

Este guia corrige essa causa onde quer que ela apareça: arquivos de código-fonte, open(), o console do Windows, páginas baixadas com Requests e Beautiful Soup e arquivos CSV para o Excel. Também mostra como ler e reparar texto corrompido. Todos os valores de bytes e mensagens de erro abaixo vêm das nossas próprias execuções com Python 3.13.

Qual é a diferença entre str e bytes no Python?

Uma str guarda caracteres, cada um com um ponto de código Unicode: o ı sem ponto do turco é U+0131 e o ã é U+00E3. Discos e redes armazenam apenas bytes, então o texto é codificado na saída e decodificado na entrada. O codec decide quais bytes representam qual caractere. Em UTF-8, o ı ocupa dois bytes, C4 B1. Na windows-1254, a antiga página de código turca do Windows, ocupa um byte, FD. A cp1252 da Europa Ocidental e a ISO-8859-1 não têm nenhum ı; o ã do português existe nas duas, como o byte E3.

Quando quem grava e quem lê usam codecs diferentes, os bytes sobrevivem, mas as letras mudam. Isso se chama mojibake e muitas vezes pode ser desfeito.

Como um caractere vira outro?

Todo bug de codificação segue os mesmos passos:

  1. Seu texto é Unicode. "Çankırı" no Python são sete pontos de código.
  2. Ele é codificado com o codec A. O Windows em turco grava C7 61 6E 6B FD 72 FD em cp1254.
  3. Só os bytes viajam. Um arquivo, um corpo HTTP ou um pipe não diz nada sobre o codec A, a menos que um cabeçalho, um BOM ou uma tag <meta> o informe.
  4. Alguém decodifica com o codec B. A ISO-8859-1 mapeia FD para ý, o que dá Çankýrý. O UTF-8 não consegue ler C7 61 como um caractere, então lança UnicodeDecodeError.
  5. A saída é codificada de novo. print() e write() usam o codec do console ou do arquivo, e um caractere que não existe nele lança UnicodeEncodeError.

O traceback diz qual direção falhou.

O que o texto corrompido revela?

O formato do estrago aponta para o par de codecs.

O que você vêTexto realO que aconteceuO que fazer
café, ação, ü, ı, ÅŸcafé, ação, ü, ı, şBytes UTF-8 (C3 A9, C4 B1) decodificados como cp1252 ou ISO-8859-1Decodifique os bytes como UTF-8 ou repare o texto (veja abaixo)
ý, þ, ð, Ýı, ş, ğ, İBytes windows-1254 (FD, FE, F0, DD) decodificados como ISO-8859-1, o padrão do RequestsDefina r.encoding = "cp1254" ou passe r.content ao Beautiful Soup
ţ no lugar de şşUm palpite escolheu windows-1250 para uma página em turcoFixe o codec para esse site
Qualquer letra não ASCIIBytes decodificados como UTF-8 com errors="replace"Perdida no texto; decodifique de novo os bytes originais
?Qualquer letraTexto codificado com errors="replace" em um codec que não tem a letraPerdida; grave com UTF-8
Um quadrado vazioA letra certaA fonte não tem o glifo (PDF, terminal antigo)Troque a fonte, não a codificação

Ainda é preciso # -*- coding: utf-8 -*- no Python 3?

Não. A PEP 3120 tornou o UTF-8 a codificação padrão dos arquivos de código-fonte no Python 3.0, então essa linha é uma sobra do Python 2. Se as strings literais do seu arquivo .py ainda quebram, o seu editor o salvou em uma página de código legada ("ANSI" no Windows); salve-o como UTF-8. Outros conselhos do Python 2, como os prefixos u"" e o codecs.open(), também não são mais necessários.

Como definir a codificação ao ler e gravar arquivos?

Passe-a sempre: open("cities.txt", "w", encoding="utf-8"). Sem ela, o Python 3.13 no Windows usa a página de código ANSI. Na nossa máquina com Windows em turco, locale.getpreferredencoding(False) retornou cp1254, e um simples open("cities.txt", "w") gravou Çankırı como C7 61 6E 6B FD 72 FD. O Windows em inglês ou em português do Brasil usa cp1252, que não tem ı, então a mesma gravação falha com 'charmap' codec can't encode character 'ı'.

Para arquivos que você recebe, use o codec com que eles foram gravados: cp1254 para exportações antigas do Windows e do Excel em turco, cp1252 para as da Europa Ocidental e do Brasil, cp1256 para árabe e persa. O argumento errors decide o que acontece com os bytes que não se encaixam:

  • strict, o padrão, lança um erro, que é o que você quer enquanto procura o codec certo.
  • replace coloca no lugar de cada byte inválido, para você ver onde está o estrago.
  • backslashreplace mantém os bytes inválidos visíveis como \xfd.
  • ignore os apaga em silêncio: Iğdır gravado em cp1254 e lido como UTF-8 virou Idr.

"É só tentar latin-1" falha do mesmo jeito. A ISO-8859-1 mapeia todos os 256 valores de byte para caracteres, então nunca lança erro, e o texto em turco vira Çankýrý sem nenhum aviso.

UnicodeDecodeError: 'utf-8' codec can't decode byte

O arquivo não é UTF-8, e o byte da mensagem é uma pista: 0xfd, 0xfe ou 0xf0 em dados turcos apontam para cp1254, enquanto 0xe7 (ç), 0xe3 (ã), 0xe9 (é) ou 0xfc (ü) sugerem cp1252. No pandas, passe o codec: pd.read_csv("export.csv", sep=";", encoding="cp1254").

UnicodeEncodeError: 'charmap' codec can't encode character no Windows

O console interativo do Windows escreve em UTF-8 desde o Python 3.6 (PEP 528), mas a saída redirecionada para um arquivo ou pipe usa a página de código ANSI. As letras turcas cabem na cp1254, uma seta não: python script.py > out.txt com print("Istanbul → Ankara") falhou com 'charmap' codec can't encode character '→'. Três soluções funcionaram:

  • set PYTHONUTF8=1 ($env:PYTHONUTF8=1 no PowerShell) ativa o modo UTF-8 para arquivos e fluxos padrão.
  • python -X utf8 script.py faz o mesmo para uma única execução.
  • PYTHONIOENCODING=utf-8 muda só os fluxos padrão, não o open().

O guia do Python no Windows documenta o modo UTF-8. A PEP 686 torna o modo UTF-8 o padrão a partir do Python 3.15, cujo lançamento final está previsto para 1º de outubro de 2026. Mantenha encoding="utf-8" no seu código até que todas as máquinas usem essa versão.

A variante 'latin-1' codec can't encode character costuma vir de um cabeçalho HTTP. O http.client, que o Requests usa por baixo, codifica os valores de cabeçalho como Latin-1, então um valor de cabeçalho Iğdır falhou dessa forma. Aplique percent-encoding a esses valores ou envie-os no corpo.

De onde vem a codificação de uma página web?

Um navegador decide a codificação de uma página nesta ordem:

  1. Uma marca de ordem de byte (BOM) no início do corpo.
  2. O parâmetro charset do cabeçalho Content-Type.
  3. Uma tag <meta charset>, que segundo o MDN precisa estar nos primeiros 1024 bytes.
  4. Um palpite a partir dos bytes.

O Requests lê só o cabeçalho. Para um tipo text/* sem charset, a documentação do Requests diz que ele segue a RFC 2616 e usa ISO-8859-1, embora a RFC 7231 tenha removido esse padrão em 2014. Uma resposta JSON sem charset é lida como UTF-8, e os outros tipos são adivinhados. Nas nossas páginas de teste servidas como text/html simples, r.encoding foi ISO-8859-1 todas as vezes. É por isso que uma página pode aparecer certa no navegador e errada no seu script.

A windows-1254 e a ISO-8859-9 são iguais?

Quase. As duas colocam as letras turcas nos mesmos bytes, de 0xA0 a 0xFF. De 0x80 a 0x9F, a windows-1254 tem , , e , enquanto a ISO-8859-9 tem códigos de controle invisíveis. O WHATWG Encoding Standard manda os navegadores lerem iso-8859-9 como windows-1254 e iso-8859-1 como windows-1252; o Python trata esses rótulos como codecs separados. Na nossa página rotulada iso-8859-9, o Beautiful Soup produziu \x93Kampanya\x94 10\x80, e com from_encoding="cp1254" os mesmos bytes viraram “Kampanya” 10€. Decodifique essas páginas como cp1254 ou cp1252.

Como a sua própria página HTML deve declarar o charset?

Salve o arquivo como UTF-8, coloque <meta charset="utf-8"> no topo do <head> e confira se o cabeçalho Content-Type do servidor não informa outro charset, porque o cabeçalho tem prioridade.

response.encoding, apparent_encoding ou Beautiful Soup?

Cada opção lê um sinal diferente:

  • r.encoding vem do cabeçalho. Definir r.encoding = "cp1254" resolve um site conhecido; definir "utf-8" para todos os sites quebra os que usam windows-1254 (�ank�r� na nossa página de teste).
  • r.apparent_encoding é o palpite do charset-normalizer a partir do corpo. Ele indicou Windows-1254 para uma das nossas páginas em turco e windows-1250 para outras duas, o que transforma ş em ţ.
  • BeautifulSoup(r.content, "html.parser") lê a própria tag <meta>. Passe bytes, não r.text; o tutorial de Beautiful Soup cobre a parte do parsing.

A nossa ordem segue a do navegador: um BOM, um charset no cabeçalho, a tag <meta> e depois o palpite, registrando no log qual deles foi usado. O HTTPX é diferente: a versão 0.28.1 assume UTF-8 quando o cabeçalho não tem charset, e a nossa página em cp1254 saiu como �ank�r� (HTTPX, Requests ou AIOHTTP).

Um scraper em Python que decodifica páginas como um navegador

O script baixa cada URL uma vez, escolhe o codec nessa ordem, mapeia os rótulos ISO como os navegadores fazem e grava os títulos em um CSV para o Excel. Ele precisa de pip install requests beautifulsoup4. Não faz novas tentativas nem rotaciona IPs; para isso, veja códigos de status HTTP no web scraping e como rotacionar proxies em Python.

python
"""Baixa páginas, decodifica cada uma como um navegador faria e salva os títulos em um CSV para o Excel."""
import csv
import logging
import sys
from email.message import Message

import requests
from bs4 import BeautifulSoup
from bs4.dammit import EncodingDetector

PROXY = None  # por exemplo "http://user:pass@pr.proxynet.io:8000"
USER_AGENT = "heading-reader/1.0 (+https://example.com/bot)"

# Os navegadores leem estes rótulos como páginas de código do Windows (WHATWG Encoding Standard); o Python não.
BROWSER_ALIASES = {
    "iso-8859-1": "cp1252", "iso8859-1": "cp1252", "latin1": "cp1252", "latin-1": "cp1252",
    "us-ascii": "cp1252", "ascii": "cp1252",
    "iso-8859-9": "cp1254", "iso8859-9": "cp1254", "latin5": "cp1254",
}

log = logging.getLogger("headings")


def header_charset(resp):
    """O charset do cabeçalho Content-Type, ou None. O próprio r.encoding do Requests
    diria ISO-8859-1 aqui para qualquer tipo text/* sem charset."""
    msg = Message()
    msg["content-type"] = resp.headers.get("Content-Type", "")
    return msg.get_param("charset")


def pick_codec(resp):
    """Retorna (codec, de onde veio): um BOM, o cabeçalho, <meta charset> e, por fim, um palpite."""
    bom = EncodingDetector.strip_byte_order_mark(resp.content)[1]
    if bom:
        return bom, "bom"
    declared = header_charset(resp)
    source = "header"
    if not declared:
        declared = EncodingDetector.find_declared_encoding(resp.content, is_html=True)
        source = "meta"
    if not declared:
        return resp.apparent_encoding or "utf-8", "guess"
    return BROWSER_ALIASES.get(declared.lower(), declared), source


def clean(text):
    """Junta sequências de espaços em branco, incluindo o espaço sem quebra U+00A0."""
    return " ".join(text.split())


def read_headings(session, url):
    resp = session.get(url, timeout=20)
    resp.raise_for_status()
    codec, source = pick_codec(resp)
    soup = BeautifulSoup(resp.content, "html.parser", from_encoding=codec)
    level = logging.WARNING if source == "guess" else logging.INFO
    log.log(level, "%s: %s from %s", url, codec, source)
    if soup.contains_replacement_characters:
        log.warning("%s: some bytes did not fit %s and became U+FFFD", url, codec)
    return [(url, codec, source, clean(h.get_text())) for h in soup.select("h1, h2")]


def main(urls, out="headings.csv"):
    session = requests.Session()
    session.headers["User-Agent"] = USER_AGENT
    if PROXY:
        session.proxies = {"http": PROXY, "https": PROXY}
    rows = []
    for url in urls:
        try:
            rows += read_headings(session, url)
        except requests.RequestException as exc:
            log.error("%s: %s", url, exc)
    # utf-8-sig grava um BOM primeiro, assim o Excel reconhece o arquivo como UTF-8
    with open(out, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["url", "codec", "source", "heading"])
        writer.writerows(rows)
    log.info("%d headings written to %s", len(rows), out)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s")
    main(sys.argv[1:] or ["https://example.com/"])

Nós o executamos com Python 3.13, Requests 2.34.2 e Beautiful Soup 4.15.0 através de um proxy HTTP local. As páginas de teste locais foram servidas como text/html sem charset, exceto utf8hdr, e a última URL é uma página pública:

text
INFO    http://127.0.0.1:8057/cp1254: windows-1254 from meta
INFO    http://127.0.0.1:8057/iso9: cp1254 from meta
INFO    http://127.0.0.1:8057/utf8meta: utf-8 from meta
INFO    http://127.0.0.1:8057/utf8hdr: utf-8 from header
INFO    http://127.0.0.1:8057/latin1: cp1252 from meta
WARNING http://127.0.0.1:8057/nometa: windows-1250 from guess
WARNING https://example.com/: ascii from guess
INFO    12 headings written to headings.csv

Todas as páginas com charset no cabeçalho ou com tag <meta> saíram certas, inclusive “quoted” 5€ na página rotulada ISO-8859-1. A página sem nenhum dos dois é o ponto fraco: o palpite indicou windows-1250 e o CSV recebeu Çankýrý e ţubat.

Se você lê muitos sites de um mesmo país em execuções agendadas, um Proxies residenciais nesse país retorna as páginas que os visitantes locais veem; a lógica do codec continua a mesma.

O que significam "can't decode byte 0x8b" e "0xa0"?

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1

Um fluxo gzip começa com 1F 8B (RFC 1952), então 0x8b na posição 1 significa que dados comprimidos estão sendo decodificados como texto; gzip.compress(...).decode("utf-8") nos deu exatamente essa mensagem. No scraping, isso acontece quando você lê r.raw diretamente ou envia um cabeçalho Accept-Encoding copiado com urllib.request, que nunca descomprime. O Requests descomprime o gzip sozinho, então remova o cabeçalho copiado e use r.content. Valores br ou zstd copiados também falham, a menos que o urllib3 tenha o pacote opcional Brotli ou Zstandard.

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0

Na cp1252, na cp1254 e na ISO-8859-1, o espaço sem quebra U+00A0 é o byte único A0, com o qual o UTF-8 nunca começa um caractere. Leia o arquivo com o codec real dele. Se o caractere já está no seu texto, "10\xa0kg".split() o remove, .split(" ") não, e unicodedata.normalize("NFKC", s) o transforma em um espaço comum (a NFKC também muda caracteres como ² para 2).

Como reparar um texto que já está corrompido?

Se nenhum byte foi perdido, desfaça o passo errado:

  • Texto como ışğ ou ação copiado de uma tela ou planilha: s.encode("cp1252").decode("utf-8")ışğ e ação.
  • O mesmo estrago vindo do r.text do Requests esconde um caractere de controle (\x9f) que a cp1252 não consegue codificar, então use s.encode("latin-1").decode("utf-8"). A cp1252 falhou nele com 'charmap' codec can't encode character '\x9f'.
  • Texto windows-1254 lido como ISO-8859-1, como Çankýrý: s.encode("latin-1").decode("cp1254")Çankırı.

Para dados grandes ou mistos, o ftfy 6.3.1 repara mojibake de UTF-8: ftfy.fix_text("ışğ") retornou ışğ. Ele deixou Çankýrý como estava porque parece texto latino válido, então corrija as trocas de página de código à mão. Texto com ? ou no lugar das letras não tem conserto; baixe o original de novo.

Por que o Excel mostra caracteres corrompidos em um CSV UTF-8?

O Excel abre corretamente um CSV UTF-8 com um clique duplo quando o arquivo começa com uma marca de ordem de byte, como observa a página de suporte da Microsoft. encoding="utf-8-sig" adiciona os três bytes EF BB BF; no pandas, df.to_csv("out.csv", encoding="utf-8-sig"). Leia esses arquivos também com utf-8-sig: com utf-8 simples, o módulo csv nos deu uma primeira coluna chamada şehir, enquanto o pandas removeu o BOM nos dois casos. Um fluxo completo do scraping ao Excel está em como extrair dados de um site.

Por que "I".lower() não retorna "ı"?

O str.lower() usa o mapeamento padrão do Unicode, que ignora o idioma. Para o turco, isso dá errado duas vezes: "ISPARTA".lower() retorna isparta em vez de ısparta, e "İ".lower() retorna i mais um ponto combinante (U+0307). O casefold() tem a mesma lacuna. Mapeie primeiro as duas letras especiais:

python
TR_LOWER = str.maketrans({"I": "ı", "İ": "i"})
TR_UPPER = str.maketrans({"i": "İ", "ı": "I"})

print("ISPARTA İZMİR".translate(TR_LOWER).lower())  # ısparta izmir
print("istanbul ışık".translate(TR_UPPER).upper())  # İSTANBUL IŞIK

Números e datas também seguem regras locais. Um preço turco ou brasileiro como 1.299,90 vira float depois de s.replace(".", "").replace(",", "."), e o pandas tem decimal="," e thousands=".". Evite locale.setlocale() em um scraper, porque ele muda o processo inteiro. A limpeza completa de preços está em monitoramento de preços da concorrência.

Casos de uso

Erros comuns

  • Silenciar o erro com errors="ignore" ou latin-1. O script roda e as letras somem ou mudam.
  • Fixar r.encoding = "utf-8" para todos os sites. Resolve algumas páginas e quebra as que usam windows-1254.
  • Decodificar duas vezes. Chamar .encode().decode() em um texto que já foi decodificado corretamente cria um erro novo em um texto que estava bom.
  • Usar iso-8859-9 ou iso-8859-1 como declarados. Os navegadores usam windows-1254 e windows-1252, e as aspas e o símbolo do euro mostram a diferença.
  • Ler um arquivo utf-8-sig como utf-8. O nome da primeira coluna começa com um caractere invisível e as buscas por ele falham.
  • Confundir codificação de caracteres com codificação de URL. %40 em uma senha de proxy é percent-encoding, um assunto à parte (caracteres especiais em senhas de proxy).

Guia de decisão

NecessidadeRecomendação
Texto não ASCII no seu próprio arquivo .pySalve-o como UTF-8; sem linha de coding
Ler ou gravar um arquivoencoding="utf-8"; uma página de código legada só para arquivos gravados com ela
Erro 'charmap' na saída redirecionada no WindowsPYTHONUTF8=1 ou python -X utf8; padrão a partir do Python 3.15
HTML sem charset no cabeçalhoPasse r.content ao Beautiful Soup e registre o codec no log
Sem charset no cabeçalho e sem tag <meta>Fixe o codec do site; apparent_encoding como último recurso
A página diz iso-8859-9 ou iso-8859-1Decodifique como cp1254 ou cp1252, como um navegador
O texto já aparece como é ou ı.encode("cp1252").decode("utf-8") ou ftfy.fix_text
O resultado vai ser aberto no ExcelGrave o CSV com utf-8-sig

Perguntas frequentes

Preciso de uma biblioteca para lidar com acentos no Python?

Não. Uma str do Python 3 é Unicode, e a biblioteca padrão traz codecs para as páginas de código mais comuns (codificações padrão). O charset-normalizer, instalado junto com o Requests, adivinha codecs desconhecidos, e o ftfy repara mojibake.

Como converter um arquivo cp1252 ou cp1254 para UTF-8?

Leia-o com o codec antigo e grave-o com o novo: Path("new.txt").write_text(Path("old.txt").read_text(encoding="cp1254"), encoding="utf-8"), com Path vindo de pathlib. Confira alguns nomes antes de apagar o original.

errors="ignore" resolve o UnicodeDecodeError?

Ele esconde o erro. Os bytes que não se encaixam são apagados sem aviso, e os nomes perdem letras. Em vez disso, encontre o codec certo.

Como descobrir qual codificação um arquivo usa?

A menos que comece com um BOM, um arquivo de texto não registra o seu codec, então toda ferramenta adivinha. charset_normalizer.from_path("old.txt").best().encoding retornou cp1254 para o nosso arquivo de teste em turco. Confirme o palpite decodificando o arquivo e lendo algumas palavras que tenham letras locais.

Qual é a diferença entre utf-8 e utf-8-sig?

utf-8-sig grava um BOM (EF BB BF) e o pula na leitura. Use-o para arquivos CSV que as pessoas abrem no Excel.

Como corrigir o UnicodeDecodeError no read_csv do pandas?

Passe o codec do arquivo: encoding="cp1254" para uma exportação do Windows em turco, cp1252 para uma da Europa Ocidental ou do Brasil, utf-8-sig se ele tiver BOM. encoding_errors="replace" faz o erro parar, mas perde letras.

Em resumo

Os erros de codificação no Python se resumem a uma regra: decodifique os bytes com o codec em que foram gravados e grave a sua própria saída em UTF-8. Leia o texto corrompido para achar o par de codecs, defina encoding em todo open(), use o modo UTF-8 no Windows até que o Python 3.15 faça isso por você e passe as páginas web ao Beautiful Soup como bytes. Para trabalhos de scraping que precisam de texto limpo de muitos sites locais, veja a nossa solução de extração de dados.