Sprievodca kódovaním znakov

1. Prehľad

V tomto výučbe sa budeme zaoberať základmi kódovania znakov a tým, ako s nimi narábame v prostredí Java.

2. Dôležitosť kódovania znakov

S textami patriacimi do viacerých jazykov musíme často narábať pomocou rôznych písacích písiem, ako sú latinka alebo arabčina. Každý znak v každom jazyku musí byť nejako namapovaný na množinu jednotiek a núl. Naozaj sa čudujeme, že počítače dokážu správne spracovať všetky naše jazyky.

Ak to chcete urobiť správne, musíme premýšľať o kódovaní znakov. Ak to neurobíte, môže to často viesť k strate údajov alebo dokonca k bezpečnostným zraniteľnostiam.

Aby sme tomu lepšie porozumeli, definujme si metódu na dekódovanie textu v Jave:

String decodeText (vstup reťazca, kódovanie reťazca) vyvolá IOException {návrat nový BufferedReader (nový InputStreamReader (nový ByteArrayInputStream (input.getBytes ()), Charset.forName (kódovanie))) .readLine (); }

Upozorňujeme, že vstupný text, ktorý tu vkladáme, používa predvolené kódovanie platformy.

Ak spustíme túto metódu s vstup „Fasádny vzor je softvérový dizajnový vzor.“ a kódovanie ako „US-ASCII“, zobrazí sa:

Vzor fasády je vzor návrhu softvéru.

No nie presne to, čo sme očakávali.

Čo sa mohlo pokaziť? Pokúsime sa to pochopiť a napraviť vo zvyšku tohto tutoriálu.

3. Základy

Predtým, ako sa pustíme do hĺbky, si rýchlo prečítajme tri výrazy: kódovanie, znakové sadya kódový bod.

3.1. Kódovanie

Počítače dokážu pochopiť iba binárne reprezentácie ako 1 a 0. Spracovanie čohokoľvek iného si vyžaduje určitý druh mapovania od textu v reálnom svete po jeho binárne vyjadrenie. Toto mapovanie poznáme ako kódovanie znakov alebo jednoducho rovnako kódovanie.

Napríklad prvé písmeno v našej správe „T“ v jazyku US-ASCII kóduje na „01010100“.

3.2. Charsety

Mapovanie znakov na ich binárne zobrazenia sa môže veľmi líšiť, pokiaľ ide o znaky, ktoré obsahujú. Počet znakov zahrnutých do mapovania sa môže v praxi líšiť od niekoľkých znakov po všetky znaky. Sada znakov, ktoré sú zahrnuté v definícii mapovania, sa formálne nazýva a znaková sada.

Napríklad ASCII má znakovú sadu 128 znakov.

3.3. Kódový bod

Bod kódu je abstrakcia, ktorá oddeľuje znak od jeho skutočného kódovania. A kódový bod je celočíselný odkaz na konkrétny znak.

Samotné celé číslo môžeme reprezentovať v obyčajných desatinných alebo alternatívnych základoch ako hexadecimálne alebo osmičkové. Pre uľahčenie odkazovania na veľké čísla používame alternatívne základne.

Napríklad prvé písmeno v našej správe, T, v Unicode má bod kódu „U + 0054“ (alebo 84 v desatinnej čiarke).

4. Porozumenie schém kódovania

Kódovanie znakov môže mať rôzne podoby v závislosti od počtu znakov, ktoré kóduje.

Počet kódovaných znakov má priamy vzťah k dĺžke každej reprezentácie, ktorá sa zvyčajne meria ako počet bajtov. Mať viac znakov na kódovanie v podstate znamená potrebovať zdĺhavejšie binárne reprezentácie.

Prejdime si dnes niektoré z populárnych kódovacích schém v praxi.

4.1. Jednobajtové kódovanie

Jedna z najskorších schém kódovania s názvom ASCII (American Standard Code for Information Exchange) používa schému kódovania jedného bajtu. To to v podstate znamená každý znak v ASCII je reprezentovaný sedembitovými binárnymi číslami. To stále ponecháva jeden bit voľný v každom bajte!

Sada 128 znakov v ASCII pokrýva anglické abecedy malými a veľkými písmenami, číslice a niektoré špeciálne a ovládacie znaky.

Definujme v Jave jednoduchú metódu na zobrazenie binárnej reprezentácie znaku v konkrétnej schéme kódovania:

Reťazec convertToBinary (vstup reťazca, kódovanie reťazca) vyvolá UnsupportedEncodingException {byte [] encoded_input = Charset.forName (kódovanie). Encode (vstup) .array (); návrat IntStream.range (0, encoded_input.length) .map (i -> encoded_input [i]) .mapToObj (e -> Integer.toBinaryString (e ^ 255)) .map (e -> String.format ("% 1 $ "+ Byte.SIZE +" s ", e) .replace (" "," 0 ")) .collect (Collectors.joining (" ")); }

Teraz má znak „T“ kódový bod 84 v US-ASCII (ASCII sa v Jave označuje ako US-ASCII).

A ak použijeme našu utilitu, môžeme vidieť jej binárne znázornenie:

assertEquals (convertToBinary ("T", "US-ASCII"), "01010100");

Toto, ako sme očakávali, je sedembitové binárne znázornenie znaku „T“.

Pôvodná ASCII nechala najvýznamnejší bit každého bajtu nepoužitý. Súčasne ASCII nechala pomerne veľa postáv nezastúpených, najmä pre neanglické jazyky.

To viedlo k úsiliu využiť tento nepoužitý bit a zahrnúť ďalších 128 znakov.

V priebehu času bolo navrhnutých a prijatých niekoľko variácií schémy kódovania ASCII. Tieto sa začali voľne označovať ako „rozšírenia ASCII“.

Mnohé z rozšírení ASCII mali rôznu úroveň úspešnosti, ale samozrejme to nebolo dosť dobré pre širšie prijatie, pretože veľa znakov stále nebolo zastúpených.

Jedným z najpopulárnejších rozšírení ASCII bolo ISO-8859-1, označovaná tiež ako „ISO Latin 1“.

4.2. Viacbajtové kódovanie

Pretože potreba čoraz väčšieho počtu znakov rástla, jednobajtové schémy kódovania ako ASCII neboli udržateľné.

To viedlo k vzniku viacbajtových kódovacích schém, ktoré majú oveľa lepšiu kapacitu, aj keď za cenu zvýšených priestorových požiadaviek.

BIG5 a SHIFT-JIS sú príklady viacbajtové schémy kódovania znakov, ktoré začali používať jeden aj dva bajty na reprezentáciu širších znakových skupín. Väčšina z nich bola vytvorená pre potrebu reprezentovať čínske a podobné skripty, ktoré majú výrazne vyšší počet znakov.

Zavolajme teraz metódu convertToBinary s vstup ako „語“, čínsky znak a kódovanie ako „Big5“:

assertEquals (convertToBinary ("語", "Big5"), "10111011 01111001");

Vyššie uvedený výstup ukazuje, že kódovanie Big5 používa na reprezentáciu znaku „語“ dva bajty.

Komplexný zoznam kódovaní znakov spolu s ich aliasmi vedie Medzinárodný úrad pre čísla.

5. Unicode

Nie je ťažké pochopiť, že aj keď je kódovanie dôležité, dekódovanie je rovnako dôležité pre pochopenie reprezentácií. To je možné v praxi iba vtedy, ak sa široko používa konzistentná alebo kompatibilná schéma kódovania.

Rôzne kódovacie schémy vyvinuté izolovane a praktizované v miestnych geografických oblastiach začali byť náročné.

Táto výzva vyvolala jedinečný štandard kódovania s názvom Unicode, ktorý má kapacitu pre všetky možné postavy na svete. Patria sem používané znaky a dokonca aj tie, ktoré už neexistujú!

Na uloženie každého znaku to musí vyžadovať niekoľko bajtov? Úprimne áno, ale Unicode má dômyselné riešenie.

Unicode ako štandard definuje body kódu pre všetky možné postavy na svete. Bod kódu pre znak „T“ v Unicode je 84 v desatinnej čiarke. Všeobecne to v Unicode označujeme ako „U + 0054“, čo nie je nič iné ako U + a za ním hexadecimálne číslo.

Ako základ pre kódové body v Unicode používame hexadecimálne číslo, pretože existuje 1 114 112 bodov, čo je dosť veľké množstvo na pohodlnú komunikáciu v desatinnej čiarke!

To, ako sú tieto body kódu kódované do bitov, je ponechané na konkrétne schémy kódovania v rámci Unicode. Niektorým z týchto kódovacích schém sa budeme venovať v nižšie uvedených sekciách.

5.1. UTF-32

UTF-32 je schéma kódovania Unicode, ktorá na reprezentáciu každého bodu kódu využíva štyri bajty definované Unicode. Je zrejmé, že je neefektívne používať pre každý znak štyri bajty.

Pozrime sa, ako je v UTF-32 zastúpený jednoduchý znak ako „T“. Použijeme metódu convertToBinary predstavené skôr:

assertEquals (convertToBinary ("T", "UTF-32"), "00000000 00000000 00000000 01010100");

Vyššie uvedený výstup ukazuje použitie štyroch bajtov na reprezentáciu znaku „T“, kde prvé tri bajty predstavujú iba stratené miesto.

5.2. UTF-8

UTF-8 je iná schéma kódovania pre Unicode, ktorá na kódovanie využíva premennú dĺžku bajtov. Aj keď na kódovanie znakov všeobecne používa jeden bajt, v prípade potreby môže použiť vyšší počet bajtov, čím šetrí miesto.

Opäť zavoláme metódu convertToBinary so vstupom ako „T“ a kódovaním ako „UTF-8“:

assertEquals (convertToBinary ("T", "UTF-8"), "01010100");

Výstup je presne podobný ako v ASCII, pričom sa používa iba jeden bajt. UTF-8 je v skutočnosti úplne spätne kompatibilný s ASCII.

Opäť zavoláme metódu convertToBinary so vstupom ako „語“ a kódovaním ako „UTF-8“:

assertEquals (convertToBinary ("語", "UTF-8"), "11101000 10101010 10011110");

Ako tu vidíme, UTF-8 používa na reprezentáciu znaku „語“ tri bajty. Toto je známe ako kódovanie s premennou šírkou.

UTF-8 je vďaka svojej priestorovej efektívnosti najbežnejším kódovaním používaným na webe.

6. Podpora kódovania v Jave

Java podporuje širokú škálu kódovaní a ich vzájomných prevodov. Trieda Charset definuje množinu štandardných kódovaní, ktoré je podporovaná každou implementáciou platformy Java.

Patria sem napríklad US-ASCII, ISO-8859-1, UTF-8 a UTF-16. Konkrétna implementácia Java môže voliteľne podporovať ďalšie kódovanie.

Spôsob, akým Java zachytáva znakovú sadu, s ktorou pracuje, je niekoľko jemností. Prejdime si ich podrobnejšie.

6.1. Predvolená znaková sada

Platforma Java vo veľkej miere závisí od vlastnosti, ktorá sa volá predvolená znaková sada. Virtuálny počítač Java (JVM) určuje predvolenú znakovú sadu počas spustenia.

To závisí od miestneho nastavenia a znakovej sady základného operačného systému, na ktorom je spustený JVM. Napríklad v systéme MacOS je predvolená znaková sada UTF-8.

Pozrime sa, ako môžeme určiť predvolenú znakovú sadu:

Charset.defaultCharset (). DisplayName ();

Ak spustíme tento útržok kódu na počítači so systémom Windows, dostaneme výstup:

windows-1252

„Windows-1252“ je teraz predvolenou znakovou sadou platformy Windows v angličtine, ktorá v tomto prípade určila predvolenú znakovú sadu JVM, ktorá je spustená v systéme Windows.

6.2. Kto používa predvolenú znakovú sadu?

Mnoho rozhraní Java API využíva predvolenú znakovú sadu určenú JVM. Vymenovať zopár:

  • InputStreamReader a FileReader
  • OutputStreamWriter a FileWriter
  • Formátovač a Skener
  • URLEncoder a URLDecoder

To znamená, že ak by sme spustili náš príklad bez zadania znakovej sady:

nový BufferedReader (nový InputStreamReader (nový ByteArrayInputStream (input.getBytes ()))). readLine ();

potom by na jeho dekódovanie použila predvolenú znakovú sadu.

Existuje niekoľko rozhraní API, ktoré majú v predvolenom nastavení rovnakú voľbu.

Predvolená znaková sada preto predpokladá dôležitosť, ktorú nemôžeme bezpečne ignorovať.

6.3. Problémy s predvolenou znakovou sadou

Ako sme videli, predvolená znaková sada v Jave sa určuje dynamicky pri spustení JVM. Vďaka tomu je platforma menej spoľahlivá alebo náchylná na chyby pri použití v rôznych operačných systémoch.

Napríklad ak bežíme

nový BufferedReader (nový InputStreamReader (nový ByteArrayInputStream (input.getBytes ()))). readLine ();

v systéme macOS bude používať UTF-8.

Ak vyskúšame rovnaký úryvok v systéme Windows, použije na dekódovanie rovnakého textu systém Windows-1252.

Alebo si predstavte, že napíšete súbor v systéme MacOS a potom ten istý súbor prečítate v systéme Windows.

Nie je ťažké pochopiť, že kvôli rôznym schémam kódovania to môže viesť k strate alebo poškodeniu údajov.

6.4. Môžeme prepísať predvolenú znakovú sadu?

Určenie predvolenej znakovej sady v jazyku Java vedie k dvom vlastnostiam systému:

  • file.encoding: Hodnota tejto systémovej vlastnosti je názov predvolenej znakovej sady
  • sun.jnu.kódovanie: Hodnota tejto systémovej vlastnosti je názov znakovej sady použitej pri kódovaní / dekódovaní ciest k súborom

Teraz je intuitívne prepísať tieto vlastnosti systému pomocou argumentov príkazového riadku:

-Dfile.encoding = "UTF-8" -Dsun.jnu.encoding = "UTF-8"

Je však dôležité poznamenať, že tieto vlastnosti sú v Jave iba na čítanie. Ich použitie, ako je uvedené vyššie, nie je uvedené v dokumentácii. Prepísanie týchto vlastností systému nemusí mať požadované alebo predvídateľné správanie.

Teda nemali by sme prepísať predvolenú znakovú sadu v Jave.

6.5. Prečo to Java nerieši?

Existuje návrh na vylepšenie Java (JEP), ktorý predpisuje použitie „UTF-8“ ako predvolenej znakovej sady v jazyku Java namiesto toho, aby sa zakladala na znakovej sady miestneho nastavenia a operačného systému.

Tento JEP je odteraz v návrhovom stave a keď (dúfajme!) Prejde, vyrieši väčšinu problémov, o ktorých sme hovorili už skôr.

Upozorňujeme, že novšie rozhrania API sa podobajú rozhraniam API v java.nio.file.files nepoužívajte predvolenú znakovú sadu. Metódy v týchto rozhraniach API čítajú alebo zapisujú prúdy znakov pomocou znakovej sady ako UTF-8, a nie ako predvolená znaková sada.

6.6. Riešenie tohto problému v našich programoch

Mali by sme normálne namiesto zadávania predvolených nastavení vyberte pri práci s textom znakovú sadu. Môžeme výslovne deklarovať kódovanie, ktoré chceme použiť v triedach, ktoré sa zaoberajú prevodmi znakov na bajty.

Náš príklad už našťastie špecifikuje znakovú sadu. Musíme len zvoliť ten pravý a zvyšok nechať na Jave.

Teraz by sme si mali uvedomiť, že znaky s prízvukom ako „ç“ nie sú prítomné v schéme kódovania ASCII, a preto potrebujeme kódovanie, ktoré ich obsahuje. Možno, UTF-8?

Skúsme to, teraz spustíme metódu dekódovaťText s rovnakým vstupom, ale kódovaním ako „UTF-8“:

Vzor fasády je softvérovo navrhnutý vzor.

Bingo! Vidíme výstup, v ktorý sme dúfali.

Tu sme v konštruktore nastavili kódovanie, ktoré podľa nás najlepšie vyhovuje našim potrebám InputStreamReader. Toto je zvyčajne najbezpečnejšia metóda riešenia znakových a bajtových prevodov v prostredí Java.

Podobne OutputStreamWriter a mnoho ďalších rozhraní API podporuje nastavenie schémy kódovania prostredníctvom ich konštruktora.

6.7. MalformedInputException

Keď dekódujeme bajtovú postupnosť, existujú prípady, v ktorých to nie je pre daný prípad legálne Charset, alebo to nie je legálny šestnásťbitový kód Unicode. Inými slovami, daná sekvencia bajtov nemá v určenom prípade žiadne mapovanie Charset.

Existujú tri vopred definované stratégie (alebo CodingErrorAction) ak má vstupná sekvencia nesprávny vstup:

  • IGNOROVAŤ bude ignorovať nesprávne tvarované znaky a obnoví operáciu kódovania
  • VYMENIŤ nahradí nesprávne tvarované znaky vo výstupnej vyrovnávacej pamäti a obnoví operáciu kódovania
  • SPRÁVA bude hádzať a MalformedInputException

Predvolená hodnota malformedInputAction pre CharsetDecoder je SPRÁVA, a predvolené malformedInputAction predvoleného dekodéra v InputStreamReader je VYMENIŤ.

Definujme dekódovaciu funkciu, ktorá prijíma zadanú hodnotu Charset, a CodingErrorAction typ a reťazec, ktorý sa má dekódovať:

Reťazec decodeText (vstup reťazca, charsetová charset, CodingErrorAction codingErrorAction) vyvolá IOException {CharsetDecoder charsetDecoder = charset.newDecoder (); charsetDecoder.onMalformedInput (codingErrorAction); vrátiť nový BufferedReader (nový InputStreamReader (nový ByteArrayInputStream (input.getBytes ()), charsetDecoder)). readLine (); }

