Programação Por Equipe DevBras

Capa do artigo: ç no lugar de ç: como consertar o dado corrompido uma vez só no MySQL

ç no lugar de ç: como consertar o dado corrompido uma vez só no MySQL

Como identificar registros com acento duplamente codificado, testar a correção com SELECT, aplicar o UPDATE só onde precisa e tirar o utf8_decode espalhado pelas páginas.

A loja tem 12 mil clientes cadastrados. Os antigos aparecem como "João" e "Conceição". Alguém resolveu com um utf8_decode() no echo da listagem. Os antigos ficaram certos. Os novos, que já nasciam corretos, passaram a aparecer como "Jo?o". Aí outra pessoa colocou um if para decidir quando decodificar. Hoje ninguém sabe qual registro está em qual estado.

A resposta curta: "ç" é UTF-8 lido como Latin-1 e gravado de novo como UTF-8. Converter na saída não conserta, só empilha camadas. Primeiro faça a cadeia inteira falar UTF-8, para nenhum registro novo nascer errado. Depois conserte os registros antigos uma vez, no banco, com um UPDATE testado em SELECT, filtrado só para as linhas quebradas e com backup antes. Por fim, apague todas as funções de conversão da saída.

O que aconteceu com os bytes

A letra "ç" em UTF-8 ocupa dois bytes: C3 A7. Em Latin-1, cada byte é uma letra: C3 é "Ã" e A7 é "§". Quando uma camada manda bytes UTF-8 e a outra os interpreta como Latin-1, o "ç" vira "ç". Se esse texto errado for gravado numa coluna UTF-8, o banco guarda, com toda fidelidade, os caracteres "Ã" e "§", agora com quatro bytes: C3 83 C2 A7.

O caso típico em sistema PHP antigo: a tabela foi criada em latin1, a conexão nunca declarou charset, e o formulário mandava UTF-8. Depois alguém migrou a coluna para utf8mb4 com ALTER TABLE ... CONVERT TO. O MySQL converteu fielmente o que achou que era Latin-1, e o mojibake ficou petrificado em UTF-8 válido.

Se a história se repete, o texto é codificado duas vezes e o "ç" vira "ç". Cada nova "correção" aplicada às cegas acrescenta uma camada. Por isso o título: o dado corrompido se conserta uma vez, com método, e não a cada exibição.

Passo 1: pare de produzir dado novo errado

Antes de mexer no que existe, garanta que um cadastro feito hoje nasce certo. Isso envolve quatro pontos: arquivos PHP salvos em UTF-8, cabeçalho HTTP com charset=utf-8, conexão com charset=utf8mb4 e colunas em utf8mb4. O passo a passo dessas camadas está em acento quebrado é encoding diferente em cada camada e em acentos que quebram só quando vêm do MySQL. Este guia cuida do que já foi gravado errado.

Teste: cadastre "Conceição Ávila" pelo formulário, sem nenhuma função de conversão no caminho, e confira no banco:

SELECT nome, HEX(nome) FROM clientes ORDER BY id DESC LIMIT 1;

O "ç" tem que aparecer como C3A7 no hexadecimal. Se aparecer C383C2A7, a cadeia ainda está quebrada e consertar os antigos agora só cria mais lixo amanhã.

Passo 2: encontre os registros quebrados

Os sinais de UTF-8 lido como Latin-1 são sequências começando por "Ã" ou "Â" seguidas de outro símbolo. Cuidado com a collation: em utf8mb4_0900_ai_ci, que ignora acento, "Ã" é igual a "A" e o LIKE casa quase tudo. Force comparação binária:

SELECT id, nome
FROM clientes
WHERE nome COLLATE utf8mb4_bin LIKE '%Ã%'
   OR nome COLLATE utf8mb4_bin LIKE '%Â%'
LIMIT 50;

Na loja do exemplo, a consulta devolveu 3.140 linhas. Olhe uma amostra com atenção: um nome estrangeiro legítimo pode conter "Ã" de verdade. Raro em cadastro brasileiro, mas acontece, e é por isso que a correção precisa de olho humano.

Passo 3: teste a correção com SELECT

A reversão faz o caminho inverso: transforma o texto em Latin-1 (recuperando os bytes originais), trata como binário e reinterpreta como UTF-8.

SELECT id,
       nome AS atual,
       CONVERT(CAST(CONVERT(nome USING latin1) AS BINARY) USING utf8mb4) AS corrigido
