Jak na UTF-8 v PHP

Přestože je kódování UTF-8 již delší dobu považováno ve světě IT za nepsaný standard, jeho podpora v nejrozšířenějším webovém jazyku současnosti není zrovna nejdokonalejší. Pojďme se tedy podívat na jaké problémy můžeme při použití UTF-8 v PHP narazit.

Unicode Byte-Order-Mark (BOM)

Unicodová kódování umožňují vložit na začátek souboru speciální znak, který usnadňuje detekci pořadí bytů (big-endian/little-endian) a zvolené kódování souboru. Tento neviditelný znak ale způsobuje značné komplikace. Pokud ho vložíme na začátek PHP skriptu, je tento znak odeslán (jako jakékoliv HTML) na výstup, což znamená, že jsou odeslány i HTTP hlavičky. Díky tomu nám náhle ve skriptu přestane fungovat volání funkce header(), session_start() a další. Díky tomu, že je znak neviditelný, bývá odhalení příčiny pěkně zapeklité.

Takže BOM rozhodně nepoužívat.

MySQL

MySQL ve výchozím nastavení používá kódování ISO-8859-1 (latin1), které nepodporuje značnou část našich znaků (ěčř...). Pokud do takto nastavené databáze uložíme text kódovaný UTF-8, některé české znaky budou nahrazeny otazníky. Je tedy nutné převést databázi, tabulky i všechny sloupce do kódování, které české znaky podporuje (utf8, cp1250, latin2).

Při komunikaci PHP interpretu s MySQL démonem může docházet k převodům mezi kódováními. Tyto převody můžeme snadno nastavit jedním ze dvou SQL příkazů:

SET NAMES UTF8;
SET CHARACTER SET UTF8;

První způsob nastaví na zvolené kódování proměnné character_set_client, character_set_connection a character_set_results. Druhý způsob nastaví jen proměnné character_set_client a character_set_results. Character_set_connection je nastaveno na hodnotu proměnné character_set_database.

Často stačí použít SET CHARACTER SET. SET NAMES musíme použít jen tehdy, když se proměnná character_set_database neshoduje s kódováním, které jsme nastavili pro naši databázi. SET NAMES pak totiž nastaví správně i proměnnou character_set_connection, která způsobí převod námi zasílaných dotazů do správného kódování před jejich provedením.

Práce s UTF-8 řetězci

Klasické funkce pro práci s řetězci zpracovávají každý byte vstupního řetězce jako samostatný znak. Proto pokud napíšeme v UTF8:

echo strlen("ěščřž");

Dostaneme výsledek 10 místo očekávaných 5. Ještě horší situace nastane, pokud budeme chtít z řetězce nějakou jeho podčást. Substr nám klidně vrátí výsledek, který končí nebo začíná půlkou dvoubytového znaku.

Řešením je používat rozšíření mbstring (--enable-mbstring), které ve většině instalací PHP už je. Mbstring nám nabízí alternativy ke klasickým řetězcovým funkcím, které ale umějí pracovat s vícebytovými kódováními. Všechny mbstring funkce začínají předponou mb_ za kterou následuje jméno původní funkce. Před začátkem práce musíme mbstring nejprve říct, v jakém kódování budou řetězce, které bude zpracovávat. Takže například:

mb_internal_encoding("UTF-8");
echo mb_strlen("ěščřž");

Nyní už dostaneme očekávaný výsledek 5.

Závěr

Podpora UTF-8 sice není do PHP moc dobře začleněná, ale pokud si zvykneme používat multibytové funkce, dáme si pozor na BOM a dopíše do třídy pro práci s MySQL jeden řádek, bude soužití s UTF-8 bezproblémové. Jednou, třeba až budeme potřebovat na webu zobrazit pár znaků v klingonštině, se nám vynaložené úsilí vrátí :)

Hodnocení

Komentáře

2010-04-16 15:20:27

V novějších verzí PHP jsou k dispozici funkce

mysql_set_charset
a
mysqli_set_charset
, které nastaví kódování tak, že ho zohledňuje i
mysql_real_escape_string
resp.
mysqli_real_escape_string
.

Častěji než extenze MB bývá k dispozici extenze iconv, která je chudší, ale délku řetězce zvládne.

Klingonština se do Unicode nedostala (je jen v oblasti pro soukromé použití, tam si ale může dát kdo chce co chce).

Na tento komentář odpověděl [2] Dundee
[2] Dundee
2010-04-16 16:56:12

#1 Jakub Vrána: Díky za informaci, o těchto funkcích jsem nevěděl.

Iconv jsem radši moc nezmiňoval, protože převádění stringů z utf-8 do jednobytového kódování, pak provedení potřebných operací (strlen, substr atd.) nad textem a nakonec převádění zpět do utf-8 nepovažuji za zrovna čisté a elegantní řešení.

Škoda, je to sympatický jazyk :)

Na tento komentář odpověděl [4] Jakub Vrána
[3] karl
2010-04-16 21:15:52

> protože převádění stringů z utf-8 do jednobytového kódování, pak provedení potřebných operací (strlen, substr atd.)

