FAV-ZCU/KIV PPA1/19. Kódování znaků.md

19 KiB
Raw Permalink Blame History

Kodování znaků

  • Určuje, jakým „číslem“ jsou jednotlivé znaky (a řetězce složené ze znaků) reprezentovány

Základní pojmy

  • Terminologie není jednotná, liší se u velkých firem a organizací
    • Stejné pojmy se používají pro různé věci
    • Pro stejné věci existují různé pojmy
  • V dalším textu bude využita terminologie z Unicode Character Encoding Model

Organizace kódování znaků

  • Pro kódování znaků lze použít několikaúrovňovou organizaci
    • Znaková sada (množina kódovaných znaků Coded Character Set CCS)
      • Mapování mezi množinou (abstraktních) znaků a množinou nezáporných celých čísel (kódové body code points)
      • Např. A = 41 (abstraktní znak A má kódový bod 41 (hexadecimálně))
      • Příklady znakových sad jsou US-ASCII, ISO-8859-1, Unicode
    • Forma kódování znaků (Character Encoding Form CEF)
      • Mapování množiny nezáporných celých čísel (prvků CCS) na množinu kódových jednotek dané šířky (např. 32bitových hodnot int)
      • Např. A = 00 00 00 41 (znak A namapovaný na 32bitový int)
    • Kódovací schéma (Character Encoding Scheme CES)
      • Alternativní název charset budeme používat dále, odpovídá třídě v Javě java.nio.charset.Charset
    • Způsob mapování kódových jednotek z CEF do posloupnosti bytů
    • Např. A = 00 41 (znak A pro znakovou sadu Unicode a kódovací schéma UTF-16BE)
    • Pro jednu znakovou sadu může existovat více charsetů
      • Např. pro znakovou sadu Unicode existují charsety UTF-8, UTF16 a UTF-32
    • Často ale pro jednu znakovou sadu existuje jen jeden charset
      • Např. ISO-8859-2 ěžně je jeden soubor napsán v jednom charsetu
  • Teoreticky je ale možné, aby byl jeden soubor současně napsán
    • Ve více znakových sadách (např. Unicode, ISO-8859-2)
    • Za použití více charsetů (UTF-8, ISO-8859-2)
    • Kombinací obou předchozích nejhorší případ
  • Různé soubory jsou běžně v různých charsetech

Trocha historie

  • Sedmibitový ASCII kód (US-ASCII)
    • American Standard Code for Information Interchage
    • Základem většiny kódování používaných v současnosti (alespoň v Evropě a Americe)
    • Protože data se standardně ukládají po bytech, je osmý (nejdůležitější) bit (Most Significant Bit MSB ten nejvíc vlevo) vždy 0
    • Umožňuje uložit 128 znaků
      • Stačí pouze pro čistě anglické texty
    • Nestačí pro uložení dalších znaků jiných jazyků (např. diakritické znaky češtiny) používajících latinku a už vůbec ne pro uložení znaků jiných abeced
  • 8bitové kódy založené na ASCII
    • Použila se zbývající polovina rozsahu jednoho bytu (MSB roven 1), což dává dalších 128 znaků
    • Protože jeden znak je jeden byte, není třeba používat speciální charset (kódovací schéma)
      • Znaková sada má stejný název jako charset
    • 128 znaků navíc ale nestačí pro požadavky všech jazyků (ani pro všechny jazyky používající latinku) najednou
      • Vzniklo mnoho charsetů, každý reprezentující specifické požadavky jednoho či skupiny jazyků)
      • I pro jeden jazyk vzniklo více (různých) charsetů
        • Vznikají charsety dle normy ISO ISO-8859-1 až ISO-8859-15 (pro různé skupiny jazyků)
        • Vznikají charsety v národních standardizačních organizacích
        • Vznikají propietární charsety, které jsou platformově závislé (Cp1250 pro Windows, MacCentralEurope)
  • 16bitový CEF
    • Řeší problém s nedostatečným počtem znaků použitím 16 bitů (2 bytů) pro jeden znak => to dává až 65536 možných znaků
    • Na vývoji pracují cca od roku 1990 paralelně dvě organizace
      • Unicode Consortium
        • Znaková sada Unicode
      • ISO (International Organization for Standardization)
        • Znaková sada ISO/EIC 10646, ve zkratce UCS (Universal Character Set)
      • Pro běžné použití netřeba rozlišovat, jednotlivé znaky mají stejné kódové body
  • 32bitový CEF
    • Při zahrnutí ideografických písem (typicky asijská písma) však ani 65536 znaků není dostatečné množství
    • Řeší se použitím 32 bitů (4 bytů) pro jeden znak => to dává teoreticky přes 4 × 10**9 možných znaků, kdy se však nevyužívá celý rozsah

