Django + MySQL: Encoding...

Django, Flask, Bottle, WSGI, CGI…
Antworten
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Ich möchte gern das Zusammenspiel von Django + MySQL besser verstehen.

Wenn ich das richtig verstehe, geht Django immer davon aus, das der MySQL server mit utf-8 läuft. In dem Falle gibt es keine Probleme. Man bekommt daten in unicode und kann unicode daten wieder in die DB packen.

Doch die Standard Installation von MySQL auf Ubuntu/Debian ist eigentlich mit "latin-1" encoding. Man kann dies leicht ändern, indem man in /etc/mysql/my.cnf das einträgt:

Code: Alles auswählen

character-set-server=utf8
collation-server=utf8_unicode_ci
So hab ich es bei meinem Server gemacht.

Doch was muß man tun, damit Django kein Problem mit "latin-1" hat? In der Doku bei https://docs.djangoproject.com/en/dev/r ... e-database steht:
Make sure your database is configured to be able to store arbitrary string data. Normally, this means giving it an encoding of UTF-8 or UTF-16. If you use a more restrictive encoding – for example, latin1 (iso8859-1) – you won’t be able to store certain characters in the database, and information will be lost.
Heißt es also, es geht nur mit UTF-8 oder UTF-16 und alles andere produziert Ärger?
In den Django MySQL sourcen kann man auch sehen, das teilweise "utf-8" hardcoded ist.

Wenn man nicht einfach zugriff auf die MySQL Server Einstellungen hat, was kann man machen?
Bei DATABASE settings gibt es die zusätzlichen connection OPTIONS. Theoretisch sollte das helfen:

Code: Alles auswählen

        'OPTIONS': {
            "use_unicode": True,
            "charset": "utf8",
        },
Zumindest sehe ich, das sie die MySQL Variablen um charaterset mit der connection ändern. Dennoch bekomme ich SQL Warnings, wie:

Code: Alles auswählen

Warning: Incorrect string value: '\xE2\x9D\x96 [u...' for column 'FooBar' at row 1

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
apollo13
User
Beiträge: 827
Registriert: Samstag 5. Februar 2005, 17:53

Wichtig ist, dass du die Datenbank/tabellen selbst mit dem korrekten Charset/Colletation anlegst (Wenn dein Hoster die DB anlegt und du das nicht ändern kannst musst es halt tabellenweise machen, sonst kannst einfach den default für ne db setzen)! Den Rest kannst du dann über das option dict regeln.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Nun weiß ich ein wenig mehr. Also der betreffende Server liefert:

Code: Alles auswählen

$ ./manage.py dbshell
...
Server version: 5.0.96 Source distribution
...
mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+--------------------------------------------+
| Variable_name            | Value                                      |
+--------------------------+--------------------------------------------+
| character_set_client     | latin1                                     |
| character_set_connection | latin1                                     |
| character_set_database   | latin1                                     |
| character_set_filesystem | binary                                     |
| character_set_results    | latin1                                     |
| character_set_server     | latin1                                     |
| character_set_system     | utf8                                       |
| character_sets_dir       | /usr/local/pd-admin2/share/mysql/charsets/ |
+--------------------------+--------------------------------------------+
Dei Warning taucht auf, beim Zeichen ❖ (ist "BLACK DIAMOND MINUS WHITE X" bzw. \u2756), siehe: http://www.pylucid.org/de/contribute/de ... char_10070

Das Zeichen stammt aus der Signatur von cofi: http://www.python-forum.de/memberlist.p ... ile&u=6642 :P

Also latin-1 geht ja IMHO bis \u255... Somit ist klar, das normale Umlaute wie äöüß kein Problem darstellen, es aber zu der Warning kommt, bei diesem Zeichen.

Was ich nun aber komisch finde: Im django admin, wenn ich per django-phpBB auf die original Tabellen zugreife, kann ich im admin das ❖ Zeichen in der Form normal sehen. Das Zeichen allerdings wieder in die DB speichern geht allerdings nicht bzw. wird durch ein ? ersetzt.
Im Traceback zum Warning kann ich sehen, das es ein unicode string ist und das Zeichen als \u2756 erscheint.

