0

Kodowanie znaków – czy wszystko w tej sprawie zostało już powiedziane?

Kodowanie znaków (m. in. utf-8, iso-8859-2)

Po co nam kodowanie znaków?

Jak dobrze wiemy, komputery same w sobie potrafią działać tylko na liczbach, a dopiero sprytne ułożenie tych liczb sprawia, że mogą one reprezentować chociażby litery, dźwięk czy muzykę. Najbardziej znanym i ciągle powszechnie uczonym w szkołach sposobem kodowania znaków, a więc przypisywania liczbom określonego znaku, jest standard ASCII. Zawiera on 128 różnych znaków umożliwiających zapis języka angielskiego, wliczając w to dodatkowe symbole (znaki zapytania, nawiasy itd.). W ten sposób, używając tego kodowania, przykładowo literka „a” będzie zapisana w pamięci komputera jako liczba 97. Wszystko byłoby fajnie, gdyby nie to, że standard ten w swojej pierwotnej wersji umożliwiał zapis praktycznie tylko języka angielskiego (nawet poprawny zapis łaciny był niemożliwy ze względu na brak niektórych dyftongów, np. æ). Próbowano to rozwiązać poprzez dodanie kolejnych 128 znaków typowych dla konkretnego języka, przez co był tworzony nowy zestaw znaków (co więcej – ciągle taki zabieg jest stosowany i nosi nazwę: kodowanie ANSI).

Niestety, jeśli nawet ktoś w czasach powstania standardu ASCII wpadł na pomysł, żeby stworzyć standard zawierający znaki wszystkich języków na świecie, to nie został on szeroko przyjęty i został zapoczątkowany okres, w którym nawet pośród jednego języka zaczęto stosowanie wielu zbiorów znaków (systemów kodowania znaków). W Polsce zaczęto stosować przede wszystkim ISO-8859-2, Windows-1250, UTF-8, czy już zapomniane: CP852, Mazovia oraz ISO 8859-13.

Jakie problemy wynikają z różnego kodowania znaków?

W teorii pomysł jest prosty i sprawny: określamy kodowanie każdego tekstu i w ten sposób powinniśmy być w stanie odczytywać i zapisywać wszystkie teksty, z użyciem dowolnego zestawu znaków. W praktyce to jednak nie działa tak fajnie – czy nigdy nikomu z Czytelników nie zdarzyło się, że system prosił go o nieużywanie „polskich znaków”? Albo, że wpisany tekst w języku polskim zamienił się w „krzaczki”? Problem zdaje się tkwić w tym, że każdy element systemu używający tekstu ma od razu ustawione pewne kodowanie znaków i nie zawsze mamy na nie wpływ. Mam tutaj na myśli między innymi: system plików, pomniejsze programy komputerowe czy bazy danych. Różnego typu niedociągnięcia edytorów tekstu też robią swoje.

Standard Unicode

Powstało wiele pomysłów na rozwiązanie wskazanych problemów, z czego najbardziej popularny jest Unicode (którego pierwsza wersja powstała w 1991 roku). Jest on właśnie zbiorem znaków zawierających w zamierzeniu wszystkie znaki występujące na świecie.
O ile w przypadku ASCII kodowanie i zbiór znaków mogły być praktycznie utożsamiane, o tyle w przypadku Unicode musimy rozróżnić, że zbiór znaków przypisuje do każdego znaku jakąś liczbę, a kodowanie znaków określa sposób w jaki zapisywać i odczytywać te liczby. Przykładowo kodowanie znaków UTF-8 zapisuje znaki korzystając z 8 bitowych słów, a UTF-16 głównie korzystając z 16 bitowych słów (nie wdając się w szczegóły), mimo że obydwa kodowania działają na tym samym zbiorze znaków.

Od początków standardu Unicode minęło już ponad 25 lat, a ciągle nie jest wszędzie obsługiwany i zdarzają się problemy z jego obsługą. Niemniej wydaje mi się, że jest to obecnie jedyne sensowne rozwiązane w „normalnych” aplikacjach (w prostych kalkulatorach spokojnie zgadzam się na używanie ASCII albo jeszcze bardziej ograniczonych zbiorów znaków 🙂 ).

 

Kodowanie znaków na przykładzie PHP

Standardowo PHP powinno poruszać się w systemie UTF-8. Niemniej zaleca się, aby w konfiguracji Apache zawrzeć: default_charset = "utf-8" oraz AddDefaultCharset UTF-8. Ustrzeże nas to przed nieoczekiwanym działaniem naszej aplikacji. Ponadto wszystkie pliki źródłowe muszą być zapisane w formacie UTF-8, a połączenie do bazy danych powinno mieć ustawione kodowanie UTF-8. Najlepiej wszystkie pozostałe pliki tekstowe na których działamy (jeśli takowe są) również powinny mieć ustawione kodowanie UTF-8.