FROM clientes
WHERE nome COLLATE utf8mb4_bin LIKE '%Ã%'
   OR nome COLLATE utf8mb4_bin LIKE '%Â%'
LIMIT 50;

Leia a coluna corrigido. "João" deve virar "João", "Conceição" deve virar "Conceição". Se aparecer "ã" ainda, aquela linha foi codificada duas vezes e precisa da conversão aplicada mais uma vez, em separado. Se aparecer vazio, NULL ou texto cortado, a linha não era mojibake desse tipo e não pode entrar no UPDATE.

Um detalhe: o latin1 do MySQL na prática é o cp1252 do Windows, que tem aspas curvas e travessão nas posições onde o Latin-1 puro tem caracteres de controle. Isso costuma jogar a favor, porque o texto colado do Word passou pelo mesmo caminho, mas confirme na amostra.

Passo 4: aplique uma vez, com rede de segurança

  1. Faça backup da tabela e confirme que ele restaura (veja backup de MySQL que você consegue restaurar).
  2. Copie as linhas filtradas para uma tabela de apoio, com o valor antigo.
  3. Apague da tabela de apoio os ids que, no SELECT, não ficaram certos: nomes legítimos com "Ã", linhas com dupla codificação, resultados vazios ou cortados. Esses você trata à parte.
  4. Rode o UPDATE só para os ids que sobraram na tabela de apoio.
  5. Rode a busca do passo 2 de novo e confira o que sobrou, um a um.
CREATE TABLE clientes_nome_antes AS
SELECT id, nome FROM clientes
WHERE nome COLLATE utf8mb4_bin LIKE '%Ã%'
   OR nome COLLATE utf8mb4_bin LIKE '%Â%';

-- depois de revisar a amostra:
DELETE FROM clientes_nome_antes WHERE id IN (812, 4410);

UPDATE clientes c
JOIN clientes_nome_antes a ON a.id = c.id
SET c.nome = CONVERT(CAST(CONVERT(a.nome USING latin1) AS BINARY) USING utf8mb4);

Rode primeiro numa cópia do banco. Repita para cada coluna de texto afetada: nome, endereço, observações. Quando tudo estiver conferido, apague a tabela de apoio.

Passo 5: arranque as conversões do código

Agora os dados estão certos e os novos nascem certos. Toda chamada a utf8_decode(), utf8_encode(), iconv() ou mb_convert_encoding() usada para "consertar a exibição" passou a estragar o que estava bom. Procure e remova:

grep -rnE "utf8_(de|en)code|iconv[(]|mb_convert_encoding" --include=*.php .

Nem toda ocorrência é para apagar: conversão ao ler um CSV exportado por um sistema em Windows-1252 é legítima. A regra é converter na fronteira, quando o dado entra, e nunca na saída. As funções utf8_decode e utf8_encode, aliás, estão marcadas como obsoletas desde o PHP 8.2.

Erros comuns

  • Converter na hora de exibir: conserta os antigos e estraga os novos. Duas populações de dados convivendo para sempre.
  • Rodar o UPDATE na tabela inteira: linhas que estavam certas viram lixo ou NULL, porque não eram mojibake.
  • Consertar antes de arrumar a conexão: o cadastro de amanhã nasce quebrado e a busca nunca zera.
  • Usar LIKE com collation que ignora acento: a busca devolve a tabela quase toda e você "corrige" o que não precisava.
  • Editar à mão por cima: alguém vê "ç" no painel, apaga e redigita no mesmo registro que o UPDATE também vai tocar. O resultado é imprevisível.
  • Não guardar o valor anterior: se algo der errado, não há como comparar nem voltar sem restaurar o banco inteiro.

Checklist

  • Um cadastro novo com "ç" aparece como C3A7 no HEX().
  • A busca por "Ã" e "Â" usa comparação binária.
  • A correção foi conferida por amostra no SELECT antes do UPDATE.
  • Existe backup testado e uma tabela com os valores anteriores.
  • O UPDATE tocou só as linhas filtradas.
  • Não sobrou função de conversão na saída das páginas.

Rode agora o SELECT ... COLLATE utf8mb4_bin LIKE '%Ã%' na sua tabela de clientes: quantas linhas voltam, e alguma delas foi cadastrada esta semana?

Avalie este artigo

Referência: http://wiki.locaweb.com/pt-br/Como_resolver_problemas_de_acentua%C3%A7%C3%B5es_em_seu_site