login  Naam:   Wachtwoord: 
Registreer je!
 Forum

latin1 tabellen omzetten naar utf8

Offline Thomas - 14/05/2014 16:23 (laatste wijziging 22/06/2014 00:29)
Avatar van ThomasModerator Inmiddels heb ik al even zitten zoeken naar dit onderwerp op internet. In de meeste artikelen die ik tegenkwam werden allerlei doemscenario's geschetst en ingewikkelde procedures uiteengezet om deze conversie voor elkaar te krijgen.

Hierbij werd er echter volgens mij in bijna alle gevallen vanuit gegaan dat er al utf-8 data in latin1 tabellen terecht was gekomen en dus (eigenlijk) al verkeerd geëncodeerd was.

EDIT: of, misschien waarschijnlijker, er zit latin1-encoded utf8-data in een utf8-tabel, bijvoorbeeld doordat je bij het inserten/editten van de data was vergeten om een character set in te stellen bij het connecten naar je database.

Ik kan mij voorstellen dat als je dan niet op een goede manier deze tabellen omzet de ramp niet te overzien is, je maakt van troep (data die eigenlijk al "corrupt" is) een nog grotere troep.

Daartoe gooit men vervolgens allerlei middelen in de strijd om de latin1-data-die-mogelijk-utf-8-content-bevat maar niet verder te corrumperen bij de omzetting. De meest gebruikte methode lijkt het omzetten van kolommen naar een BLOB (binary) variant, omdat deze blijkbaar niet character-set-aware zijn (en er dus ook geen omzetting plaatsvindt). Vervolgens, na het "redden" van deze kolommen, wordt de tabel omgezet naar utf-8 en daarna krijgen de kolommen weer hun oorspronkelijke type terug. Operatie geslaagd.

Maar wat als je nu geen utf-8 data in je latin1 tabellen hebt? Wat als je een latin1 database hebt, waar je met latin1 mee communiceert en alleen maar vult met latin1 data (dit is (bijna) gelijk aan iso-8859-1 maar wordt vaak als hetzelfde beschouwd)? De karakters die "ondersteund" worden door latin1 vormen zelf toch een subset van de karakters die door utf-8 worden ondersteund [1]? Oftewel, er hoeft in dat geval toch geen omzetting plaats te vinden? Oftewel, in dat geval is de conversie latin1 > utf8 simpelweg het aanpassen van de character sets van de tabellen (en van de database, als je slim bent). Of mis ik nu iets? In alle artikelen die ik tegenkwam werd dit onderwerp altijd als iets complex benaderd, maar als je je aan de spelregels had gehouden lijkt mij dit eenvoudig?

Wel zul je na afloop volledig moeten overschakelen naar utf-8, dat snap ik [2]. Je bent dus niet klaar met het simpelweg omzetten van je tabellen. Je moet je pagina's serveren met de utf-8 charset en de connectie met je database moet verlopen via utf8. PHP-, CSS en HTML-bestanden hoeven eigenlijk niet eens te worden opgeslagen met utf-8 encodering (als ik het goed begrijp). Hier is immers dezelfde stelregel van toepassing als bij de database-data: als het latin1 compliant is, is het eveneens utf-8 compliant.

Klopt dit een beetje zo? Ik denk dat dit onderwerp wat moeilijker wordt gebracht dan het daadwerkelijk is, zolang je op het fietspad blijft en de regels omtrent character encoding in acht neemt.

EDIT:
[1] Blijkbaar niet helemaal. Als de karakters buiten US-ASCII vallen (zoals ë (e met trema), ï (i met trema) en € (euro-teken)) dan worden deze als multibyte chars opgeslagen in utf8 (bron). Het euro-teken maakt eigenlijk ook deel uit van ISO-8859-15, en niet van ISO-8859-1 (bron) maar wordt inmiddels stilzwijgend geaccepteerd in ISO-8859-1?
[2] Dit is dus ook niet helemaal waar, MySQL kan achter de schermen vertalingen uitvoeren, zie EDIT3.

