måndag 9 maj 2011

Från latin1 till UTF-8 i PHP och MySql

När jag skapade databasen till Sovrat som senare skulle bli Pusha.se för ganska precis 5 år sedan hade jag inga större kunskaper om encoding. Latin1 (ISO-8859-1) var standard i min MySql-installation så latin1 fick det bli. För ett par veckor sedan tog jag tjuren vid hornen och konverterade allt till utf-8.

Tänkte skriva ned några av de lärdomar jag dragit av konverteringsarbetet. Mest för att jag själv ska komma ihåg det om jag behöver göra det fler gånger och även för att någon annan stackare i samma situation ska få det lite lättare.

Ändra encoding överallt
Det finns ganska många ställen man måste byta encoding på för att det ska få önskad effekt. Man ser inte heller om man gjort rätt förrän man bytt på alla dessa platser. Se till att följande använder utf-8 som encoding:
  • MySql-databasen
    ALTER DATABASE databasnamn CHARACTER SET utf8;
  • Varje databastabell
    ALTER TABLE tabell CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT = DYNAMIC;
  • Varje kolumn i varje databas
    ALTER TABLE tabell MODIFY COLUMN kolumn VARCHAR(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
  • PHP-filerna med själva PHP-koden
  • setlocale i PHP
    setlocale(LC_ALL, 'sv_SE.UTF-8', 'sve');
  • Content-Type i HTTP-headern
    header('Content-Type: text/html; charset=UTF-8');
  • Content-Type i HTML-headern
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  • MySql-anslutningen
    mysql_set_charset('utf8',$link);

PHP inte byggt för UTF-8
Av historiska skäl är PHP:s stöd för UTF-8 inte det bästa. T.ex. fungerar nästan ingen av de vanliga strängfunktionerna som strlen(), substr() m.fl. Detta beror på att PHP jobbar med en teckenkodning där varje tecken består av 1 byte medan det i UTF-8 även finns tecken som tar 2 eller 3 byte.

Ett möjligt sätt att ta sig runt detta är att använda utf8_decode() och utf8_encode(). Om du t.ex. vill mäta längden på en sträng som är encodad med utf8 så kan du först decoda den innan du mäter.

$len = strlen(utf8_decode($utf8str));

Vill man klippa en utf8-sträng så decodar man först, klipper och sen encodar man igen.

$utf8str = utf8_encode(substr(utf8_decode($utf8str),0,10));

Ovanstående lösning blir dock ganska snabbt väldigt svårläst och ful och verkar dessutom inte fungera för riktigt konstiga tecken som kräver 3-byte. Vill man ha en snyggare lösning så får man istället använda PHP:s multibyte-funktioner. Dessvärre är mbstring-modulen inte installerad i PHP per default utan du måste själv installera den.

När du väl har mbstring aktiverat så heter alla strängfunktioner precis som tidigare men med mb_ före. T.ex. mb_strlen(), mb_substr(), mb_strtolower() o.s.v.

Vissa strängfunktioner finns tyvärr inte i mbstring men däremot tar dessa funktioner en extra parameter där man kan specificera en encoding. Exempel på sådana funktioner är t.ex. htmlentities(), htmlspecialchars() m.fl.

$text = htmlentities($text,ENT_COMPAT,'UTF-8');

Vissa reguljära uttryck kan förenklas lite när man använder UTF-8. Tidigare var jag tvungen att använda a-zA-Z\xe5\xe4\xf6\xc5\xc4\xd6 för att få med åäöÅÄÖ men efter övergången fungerade det istället bra med a-öA-Ö.

Övrigt
Jag har sett många rekommendationer att man ska köra följande två kommandon så fort anslutningen till MySql har skapats:

mysql_query("SET NAMES 'utf8'");
mysql_query("SET CHARACTER SET utf8");

Det har dock inte varit nödvändigt i mitt fall men det är något att testa om man har problem.

Om man vill slippa ändra till UTF-8 manuellt för varje kolumn i en tabell så ska det fungera att använda följande query istället:

ALTER TABLE tabellnamn CONVERT TO CHARACTER SET utf8;

Jag tyckte dock att det kändes tryggare att konvertera en kolumn i taget. Speciellt då det ibland uppstod konverteringsfel som gjorde att man manuellt var tvungen att fixa någon rad i databasen. När man har väldigt många rader så tar det ett tag att bara konvertera en enda kolumn.

I vissa fall kan det ha betydelse vad för collation man väljer för en kolumn i MySql. Det vanliga är utf8_general_ci men den har vissa brister som t.ex. innebär att åäö sorteras som aao. Vill man ha korrekt sortering kan man använda utf8_unicode_ci eller utf8_bin. Den senare jämför tecknen binärt vilket gör dina querys case sensetive.

Se för allt i världen till att ha backup på allt innan du påbörjar ett konverteringsarbete.

Lycka till!

3 kommentarer:

  1. Tack för denna rad:
    setlocale(LC_ALL, 'sv_SE.UTF-8', 'sve');

    Det var den jag hade missat! Har gjort en ganska omfattande konvertering från Latin 1 till UTF-8 av en PHP/MySQL-sida med en databas på över 700 MB.

    Hade jag haft den här guiden innan hade det nog gått lite smidigare! ^^

    SvaraRadera
  2. Toppenguide. Jag upptäckte att jag efter konvereteringen behövde använda htmlentities när värden skall sättas i formulärelement, något jag slapp med ISO-8859-1

    SvaraRadera