Takže ak dekódujeme „Fasádny vzor je softvérový vzor.“ s US_ASCII, výstup pre každú stratégiu by bol iný. Najprv použijeme CodingErrorAction.IGNORE ktoré preskočí nelegálne znaky:

Assertions.assertEquals („Vzor fasády je vzor návrhu softvéru.“, CharacterEncodingExamples.decodeText („Vzor fasády je vzor návrhu softvéru.“, StandardCharsets.US_ASCII, CodingErrorAction.IGNORE));

Pre druhý test použijeme CodingErrorAction.REPLACE ktorý namiesto nelegálnych znakov umiestni :

Assertions.assertEquals ("Vzor fasády je vzor návrhu softvéru.", CharacterEncodingExamples.decodeText ("Vzor fasády je vzor návrhu softvéru.", StandardCharsets.US_ASCII, CodingErrorAction.REPLACE));

Pri treťom teste používame CodingErrorAction.REPORT čo vedie k vhadzovaniu MalformedInputException:

Assertions.assertThrows (MalformedInputException.class, () -> CharacterEncodingExamples.decodeText („Vzor fasády je vzor návrhu softvéru.“, StandardCharsets.US_ASCII, CodingErrorAction.REPORT));

7. Ďalšie miesta, kde je dôležité kódovanie

Pri programovaní nemusíme brať do úvahy iba kódovanie znakov. Texty sa môžu terminálne pokaziť na mnohých ďalších miestach.

The najčastejšou príčinou problémov v týchto prípadoch je konverzia textu z jednej kódovacej schémy do druhej, čím by mohlo dôjsť k strate údajov.

Poďme rýchlo na niekoľko miest, kde by sme sa mohli stretnúť s problémami pri kódovaní alebo dekódovaní textu.

7.1. Textové editory

Vo väčšine prípadov textový editor je pôvodcom textov. Existuje mnoho textových editorov, medzi ktoré patria vi, Poznámkový blok a MS Word. Väčšina z týchto textových editorov nám umožňuje zvoliť schému kódovania. Preto by sme sa mali vždy ubezpečiť, že sú vhodné pre text, s ktorým pracujeme.

7.2. Systém súborov

Keď vytvoríme texty v editore, musíme ich uložiť v nejakom súborovom systéme. Súborový systém závisí od operačného systému, na ktorom je spustený. Väčšina operačných systémov má vlastnú podporu viacerých schém kódovania. Stále však môžu existovať prípady, keď kódovacia konverzia vedie k strate údajov.

7.3. Sieť

Texty prenášané cez sieť pomocou protokolu, ako je File Transfer Protocol (FTP), zahŕňajú aj prevod medzi kódovaním znakov. Pre čokoľvek kódované v Unicode je najbezpečnejšie prenášať ako binárne, aby ste minimalizovali riziko straty pri prevode. Prenos textu po sieti je však jednou z menej častých príčin poškodenia údajov.

7.4. Databázy

Väčšina populárnych databáz ako Oracle a MySQL podporuje výber schémy kódovania znakov pri inštalácii alebo vytváraní databáz. Musíme si to zvoliť v súlade s textami, ktoré očakávame, že uložíme do databázy. Toto je jedno z častejších miest, kde dochádza k poškodeniu textových údajov v dôsledku kódovania konverzií.

7.5. Prehliadače

Nakoniec vo väčšine webových aplikácií vytvárame texty a prechádzame ich rôznymi vrstvami so zámerom zobraziť ich v používateľskom rozhraní, napríklad v prehliadači. Aj tu je nevyhnutné zvoliť správne kódovanie znakov, ktoré dokáže znaky správne zobraziť. Najobľúbenejšie prehliadače ako Chrome alebo Edge umožňujú výber kódovania znakov prostredníctvom svojich nastavení.

8. Záver

V tomto článku sme diskutovali o tom, ako môže byť problém s kódovaním pri programovaní.

Ďalej sme diskutovali o základných princípoch vrátane kódovania a charsetov. Navyše sme prešli rôznymi schémami kódovania a ich použitiami.

Tiež sme vyzdvihli príklad nesprávneho použitia kódovania znakov v Jave a videli sme, ako to napraviť. Nakoniec sme diskutovali o ďalších bežných chybových scenároch týkajúcich sa kódovania znakov.

Ako vždy, kód príkladov je k dispozícii na GitHub.


$config[zx-auto] not found$config[zx-overlay] not found