EDIT2:
Ondanks het feit dat de karakters die utf8 ondersteunen een superset vormen van de karakters die ISO-8859-1(5) ondersteunt, wil dit niet zeggen dat deze karakters op dezelfde wijze geëncodeerd worden. Dit heb ik inmiddels getest met twee testscripts: een waarbij alles ISO-8859-15 / latin1 is en een waarbij alles UTF-8 / utf8 is. Hieruit blijkt bijvoorbeeld dat de encodering van de string "éëïî€" in beide varianten sterk uiteen loopt. Deze lijkt echter hetzelfde als je via de DOS-prompt in je database kijkt maar is dat niet. Los van het feit dat Windows nogal wat probleempjes heeft met UTF-8 / speciale karakters in het algemeen en deze daardoor in een DOS-venster door de vleesmolen gaan moet je het volgende niet vergeten: je connectie gaat ook uit van een character encoding (ook als je deze niet opgeeft). Oftewel, als je data van een utf8-encoded tabel bekijkt met een latin1-connectie, probeert MySQL dit (waarschijnlijk) voor je te vertalen:
  1. mysql> SELECT * FROM test_latin1 WHERE id = 1;
  2. +----+-------+
  3. | id | tekst |
  4. +----+-------+
  5. | 1 | éëïî? |
  6. +----+-------+
  7. 1 row IN SET (0.00 sec)

versus
  1. mysql> SELECT * FROM test_utf8 WHERE id = 1;
  2. +----+-------+
  3. | id | tekst |
  4. +----+-------+
  5. | 1 | éëïî? |
  6. +----+-------+
  7. 1 row IN SET (0.05 sec)

In het tweede geval (dit betreft een utf8-tabel met utf8-data) wordt de string hetzelfde weergegeven als in de eerste tabel (latin1-tabel met dezelfde data, maar dan in latin1-encoding). Overigens gaat het tonen van een euro-teken in de DOS-box mis .

Je kunt alsnog de encoding controleren door de data in binaire vorm weer te geven, dit doe je met HEX(). Deze data ziet er compleet verschillend uit (en dit toont ook min of meer aan dat MySQL automatisch vertalingen uitvoert):
  1. mysql> SELECT *, HEX(tekst) FROM test_latin1 WHERE id = 1;
  2. +----+-------+------------+
  3. | id | tekst | HEX(tekst) |
  4. +----+-------+------------+
  5. | 1 | éëïî? | E9EBEFEE80 |
  6. +----+-------+------------+
  7. 1 row IN SET (0.00 sec)

versus
  1. mysql> SELECT *, HEX(tekst) FROM test_utf8 WHERE id = 1;
  2. +----+-------+------------------------+
  3. | id | tekst | HEX(tekst) |
  4. +----+-------+------------------------+
  5. | 1 | éëïî? | C3A9C3ABC3AFC3AEE282AC |
  6. +----+-------+------------------------+
  7. 1 row IN SET (0.00 sec)


EDIT3: Als je latin1-tabel al utf8-encoded is, zet je dus alles weg (zet je alles om) in een binary kolom, dan pas je je tabel-definitie aan naar utf8 (mogelijk met een afwijkende collation (oeh nog een leuk onderwerp)), en dan verander je je kolom weer terug naar het oorspronkelijke type. Effectief verandert je data dan inhoudelijk niet. Als je deze truuk toepast op latin1-data dan worden je kolommen helemaal leeggegooid . Als je je kolom dan namelijk terugverandert krijg je de volgende warning (zou dit niet een error moeten zijn?):
  1. Level: Warning
  2. Code: 1366
  3. Message: Incorrect string value: '\xE9\xEB\xEF\xEE\x80' FOR COLUMN 'tekst' at row 1

Stackoverflow gaf mij toen de oplossing: laat MySQL de omzetting voor je doen. Het volgende werkt wel:
  1. mysql> ALTER TABLE test_latin1 CONVERT TO CHARACTER SET utf8;
  2. Query OK, 1 row affected (0.06 sec)
  3. Records: 1 Duplicates: 0 Warnings: 0

en de controle:
  1. mysql> SELECT *, HEX(tekst) FROM test_latin1 WHERE id = 1;
  2. +----+-------+------------------------+
  3. | id | tekst | HEX(tekst) |
  4. +----+-------+------------------------+
  5. | 1 | éëïî? | C3A9C3ABC3AFC3AEE282AC |
  6. +----+-------+------------------------+
  7. 1 row IN SET (0.00 sec)

MySQL heeft de data dus echt omgezet naar utf8. MySQL is dus best intelligent en regelt veel achter de schermen. Zo kon ik nog steeds met het latin1-script de (inmiddels naar utf8 geconverteerde) latin1-tabel (test_latin1) uitlezen en correct weergeven in ISO-8859-1. Dit komt (waarschijnlijk) omdat MySQL ziet dat je met latin1 connect en de tabel (inmiddels) utf8 is. Deze probeert dan (in ieder geval) de (bekende) karakters terug te vertalen naar latin1. Voor latin1 onbekende (multibyte) karakters worden dan weergegeven als een (enkel) vraagteken ("?").
Ondanks het feit dat je zelf een zooi maakt van welke character sets je hanteert, zolang je data in je database van de encodering is overeenkomstig met de tabel of kolom kan MySQL hiermee overweg. Wel moet $charset in de _set_charset($charset) aanroep overeenkomen met de charset waarin je een en ander wilt tonen, anders gaat het alsnog mis.

EDIT4: Je kunt nagaan of MySQL data heeft vertaald door na het ophalen van het resultaat (wat dus mogelijk vertaald is) hier bin2hex() op uit te voeren. De HEX in de bovenstaande queries is de HEX zoals deze in de database geldt. Deze is dus mogelijk anders dan de hex-waarde (te bepalen met bin2hex()) van de opgehaalde data. Je kunt dit natuurlijk alleen bepalen als de tekst speciale karakters bevat, gewone (ASCII) teksten zonder speciale karakters geven hier geen uitsluitsel over, daar zal de HEX-waarde altijd gelijk zijn aan de bin2hex()-waarde van het opgehaalde resultaat.

gotcha: HEX() retourneert letters in UPPERCASE, bin2hex in LOWERCASE.

2 antwoorden

Gesponsorde links
Offline UpLink - 22/06/2014 12:34 (laatste wijziging 22/06/2014 12:35)
Avatar van UpLink ... convert-mysql-d...-right-way

het is een artikeltje van 2010... maar als het toen kon, waarom dan nu niet meer he 
Offline Thomas - 22/06/2014 22:28 (laatste wijziging 23/06/2014 10:37)
Avatar van Thomas Moderator Ik denk niet dat er zoiets is als een standaard oplossing. Dit hangt helemaal af van je uitgangspositie. Als je een database-tabel hebt die puur uit latin1-data bestaat, dan volstaat volgens mij het simpelweg converteren van de tabel met:

  1. ALTER TABLE latin1_table CONVERT TO CHARACTER SET utf8;


De toverformule waaraan gerefereerd wordt in het bovenstaande artikel maakt gebruik van een extern (unix?) programma (iconv). Wat als je hier niet de beschikking over hebt? Daarnaast, het is een extern programma, je zou verwachten dat MySQL zelf (beter?) weet hoe deze conversie uit te voeren?

De auteur maakt ook niet bepaald een overtuigende indruk "correct me if I'm wrong" lol. Dat is net zoiets als "This unstable handgrenade did not blow up in my face so here! Try your luck!".

Zo'n ALTER TABLE conversie houdt bijvoorbeeld ook rekening met het feit dat de utf8-encodering van data mogelijk meer ruimte (op byte-niveau) in beslag neemt dan bij latin1. Zo worden ook kolommen van text bijvoorbeeld omgezet naar mediumtext. Hier houdt zo'n "externe conversie" geen rekening mee, dus dat zou kunnen resulteren op het afkappen, corrumperen of compleet truncaten van kolom-data bij de import! Dit is dus waarschijnlijk niet in alle gevallen een veilige methode.

Neem bijvoorbeeld de tekst "éëïî€"; in latin1 wordt dit als volgt in bytevorm opgeslagen: E9 EB EF EE 80 (5 bytes). In utf-8 is dit: C3 A9 C3 AB C3 AF C3 AE E2 82 AC (11 bytes). Als je dan een VARCHAR kolom hebt die ramvol staat met karakters in latin1, hoe kun je dan garanderen dat dit ook past in het utf8-equivalent als je de kolomdefinitie niet verandert?

EDIT: Ah, MySQL(5) telt tegenwoordig blijkbaar echt (multibyte) karakters, in plaats van bytes (MySQL 4).

herp
derp

Dat neemt echter waarschijnlijk niet weg dat een tabelkolom genoeg ruimte moet hebben om daar data uit een import in weg te te kunnen schrijven. Bij alle conversies moet je gewoon voorzichtig te werk gaan. Zorgen dat dit soort randgevallen goed gaan lijkt mij sowieso al een (grote) stap in de goede richting.
Gesponsorde links
Je moet ingelogd zijn om een reactie te kunnen posten.
Actieve forumberichten
© 2002-2024 Sitemasters.be - Regels - Laadtijd: 0.237s