PHP, MySQL 4.1 i unicode

W tym artykule postanowiłem wyjaśnić trochę tajniki działania systemów kodowań i porównań, które pojawiły się w MySQL 4.1 i mogą sprawić wiele kłopotów, szczególnie początkującym. Dodatkowo pokazane jest też, co trzeba zrobić, aby nasza strona WWW obsługiwała Unicode.

Ost. modyfikacja: sobota, 29 marca 2008

Czym są systemy kodowania i porównań?

Jak wiadomo, podstawowy kod ASCII to tylko 128 znaków zapisywanych na siedmiu bitach. W tak małej liczbie nie mogą zmieścić się zatem litery narodowe, o alfabetach azjatyckich nie wspominając. Problem ominięto, tworząc strony kodowe, albo systemy kodowań. Jako że do dyspozycji został jeszcze jeden bit, dający dodatkowe 128 wolnych pozycji, mogą one je gospodarować według uznania. Obecnie standardem do zapisu liter na jednym bajcie jest ISO-8859 występujący w odmianach dla różnych regionów Europy/świata. Gdy jednak chcemy wymieszać ze sobą w jednym dokumencie teksty pochodzące z dwóch regionów, powstaje problem. Dwóch odmian ISO raczej nie da się pogodzić. Tu z pomocą wkracza Unicode. Do zapisu znaków wykorzystuje on więcej, niż jeden bajt, a to daje nam tysiące, a nawet miliony dozwolonych znaków do użycia w jednym dokumencie! Tu mamy z kolei dwie odmiany:

  • UTF8 - podstawowe 128 znaków zapisywanych jest za pomocą jednego bajtu. Litery języków i alfabetów europejskich zapisywane są na dwóch bajtach, a znaki alfabetów azjatyckich - na trzech. Do powiadomienia, że zaczyna się kilkubajtowy znak, używa się wolnego ósmego bitu.
  • UCS - wszystkie znaki zapisywane są na dwóch bajtach.
Wraz z systemami kodowań pojawia się także pojęcie systemu porównań. Tu sprawa jest bardzo prosta. Weź do ręki np. słownik języka polskiego. Hasła ułożone są tam alfabetycznie... z uwzględnieniem znaków narodowych (ś jest po s itd.). I to jest to! Systemy porównań opisują, który znak znajduje się po którym, co usprawnia sortowanie.

Kodowanie a MySQL

MySQL 4.0 i wcześniejsze nie obsługiwały systemów porównań i kodowania w ogóle. O ile do samego przechowywania tekstów nie robiło to wielkiej różnicy (jak nawala kodowanie, to wina jest nasza, a nie bazy), ale gdy przyszło cokolwiek posortować, zaczynały się schody. Do dziś wspominam rozmowy na forach dyskusyjnych w stylu "Jak zrobić sortowanie z uwzględnieniem polskich liter?" Rozwiązania były naprawdę przeróżne. Proponowano np. ich zamianę na znaki ASCII według reguły "ł -> lzz", "ą -> azz" itd. Oczywiście nie było to zbyt skuteczne. Obecnie moja rada na taki problem jest jedna: zainstaluj MySQL 4.1

Ustawienia od strony bazy

Przykładową konfigurację bazy opiszę na przykładzie UTF8. MySQL pozwala przydzielać kodowanie i system porównań na trzech poziomach: bazy, tabeli i pojedynczego pola. Niezwykle ważne jest, byśmy już na samym początku zdecydowali się na wybór jednego, GÓRA DWÓCH takowych, które użyjemy. Dlaczego? Otóż niektóre operacje nie pozwalają się wykonać, gdy wykonywane są na polach o różnych systemach porównań i należy z tym uważać. My wybierzemy "utf8_polish_ci" (można zamiast tego wziąć "utf8_unicode_ci", gdzie do sortowania użyty jest algorytm porównywania znaków Unicode), a dla pól z danymi binarnymi - "utf8_bin". Wybrane pozycje ustawiamy całej bazie, wszystkim tabelom oraz polom tekstowym (VARCHAR, CHAR, TEXT, BLOB).

Sprawa może skomplikować się, gdy przenosimy bazę z wcześniejszych wersji MySQL'a. Ten ustawia wtedy wszędzie domyślny system kodowania, niekoniecznie zgodny z naszymi zamiarami. Wtedy możemy zastosować taki wariant:

  1. Dzielimy listę zapytań na dwie części. Pierwsza niech tworzy strukturę, druga - zapisuje dane.
  2. Wgrywamy strukturę
  3. Odpalamy ręcznie napisany skrypt, który podmieni nam systemy kodowania. Przykładowy podaję pod tą instrukcją.
  4. Upewniamy się, czy phpMyAdmin pracuje na połączeniu z użyciem naszego systemu porównań (widoczne jest to na stronie głównej). Jeśli nie - zmieniamy.
  5. Wgrywamy dane