Nun Frage ich mich, warum erhalte ich überhaupt Zeichen die >latin-1 sind aus der Datenbank?
apollo13 hat geschrieben:Wichtig ist, dass du die Datenbank/tabellen selbst mit dem korrekten Charset/Colletation anlegst (Wenn dein Hoster die DB anlegt und du das nicht ändern kannst musst es halt tabellenweise machen, sonst kannst einfach den default für ne db setzen)! Den Rest kannst du dann über das option dict regeln.
Die Tabellen "Kollation" per phpMyAdmin:
Die phpBB source Tabellen ist: "latin1_swedish_ci"
Die neu angelegten Tabellen durch Django: "utf8_bin"

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

jens hat geschrieben:Die Tabellen "Kollation" per phpMyAdmin:
Die phpBB source Tabellen ist: "latin1_swedish_ci"
Die neu angelegten Tabellen durch Django: "utf8_bin"
Ne, stop. Das stimmt nicht. Es einige Tabellen sind "latin1_swedish_ci" und andere "utf8_bin". Hat nichts damit zu tun, ob die alten von phpBB erzeugt waren oder neue mit Django. Ist wild gemixt :shock: Keine Ahnung wie das passieren konnte.

EDIT: Ich hab nun mal in phpMyAdmin die Dtaenbank unter Operationen / Kollation auf "utf8_unicode_ci" umgestellt. Das wird dabei gemacht:

Code: Alles auswählen

ALTER DATABASE `FooBar` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
Danach gibt es keine Probleme beim migrieren. Testweise hab ich es danach wieder umgestellt, auf "latin1_swedish_ci" und der Fehler ist wieder da.

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
BlackJack

jens hat geschrieben:Was ich nun aber komisch finde: Im django admin, wenn ich per django-phpBB auf die original Tabellen zugreife, kann ich im admin das ❖ Zeichen in der Form normal sehen. Das Zeichen allerdings wieder in die DB speichern geht allerdings nicht bzw. wird durch ein ? ersetzt.
Im Traceback zum Warning kann ich sehen, das es ein unicode string ist und das Zeichen als \u2756 erscheint.

Nun Frage ich mich, warum erhalte ich überhaupt Zeichen die >latin-1 sind aus der Datenbank?
Bekommst Du nicht. Du bekommst drei Zeichen im Bereich von 0-255, nämlich die drei Bytes die in UTF-8 das '❖' kodieren. Und da Django von UTF-8 ausgeht, werden die als eben dieses Zeichen angezeigt.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

jens hat geschrieben:

Code: Alles auswählen

ALTER DATABASE `FooBar` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci
Von dem hin und her, hab ich nun das:

Code: Alles auswählen

django.db.utils.DatabaseError: (1267, "Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='")
EDIT: Ich probiere mal das hier:

Code: Alles auswählen

echo "SHOW TABLES" | ./manage.py dbshell > /tmp/tables
for t in $(cat /tmp/tables) ; do
    echo $t
    SQL="ALTER TABLE $t CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci"
    echo ${SQL}
    echo ${SQL} | ./manage.py dbshell
done
btw. es muß doch auch alles komplett in SQL gehen, oder nicht?

EDIT2: So, die Datebank + alle Tabellen sind nun "utf8_unicode_ci", doch der Fehler bleibt. Da Frage ich mich doch, woher kommt nun (latin1_swedish_ci,IMPLICIT) ?

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Jemand eine Idee, was ich hier machen könnte?

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

jens hat geschrieben:

Code: Alles auswählen

django.db.utils.DatabaseError: (1267, "Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='")
Was ich nun gern wüßte, kommt man nicht drum herrum den MySQL Server komplett auf UTF-8 um zu stellen?

Zum Fehler findet man im Netz auch Informationen. Ganz nett ist: http://airbladesoftware.com/notes/fixin ... collations

EDIT: Gab dazu mal ein Ticket: https://code.djangoproject.com/ticket/11950 interessant ist "comment:3":
There are a couple of ways you could avoid this problem. First, you could use a utf-8 encoding for the table on MySQL. Since Django always uses utf-8 to talk to the database you will then never run into trouble with illegal mixtures of collations. Alternatively, if you want to keep the table using latin1 encoding, you can ensure that any parameters you pass can be encoded in latin1. (Do not actually pass latin-1 encoded bytestrings, just verify that what you are sending can in fact be encoded in latin1.) So far as I've been able to determine, this illegal mix of collations error only crops up when you send data that cannot be encoded in the table's charset.
Demnach ist es wohl das beste auf UTF-8 zu wechseln. Alles andere macht Probleme...

