Kodowanie tekstu

Od litery do bajtu — od ASCII przez UTF-8 po Base64

15 lip 2026·10 min czytania·1780 słów
Świecące szmaragdowe litery rozpadające się na strumienie bitów 0/1, par szesnastkowych i sekwencji Base64, na tle kosmicznej mgławicy w fiolecie i magencie

Komputer nie zna liter. Zna tylko napięcie — jest albo go nie ma — które umawiamy się czytać jako zero i jedynkę. Wszystko, co widzisz na tym ekranie, musiało więc najpierw zostać zamienione na liczby, a liczby na ciągi bitów. Litera „A" to w tej układance liczba 65. Ale polskie „ł" to już dwa bajty, symbol euro „€" — trzy, a emoji rakiety „🚀" — aż cztery. Ta opowieść jest o tym, jak i dlaczego pismo staje się bajtami, oraz o pułapkach, które po drodze potrafią zepsuć tekst albo otworzyć serwer na atak.

Dlaczego tekst to bajty

Najmniejsza porcja informacji, bit, przechowuje jeden z dwóch stanów. Osiem bitów grupujemy w bajt, który mieści jedną z 2⁸ = 256 wartości — od 0 do 255. Żeby maszyna mogła zapisać, przesłać i wyświetlić ludzki tekst, ktoś musiał ustalić reguły przypisujące znakom konkretne liczby. Taki zestaw reguł nazywamy kodowaniem.

Warto od razu rozdzielić dwie warstwy, bo mylenie ich jest źródłem większości nieporozumień. Sam wybór kodowania jest umowny — decyzja, że „A" to akurat 65, a nie 41, wynika z historii i kompromisów, nie z żadnego prawa natury. Ale kiedy standard już przyjmiemy, zamiana znaku na bity staje się procesem ściśle deterministycznym: przesunięcia bitów i operacje logiczne nie zostawiają miejsca na interpretację. Umowa jest arbitralna, wykonanie umowy — matematyczne.

Zanim był komputer: pięć bitów Baudota

Problem jest starszy od tranzystora. Już telegrafy z XIX wieku musiały reprezentować litery sygnałami. Wynaleziony w latach 70. XIX wieku kod Baudota (w ustandaryzowanej wersji znany jako ITA2) był zapisem pięciobitowym, co daje zaledwie 2⁵ = 32 kombinacje — za mało, by pomieścić litery, cyfry i interpunkcję naraz. Émile Baudot obszedł ten limit rejestrami: specjalny kod „liter" i kod „cyfr" przełączał znaczenie kolejnych sekwencji, trochę jak klawisz Shift.

Rozwiązanie miało wadę, która wróci do nas przy UTF-8. Jeśli zakłócenie na linii zgubiło znak przełączający rejestr, cała reszta wiadomości zamieniała się w bełkot, aż do następnego kodu sterującego. Pojedynczy błąd kaskadował na cały dalszy tekst.

ASCII: siedem bitów, które wystarczały (tylko) Ameryce

W 1963 roku powstał ASCII — kodowanie siedmiobitowe, a więc 2⁷ = 128 pozycji, uporządkowanych w czytelne przedziały:

  • 0–31 oraz 127 — znaki sterujące (Enter, tabulator, Backspace), pozostałość po sterowaniu dalekopisami;
  • 32 — spacja;
  • 48–57 — cyfry 09;
  • 65–90 — wielkie litery AZ;
  • 97–122 — małe litery az.

Ten porządek nie jest przypadkowy i do dziś ułatwia życie programistom: skoro A to 65, a a to 97, różnica wielkości liter to zawsze te same 32. ASCII miało jednak wbudowany defekt — było amerykańskie. W 128 pozycjach nie zmieściło się miejsce na „ł", „ä" czy „ń".

Kiedy komputery rozeszły się po świecie, każdy kraj dorabiał własne rozwiązanie, rozszerzając kod do pełnych 8 bitów (256 znaków) i upychając lokalne litery w górnej połowie. Problem w tym, że robiono to niezależnie: powstały dziesiątki niekompatybilnych „stron kodowych". W Europie Środkowej rywalizowały ISO-8859-2 i windowsowa Windows-1250, a w polskim DOS-ie krążyła jeszcze rodzima Mazovia — z lat 80., stworzona dla krajowego komputera o tej samej nazwie. Umieszczono w niej nawet ligaturę „zł", której międzynarodowy Unicode nigdy nie przejął, przez co konwersja archiwalnych dokumentów potrafi do dziś wymagać ręcznej poprawki.

Gdy plik zapisany jedną stroną kodową otwierano w programie zakładającym inną, litery zamieniały się w losowe symbole. To zjawisko ma fachową nazwę mojibake, a w Polsce swojską — „krzaczki". Każdy, kto w latach 90. dostał e-mail pełen ¹æê³, wie, o czym mowa.

