Erro "valor incorreto da string" do MySQL ao salvar a string unicode no Django

158

Recebi uma mensagem de erro estranha ao tentar salvar first_name, last_name no modelo auth_user do Django.

Exemplos com falha

user = User.object.create_user(username, email, password)
user.first_name = u'Rytis'
user.last_name = u'Slatkevičius'
user.save()
>>> Incorrect string value: '\xC4\x8Dius' for column 'last_name' at row 104

user.first_name = u'Валерий'
user.last_name = u'Богданов'
user.save()
>>> Incorrect string value: '\xD0\x92\xD0\xB0\xD0\xBB...' for column 'first_name' at row 104

user.first_name = u'Krzysztof'
user.last_name = u'Szukiełojć'
user.save()
>>> Incorrect string value: '\xC5\x82oj\xC4\x87' for column 'last_name' at row 104

Exemplos de sucesso

user.first_name = u'Marcin'
user.last_name = u'Król'
user.save()
>>> SUCCEED

Configurações do MySQL

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       | 
| character_set_connection | utf8                       | 
| character_set_database   | utf8                       | 
| character_set_filesystem | binary                     | 
| character_set_results    | utf8                       | 
| character_set_server     | utf8                       | 
| character_set_system     | utf8                       | 
| character_sets_dir       | /usr/share/mysql/charsets/ | 
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

Conjunto de caracteres e agrupamento de tabelas

A tabela auth_user possui utf-8 charset com agrupamento utf8_general_ci.

Resultados do comando UPDATE

Não gerou nenhum erro ao atualizar os valores acima para a tabela auth_user usando o comando UPDATE.

mysql> update auth_user set last_name='Slatkevičiusa' where id=1;
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select last_name from auth_user where id=100;
+---------------+
| last_name     |
+---------------+
| Slatkevi?iusa | 
+---------------+
1 row in set (0.00 sec)

PostgreSQL

Os valores com falha listados acima podem ser atualizados na tabela do PostgreSQL quando eu mudei o back-end do banco de dados no Django. É estranho.

mysql> SHOW CHARACTER SET;
+----------+-----------------------------+---------------------+--------+
| Charset  | Description                 | Default collation   | Maxlen |
+----------+-----------------------------+---------------------+--------+
...
| utf8     | UTF-8 Unicode               | utf8_general_ci     |      3 | 
...

Mas em http://www.postgresql.org/docs/8.1/interactive/multibyte.html , encontrei o seguinte:

Name Bytes/Char
UTF8 1-4

Isso significa que o unicode char possui no máximo 4 bytes no PostgreSQL, mas 3 bytes no MySQL, o que causou um erro acima?

jack
fonte
2
É um problema do MySQL, não o Django: stackoverflow.com/questions/1168036/…
Vanuan

Respostas:

140

Nenhuma dessas respostas resolveu o problema para mim. A causa raiz é:

Você não pode armazenar caracteres de 4 bytes no MySQL com o conjunto de caracteres utf-8.

O MySQL tem um limite de 3 bytes em caracteres utf-8 (sim, é maluco, bem resumido por um desenvolvedor do Django aqui )

Para resolver isso, você precisa:

  1. Altere seu banco de dados MySQL, tabela e colunas para usar o conjunto de caracteres utf8mb4 (disponível somente no MySQL 5.5 em diante)
  2. Especifique o conjunto de caracteres no seu arquivo de configurações do Django como abaixo:

settings.py

DATABASES = {
    'default': {
        'ENGINE':'django.db.backends.mysql',
        ...
        'OPTIONS': {'charset': 'utf8mb4'},
    }
}

Nota: Ao recriar seu banco de dados, você pode executar o problema 'A chave especificada era muito longa '.

A causa mais provável é uma CharFieldque tenha um comprimento máximo de 255 e algum tipo de índice (por exemplo, exclusivo). Como o utf8mb4 usa 33% mais espaço que o utf-8, você precisará tornar esses campos 33% menores.

Nesse caso, altere o max_length de 255 para 191.

Como alternativa, você pode editar sua configuração do MySQL para remover essa restrição, mas não sem algum hackery do django

ATUALIZAÇÃO: Acabei de encontrar esse problema novamente e acabei migrando para o PostgreSQL porque não consegui reduzir meus VARCHAR191 caracteres.