Aber wie ist es nun genau, bei MySQL. Wenn der Server mit latin-1 läuft, dann kann man in den Tabellen auf UTF-8 und utf8_general_ci umstellen, dennoch kann man nur latin-1 speichern?!?

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
apollo13
User
Beiträge: 827
Registriert: Samstag 5. Februar 2005, 17:53

jens hat geschrieben:Aber wie ist es nun genau, bei MySQL. Wenn der Server mit latin-1 läuft, dann kann man in den Tabellen auf UTF-8 und utf8_general_ci umstellen, dennoch kann man nur latin-1 speichern?!?
Was ist denn das connection encoding nun? Wenn die Tabelle utf-8 ist, aber die Connection latin-1 hast natürlich nicht viel gewonnen ;)
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Die sollte utf-8 sein, weil ich nichts verändert habe. Also default von Django. Ich werde mal versuchen das zu klären.

Hab noch neue Informationen gewonnen:

Wenn ich das richtig verstehe, ist die "collation" Angabe nur wichtig, wenn es das sortieren mit "ORDER BY" geht. Warum MySQL als default "latin1_swedish_ci" ist aber unklar. Siehe auch: http://www.sirmark.de/computer/mysql/my ... -1086.html

Django setzt nicht "fest" auf "utf-8", es ist nur die Vorgabe, die man mit OPTIONS["charset"] ändern kann, siehe: https://github.com/django/django/blob/8 ... se.py#L383

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

jens hat geschrieben:Die sollte utf-8 sein, weil ich nichts verändert habe. Also default von Django. Ich werde mal versuchen das zu klären.
So (Ausgaben ein wenig verschönert):

Code: Alles auswählen

phpBB2DjangoBB_project$ ./manage.py shell
...
Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
...
>>> from django.db import connection
>>> cursor = connection.cursor()
>>> cursor.execute("SHOW VARIABLES LIKE %s;", ("char%",))
8L
>>> cursor.fetchall()
((u'character_set_client', u'utf8'),
(u'character_set_connection', u'utf8'),
(u'character_set_database', u'utf8'),
(u'character_set_filesystem', u'binary'),
(u'character_set_results', u'utf8'),
(u'character_set_server', u'latin1'),
(u'character_set_system', u'utf8'),
(u'character_sets_dir', u'/usr/local/pd-admin2/share/mysql/charsets/'))
>>> cursor.execute("SHOW VARIABLES LIKE %s;", ("collation%",))
3L
>>> cursor.fetchall()
((u'collation_connection', u'utf8_general_ci'),
(u'collation_database', u'utf8_unicode_ci'),
(u'collation_server', u'latin1_swedish_ci'))
Ich denke der client kommuniziert also mit utf-8, doch der server läuft halt mit latin-1.

Meine Tabellen hab ich ja geändert auf CHARACTER SET utf8 COLLATE utf8_unicode_ci

Die ganzen Encoding Einstellungen stelle ich mir mal so vor (Bitte korrigiert mich):
* Server: Encoding in dem die Daten letztlich auf Platte gespeichert werden
* Tabelle: Encoding in der Tabelle
* Connection: Encoding mit dem zwischen client und server kommuniziert wird
* Client: Encoding die der client dann letztlich zum "Programm" zurück liefert

Wenn die Encodings unterschiedlich sind, dann muss zwischen den Schichten immer wieder konvertiert werden:
HDD <-> Server <-> Tabelle <-> Connection <-> Client <-> Programm

Bei Unterschiedlichen encodings kann man eigentlich nur Zeichen speichern, die im "kleinsten gemeinsamen Nenner" sind, oder? In meinem Fall: Server läuft mit latin-1 kann man also auch nur Zeichen in latin-1 speichern?

Doch mein vorheriger Fehler "Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE)...
Könnte man vielleicht so erklären, das Tabelle & Connection zwar mit 'utf8_general_ci' laufen, aber 'collation_server' == 'latin1_swedish_ci' ist.
Wenn das so wäre, macht es doch aber keinen Sinn, das man überhaupt verschiedene Codecs mischen kann.

d.h. Connection/Client/Tabellen Encoding kann ich nur theoretisch einstellen wie ich will. Doch Sinn macht nur latin1, weil vom server festgelet und weil ich somit A eh nur den latin-1 Zeichenraum nutzten kann und B die collation latin1_* sein muß...

Ich bin verwirrt.

EDIT: Hier die Werte mit OPTIONS["charset"] == "latin1":

Code: Alles auswählen