Unicode: jeden katalog dla wszystkich pism

Wyjściem z tego chaosu był Unicode — projekt, który postanowił nadać każdemu znakowi każdego pisma świata jeden, niepowtarzalny numer. Ten numer to punkt kodowy (ang. code point), zapisywany szesnastkowo z przedrostkiem U+ — na przykład U+0142 to litera „ł", a U+20AC to „€".

Kluczowa subtelność: punkt kodowy nie jest bajtem. To abstrakcyjny identyfikator, oderwany od sposobu zapisu w pamięci. Unicode mówi „ł ma numer 322", ale milczy o tym, jak zapisać 322 w pliku. Tę robotę biorą na siebie osobne formaty — UTF-8, UTF-16 i UTF-32 — które dopiero przekładają punkty kodowe na konkretne bajty. Można powiedzieć, że Unicode prowadzi katalog, a formaty UTF decydują o opakowaniu.

UTF-8: zmienna długość i samo-synchronizacja

Wśród nich wygrał UTF-8, zaprojektowany we wrześniu 1992 roku przez Kena Thompsona i Roba Pike'a z Bell Labs. Jego genialność polega na tym, że jest kodowaniem o zmiennej długości: znak zajmuje od 1 do 4 bajtów, w zależności od tego, jak wysoki jest jego punkt kodowy. Bajty rozdziela się według sztywnych szablonów:

BajtyZakres punktów kodowychSzablon bitowyBity na dane
1U+0000U+007F0xxxxxxx7
2U+0080U+07FF110xxxxx 10xxxxxx11
3U+0800U+FFFF1110xxxx 10xxxxxx 10xxxxxx16
4U+10000U+10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx21

Z tej tabeli płyną dwie wspaniałe konsekwencje. Po pierwsze, pełna zgodność z ASCII: każdy znak z zakresu U+0000U+007F zapisuje się jednym bajtem zaczynającym się od zera, dokładnie tak jak w ASCII. Stary plik ASCII jest binarnie identyczny z plikiem UTF-8 — nie trzeba niczego konwertować.

Po drugie — i tu UTF-8 rozwiązuje odwieczny problem Baudota — kodowanie potrafi się samo synchronizować. Każdy bajt „kontynuacji" zaczyna się od bitów 10, a żaden bajt startowy tak nie wygląda (te zaczynają się od 0, 110, 1110 lub 11110). Dzięki temu dekoder, który wskoczy w środek strumienia albo zgubi kawałek danych, natychmiast pozna, gdzie kończy się jeden znak, a zaczyna następny. Utrata jednego bajtu psuje jeden znak, a nie — jak w Baudocie czy UTF-16 — całą resztę tekstu.

Trzy litery, trzy długości

Zobaczmy tę mechanikę na żywo. Wszystkie trzy przykłady możesz sprawdzić w konwerterze kodowań tekstu, który pokazuje bajty w hex, dziesiętnie i binarnie naraz.

Polskie „ł" (U+0142, dziesiętnie 322). Wartość mieści się w przedziale dwubajtowym. Zapisujemy 322 na 11 bitach: 00101 000010. Pięć starszych bitów wpada do szablonu pierwszego bajtu, sześć młodszych do drugiego:

110+0010111000101 = 0xC5
10+00001010000010 = 0x82

Litera „ł" to więc bajty C5 82.

Euro „€" (U+20AC, dziesiętnie 8364) wchodzi w przedział trzybajtowy: 0010 000010 101100 rozkłada się na E2 82 AC.

Rakieta „🚀" (U+1F680, dziesiętnie 128 640) leży poza podstawową płaszczyzną Unicode i wymaga czterech bajtów: F0 9F 9A 80.

Ta arytmetyka jest bezwzględna — nie ma tu żadnego zaokrąglenia ani miejsca na „mniej więcej". Emoji zajmuje cztery bajty nie z kaprysu, tylko dlatego, że jego numer nie mieści się w krótszym szablonie.

Base64: bajty, które przejdą przez kanał tekstowy

Skoro potrafimy już zamienić tekst na bajty, pojawia się odwrotny kłopot: jak przesłać surowe bajty kanałem, który akceptuje wyłącznie zwykły tekst? Tak działa poczta e-mail (standard MIME) czy część nagłówków HTTP — bajt o wartości sterującej mógłby zostać wzięty za koniec wiadomości. Rozwiązaniem jest Base64.