Proč bys něco takového dělal?

Stačí použít:
http://www.php.net/manual/en/function.iconv-strlen.php
http://www.php.net/manual/en/function.iconv-substr.php
atd.

Na tento komentář odpověděl [4] Jakub Vrána
Na tento komentář odpověděl [5] Dundee
2010-04-16 21:46:42

#2 Dundee: #3 karl: Pravdu máte skoro oba. Schválně jsem se koukal na implementaci iconv_strlen() ve zdrojáku PHP (http://svn.php.net/viewvc/php/php-src/trunk/ext/iconv/iconv.c?view=markup) a funguje tak, že převede řetězec do UCS4 a ten změří. Takže efektivita nic moc. Udělal jsem si malé srovnání rychlosti:

iconv_strlen($s, "utf8"); // 0.52 s
mb_strlen($s, "utf8"); // 1.48 s
utf8_length($s); // 0.98 s
strlen(utf8_decode($s)); // 0.16 s

function utf8_length($s) {
	$return = 0;
	$length = strlen($s);
	for ($i=0; $i < $length; $i++) {
		$return++;
		$ord = ord($s[$i]);
		if ($ord >= 240) {
			$i += 3;
		} elseif ($ord >= 224) {
			$i += 2;
		} elseif ($ord >= 192) {
			$i++;
		}
	}
	return $return;
}

Takže nejrychlejší je utf8_decode(), která sice převádí neznámé znaky na otazník, to ale pro zjištění délky nevadí.

[5] Dundee
2010-04-17 11:37:29

#3 karl: Protože iconv sice poskytuje alternativy k funkcím strlen, strpos, strrpos a substr, ale co ten zbytek? Řetězcových funkcí je přeci daleko více.

Na tento komentář odpověděl [6] karl
[6] karl
2010-04-17 16:53:17

#5 Dundee: Nerozumím jakou souvislost má existence fce iconv_strlen s tím, že řetězcových funkcí je více.

Na tento komentář odpověděl [7] Dundee
[7] Dundee
2010-04-17 17:09:15

#6 karl: Rozšíření iconv neposkytuje alternativy ke všem řetězcovým funkcím. Proto budeš nakonec stejně nucen převést řetězec do jednobytového kódování a zpět. To jsem chtěl říct.

[8] Al
2010-04-28 07:38:46

Ještě upozorňuji na modifikátor u u preg_replace(). Když jsem to kdysi potřeboval, nemohl jsem na to přijít. Např. obarvování výsledků hledání, jak je popsáno ve starém článku tady: http://php.vrana.cz/zvyrazneni-vysledku-vyhledavani.php
s Unicode nefunguje. Musí se použít:

$text = preg_replace("~$search~iu", '<span class="search-result">\0</span>', $text);

[9] Carlos
2010-08-29 18:38:15

Ahoj, mám dotaz, který s tímto tématem malinko souvisí.

Při kódování utf8 používám na zpracovíní některou z níže napsaných kódů. Který si myslíš, že je nejvhodnější? Děkuji
PS: použití

$jmeno = fix_input($_POST['name']);
$prijmeni ...
.
.
a pak s promněnými dále pracuji/ukládám do db atd ____
function fix_input($input)
{
 $output = strtolower($input);
 $output = trim($output);
 $output = str_replace("  "," ",$output);
 $output = strtr($output," ěščřžýáíéúůďňť","_escrzyaieuudnt");
 $output = str_replace("+","",$output);
 $output = str_replace("*","",$output);
 $output = str_replace("'","",$output);
 $output = str_replace(""","",$output);
 $output = str_replace("\","",$output);
 $output = str_replace("/","",$output);
 $output = str_replace("!","",$output);
 $output = str_replace("?","",$output);
 $output = str_replace("%","",$output);
 $output = str_replace("&","",$output);
 $output = str_replace(" ","_",$output);
 return($output);
}
nebo function uprav_vstup($hodnota) { $hodnota = trim($hodnota); $hodnota = str_replace(""","",$hodnota); $hodnota = str_replace("'","",$hodnota); return($hodnota); } [/code] nebo
function fix_input($value)
{
 $result = trim($value);
 $result = addslashes($result);
 return($result);
}

Díky moc!

Na tento komentář odpověděl [10] Carlos
[10] Carlos
2010-08-29 18:39:52

#9 Carlos: oprava

function uprav_vstup($hodnota)
{
 $hodnota = trim($hodnota);
 $hodnota = str_replace(""","",$hodnota);
 $hodnota = str_replace("'","",$hodnota);
 return($hodnota);
}

Na tento komentář odpověděl [11] Dundee
2010-08-31 11:23:23

#10 Carlos: Doporučuju přečíst článek Davida http://phpfashion.com/escapovani-definitivni-prirucka, tam je tato problematika dobře rozepsána.

2011-01-23 11:37:16

Díky za nakopnutí, bohužel budu muset jít opravit jeden web, kde jsem si problémy s UTF8 vůbec neuvědomil. :)

Komentáře již nelze přidávat