>>> cursor.execute("SHOW VARIABLES LIKE %s;", ("char%",))
8L
>>> cursor.fetchall()
((u'character_set_client', u'latin1'),
(u'character_set_connection', u'latin1'),
(u'character_set_database', u'utf8'),
(u'character_set_filesystem', u'binary'),
(u'character_set_results', u'latin1'),
(u'character_set_server', u'latin1'),
(u'character_set_system', u'utf8'),
(u'character_sets_dir', u'/usr/local/pd-admin2/share/mysql/charsets/'))
>>> cursor.execute("SHOW VARIABLES LIKE %s;", ("collation%",))
3L
>>> cursor.fetchall()
((u'collation_connection', u'latin1_swedish_ci'),
(u'collation_database', u'utf8_unicode_ci'),
(u'collation_server', u'latin1_swedish_ci'))
Ich werde jetzt mal versuchen alle auf latin-1 um zu stellen...

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

jens hat geschrieben:Ich werde jetzt mal versuchen alle auf latin-1 um zu stellen...
War klar, das dies nicht die Lösung ist:

Code: Alles auswählen

  File "/home/pyfodjangobb/DjangoBB_env/src/django-phpbb3/django_phpBB3/management/commands/phpbb2djangobb.py", line 301, in migrate_users
    cleaned_signature = phpbb_user.get_cleaned_signature()
  File "/home/pyfodjangobb/DjangoBB_env/src/django-phpbb3/django_phpBB3/models.py", line 417, in get_cleaned_signature
    text = smart_unicode(self.sig)
  File "/home/pyfodjangobb/DjangoBB_env/lib/python2.6/site-packages/django/utils/encoding.py", line 39, in smart_unicode
    return force_unicode(s, encoding, strings_only, errors)
  File "/home/pyfodjangobb/DjangoBB_env/lib/python2.6/site-packages/django/utils/encoding.py", line 93, in force_unicode
    raise DjangoUnicodeDecodeError(s, *e.args)
django.utils.encoding.DjangoUnicodeDecodeError: 'utf8' codec can't decode byte 0x93 in position 18: invalid start byte. You passed in '[size=85:1ssp2bva]\x93Real efficiency comes from elegant solutions, not optimized programs.\n Optimization is always just a few correctness-preserving transformations away.\x94 \x97 Jonathan Sobel[/size:1ssp2bva]' (<type 'str'>)
Der Text ist eigentlich (Ausgelesen mir phpMyAdmin):

Code: Alles auswählen

[size=85:1ssp2bva]“Real efficiency comes from elegant solutions, not optimized programs.
 Optimization is always just a few correctness-preserving transformations away.” — Jonathan Sobel[/size:1ssp2bva]
Dabei ist “ == \u201C (liegt also außerhalb von latin-1)

Im Django Admin bekommt man auch schnell Fehler:
DjangoUnicodeDecodeError: 'utf8' codec can't decode byte 0xf6 in position 15: invalid start byte. You passed in 'schlangenbeschw\xf6rer' (<type 'str'>)
Hier ist 'schlangenbeschw\xf6rer' offensichtlich: 'schlangenbeschwörer'
Wobei 'ö' == \u00F6 bzw. \xF6 ist und innerhalb von latin-1 ist.

Kann es sein, das OPTIONS["charset"] == "latin1", weil django intern versucht alles in utf-8 zu handhaben? Denn so sachen wie smart_unicode(value) nutzt als default utf-8

Mal ein manuellen Test:

Code: Alles auswählen

>>> cursor.execute("SELECT username FROM phpbb_users WHERE user_id=%s;", (2473,))
1L
>>> cursor.fetchall()[0]
('schlangenbeschw\xf6rer',)
Sollte ich hier nicht unicode bekommen und keine string? Steckt der Fehler somit in MySQLdb ?

EDIT: Aus http://mysql-python.sourceforge.net/MySQLdb.html :
use_unicode

If True, CHAR and VARCHAR and TEXT columns are returned as Unicode strings, using the configured character set. It is best to set the default encoding in the server configuration, or client configuration (read with read_default_file). If you change the character set after connecting (MySQL-4.1 and later), you'll need to put the correct character set name in connection.charset.

If False, text-like columns are returned as normal strings, but you can always write Unicode strings.
Das Betreffende Feld "username" ist varchar.

Version von MySQLdb die ich nutzte:
>>> from django.db import backend
>>> backend.Database.__version__
'1.2.2'
>>> import MySQLdb
>>> MySQLdb.version_info
(1, 2, 2, 'final', 0)

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Antworten