Problém pojmenování charsetů

  • Je běžné, že jeden charset má několik jmen, které se od sebe částečně nebo zcela liší
    • Způsobeno tím, že charsety nově pojmenovávají i různí výrobci (HW a/nebo SW), i když už jméno charsetu existuje
    • Např. US-ASCII má 14 dalších evidovaných jmen
      • ISO646-US, IBM367, ASCII, cp376, default, ascii7, ANSI_X3.4-1986, iso-ir-6, us, 646, iso_646.irv:1983, csASCII, ANSI_X3.4-1968, ISO_646.irv:1991
  • Pořádek zavádí IANA (Internet Assigned Numbers Authority)
  • Rozlišuje se základní pojmenování (nejoficiálnější), tzv. kanonické jméno, a ostatní evidovaná jména, tzv. aliasy
    • Např. US-ASCII je kanonické jméno a iso-ir-6 je jeho evidovaný alias
  • Může se stát, že jméno charsetu není evidováno v IANA, ale charset je podporován některými aplikacemi
    • Pak se používá stejný princip kanonického jména a aliasů, ale kanonické jméno musí začínat „x-“ nebo „X-“
    • Např. Java Core API podporuje charset x-MacCentralEurope s aliasem MacCentralEurope

Jednobytové kódy

  • Ačkoliv se čím dál více užívá Unicode, stále se můžeme setkat s použitím jednobytových kódu
    • Především historické soubory, které (dosud) nebyly převedeny do Unicode

US-ASCII (ASCII)

  • Většina (nejen) jednobytových kódů vychází ze US-ASCII
  • Původně využito pouze 7 bitů
    • 001F (0 až 31) řídící znaky (např. <CR>, <LF> atd.)
    • 20 (32) mezera
    • 212F (33 až 47) interpunkce (např. „!“, „,“, „"“ atd.)
    • 3039 (48 až 57) číslice „0“ až „9“
    • 3A40 (58 až 64) další znaky (např. „;“, „:“, „<“, „@“ atd.)
    • 415A (65 až 90) velká písmena „A“ až „Z“
    • 5B60 (91 až 96) další znaky (např. „^“, „[“, atd.)
    • 617A (91 až 122) malá písmena „a“ až „z“
    • 7B7F (123 až 127) další znaky (např. „{“, „~“, „|“ atd.)
  • I v anglicky mluvících zemích se začalo využívat dalších 128 znaků (MSB roven 1), ač nebyly potřeba pro běžné znaky anglické abecedy
    • Využití mj. pro znaky s čárami umožňující vykreslení „grafických“ oken v textovém prostředí

Různá jednobytová kódování češtiny

  • Stejně jako pro mnoho jiných jazyků i pro češtinu vznikl jednobytový kód vycházející z ASCII
    • Prvních 128 znaků stejných jako US-ASCII (MSB roven 0)
    • Dalších 128 znaků použito pro diakritické a další znaky (MSB roven 1)
  • Bohužel vzniklo hned několik kódů, které se vzájemně liší pozicí některých znaků s diakritikou
    • Běžněji se vyskytujících je cca 11
    • Pokud předpokládáme, že soubor je uložen v jednom kódování a ve skutečnosti je uložen v jiném, některé znaky s diakritikou nebudou zobrazeny správně (tj. budou místo nich zobrazeny jiné znaky)
    • Některá kódování se liší jen v několika málo znacích, proto je možné si nesrovnalostí na první pohled nevšimnout
  • Jednobytová kódování pro češtinu (a slovenštinu a další středo a východoevropské jazyky), se kterými je možné se setkat
    • ISO-8859-2 Latin Alphabet No. 2
      • Aliasy ibm912, l2, ibm-912, ISO_8859-2, latin2, csISOLatin2, iso8859_2, 912, 8859_2, ISO8859-2, iso-ir-101
      • Základní charset pro východoevropské země mezinárodní standard dle ISO
      • Dříve se používalo na Linuxu téměř výhradně (dnes na Linuxu většinou nahrazeno UTF-8)
    • windows-1250 Windows Eastern European
      • Aliasy cp1250, cp5346
      • Proprietární charset firmy Microsoft
      • Podporován operačními systémy (Windows) a aplikacemi této firmy
      • Od ISO-8859-2 se v češtině liší pouze ve znacích „š“, „Š“, „ž“, „Ž“, „ť“, „Ť“
    • IBM852 MS-DOS Latin-2 (POZOR! Liší se od ISO-8859-2 ale i od windows1250)
      • Aliasy 852, ibm-852, csPCp852, ibm852
      • Proprietární charset firmy IBM
      • Používaný charset v českém MS-DOS
      • Stále používaný implicitní charset v konzoli českých Windows
        • Obzvláště bizardní situace, kdy konzole používá IBM852 a zbytek systému používá windows-1250
        • Důvod, proč mohou nastat problémy s diakritickými znaky při jejich vstupu/výstupu z/do konzole
          • Java předpokládá na standardním vstupu/výstupu charset operačního systému (což je windows-1250), ale konzole používá IBM852
          • Od verze Java 1.8 zlepšení, ale stále nefunguje univerzálně
    • x-MacCentralEurope Macintosh Latin-2
      • Alias MacCentralEurope
      • Proprietární charset firmy Apple

Unicode

  • Řeší problému s nedostatkem znaků použitím více bitů (bytů)
    • Původně (verze 1.0, 1991) 16 bitů (2 byty) až 65536 znaků
    • Brzy se ukázalo (verze 2.0, 1996), že 16 bitů není dost a přešlo se na 32 bitů teoreticky přes 4 × 109 možných znaků, ale v současnosti se neplánuje využití více než 21 bitů
    • Prvních 128 znaků mají stejné kódové body (code points) jako znaky v 7bitové US-ASCII
  • Kódové body (code points) jednotlivých znaků se označují jako U+hexaČíslo
    • hexaČíslo jsou typicky 4 hexadecimální číslice, může jich být až 6 (vzhledem k uvažovanému rozsahu maximálně 21 bitů každé 2 číslice reprezentují 1 byte)
    • Např. U+0041 je znak „A odpovídá zápisu \u0041 v Javě

Současné rozdělení rozsahu znaků Unicode

  • Ze 32 bitů se využívá pouze 21 bitů
    • Konkrétně hodnoty U+000000U+10FFFF
  • Tento rozsah je rozdělen na 17 skupin (sfér planes), každá o velikosti 65536 znaků => celkem přes 10**6 znaků
    • Původní sada znaků, která se vejde do 16 bitů (2 byty) se označuje jako BMP (Basic Multilingual Plane)
      • Je první v pořadí, rozsah U+000000U+00FFFF
      • Zahrnuje všechny znaky používané v Evropě a Americe a základní ideografická písma čínštiny, japonštiny a korejštiny (HAN písmo)
      • Samotné BMP je vnitřně děleno do bloků, které (na rozdíl od sfér) nemají konstantní velikost
        • Např. ASCII (rozsah U+000000U+00007F) je tzv. Basic Latin Block
    • Dalších 16 sfér v rozsahu U+010000U+10FFFF jsou tzv. doplňkové sféry (supplementary planes), které se v oblasti střední Evropy téměř nikdy nepoužívají
      • V současnosti (2022 Unicode verze 15.0) má pět doplňkových sfér přiřazeny znaky a celkem šest sfér je pojmenováno
    • V současnosti (2022 Unicode verze 15.0) je namapováno (tj. kódovým bodům jsou přiřazeny znaky) 149697 znaků
      • Unicode verze 1.0.1 měl 28327 znaků

Kódovací schémata (charsety) Unicode

  • Protože znak může být uložen na více bytech, je možné používat více charsetů (kódovacích) schémat
  • Unicode má tři základní charsety UTF (Unicode Transformation Format), přičemž dva z nich mají další varianty celkem 7 charsetů
    • UTF-8
    • UTF-16
      • Další varianty UTF-16BE a UTF-16LE přesně specifikující pořadí uložení bytů
    • UTF-32
      • Další varianty UTF-32BE a UTF-32LE přesně specifikující pořadí uložení bytů
    • Všechny charsety jsou schopny uložit celý rozsah Unicode (21 bitů)
    • Jednotlivé charsety jsou popsány v Kap. 28.3.4 až 28.3.6

Problém pořadí bytů, značka bytového pořadí

  • Pokud ukládáme do paměti či souboru vícebytové entity (v tomto případě znaky), je potřeba rozlišit pořadí bytů
    • Např. pokud uvažujeme uložení znaku „A“ (U+0041) na dvou bytech, může být uložen jako
      • Little Endian (LE „obrácené uložení“) 41 00
      • Big Endian (BE „přirozené uložení“) 00 41
    • Pokud uvažujeme uložení znaku „A“ na čtyřech bytech, může být uložen jako
      • Little Endian 41 00 00 00
      • Big Endian 00 00 00 41
  • Způsob ukládání závisí na platformě (Windows LE), programovacím jazyku (Java vždy BE), aplikaci atd.
  • Jaký způsob je použit, je důležité při čtení souboru pro správné načtení vícebytových znaků
  • Charsety Unicode mohou pro identifikaci používat počáteční značku bytového pořadí
    • Byte Order Mark (BOM)
    • Zapisuje se na úplný začátek souboru
    • Pro tento účel Unicode definuje dva kódové body
      • U+FEFF pevná mezera nulové délky (zero width no-break space)
      • U+FFFE není kód znaku (not a character code)
    • Pro UTF-16 má BOM tvar FE FF pro BE a FF FE pro LE
      • Pokud je značka načtena správně, pak by se pevná mezera nulové délky neměla ze své podstaty zobrazit
      • Pokud je načtena nesprávně (zamění se BE za LE nebo naopak), opět by se neměla zobrazit, protože se jedná o neplatný znak
    • BOM se může nebo nesmí vyskytovat, což je dáno definicí konkrétního charsetu
    • U UTF-8 má BOM tvar EF BB BF
      • Není vyžadována ani doporučována, nicméně není zakázána
      • Může nastat problém se zdrojovými soubory .java, které mohou být uloženy v UTF-8
      • Překladač javac nepředpokládá na začátku souboru BOM (i když není zakázáno, aby tam byla), některé editory ji tam však umístí program pak nelze přeložit

UTF-8

  • Bylo vytvořeno, aby se znaky Unicode daly zakódovat posloupností bytů, se kterými umí pracovat každá aplikace a každý souborový systém
  • Obecně rozšířený a používaný charset
    • Např. řetězce v .class souborech jsou uloženy v UTF-8
    • Ze zmíněných sedmi charsetů Unicode se UTF-8 používá v Evropě a v Americe pravděpodobně nejčastěji
  • Pro texty využívající pouze znaky anglické abecedy je UTF-8 totožné s US-ASCII
    • Využívá se jen jeden byte na jeden znak
    • Soubory tak zabírají stejně místa, jako kdyby byly kódovány v US-ASCII
  • Pro diakritické znaky se využívají dva byty, pro speciálnější znaky z BMP tři byty
    • Protože diakritických znaků je např. v českém textu cca 10 %, velikost souboru s českým textem naroste oproti použití jednobytového kódování (např. windows-1250) pouze o cca 10 %
  • Pro znaky mimo BMP se využívají čtyři byty
  • Základní nevýhoda UTF-8
    • Znaky obecně nemají stejnou délku => není možné skočit přímo na určitý znak
      • „Přeskoč prvních 20 znaků“
  • Princip kódování znaků v UTF-8
    • Aby bylo jasné, zda daný znak je uložen jako 1, 2, 3 nebo 4 byty, používá se MSB
    • Principiálně je možné zakódovat pomocí UTF až 31 bitů
      • 1111 110u 10vv vvvv 10ww www 10xx xxxx 10yy yyyy 10zz zzzz
  • Princip čtení UTF-8
    • Pokud má byte nastaveno MSB na 1, pak počet jedničkových bitů za ním udává počet následujících bytů za prvním bytem znaku, přičemž každý následující byte začíná 10 (viz Tab. 28.2)
    • Pokud nečteme text od začátku a „trefíme“ se doprostřed vícebytového znaku, poznáme to podle bitů 10 => pak je třeba přeskočit všechny byty začínající 10
  • V UTF-8 je zbytečná BOM je jen jedno možné pořadí bytů
    • Podle specifikace není ani vyžadována ani doporučena, není však zakázána
    • Některé aplikace však BOM u UTF-8 vyžadují a některé s ní naopak mají problémy

UTF-16

  • Vychází z UCS-2, což je kódování ISO s pevnou šířkou 2 byty na znak, což pokryje celou BMP
  • Protože však Unicode přešel na 21 bitů, 2 byty (16 bitů) nestačí (pro znaky mimo BMP)
  • Proto nastupuje UTF-16, které některé znaky kóduje 4 byty (tj. dvěma znaky UCS-2)
    • Oba „znaky“ se dohromady nazývají zástupné páry (surrogate pairs)
    • Mezi UCS-2 a UTF-16 je podobný vztah jako mezi ASCII a UTF-8
  • UTF-16 se využívá pro uložení řetězců v operační paměti v Javě
  • Při uložení UTF-16 do souborů se využívá BOM pro určení pořadí uložení bytů (LE nebo BE)
    • Varianty UTF-16LE a UTF-16BE mají pořadí uložení bytů přímo určeno a BOM nesmí obsahovat
      • Pokud BOM přesto obsahují, je ignorována
  • Oproti UTF-8 zabírá pro běžné texty psané latinkou téměř dvojnásobek místa
    • Pro texty psané pouze znaky anglické abecedy přesně dvojnásobek

UTF-32

  • Kódování s pevnou šířkou 4 byty na znak
    • Každý znak je uložen jako 4 byty
    • Na 4 bytech jsou tak přímo uloženy kódové body Unicode
    • Pouze 21 bitů je významových
    • Prakticky odpovídá UCS-4 (kódování ISO, rovněž 4 byty na znak)
  • Při uložení UTF-32 do souboru se rovněž využívá BOM pro určení pořadí uloženy bytů (LE nebo BE)
    • Varianty UTF-32LE a UTF-32BE mají pořadí uložení bytů přímo určeno a BOM nesmí obsahovat
      • Pokud ho přesto obsahují, je ignorován

Praktické použití v Javě

  • Java vnitřně ukládá řetězce do paměti jako UTF-16, většina souborů je však uložena v UTF-8 nebo v různých jednobytových charsetech
    • Při čtení a zápisu řetězců z/do souborů (i na standardní vstup a výstup) je tedy nutná konverze

Nastavení charsetu při čtení a zápisu z/do textových souborů

  • Konverzi provádějí třídy pro práci se soubory, jejichž název končí …Reader/Writer
    • Jsou určeny pro práci se znaky
  • Charset vstupního/výstupního souboru lze popsat instancí třídy Charset
    • Metoda třídy Charset.forName(kódování)
      • Vrátí instanci třídy Charset reprezentující daný charset na základě jeho jména
      • Lze použít kanonické jméno i aliasy
    • Metody z třídy Files pro čtení a zápis z/do souboru umožňují zadat charset souboru jako instanci třídy Charset
      • Metoda Files.readAllLines(soubor, charset)
      • Metoda Files.newBufferedReader(soubor, charset)
      • Metoda Files.newBufferedWriter(soubor, charset)

Správné zobrazení češtiny v konzoli Windows

  • Od verze Javy 1.8 se čeština v konzoli Windows někdy zobrazuje správně, někdy však stále chybně
    • Správně se zobrazují literály s diakritickými znaky zapsané přímo ve zdrojovém kódu
    • Špatně se zobrazují řetězce načtené ze standardního vstupu a vypsané na standardní výstup
    • Do verze Javy 1.7 včetně se zobrazovaly špatně i literály zapsané přímo ve zdrojovém kódu
  • Problém je způsoben rozdílným standardním charsetem Windows (windows-1250) a konzole (implicitně IBM852 lze změnit)
    • Tyto charsety nejsou totožné (liší se v některých znacích)
    • Java tak očekává na standardním vstupu a výstupu charset windows-1250, ale v konzoli je IBM852
  • Korektní zobrazení v Java programech v konzoli Windows lze zařídit nastavením odpovídajícího charsetu pro standardní vstup a standardní výstup
    • Pro standardní vstup stačí nastavit charset v konstruktoru třídy Scanner
      • Zadává se pouze název charsetu jako řetězec
    • Standardní výstup funguje od Javy 1.8 korektně
  • POZOR!
    • Problémy se týkají pouze konzole (příkazové řádky) Windows
    • Konzole IDE nástrojů (např. Eclipse) přebírají charset Windows (jsou to grafická okna) a čeština v nich funguje správně