Problem z BOM (Byte Order Mark)

Żeby jeszcze bardziej uprzykrzyć ludziom życie wprowadzono tzw. BOM, czyli trzy dodatkowe znaki na początku pliku zapisanego z użyciem zbioru znaków Unicode. Teoretycznie miały z tego wynikać same zalety (m. in. określać kolejność bitów), ale w praktyce powoduje to między innymi wyświetlanie błędu PHP „Headers already sent”, a czasem pustych linii lub paru dziwnych znaków na początku dokumentu. Rozwiązanie jest jedno – zawsze zapisywać pliki źródłowe w UTF-8 bez BOM.

Zmiana kodowania w locie

PHP oferuje kilka funkcji umożliwiających zmianę kodowania wskazanego tekstu. Jedną z nich jest iconv(), która przyjmuje jako parametry nazwę kodowania „z”, „do”, oraz transformowany tekst.

<?php

$stringUTF_8 = "Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.";

// wypisanie tekstu przy użyciu kodowania UTF-8
echo $stringUTF_8 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.


$stringISO_8859_2 = iconv("UTF-8", "ISO-8859-2", $stringUTF_8);

// Wypisanie tekstu przy użyciu kodowania ISO-8859-2
echo $stringISO_8859_2 . "\n";
// ��ta g� idzie sobie szos� przegl�daj�c Szalonego Peceta. (sic!)

$stringUTF_8= iconv("ISO-8859-2", "UTF-8", $stringISO_8859_2);

// Tekst ponownie skonwertowany do UTF-8
echo $stringUTF_8 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

Czy to rozwiązuje wszystkie problemy? Absolutnie nie. Ta funkcja może sobie nie radzić, jeśli w zadanym tekście pojawią się jakieś niepoprawnie zakodowane znaki. Wtedy na ratunek rusza funkcja mb_convert_encoding(), która przyjmuje odwrócone parametry w stosunku do iconv():

<?php

$stringUTF_8 = "Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.";
// wypisanie tekstu przy użyciu kodowania UTF-8
echo $stringUTF_8 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

$stringISO_8859_2 = mb_convert_encoding($stringUTF_8, "ISO-8859-2", "UTF-8");
// Wypisanie tekstu przy użyciu kodowania ISO-8859-2
echo $stringISO_8859_2 . "\n";
// ��ta g� idzie sobie szos� przegl�daj�c Szalonego Peceta. (sic!)

$stringUTF_8= mb_convert_encoding( $stringISO_8859_2, "UTF-8", "ISO-8859-2");
// Tekst ponownie skonwertowany do UTF-8
echo $stringUTF_8 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

Musimy się jednak wystrzegać tego, aby nie wywołać kilkukrotnie tego samego konwertowania na wskazanym tekście (zarówno w przypadku iconv() jak i mb_convert_encoding()):

<?php

$stringUTF_8 = "Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.";

// wypisanie tekstu przy użyciu kodowania UTF-8
echo $stringUTF_8 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

$stringISO_8859_2 = mb_convert_encoding($stringUTF_8, "ISO-8859-2","UTF-8");

// wypisanie tekstu przy użyciu kodowania ISO-8859-2
echo $stringISO_8859_2 . "\n";
// ��ta g� idzie sobie szos� przegl�daj�c Szalonego Peceta. (sic!)

// podwójnie konwertujemy tekst do UTF-8
// mamy wtedy źle określone kodowanie na wejściu! - czasem tak się może zdarzyć
$stringUTF_8= mb_convert_encoding( $stringISO_8859_2,  "UTF-8", "ISO-8859-2");
$stringUTF_8= mb_convert_encoding( $stringUTF_8,  "UTF-8", "ISO-8859-2");
echo $stringUTF_8 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta. (sic!)

$stringISO_8859_2 = mb_convert_encoding($stringUTF_8, "ISO-8859-2","UTF-8");

// wypisanie tekstu przy użyciu kodowania UTF-8
echo $stringISO_8859_2 . "\n";
//Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

Funkcje utf8_encode() oraz utf8_decode() konwertują natomiast jedynie pomiędzy kodowaniami UTF-8 oraz ISO-8859-1.

Funkcje mb (Multi Byte)