Skrypt PHP podmieniający kodowanie może wyglądać tak. Do jego napisania zostało użyte rozszerzenie Improved MySQL, które jest obecne w PHP 5. Jeżeli chcesz użyć starego, pozamieniaj prefiksy funkcji mysqli_ na mysql_ oraz usuń z ich wywołań parametr $n.
<?php
	// konfiguracja polaczenia
	$dbhost = 'localhost';
	$dbuser = 'root';
	$dbpass = 'root';
	$dbname = 'test';
	
	// na te kodowania bedziemy zamieniac
	$binary = 'utf8_bin';
	$text = 'utf8_polish_ci';
	$encoding = 'utf8';
 
	// polecenia
	$n = mysqli_connect($dbhost, $dbuser, $dbpass);
	mysqli_select_db($n, $dbname);
	
	mysqli_query($n, 'ALTER DATABASE `'.$dbname.'` DEFAULT CHARACTER SET '.$encoding.' COLLATE '.$text);
	echo '<b>database `'.$dbname.'` converted to `'.$text.'`</b><br/>';
	
	$r = mysqli_query($n, 'SHOW TABLE STATUS');
	
	while($row = mysqli_fetch_assoc($r))
	{
		if(strpos($row['Collation'], '_bin') !== FALSE)
		{
			mysqli_query($n, 'ALTER TABLE '.$dbname.'.'.$row['Name'].' DEFAULT CHARACTER SET '.$encoding.' COLLATE '.$binary);
			echo '`'.$row['Name'].'` converted from `'.$row['Collation'].'` to `'.$binary.'`<br/>';
		}
		else
		{
			mysqli_query($n, 'ALTER TABLE '.$dbname.'.'.$row['Name'].' DEFAULT CHARACTER SET '.$encoding.' COLLATE '.$text);
			echo '`'.$row['Name'].'` converted from `'.$row['Collation'].'` to `'.$text.'`<br/>';
		}
		$r2 = mysqli_query($n, 'SHOW FULL COLUMNS FROM '.$dbname.'.'.$row['Name']);		
		while($row2 = mysqli_fetch_assoc($r2))
		{
			if($row2['Collation'] != '')
			{
				if(strpos($row2['Collation'], '_bin') !== FALSE)
				{
					// pole binarne
					mysqli_query($n, 'ALTER TABLE `'.$row['Name'].'` CHANGE `'.$row2['Field'].'` `'.$row2['Field'].'` '.$row2['Type'].' CHARACTER SET '.$encoding.' COLLATE '.$binary.' '.($row2['Null'] != 'YES' ? 'NOT NULL' : 'NULL'));
					echo $row['Name'].'.'.$row2['Field'].' converted from `'.$row2['Collation'].'` to `'.$binary.'`<br/>';
				}
				else
				{
					// pole tekstowe
					mysqli_query($n, 'ALTER TABLE `'.$row['Name'].'` CHANGE `'.$row2['Field'].'` `'.$row2['Field'].'` '.$row2['Type'].' CHARACTER SET '.$encoding.' COLLATE '.$text.' '.($row2['Null'] != 'YES' ? 'NOT NULL' : 'NULL'));
					echo $row['Name'].'.'.$row2['Field'].' converted from `'.$row2['Collation'].'` to `'.$text.'`<br/>';
				}
			}
		}
	}
	
	mysqli_close($n);
?>

Ustawienia od strony skryptu

Kodowanie możemy przypisać także do połączenia z bazą danych. Ba, nie tylko możemy, a raczej musimy. Inaczej MySQL użyje domyślnego, który niekoniecznie będzie nam odpowiadać. Dlatego natychmiast po nawiązaniu połączenia należy wysłać takie zapytanie:

<?php
	mysql_query('SET NAMES \'utf8\'');
?>
Dzięki temu podczas wprowadzania znaków unicode do bazy nie zostaną one uszkodzone, jak to się mi zdarzyło przy pisaniu tej strony. Kolejna rzecz dotyczy już samego nakłonienia przeglądarki, aby wyświetlała znaki w formacie UTF8. Informacja o użytym kodowaniu musi być wysłana w połączeniu z Content-type:
header('Content-type: text/html;charset=utf-8');
To wszystko. Dzięki tym zabiegom utworzyliśmy stronę pracującą w Unicode oraz skonfigurowaliśmy bazę do pełnej obsługi tego standardu. Tutaj wspomnę, że wypadałoby mieć jeszcze edytor HTML obsługujący UTF8. Ze swej strony polecam czeskiego PSPada. Pracuje mi się z nim znakomicie.

Na tym kończę ten artykuł i życzę powodzenia w dalszych eksperymentach!

Dla redaktorów EIOBA.COM i innych: Jeśli czytany artykuł udostępniony jest na licencji Creative Commons, to proszę wgrywać go do własnych baz danych z takimi oznaczeniami o autorze, jakie chcę aby były i o jakich jest informacja w pliku PDF. Nie mam zamiaru latać po całej sieci i każdemu z osobna te sprawy wyjaśniać. Jeśli natomiast jesteś z EIOBA.COM, nie wgrywaj tego tekstu - sam mam tam konto i uczynię to osobiście w odpowiednim czasie.

© Tomasz "Zyx" Jędrzejewski 2005 - 2008 | Wykonanych zapytań: 1 | Serwer wirtualny zapewnia