Od podstaw: Unicode i UTF-8
Thursday, 16 June 2016, 10:47
Odrobina historii
W Internecie od niemal zawsze mają miejsce problemy z kodowaniem (zapisem znaków). Dziś praktycznie powszechnie korzysta się z Unicode (czy też po spolszczeniu - Unikod). Jednak nadal wiele osób, szczególnie nieobeznanych z nim nie ma zbytnio pojęcia czym on jest i dlaczego jest taki ważny. Przeważnie dotyczy to osób korzystających ze środowiska Microsoft Windows i żadnego innego, ponieważ nie zdają sobie oni nieraz sprawy z różnorodności pozostałych platform. Skąd jednak całe to zamieszanie? Zacznijmy od początku.
Dawno temu, kiedy Internet nawet się jeszcze nikomu nie śnił w USA powstał standard ASCII - American Standard Code for Information Interchange. Jak sama nazwa wskazuje był to kod przewidziany głównie do użytku na terenie Stanów Zjednoczonych, a więc nie zawierał żadnych krzaczkowatych znaków. Z tego powodu ilość znaków potrzebnych do uwzględnienia była dość niewielka - do zapisania wszystkich wystarczało 7 bitów. Mimo iż wszystkie maszyny fizycznie pracowały na ośmiu, standard ten zamykał się w siedmiu - ósmy bit często był wykorzystywany jako bit parzystości w transmisji danych. Komputery jednak dotarły do krajów takich jak Polska, Niemcy, gdzie potrzebne było dodanie znaków diakrytycznych, czy też o zupełnie odmiennym piśmie jak cyrylica, czy alfabety z rodziny CJK (daleki wschód).
Strony kodowe
Nawet ósmy bit nie wystarczał do zakodowania wszystkich znaków. Utworzono zatem strony kodowe, organizacja ISO stworzyła standard ISO-8859, który opisuje kodowania znaków w różnych zestawach. Czym w ogóle jest takie kodowanie? Strona kodowa to tablica przyporządkowująca binarnym reprezentacjom znaki pisma (i nie tylko, bo oprócz nich są też znaki kontrolne, sterowania i inne, ale to w tej chwili mało istotne). W zależności od tego jaka strona kodowa jest używana, takie znaki będą użyte. Na przykład bajt o wartości 0xA3 w stronie kodowej ISO-8859-1 zostanie zamieniony na £ (znak funta), natomiast w stronie kodowej ISO-8859-2 na wielką polską literę Ł. Niemniej wszystkie kodowania mają jedną wspólną część - pierwsze 128 znaków (te, które da się zapisać za 7 bitach) to podstawowe kodowanie ASCII.
Oczywiście istnieje pewna korporacja, której żadne standardy nie są straszne. Świat byłby zbyt piękny, gdyby pod Windowsem obowiązywały standardy ISO - tam są jeszcze inne strony kodowe. Nie jest tragicznie - wszystkie one również bazują na standardzie ASCII. Poza tym różnice pomiędzy ISO, a CP (Windows) też nie są drastyczne, jednak fakt iż te same znaki koduje się innymi wartościami sprawia, że jest to duże utrudnienie. Przykładowo wartość 0xB1 w kodowaniu ISO-8859-2 odpowiada literze ą, podczas gdy w kodowaniu CP-1250 jest to znak ± (plus-minus, błąd pomiaru).
Oprócz tego istnieją też kodowania dla języków azjatyckich takie jak Big5 i inne z rodziny GB. Jednak ze względu na zupełnie inną specyfikę tamtejszych języków działają one inaczej.
Kodowania polskich znaków
Do kodowania polskich znaków nadają się dwa wspomniane już wcześniej kodowania - ISO-8859-2 i CP-1250. Różnice między nimi nie są drastyczne - to bodajże kilkanaście znaków. Niestety jak na złość są to między innymi litery polskiego alfabetu: ś, Ś, ź, Ź, ą, Ą. Oczywiście standardem domyślnie jest standard ISO, jednak zgadnijcie, czy był on popularniejszy od kodowania Windows w czasach, kiedy Linux na desktopach brzmiał podejrzanie podobnie do wirus, o innych systemach nie wspominając (mówimy o sytuacji w Polsce).
Problemy w Internecie
Taka sytuacja (nie tylko dotycząca polskich znaków) w połączeniu z wymianą informacji na globalną skalę (Internet) doprowadziła do wielu problemów. Dane przesyłane przez sieć to po prostu strumienie bajtów. Kodowanie odpowiada jedynie za ich graficzną prezentację, nie zmienia w żaden sposób ich zawartości. Oznacza to, że kiedy jeden użytkownik wysyłał ciąg Śląsk korzystając z systemu Windows, to wysyłał de facto wartości (w zapisie szesnastkowym):
8C 6C B9 73 6B
Kiedy taki plik otworzyłby ktoś korzystający z innego systemu i domyślnie ustawionym kodowaniem ISO zobaczyłby szlaczek w postacii lšsk. W większości protokołów służących do przesyłania informacji tekstowych w Internecie (takich jak HTTP, czy SMTP) istnieje możliwość wysyłania dodatkowych meta-danych (nagłówków) określających kodowanie przesyłanej zawartości. Problem jednak pozostaje w przypadku danych przesyłanych bez żadnego przetwarzania (chociażby w przypadku protokołu IRC - dane są tam przesyłane do rozmówców w formie takiej, w jakiej wysłał je użytkownik, więc jeśli 3 osoby będą miały różne kodowania, każda zobaczy znaki z poza podstawowej tablicy ASCII w inny sposób). Może się też zdarzyć, że opisane kodowanie nie odpowiada temu, w jakim przesyłany jest dokument - na przykład gdy na stronie forum, na którym używane jest kodowanie ISO ktoś z premedytacją wpisze tekst kodowany w Windows (normalnie taka sytuacja nie będzie mieć miejsca, ponieważ domyślnie używane jest kodowanie strony).
Unicode
Problem dotyczy nie tylko Polski - chyba w każdym języku (oprócz angielskiego) istnieją znaki diakrytyczne (czyli właśnie te, które pisze się odmiennie, z różnymi akcentami jak ogonki, skreślenia, kropki, daszki…). Dlatego sytuacja przeszkadzała praktycznie wszystkim. Aby rozwiązać problem stworzono tablicę znaków jedną, wspólną dla wszystkich języków - zarówno łacińskich, jak i tych opartych o cyrylicę, pismo arabskie, hebrajskie, czy nawet języków takich jak chińskie, japońskie, czy koreańskie. Standard ten nazywa się właśnie Unicode. W odróżnieniu od poprzednio omawianych tablic znaków nie ogranicza on zapisu znaku do jednego bajta dzięki czemu do dyspozycji jest znacznie szersza przestrzeń wartości pozwalająca na pomieszczenie wszystkich znaków obecnie używanych alfabetów.
Mało tego - obejmuje także wiele znaków graficznych, symbolik (układy scalone, elektryczne, wzory matematyczne) oraz znaków interpunkcyjnych, które były pomijane w normalnych kodowaniach. Jeśli próbowaliście na przykład kiedyś wstawić czy to w edytorze tekstu, czy na przykład na stronie internetowej znaki » musieliście go wstawić jako znak specjalny (w przypadku HTMLa można to osiągnąć wstawiając encję »). W przypadku Unicodu nie ma konieczności odwoływania się do encji, wystarczy wpisać ten znak (chociaż oczywiście nie zrobimy tego tak po prostu z klawiatury). Podobnie jest z wieloma innymi znakami - wszystkie poniższe symbole zostały zapisane jako znaki Unicode, bez korzystania z encji HTML (jeśli klikniecie podgląd źródła strony zauważycie, że są one widoczne w taki sam sposób):
» « › ‹ … φ Ϣ Ж Я ▨ ✈ ✮ ❼ ➷
Unicode, a UTF
Wydawało by się, że już jest pięknie, że nie da się tego spieprzyć. Jednak pesymiści mogli śmiało obstawiać w totka. Jeśli ktoś słyszał o Unicodzie to z pewnością słyszał też o UTF. Jednak wielu ludzi uznaje te pojęcia za tożsame, co jest wielkim błędem. Otóż Unicode to tablica znaków - przypisuje znaki graficzne poszczególnym wartościom. Zastępuje wszystkie kodowania jedno-bajtowe. Warto też zaznaczyć, że pierwsze 128 znaków jest wciąż zgodne z tablicą ASCII. Jednak z jednego problemu narodził się inny - w przypadku Unicodu nie minął kłopot kodowania znaków. Tutaj co prawda mamy już jedną tablicę wartości - pozostaje jednak problem jak te znaki zapisywać w formie binarnej. Jednym z takich kodowań jest UTF - Unicode Transformation Format, innym z kolei jest GB18030 (kodowanie, które uwzględnia kompatybilność z kodowaniami języków azjatyckich), a istnieją i bardziej egzotyczne.
Ogólnie szeroko używane jest przede wszystkim kodowanie UTF, jednak wcale nie na tym koniec rozbieżności. Otóż jest kilka wariantów tego kodowania, które definiują długość słowa - w UTF-8 jest to 8 bitów (1 bajt), w UTF-16 jest to 16 bitów (2 bajty), UTF-32 32 bity (4 bajty), a oprócz tego istnieje także (nie licząc wersji, które zostały porzucone i nie warto się nimi zajmować) UTF-7, który jest najczęściej używany do przesyłania maili i wykorzystuje tylko 7 bitów (czyli tablicę ASCII, a pozostałe znaki są kodowane w specjalny sposób). Jak to możliwe zatem, żeby używać 8-bitowego kodowania do zapisu znaku zajmującego więcej niż jeden bajt? Otóż znaki mogą się składać z więcej niż jedno słowo jeśli jest taka potrzeba. Zapisując znak z wykorzystaniem większej ilości słów postępuje się według następującego schematu - ilość jedynek (ustawionych bitów) z pierwszego bajtu oznacza ilość użytych słów; w każdym bajcie dane są przechowywane za pierwszym zerem. I tak na przykład litera ą, która ma w tablicy Unicode pozycję 261 (105 szesnastkowo, 100000101 binarnie) została by zapisana w taki sposób (pogrubione fragmenty to bity, które kodują znak):
11000100 10000101
Problemy techniczne
Unicode i jego kodowania powodują wiele problemów natury technicznej. Jest to zupełnie inny sposób zapisu danych niż w przypadku starych kodowań jedno-bajtowych. Przede wszystkim ilość słów nie jest tutaj równa ilości znaków (może tak się zdarzyć jeżeli mamy dokument wykorzystujący jedynie znaki ASCII w kodowaniu UTF-8). Oznacza to, że od strony programistycznej należy używać bibliotek opracowanych do pracy z takimi kodowaniami znaków - zwykłe funkcje dostępne w wielu językach takie jak strpos(), substr(), strlen() i pokrewne nie będą działać poprawnie. Wspomniany znak ą zostałby odczytany jako łańcuch o długości 2 ponieważ zajmuje 2 bajty.
Innym problemem jest BOM. Jest to znacznik zapisany na początku danych określający format danych. W przypadku kodowań o długości słowa większej niż 1 bajt możliwe jest kodowanie little-endian i big-endian. W przypadku kodowania UTF-8 nie ma takiej możliwości - słowo ma 1 bajt, a kolejność bajtów jest jednoznaczna. Jednak BOM istnieje też w tym przypadku, jest po prostu zawsze taki sam, a z tego powodu dla UTF-8 jest on nieobowiązkowy. Okazuje się jednak, że jest on niezwykle problematyczny. Istnieją programy, ktróre nie potrafią otworzyć plików zapisanych w formacie UTF-8 jeśli nie posiadają BOM'a. Inne z kolei nie potrafią poprawnie obsłużyć tego bloku. Najprostszy przykład - Notatnik i Apache z PHP. Kiedy otworzymy plik w Notatniku niezależnie czy posiadał on BOM, czy nie, to po zapisaniu program automatycznie doda go na początku pliku. Ale jeśli wrzucimy taki plik na swój serwer WWW to PHP odczyta nagłówek BOM jako zwykłe dane i wyśle do klienta. Szczególnie uciążliwe jest to w przypadku nagłówków (przekierowania, sesje, ciasteczka) - BOM jest w takim wypadku traktowany jako treść dokumentu i nie można po jego wysłaniu wysłać nagłówków HTTP, a ponieważ BOM znajduje się na samym początku pliku w rzeczywistości po prostu uniemożliwia pracę z sesją, ciasteczkami, cachem (w rozumieniu HTTP) i wszelkimi innymi aspektami wymagającymi użycia nagłówków HTTP.
UTF-8
Tak jak wspominałem najpopularniejsze kodowanie Unicode to UTF-8. Jest to spowodowane przede wszystkim tym, że długość słowa w nim wynosi 1 bajt, a co za tym idzie dokumenty zapisane znakami ASCII są dokładnie identyczne w przypadku tego kodowania, jak i wszystkich starych kodowań jedno-bajtowych. Również w przypadku znaków z poza ASCII wielkość takiego znaku w przypadku języków europejskich wzrasta jedynie do 2 bajtów. UTF-8 to domyślne kodowanie dokumentów XML i formatach opartych na nim (w tym XHTML).
Problem natomiast pojawia się w przypadku alfabetów takich jak arabski, czy cyrylica - tutaj praktycznie każdy znak się rozrasta w wyniku czego dokumenty są dwa razy większe. Problemem jest też zmienna długość słowa (czego nie ma w przypadku kodowań UTF-32).
Wybierz UTF-8
Mimo tego, głównym celem samego Unicodu, który w zupełności spełnia jest ujednolicenie zapisu znaków. Problemem jest jedynie dobór kodowania. Jeśli nie używasz wyspecjalizowanych narzędzi narzucających dane kodowanie, jeśli nie jesteś ograniczony do 7 bitów (gdzie konieczne jest użycie UTF-7), jeśli nie masz ogromnych narzutów na operacje na łańcuchach (gdzie bardziej wydajnym może się okazać użycie bardziej obszernego kodowania UTF-16 lub UTF-32, zyskując tym pewność stałej długości znaku), jeśli piszesz głównie z użyciem alfabetu łacińskiego (niewielka ilość znaków diakrytycznych w tekście powoduje znikomy wzrost objętości), to kodowanie UTF-8 jest najodpowiedniejszym do zastosowań w nowoczesnych środowiskach, w którym ma zachodzić wymiana informacji. To odpowiedni wybór jeśli chcesz zminimalizować problemy związane z różnym kodowaniem znaków.