PHP wiele funkcji udostępnia również w wersji mb (tzn. zaczynającej się od mb_, np. mb_strpos()). Powstały one właśnie po to, żeby poprawnie działać z tekstami Unicode. Mówiąc bardziej precyzyjnie: okazuje się, że standardowe funkcje tekstowe PHP działają w sposób binarny, tzn. porównują poszczególne bity tekstu bez zwracania uwagi na jego kodowanie. Funkcje mb jednak interpretują użyte kodowanie i porównują odpowiednie znaki zamiast bitów.

<?php

$str = "ąćęłńóśźż";

$str = strtoupper($str);
echo $str;
// ąćęłńóśźż

$str = mb_strtoupper($str);
echo $str;
// ĄĆĘŁŃÓŚŹŻ
<?php

$string = '我是Szalony Pecet';

echo substr($string, 2). "\n";
// �是Szalony Pecet

echo mb_substr($string, 2). "\n";
// Szalony Pecet

Rozpoznawanie kodowania

Jak Czytelnik pewnie zauważył, zawsze musimy podać kodowanie z którego i do którego chcemy konwertować tekst. Nie zawsze jednak wiemy w jakim kodowaniu zapisany jest nasz tekst. Szczerze mówiąc najlepiej jednak jest się w jakiś sposób dowiedzieć jakie to jest kodowanie, np. sprawdzając META CHARSET w pliku HTML. Jeśli jednak nie ma takiej możliwości to poniższy kod, przynajmniej w teorii, powinien radzić sobie z automatycznym wykrywaniem kodowania oraz konwertowaniem go do UTF-8. Innym sposobem jest skorzystanie z dodatkowych wtyczek PHP, np. Force UTF-8, co wydaje mi się bezpieczniejszym podejściem.

<?php

$text = "zażółć żółtą gęś";

echo iconv(mb_detect_encoding($text, mb_detect_order(), true), "UTF-8", $text);

Przechodzenie po dokumencie XML

Wydawałoby się, że już więcej niespodzianek nas nie może spotkać. A jednak! Trzeba zwrócić uwagę, że DomDocument działa podobnie jak przeglądarka, tzn. nie wystarczy, żeby plik/string był zapisany w odpowiednim kodowaniu, ale również odpowiednie metadane pliku XML/HTML muszą się zgadzać z tym stanem rzeczy. Wydaje się, że inaczej nie da się wymusić parsowania takich dokumentów przy użyciu UTF-8.

<?php

$string = '<html><head><meta charset="iso-8859-2" /></head><body>Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.</body></html>';
$documentISO_8859_2 = iconv("UTF-8", "ISO-8859-2", $string);;
echo $documentISO_8859_2;
// <html><head><meta charset="iso-8859-2" /></head><body>��ta g� idzie sobie szos� przegl�daj�c Szalonego Peceta.</body></html>Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

// load as iso-8859-2
$dom = new DOMDocument();
$dom->loadHTML($documentISO_8859_2);
$xpath = new DOMXPath($dom);
$match = $xpath->query('//body/text()');
echo $match[0]->nodeValue ."\n";
// Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

// load as utf-8
$documentUTF_8 = iconv("ISO-8859-2", "UTF-8", $documentISO_8859_2);
$dom = new DOMDocument();
$dom->loadHTML($documentUTF_8);
$xpath = new DOMXPath($dom);
$match = $xpath->query('//body/text()');
echo $match[0]->nodeValue ."\n";
// Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

// load as utf-8, but also change meta charset
$documentUTF_8 = str_ireplace("iso-8859-2", "utf-8", $documentUTF_8);
$dom = new DOMDocument();
$dom->loadHTML($documentUTF_8);
$xpath = new DOMXPath($dom);
$match = $xpath->query('//body/text()');
echo $match[0]->nodeValue ."\n";
// Żółta gęś idzie sobie szosą przeglądając Szalonego Peceta.

Podsumowanie

Różne kodowania znaków są zmorą wszystkich osób korzystających z komputerów, praktycznie od początku ich istnienia. Obecnie mamy standard Unicode, który poprawnie użyty powinien rozwiązywać większość problemów z tym związanych, chociaż może nastręczać dodatkowych utrudnień przy programowaniu aplikacji. Zdecydowanie zalecam używanie kodowania znaków UTF-8, gdyż wszystkie inne będą tylko powodować niepotrzebne transformacje i potencjalne problemy. Przy zastosowaniu UTF-8 wszystko będzie działało poprawnie… oprócz bazy danych 😉 Tam polecam użycie binarnego kodowania UTF-8 („utf8_bin”). Ktoś wie dlaczego? 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *