Vodič za kodiranje znakova

1. Pregled

U ovom uputstvu razgovarat ćemo o osnovama kodiranja znakova i načinu na koji to rješavamo u Javi.

2. Važnost kodiranja znakova

Često imamo posla s tekstovima koji pripadaju više jezika s raznim skriptama za pisanje poput latinskog ili arapskog. Svaki lik u svakom jeziku treba nekako preslikati u skup jedinica i nula. Zaista, čudo je što računala mogu ispravno obraditi sve naše jezike.

Da biste to učinili ispravno, moramo razmišljati o kodiranju znakova. Ako to ne učinite, često mogu dovesti do gubitka podataka, pa čak i sigurnosnih ranjivosti.

Da bismo ovo bolje razumjeli, definirajmo metodu za dekodiranje teksta na Javi:

String decodeText (String input, String encoding) baca IOException {return new BufferedReader (new InputStreamReader (new ByteArrayInputStream (input.getBytes ()), Charset.forName (encoding))) .readLine (); }

Imajte na umu da ulazni tekst koji ovdje donosimo koristi zadano kodiranje platforme.

Ako ovu metodu pokrenemo s ulazni kao što je "Uzorak fasade uzorak softverskog dizajna." i kodiranje kao "US-ASCII", izlazit će:

Uzorak fasade uzorak je softverskog dizajna.

Pa, ne baš ono što smo očekivali.

Što je moglo poći po zlu? Pokušat ćemo to razumjeti i ispraviti u nastavku ovog vodiča.

3. Osnove

Prije nego što dublje zakopamo, na brzinu pregledajmo tri pojma: kodiranje, charcets, i kodna točka.

3.1. Kodiranje

Računala mogu razumjeti samo binarne prikaze poput 1 i 0. Za obradu bilo čega drugog potrebna je neka vrsta preslikavanja iz stvarnog teksta u njegov binarni prikaz. Ovo mapiranje je ono što mi znamo kodiranje znakova ili jednostavno baš kao kodiranje.

Na primjer, prvo slovo u našoj poruci, "T", u US-ASCII kodira do “01010100”.

3.2. Charsets

Mapiranje znakova u njihove binarne prikaze može se uvelike razlikovati u smislu znakova koje uključuju. Broj znakova uključenih u mapiranje može varirati od samo nekoliko do svih znakova u praktičnoj upotrebi. Skup znakova koji su uključeni u definiciju preslikavanja formalno se naziva a charset.

Na primjer, ASCII ima skup znakova od 128 znakova.

3.3. Šifra točke

Točka koda je apstrakcija koja odvaja znak od stvarnog kodiranja. A kodna točka je cjelobrojna referenca na određeni znak.

Sam cijeli broj možemo predstaviti u običnim decimalnim ili zamjenskim osnovama poput heksadecimalne ili osminske. Za jednostavnost upućivanja velikih brojeva koristimo zamjenske baze.

Na primjer, prvo slovo u našoj poruci, T, u Unicodeu ima kodnu točku "U + 0054" (ili 84 u decimalu).

4. Razumijevanje shema kodiranja

Kodiranje znakova može imati različite oblike, ovisno o broju znakova koje kodira.

Broj kodiranih znakova ima izravan odnos s duljinom svakog prikaza koji se obično mjeri brojem bajtova. Imati više znakova za kodiranje u biti znači da su potrebni duži binarni prikazi.

Prođimo kroz neke od popularnih shema kodiranja danas u praksi.

4.1. Jednobajtno kodiranje

Jedna od najranijih shema kodiranja, nazvana ASCII (američki standardni kod za razmjenu informacija), koristi shemu kodiranja od jednog bajta. To u biti znači da svaki znak u ASCII predstavljen je sedmerobitnim binarnim brojevima. Ovo još uvijek ostavlja malo bita u svakom bajtu!

ASCII-ov set od 128 znakova obuhvaća engleske abecede malim i velikim slovima, znamenke i neke posebne i kontrolne znakove.

Definirajmo jednostavnu metodu u Javi za prikaz binarnog prikaza za znak pod određenom shemom kodiranja:

String convertToBinary (String input, String encoding) baca UnsupportedEncodingException {byte [] encoded_input = Charset.forName (encoding) .encode (input) .array (); return 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 (" ")); }

Sada znak 'T' ima kodnu točku 84 u US-ASCII (ASCII se u Javi naziva US-ASCII).

A ako koristimo našu korisnu metodu, možemo vidjeti njezin binarni prikaz:

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

Ovo je, kao što smo i očekivali, sedmerobitni binarni prikaz za lik 'T'.

Izvorni ASCII ostavio je najznačajniji bit svakog bajta neiskorištenim. Istodobno, ASCII je ostavio dosta znakova bez predstavljanja, posebno za neengleske jezike.

To je dovelo do napora da se iskoristi taj neiskorišteni bit i uključi dodatnih 128 znakova.

Bilo je nekoliko varijacija sheme kodiranja ASCII koje su predložene i prihvaćene tijekom vremena. Njih se slobodno nazivalo "ASCII proširenjima".

Mnoga proširenja ASCII imala su različite razine uspjeha, ali očito to nije bilo dovoljno dobro za šire usvajanje jer mnogi likovi još uvijek nisu bili zastupljeni.

Jedno od popularnijih proširenja ASCII bilo je ISO-8859-1, koji se naziva i "ISO Latin 1".

4.2. Višebajtno kodiranje

Kako je rasla potreba za smještanjem sve više i više znakova, jednobajtni shemi kodiranja poput ASCII nisu bili održivi.

To je dovelo do višebajtnih shema kodiranja koje imaju puno bolji kapacitet, iako po cijenu povećanih zahtjeva za prostorom.

BIG5 i SHIFT-JIS su primjeri višebajtne sheme kodiranja znakova koje su počele koristiti jedan i dva bajta za predstavljanje širih znakova. Većina ih je stvorena zbog potrebe predstavljanja kineskih i sličnih pisama koja imaju znatno veći broj znakova.

Nazovimo sada metodu convertToBinary s ulazni kao "語", kineski znak, i kodiranje kao "Big5":

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

Izlaz gore pokazuje da Big5 kodiranje koristi dva bajta za predstavljanje znaka '語'.

Sveobuhvatan popis kodiranja znakova, zajedno s njihovim pseudonimima, održava Međunarodno tijelo za brojeve.

5. Unicode

Nije teško razumjeti da iako je kodiranje važno, dekodiranje je podjednako važno za razumijevanje prikaza. To je u praksi moguće samo ako se široko koristi konzistentna ili kompatibilna shema kodiranja.

Različite sheme kodiranja razvijene izolirano i prakticirane u lokalnim zemljopisima počele su postajati izazovne.

Ovaj izazov je dao povod jedinstveni standard kodiranja pod nazivom Unicode koji ima kapacitet za svaki mogući lik na svijetu. To uključuje znakove koji se koriste, pa čak i one koji su nestali!

Pa, to zahtijeva nekoliko bajtova za pohranu svakog znaka? Iskreno da, ali Unicode ima genijalno rješenje.

Unicode kao standard definira kodne točke za sve moguće znakove na svijetu. Točka koda za znak „T“ u Unicodeu je 84 u decimalnom znaku. To se u Unicodeu obično naziva "U + 0054", što je ništa drugo do U + iza kojeg slijedi heksadecimalni broj.

Koristimo heksadecimalni kao bazu za kodne točke u Unicodeu, jer postoji 1,114,112 točaka, što je prilično velik broj za ugodnu komuniciranje u decimalu!

Kako su ove kodne točke kodirane u bitove, prepušteno je određenim shemama kodiranja unutar Unicode-a. Neke od ovih shema kodiranja pokriti ćemo u donjim pododjeljcima.

5.1. UTF-32

UTF-32 je shema kodiranja za Unicode koja koristi četiri bajta za predstavljanje svake kodne točke definirao Unicode. Očito je da je neučinkovito koristiti četiri bajta za svaki znak.

Pogledajmo kako je jednostavan znak poput „T“ predstavljen u UTF-32. Mi ćemo se služiti metodom convertToBinary predstavljen ranije:

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

Gore navedeni izlaz prikazuje upotrebu četiri bajta za predstavljanje znaka 'T', gdje su prva tri bajta samo izgubljeni prostor.

5.2. UTF-8

UTF-8 je druga shema kodiranja za Unicode koja koristi promjenjivu duljinu bajtova za kodiranje. Iako koristi jedan bajt za općenito kodiranje znakova, po potrebi može koristiti veći broj bajtova, čime štedi prostor.

Nazovimo opet metodu convertToBinary s ulazom kao "T" i kodiranjem kao "UTF-8":

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

Izlaz je potpuno sličan ASCII-u koristeći samo jedan bajt. U stvari, UTF-8 je potpuno kompatibilan s ASCII-om.

Nazovimo opet metodu convertToBinary s ulazom kao '語' i kodiranjem kao "UTF-8":

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

Kao što ovdje možemo vidjeti, UTF-8 koristi tri bajta za predstavljanje znaka '語'. Ovo je poznato kao kodiranje promjenjive širine.

UTF-8, zbog svoje svemirske učinkovitosti, najčešće je kodiranje koje se koristi na webu.

6. Podrška za kodiranje u Javi

Java podržava širok spektar kodiranja i njihove međusobne pretvorbe. Razred Charset definira skup standardnih kodiranja koje svaka implementacija Java platforme mora podržavati.

To uključuje US-ASCII, ISO-8859-1, UTF-8 i UTF-16 da nabrojimo samo neke. Određena implementacija Jave može po želji podržati dodatna kodiranja.

Postoje neke suptilnosti u načinu na koji Java odabire skup znakova za rad. Prođimo ih detaljnije.

6.1. Zadana charset

Java platforma uvelike ovisi o svojstvu tzv zadani skup znakova. Java virtualni stroj (JVM) određuje zadani skup znakova tijekom pokretanja.

To ovisi o lokalnom okruženju i znaku osnovnog operativnog sustava na kojem je JVM pokrenut. Na primjer, na MacOS-u, zadani skup znakova je UTF-8.

Pogledajmo kako možemo odrediti zadani skup znakova:

Charset.defaultCharset (). DisplayName ();

Ako pokrenemo ovaj isječak koda na Windows računalu, dobit ćemo izlaz:

windows-1252

Sada je "windows-1252" zadani skup znakova Windows platforme na engleskom jeziku, koji je u ovom slučaju odredio zadani skup JVM-a koji je pokrenut na sustavu Windows.

6.2. Tko koristi zadani skup znakova?

Mnogi Java API-ji koriste zadani skup znakova kako je odredio JVM. Da navedemo samo nekoliko:

  • InputStreamReader i Čitač datoteka
  • OutputStreamWriter i FileWriter
  • Formatter i Skener
  • URLEncoder i URLDecoder

Dakle, to znači da ako bismo pokrenuli naš primjer bez navođenja znaka:

novi BufferedReader (novi InputStreamReader (novi ByteArrayInputStream (input.getBytes ()))). readLine ();

tada bi za dekodiranje koristio zadani charset.

Postoji nekoliko API-ja koji prema zadanim postavkama čine isti izbor.

Zadani skup znakova stoga poprima važnost koju ne možemo sigurno zanemariti.

6.3. Problemi sa zadanim znakom

Kao što smo vidjeli, zadani skup znakova u Javi određuje se dinamički kada se JVM pokrene. To platformu čini manje pouzdanom ili sklonom pogreškama kada se koristi u različitim operativnim sustavima.

Na primjer, ako trčimo

novi BufferedReader (novi InputStreamReader (novi ByteArrayInputStream (input.getBytes ()))). readLine ();

na macOS-u, koristit će UTF-8.

Ako isprobamo isti isječak na sustavu Windows, on će upotrijebiti Windows-1252 za ​​dekodiranje istog teksta.

Ili zamislite da napišete datoteku na macOS, a zatim tu istu datoteku pročitate na sustavu Windows.

Nije teško razumjeti da zbog različitih shema kodiranja to može dovesti do gubitka podataka ili oštećenja.

6.4. Možemo li poništiti zadani znak?

Određivanje zadanog skupa znakova u Javi dovodi do dva svojstva sustava:

  • datoteka.kodiranje: Vrijednost ovog svojstva sustava naziv je zadanog skupa znakova
  • sunce.jnu.kodiranje: Vrijednost ovog svojstva sustava naziv je znakova koji se koriste prilikom kodiranja / dekodiranja staza datoteka

Sada je intuitivno nadjačati ova svojstva sustava pomoću argumenata naredbenog retka:

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

Međutim, važno je napomenuti da su ova svojstva u Javi samo za čitanje. Njihova upotreba kao gore nije prisutna u dokumentaciji. Nadjačavanje ovih svojstava sustava možda nema željeno ili predvidljivo ponašanje.

Stoga, trebali bismo izbjegavati nadjačavanje zadanog skupa znakova u Javi.

6.5. Zašto Java to ne rješava?

Postoji Prijedlog za poboljšanje Jave (JEP) koji propisuje upotrebu "UTF-8" kao zadani skup znakova u Javi, umjesto da se temelji na područnom znaku i skupu operativnog sustava.

Ovaj je JEP trenutno u nacrtu i kad prođe (nadam se!) Riješit će većinu problema o kojima smo ranije razgovarali.

Imajte na umu da noviji API-ji poput onih u java.nio.file.File nemojte koristiti zadani skup znakova. Metode u ovim API-ima čitaju ili zapisuju tokove znakova sa znakom kao UTF-8, a ne sa zadanim znakom.

6.6. Rješavanje ovog problema u našim programima

Trebali bismo normalno odlučite odrediti skup znakova kada se bavite tekstom, a ne oslanjajući se na zadane postavke. Možemo izričito deklarirati kodiranje koje želimo koristiti u klasama koje se bave pretvaranjem znakova u bajt.

Srećom, naš primjer već navodi charset. Samo trebamo odabrati pravog, a ostalo dopustiti Javi.

Do sada bismo trebali shvatiti da naglašeni znakovi poput 'ç' nisu prisutni u shemi kodiranja ASCII i stoga nam treba kodiranje koje ih uključuje. Možda, UTF-8?

Pokušajmo to, sada ćemo pokrenuti metodu decodeText s istim ulazom, ali kodiranim kao "UTF-8":

Uzorak fasade uzorak je softverskog dizajna.

Bingo! Možemo vidjeti izlaz koji smo se nadali vidjeti sada.

Ovdje smo postavili kodiranje za koje mislimo da najbolje odgovara našim potrebama u konstruktoru InputStreamReader. Ovo je obično najsigurnija metoda rješavanja pretvorbi znakova i bajtova u Javi.

Slično tome, OutputStreamWriter i mnogi drugi API-ji podržavaju postavljanje sheme kodiranja putem njihovog konstruktora.

6.7. MalformedInputException

Kad dekodiramo niz bajtova, postoje slučajevi u kojima to nije legalno za dano Charsetili inače nije legalni šesnaest-bitni Unicode. Drugim riječima, zadani niz bajtova nema mapiranje u navedenom Charset.

Postoje tri unaprijed definirane strategije (ili CodingErrorAction) kada ulazni niz ima pogrešno oblikovan ulaz:

  • ZANEMARITI ignorirat će neispravne znakove i nastaviti s kodiranjem
  • ZAMIJENITI zamijenit će neispravne znakove u izlaznom međuspremniku i nastaviti s kodiranjem
  • IZVJEŠĆE bacit će a MalformedInputException

Zadana vrijednost malformedInputAction za CharsetDecoder je PRIJAVA, i zadani malformedInputAction zadanog dekodera u InputStreamReader je ZAMIJENITI.

Definirajmo funkciju dekodiranja koja prima navedeni Charset, a CodingErrorAction vrsta i niz za dekodiranje:

String decodeText (String input, charset charset, CodingErrorAction codingErrorAction) baca IOException {CharsetDecoder charsetDecoder = charset.newDecoder (); charsetDecoder.onMalformedInput (codingErrorAction); vrati novi BufferedReader (novi InputStreamReader (novi ByteArrayInputStream (input.getBytes ()), charsetDecoder)). readLine (); }

Dakle, ako dekodiramo "Uzorak fasade uzorak je softverskog dizajna." s US_ASCII, izlaz za svaku strategiju bio bi različit. Prvo, koristimo CodingErrorAction.IGNORE koji preskače ilegalne znakove:

Assertions.assertEquals ("Uzorak pročelja uzorak je softverskog dizajna.", CharacterEncodingExamples.decodeText ("Uzorak pročelja uzorak je softverskog dizajna.", StandardCharsets.US_ASCII, CodingErrorAction.IGNORE));

Za drugi test koristimo CodingErrorAction.REPLACE koji stavlja umjesto nedopuštenih znakova:

Assertions.assertEquals ("Uzorak fasade uzorak je softverskog dizajna.", CharacterEncodingExamples.decodeText ("Uzorak fasade uzorak je softverskog dizajna.", StandardCharsets.US_ASCII, CodingErrorAction.REPLACE));

Za treći test koristimo CodingErrorAction.REPORT što dovodi do bacanja MalformedInputException:

Assertions.assertThrows (MalformedInputException.class, () -> CharacterEncodingExamples.decodeText ("Uzorak fasade uzorak je softverskog dizajna.", StandardCharsets.US_ASCII, CodingErrorAction.REPORT));

7. Ostala mjesta na kojima je kodiranje važno

Tijekom programiranja ne trebamo uzeti u obzir samo kodiranje znakova. Tekstovi mogu krajnje pogrešiti na mnogim drugim mjestima.

The najčešći uzrok problema u tim slučajevima je pretvorba teksta iz jedne sheme kodiranja u drugu, čime se eventualno uvodi gubitak podataka.

Prođimo brzo kroz nekoliko mjesta na kojima možemo naići na probleme prilikom kodiranja ili dekodiranja teksta.

7.1. Uređivači teksta

U većini slučajeva tekstovi potječu iz uređivača teksta. Brojni su uređivači teksta u popularnom izboru, uključujući vi, Notepad i MS Word. Većina ovih uređivača teksta omogućuje nam odabir sheme kodiranja. Stoga bismo uvijek trebali biti sigurni da su prikladni za tekst koji obrađujemo.

7.2. Sustav datoteka

Nakon što stvorimo tekstove u uređivaču, trebamo ih pohraniti u neki datotečni sustav. Datotečni sustav ovisi o operativnom sustavu na kojem je pokrenut. Većina operativnih sustava ima svojstvenu podršku za više shema kodiranja. Međutim, još uvijek mogu biti slučajevi kada pretvorba kodiranja dovodi do gubitka podataka.

7.3. Mreža

Tekstovi kada se prenose preko mreže pomoću protokola poput FTP (File Transfer Protocol) također uključuju pretvaranje između kodiranja znakova. Za bilo što kodirano u Unicodeu najsigurnije je prenijeti kao binarno kako bi se minimalizirao rizik od gubitka u konverziji. Međutim, prijenos teksta putem mreže jedan je od rjeđih uzroka oštećenja podataka.

7.4. Baze podataka

Većina popularnih baza podataka kao što su Oracle i MySQL podržavaju odabir sheme kodiranja znakova prilikom instalacije ili izrade baza podataka. To moramo odabrati u skladu s tekstovima koje očekujemo da će se pohraniti u bazu podataka. Ovo je jedno od češćih mjesta na kojima se zbog kodiranja pretvorbi događa oštećenje tekstualnih podataka.

7.5. Preglednici

Napokon, u većini web aplikacija stvaramo tekstove i prolazimo ih kroz različite slojeve s namjerom da ih pregledamo u korisničkom sučelju, poput preglednika. I ovdje nam je imperativ odabrati pravo kodiranje znakova koje može pravilno prikazati znakove. Najpopularniji preglednici poput Chromea, Edge omogućuju odabir kodiranja znakova putem svojih postavki.

8. Zaključak

U ovom smo članku razgovarali o tome kako kodiranje može predstavljati problem tijekom programiranja.

Dalje smo razgovarali o osnovama, uključujući kodiranje i znakove. Štoviše, prošli smo različite sheme kodiranja i njihovu upotrebu.

Također smo uzeli primjer pogrešne upotrebe kodiranja znakova u Javi i vidjeli kako to ispraviti. Na kraju smo razgovarali o nekim drugim uobičajenim scenarijima pogrešaka koji se odnose na kodiranje znakova.

Kao i uvijek, kod za primjere dostupan je na GitHub-u.