não voltar
fonte
13
essa resposta precisa de muito, muito mais votos positivos. Obrigado! O verdadeiro problema é que seu aplicativo pode funcionar bem por anos até que alguém tente inserir um caractere de 4 bytes.
precisa saber é o seguinte
2
Esta é absolutamente a resposta certa. A configuração OPTIONS é essencial para fazer o django decodificar caracteres emoji e armazená-los no MySQL. Apenas mudar o mysql charset para utf8mb4 via comandos SQL não é suficiente!
Xerion
Não há necessidade de atualizar o conjunto de caracteres de toda a tabela para utf8mb4. Apenas atualize o conjunto de caracteres das colunas necessárias. Também a 'charset': 'utf8mb4'opção nas configurações do Django é crítica, como disse o @Xerion. Finalmente, o problema do índice está uma bagunça. Remova o índice na coluna, ou faça seu comprimento não superior a 191, ou use um TextField!
Rockallite 23/09/16
2
Adoro o seu link para esta citação: Este é apenas mais um caso do MySQL danificado de maneira proposital e irreversível. :)
Qback
120

Eu tive o mesmo problema e resolvi-o alterando o conjunto de caracteres da coluna. Mesmo que seu banco de dados tenha um conjunto de caracteres padrão, utf-8acho que é possível que as colunas do banco de dados tenham um conjunto de caracteres diferente no MySQL. Aqui está o SQL QUERY que eu usei:

    ALTER TABLE database.table MODIFY COLUMN col VARCHAR(255)
    CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
gerdemb
fonte
14
Ugh, mudei todos os conjuntos de caracteres em tudo o que pude até realmente reler esta resposta: as colunas podem ter seus próprios conjuntos de caracteres, independentemente das tabelas e do banco de dados. Isso é loucura e também foi exatamente o meu problema.
markpasc
1
Isso funcionou para mim também, usando o mysql com os padrões, em um modelo TextField.
madprops 9/09/11
Isso resolveu meu problema. A única alteração que fiz foi usar utf8mb4 e utf8mb4_general_ci em vez de utf8 / utf8_general_ci.
Michal Przysucha
70

Se você tiver esse problema, aqui está um script python para alterar todas as colunas do seu banco de dados mysql automaticamente.

#! /usr/bin/env python
import MySQLdb

host = "localhost"
passwd = "passwd"
user = "youruser"
dbname = "yourdbname"

db = MySQLdb.connect(host=host, user=user, passwd=passwd, db=dbname)
cursor = db.cursor()

cursor.execute("ALTER DATABASE `%s` CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'" % dbname)

sql = "SELECT DISTINCT(table_name) FROM information_schema.columns WHERE table_schema = '%s'" % dbname
cursor.execute(sql)

results = cursor.fetchall()
for row in results:
  sql = "ALTER TABLE `%s` convert to character set DEFAULT COLLATE DEFAULT" % (row[0])
  cursor.execute(sql)
db.close()
madprops
fonte
4
Esta solução resolveu todos os meus problemas com um aplicativo django que estava armazenando caminhos de arquivos e diretórios. Atire dbname como seu banco de dados django e deixe-o rodar. Trabalhou como um encanto!
Chris
1
Este código não funcionou para mim até que eu adicionei db.commit()antes db.close().
Mark Erdmann
1
Será que esta solução evitar o problema discutido no comentário @markpasc: '... 4 bytes UTF-8 caracteres como emoji em MySQL 5.1 de 3 bytes conjunto de caracteres utf8'
CatShoes
a solução me ajudou quando eu estava excluindo um registro através do django admin, não tive nenhum problema ao criar a edição ... estranho! Eu ainda era capaz de apagar diretamente no db
Javier Vieira
Devo fazer isso toda vez que mudar o modelo?
Vanuan
25

Se for um novo projeto, basta soltar o banco de dados e criar um novo com um conjunto de caracteres adequado:

CREATE DATABASE <dbname> CHARACTER SET utf8;
Vanuan
fonte
Oi ajuda gentilmente verificar esta questão stackoverflow.com/questions/46348817/...
Rei
No meu caso, o nosso db é criado pela janela de encaixe de modo a fixar eu adicionei o seguinte ao db: comando: instrução no meu arquivo de composição:- --character-set-server=utf8
followben
1
Tão simples como isso. Obrigado @Vanuan
Enku
se esse não é um projeto novo, obtemos o backup do db, o descartamos e o recriamos com utf8 charset e, em seguida, restauramos o backup. Eu fiz isso no meu projeto que não era novo ...
Mohammad Reza
8

Eu apenas descobri um método para evitar os erros acima.

Salvar no banco de dados

user.first_name = u'Rytis'.encode('unicode_escape')
user.last_name = u'Slatkevičius'.encode('unicode_escape')
user.save()
>>> SUCCEED

print user.last_name
>>> Slatkevi\u010dius
print user.last_name.decode('unicode_escape')
>>> Slatkevičius

Esse é o único método para salvar seqüências de caracteres como essa em uma tabela MySQL e decodificá-la antes de renderizar em modelos para exibição?

jack
fonte
12
Estou com um problema semelhante, mas não concordo que seja uma solução válida. Quando você .encode('unicode_escape')não está realmente armazenando caracteres unicode no banco de dados. Você está forçando todos os clientes a descriptografar antes de usá-los, o que significa que não funcionará corretamente com o django.admin ou com qualquer outra coisa.
muudscope
3
Embora pareça desagradável armazenar códigos de escape em vez de caracteres, esta é provavelmente uma das poucas maneiras de salvar caracteres UTF-8 de 4 bytes, como emoji, no utf8conjunto de caracteres de 3 bytes do MySQL 5.1 .
markpasc
2
Existe uma codificação chamada utf8mb4que permite que mais do que o plano multilíngue básico seja armazenado. Eu sei, você pensaria que "UTF8" é tudo o que é necessário para armazenar totalmente o Unicode. Bem, como você sabe, não é. Veja dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
Mihai Danila
@jack que você pode querer considerar mudar a resposta aceita para um que é mais útil
donturner
é uma solução viável, mas eu não recomendo usá-lo também (como defendido pelo @muudscope). Ainda não consigo armazenar, por exemplo, emoji em bancos de dados mysql. Alguém conseguiu isso?
Marcelo Sardelich 17/03/14
6

Você pode alterar o agrupamento do seu campo de texto para UTF8_general_ci e o problema será resolvido.

Observe que isso não pode ser feito no Django.

Wei An
fonte
1

Você não está tentando salvar seqüências de caracteres unicode, mas também está tentando salvar cadeias de caracteres na codificação UTF-8. Torne-os literais reais de cadeias unicode:

user.last_name = u'Slatkevičius'

ou (quando você não possui literais de string) decodifique-os usando a codificação utf-8:

user.last_name = lastname.decode('utf-8')
Thomas Wouters
fonte
@ Thomas, eu tentei exatamente como o que você disse, mas ainda gera os mesmos erros.
jack
0

Simplesmente altere sua mesa, sem necessidade de nada. basta executar esta consulta no banco de dados. ALTER TABLE table_nameCONVERT TO CHARACTER SET utf8

definitivamente funcionará.

Rishabh Jhalani
fonte
0

Melhoria na resposta @madprops - solution como um comando de gerenciamento do django:

import MySQLdb
from django.conf import settings

from django.core.management.base import BaseCommand


class Command(BaseCommand):

    def handle(self, *args, **options):
        host = settings.DATABASES['default']['HOST']
        password = settings.DATABASES['default']['PASSWORD']
        user = settings.DATABASES['default']['USER']
        dbname = settings.DATABASES['default']['NAME']

        db = MySQLdb.connect(host=host, user=user, passwd=password, db=dbname)
        cursor = db.cursor()

        cursor.execute("ALTER DATABASE `%s` CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'" % dbname)

        sql = "SELECT DISTINCT(table_name) FROM information_schema.columns WHERE table_schema = '%s'" % dbname
        cursor.execute(sql)

        results = cursor.fetchall()
        for row in results:
            print(f'Changing table "{row[0]}"...')
            sql = "ALTER TABLE `%s` convert to character set DEFAULT COLLATE DEFAULT" % (row[0])
            cursor.execute(sql)
        db.close()

Espero que isso ajude alguém, exceto eu :)

Ron
fonte