Pomysł opiera się na potęgach dwójki. Ponieważ 2⁶ = 64, jeden znak Base64 przenosi dokładnie 6 bitów. Algorytm bierze bajty po trzy (3 × 8 = 24 bity) i tnie je na cztery grupy po 6 bitów, a każdą grupę zamienia na jeden z 64 bezpiecznych znaków: AZ, az, 09 oraz + i /. Ponieważ z 3 bajtów zawsze robią się 4 znaki, Base64 powiększa dane o stałe ⅓ (≈ 33%). Gdy na końcu braknie do pełnej trójki, puste miejsca uzupełnia znak wypełnienia =: litera „a" koduje się jako YQ==, a dwubajtowe „ł" jako xYI=.

Tu pada najważniejsze zdanie tego tekstu: Base64 to nie szyfrowanie. Algorytm jest publiczny i w pełni odwracalny bez żadnego klucza — każdy pośrednik odczyta zawartość w ułamku sekundy. Base64 zmienia tylko reprezentację danych, nie ich poufność. Hasło „zabezpieczone" Base64 w nagłówku HTTP jest tak samo jawne jak wypisane wprost.

Istnieje jeszcze odmiana URL-safe: ponieważ + i / mają w adresach własne znaczenie, zastępuje się je przez - i _, a wypełnienie = zwykle się pomija.

Kodowanie URL: procenty w adresie

Adres URL też może się składać wyłącznie z wąskiego zestawu znaków ASCII. Litery, cyfry i garść znaków „bezpiecznych" (-, ., _, ~) wolno wpisać wprost; całą resztę trzeba zapisać jako sekwencję procentową — znak % i dwie cyfry szesnastkowe wartości bajtu (w UTF-8). Dlatego spacja staje się %20, a sam znak procentu — %25. To właśnie robi konwerter, gdy wybierzesz format URL: Cześć świecie zamienia na Cze%C5%9B%C4%87%20%C5%9Bwiecie.

Jest tu klasyczna pułapka. W adresach strona %20 oznacza spację, ale w danych formularzy HTML (application/x-www-form-urlencoded) spację koduje się jako +. Ta drobna niespójność między dwoma standardami potrafi wysypać podpisy cyfrowe i uwierzytelnianie, gdy jedna biblioteka użyje +, a druga oczekuje %20.

Gdy kodowanie staje się luką

Na koniec przestroga, bo kodowanie bywa mylone z rzeczami, którymi nie jest. Nie jest kompresją — Base64 i kodowanie URL zawsze powiększają dane. Nie jest szyfrowaniem — o czym już wiemy. A źle zaimplementowane, bywa luką bezpieczeństwa.

Klasyczny przykład to nadmiarowe kodowanie UTF-8 (overlong). Ukośnik / ma numer U+002F i zgodnie ze specyfikacją musi być zapisany jednym bajtem 0x2F. Ale gdyby napastnik złośliwie wcisnął go w szablon dwubajtowy, dostałby C0 AF — i naiwny, niezgodny ze standardem dekoder i tak odczytałby z tego ukośnik. Filtry bezpieczeństwa sprawdzające, czy w żądaniu nie ma ciągu ../, przepuszczały wersję zakodowaną ..%C0%AF, a serwer rozwijał ją z powrotem do ../ — i pozwalał czytać pliki spoza katalogu strony. Ta podatność (masowo wykorzystywana m.in. na serwerach IIS) jest powodem, dla którego dzisiejsze specyfikacje nakazują odrzucać sekwencje overlong jako błąd, a nie grzecznie je „naprawiać".

Tabela reprezentacji

ZnakPunkt kodowyUTF-8 (hex)UTF-8 (binarnie)Base64
aU+00616101100001YQ==
łU+0142C5 8211000101 10000010xYI=
U+20ACE2 82 AC11100010 10000010 101011004oKs
🚀U+1F680F0 9F 9A 8011110000 10011111 10011010 100000008J+agA==

Te same cztery znaki, a każda kolumna to inna umowa o tym, jak zapisać dokładnie tę samą treść.

Jedna treść, wiele ubrań

Historia kodowania tekstu to opowieść o oddzielaniu znaczenia od zapisu. Litera „ł" jest jedna, ale jej numer (U+0142), jej bajty (C5 82), jej Base64 (xYI=) i jej postać w adresie (%C5%82) to cztery różne ubrania tej samej idei. Wybór ubrania jest umowny; sama zamiana — matematyczna i bezstratna, dopóki nadawca i odbiorca umówią się, w którym standardzie właśnie rozmawiają. Gdy się nie umówią, dostajemy krzaczki. Gdy dekoder jest zbyt pobłażliwy — dostajemy dziurę w serwerze. A gdy wszystko zagra, tekst po prostu dociera na drugi koniec świata dokładnie taki, jaki był.

Dalsza lektura

Wypróbuj

Konwerter — Kodowania tekstu

Otwórz konwerter