Gids voor tekencodering

1. Overzicht

In deze tutorial bespreken we de basisprincipes van tekencodering en hoe we ermee omgaan in Java.

2. Belang van tekencodering

We hebben vaak te maken met teksten uit meerdere talen met verschillende schrijfscripts zoals Latijn of Arabisch. Elk personage in elke taal moet op de een of andere manier worden toegewezen aan een reeks enen en nullen. Het is echt een wonder dat computers al onze talen correct kunnen verwerken.

Om dit goed te doen, we moeten nadenken over tekencodering. Als u dit niet doet, kan dit vaak leiden tot gegevensverlies en zelfs beveiligingsproblemen.

Om dit beter te begrijpen, gaan we een methode definiëren om een ​​tekst in Java te decoderen:

String decodeText (String-invoer, String-codering) gooit IOException {retourneer nieuwe BufferedReader (nieuwe InputStreamReader (nieuwe ByteArrayInputStream (input.getBytes ()), Charset.forName (codering))) .readLine (); }

Merk op dat de invoertekst die we hier invoeren de standaard platformcodering gebruikt.

Als we deze methode uitvoeren met invoer als "Het gevelpatroon is een softwareontwerppatroon." en codering als "US-ASCII", het zal uitvoeren:

Het gevelpatroon is een softwareontwerppatroon.

Nou, niet precies wat we hadden verwacht.

Wat had er mis kunnen gaan? We zullen dit in de rest van deze tutorial proberen te begrijpen en corrigeren.

3. Grondbeginselen

Laten we echter, voordat we dieper graven, kort drie termen bekijken: codering, tekensets, en codepunt.

3.1. Codering

Computers kunnen alleen binaire representaties zoals 1 en 0. Voor het verwerken van iets anders is een soort mapping nodig van de echte tekst naar de binaire weergave. Deze mapping is wat we kennen tekencodering of gewoon zoals codering.

Bijvoorbeeld de eerste letter in ons bericht, "T", in US-ASCII codeert naar "01010100".

3.2. Tekensets

De toewijzing van karakters aan hun binaire representaties kan sterk variëren in termen van de karakters die ze bevatten. Het aantal karakters in een mapping kan variëren van slechts enkele tot alle karakters in praktisch gebruik. De set tekens die in een toewijzingsdefinitie is opgenomen, wordt formeel een tekenset.

ASCII heeft bijvoorbeeld een karakterset van 128 tekens.

3.3. Codepunt

Een codepunt is een abstractie die een teken scheidt van de feitelijke codering. EEN codepunt is een integer verwijzing naar een bepaald teken.

We kunnen het gehele getal zelf weergeven in gewone decimale of alternatieve basen zoals hexadecimaal of octaal. We gebruiken alternatieve bases om het doorverwijzen van grote nummers gemakkelijker te maken.

De eerste letter in ons bericht, T, in Unicode heeft bijvoorbeeld een codepunt "U + 0054" (of 84 in decimaal).

4. Inzicht in coderingsschema's

Een tekencodering kan verschillende vormen aannemen, afhankelijk van het aantal tekens dat wordt gecodeerd.

Het aantal gecodeerde tekens heeft een directe relatie met de lengte van elke weergave, die doorgaans wordt gemeten als het aantal bytes. Het hebben van meer tekens om te coderen betekent in wezen dat er langere binaire representaties nodig zijn.

Laten we enkele van de populaire coderingsschema's in de praktijk eens doornemen.

4.1. Single-byte-codering

Een van de eerste coderingsschema's, ASCII (American Standard Code for Information Exchange) genaamd, gebruikt een coderingsschema van één byte. Dit betekent in wezen dat elk teken in ASCII wordt weergegeven met zeven-bits binaire getallen. Hierdoor blijft er nog steeds een bit vrij in elke byte!

De set van 128 tekens van ASCII omvat Engelse alfabetten in hoofdletters en kleine letters, cijfers en enkele speciale en besturingstekens.

Laten we een eenvoudige methode in Java definiëren om de binaire weergave voor een teken onder een bepaald coderingsschema weer te geven:

String convertToBinary (String-invoer, String-codering) genereert UnsupportedEncodingException {byte [] encoded_input = Charset.forName (codering) .encode (invoer) .array (); retourneer IntStream.range (0, gecodeerde_input.length) .map (i -> gecodeerde_input [i]) .mapToObj (e -> Integer.toBinaryString (e ^ 255)) .map (e -> String.format ("% 1) $ "+ Byte.SIZE +" s ", e) .replace (" "," 0 ")) .collect (Collectors.joining (" ")); }

Nu heeft teken 'T' een codepunt van 84 in US-ASCII (ASCII wordt in Java US-ASCII genoemd).

En als we onze utility-methode gebruiken, kunnen we de binaire representatie ervan zien:

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

Dit is, zoals we hadden verwacht, een binaire weergave van zeven bits voor het teken 'T'.

De originele ASCII liet het meest significante bit van elke byte ongebruikt. Tegelijkertijd had ASCII nogal wat karakters onvertegenwoordigd gelaten, vooral voor niet-Engelse talen.

Dit leidde tot een poging om dat ongebruikte bit te gebruiken en nog eens 128 tekens op te nemen.

Er zijn in de loop van de tijd verschillende varianten van het ASCII-coderingsschema voorgesteld en aangenomen. Deze werden losjes aangeduid als "ASCII-extensies".

Veel van de ASCII-extensies hadden verschillende succesniveaus, maar dit was duidelijk niet goed genoeg voor een bredere acceptatie, aangezien veel karakters nog steeds niet vertegenwoordigd waren.

Een van de meer populaire ASCII-extensies was ISO-8859-1, ook wel "ISO Latin 1" genoemd.

4.2. Multi-byte-codering

Naarmate de behoefte om steeds meer tekens te huisvesten groeide, waren single-byte coderingsschema's zoals ASCII niet duurzaam.

Dit gaf aanleiding tot coderingsschema's met meerdere bytes die een veel betere capaciteit hebben, zij het ten koste van toegenomen ruimtevereisten.

BIG5 en SHIFT-JIS zijn daar voorbeelden van multi-byte tekencoderingsschema's die zowel één als twee bytes begonnen te gebruiken om bredere tekensets weer te geven. De meeste hiervan zijn gemaakt om Chinese en soortgelijke scripts te vertegenwoordigen die een aanzienlijk hoger aantal tekens hebben.

Laten we nu de methode noemen convertToBinary met invoer als ‘語 ', een Chinees karakter, en codering als "Big5":

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

De uitvoer hierboven laat zien dat Big5-codering twee bytes gebruikt om het teken ‘語 'weer te geven.

Een uitgebreide lijst van tekencoderingen, samen met hun aliassen, wordt bijgehouden door de International Number Authority.

5. Unicode

Het is niet moeilijk te begrijpen dat hoewel codering belangrijk is, decodering even belangrijk is om de weergaven te begrijpen. Dit is in de praktijk alleen mogelijk als een consistent of compatibel coderingsschema algemeen wordt gebruikt.

Verschillende coderingsschema's die afzonderlijk werden ontwikkeld en in lokale regio's werden toegepast, begonnen een uitdaging te worden.

Deze uitdaging gaf aanleiding tot een unieke coderingsstandaard genaamd Unicode die de capaciteit heeft voor elk mogelijk personage in de wereld. Dit omvat de karakters die in gebruik zijn en zelfs degenen die niet meer bestaan!

Welnu, daarvoor zijn meerdere bytes nodig om elk teken op te slaan? Eerlijk gezegd ja, maar Unicode heeft een ingenieuze oplossing.

Unicode definieert als standaard codepunten voor elk mogelijk teken in de wereld. Het codepunt voor teken ‘T 'in Unicode is 84 in decimaal. Over het algemeen noemen we dit "U + 0054" in Unicode, wat niets anders is dan U + gevolgd door het hexadecimale getal.

We gebruiken hexadecimaal als basis voor codepunten in Unicode, aangezien er 1.114.112 punten zijn, wat een behoorlijk groot aantal is om gemakkelijk in decimalen te communiceren!

Hoe deze codepunten in bits worden gecodeerd, wordt overgelaten aan specifieke coderingsschema's binnen Unicode. We behandelen enkele van deze coderingsschema's in de onderstaande subsecties.

5.1. UTF-32

UTF-32 is een coderingsschema voor Unicode dat vier bytes gebruikt om elk codepunt te vertegenwoordigen gedefinieerd door Unicode. Het is duidelijk dat het inefficiënt is om vier bytes voor elk teken te gebruiken.

Laten we eens kijken hoe een eenvoudig teken als 'T' wordt weergegeven in UTF-32. We zullen de methode gebruiken convertToBinary eerder geïntroduceerd:

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

De uitvoer hierboven toont het gebruik van vier bytes om het teken 'T' weer te geven, waarbij de eerste drie bytes gewoon verspilde ruimte zijn.

5.2. UTF-8

UTF-8 is een ander coderingsschema voor Unicode dat een variabele lengte van bytes gebruikt om te coderen. Hoewel het een enkele byte gebruikt om tekens in het algemeen te coderen, kan het indien nodig een hoger aantal bytes gebruiken, waardoor ruimte wordt bespaard.

Laten we opnieuw de methode noemen convertToBinary met invoer als 'T' en codering als 'UTF-8':

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

De uitvoer is precies vergelijkbaar met ASCII met slechts één byte. In feite is UTF-8 volledig achterwaarts compatibel met ASCII.

Laten we opnieuw de methode noemen convertToBinary met invoer als ‘語 'en codering als‘ UTF-8 ’:

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

Zoals we hier kunnen zien, gebruikt UTF-8 drie bytes om het teken ‘語 'weer te geven. Dit staat bekend als codering met variabele breedte.

UTF-8 is vanwege zijn ruimtebesparing de meest gebruikte codering op internet.

6. Coderingsondersteuning in Java

Java ondersteunt een breed scala aan coderingen en hun conversies naar elkaar. De klas Tekenset definieert een set standaardcoderingen die elke implementatie van het Java-platform moet ondersteunen.

Dit omvat US-ASCII, ISO-8859-1, UTF-8 en UTF-16 om er maar een paar te noemen. Een bepaalde implementatie van Java kan optioneel aanvullende coderingen ondersteunen.

Er zijn enkele subtiliteiten in de manier waarop Java een tekenset oppikt om mee te werken. Laten we ze in meer details doornemen.

6.1. Standaard tekenset

Het Java-platform is sterk afhankelijk van een eigenschap genaamd de standaard tekenset. De Java Virtual Machine (JVM) bepaalt de standaard tekenset tijdens het opstarten.

Dit is afhankelijk van de landinstelling en de tekenset van het onderliggende besturingssysteem waarop JVM wordt uitgevoerd. Op MacOS is de standaard tekenset bijvoorbeeld UTF-8.

Laten we eens kijken hoe we de standaard karakterset kunnen bepalen:

Charset.defaultCharset (). DisplayName ();

Als we dit codefragment op een Windows-computer uitvoeren, krijgen we de uitvoer:

windows-1252

Nu is "windows-1252" de standaard tekenset van het Windows-platform in het Engels, die in dit geval de standaard tekenset van JVM heeft bepaald die op Windows wordt uitgevoerd.

6.2. Wie gebruikt de standaardtekenset?

Veel van de Java API's maken gebruik van de standaard karakterset zoals bepaald door de JVM. Om er een paar te noemen:

  • InputStreamReader en FileReader
  • OutputStreamWriter en FileWriter
  • Formatter en Scanner
  • URLEncoder en URLDecoder

Dit betekent dus dat als we ons voorbeeld zouden uitvoeren zonder de karakterset op te geven:

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

dan zou het de standaard karakterset gebruiken om het te decoderen.

En er zijn verschillende API's die standaard dezelfde keuze maken.

De standaard tekenset neemt dus een belang aan dat we niet veilig kunnen negeren.

6.3. Problemen met de standaard tekenset

Zoals we hebben gezien, wordt de standaard tekenset in Java dynamisch bepaald wanneer de JVM start. Dit maakt het platform minder betrouwbaar of foutgevoelig bij gebruik in verschillende besturingssystemen.

Als we bijvoorbeeld rennen

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

op macOS wordt UTF-8 gebruikt.

Als we hetzelfde fragment op Windows proberen, wordt Windows-1252 gebruikt om dezelfde tekst te decoderen.

Of stel je voor dat je een bestand schrijft op een macOS en datzelfde bestand vervolgens leest in Windows.

Het is niet moeilijk te begrijpen dat dit vanwege verschillende coderingsschema's kan leiden tot gegevensverlies of -beschadiging.

6.4. Kunnen we de standaardtekenset negeren?

De bepaling van de standaard tekenset in Java leidt tot twee systeemeigenschappen:

  • file.encoding: De waarde van deze systeemeigenschap is de naam van de standaard tekenset
  • sun.jnu.encoding: De waarde van deze systeemeigenschap is de naam van de tekenset die wordt gebruikt bij het coderen / decoderen van bestandspaden

Nu is het intuïtief om deze systeemeigenschappen te overschrijven via opdrachtregelargumenten:

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

Het is echter belangrijk op te merken dat deze eigenschappen alleen-lezen zijn in Java. Hun gebruik zoals hierboven is niet aanwezig in de documentatie. Het overschrijven van deze systeemeigenschappen heeft mogelijk niet het gewenste of voorspelbare gedrag.

Vandaar, we moeten voorkomen dat de standaard tekenset in Java wordt overschreven.

6.5. Waarom lost Java dit niet op?

Er is een Java Enhancement Proposal (JEP) dat voorschrijft dat “UTF-8” als de standaard tekenset in Java wordt gebruikt in plaats van deze te baseren op de landinstelling en de tekenset van het besturingssysteem.

Dit GEP is vanaf nu in een conceptstaat en zal, wanneer het (hopelijk!) Doorloopt, de meeste problemen oplossen die we eerder hebben besproken.

Merk op dat de nieuwere API's zoals die in java.nio.file.Files gebruik niet de standaard tekenset. De methoden in deze API's lezen of schrijven tekenstromen met tekenset als UTF-8 in plaats van de standaard tekenset.

6.6. Dit probleem oplossen in onze programma's

Normaal gesproken zouden we dat moeten doen kies ervoor om een ​​tekenset te specificeren bij het omgaan met tekst in plaats van te vertrouwen op de standaardinstellingen. We kunnen de codering die we willen gebruiken expliciet aangeven in klassen die te maken hebben met conversies van teken naar byte.

Gelukkig specificeert ons voorbeeld de karakterset al. We hoeven alleen de juiste te selecteren en Java de rest te laten doen.

We zouden ons inmiddels moeten realiseren dat tekens met accenten zoals ‘ç 'niet aanwezig zijn in het coderingsschema ASCII en daarom hebben we een codering nodig die ze bevat. Misschien UTF-8?

Laten we dat proberen, we zullen nu de methode uitvoeren decodeText met dezelfde invoer maar met codering als "UTF-8":

Het gevelpatroon is een softwarematig ontwerppatroon.

Bingo! We kunnen nu de output zien die we hoopten te zien.

Hier hebben we de codering ingesteld waarvan we denken dat het het beste past bij onze behoefte in de constructor van InputStreamReader. Dit is meestal de veiligste methode om met tekens en byteconversies in Java om te gaan.

Evenzo OutputStreamWriter en vele andere API's ondersteunen het instellen van een coderingsschema via hun constructor.

6.7. MalformedInputException

Wanneer we een bytesequentie decoderen, zijn er gevallen waarin het niet legaal is voor het gegeven Tekenset, of anders is het geen legale zestien-bits Unicode. Met andere woorden, de gegeven bytesequentie heeft geen toewijzing in de opgegeven Tekenset.

Er zijn drie voorgedefinieerde strategieën (of CodingErrorAction) wanneer de invoerreeks een onjuiste invoer heeft:

  • NEGEREN zal misvormde tekens negeren en de codering hervatten
  • VERVANGEN zal de misvormde tekens in de uitvoerbuffer vervangen en de coderingsoperatie hervatten
  • VERSLAG DOEN VAN zal een MalformedInputException

De standaard misvormdeInputAction voor de CharsetDecoder is RAPPORT, en de standaard misvormdeInputAction van de standaard decoder in InputStreamReader is VERVANGEN.

Laten we een decoderingsfunctie definiëren die een opgegeven Tekenset, een CodingErrorAction type en een tekenreeks die moet worden gedecodeerd:

String decodeText (String-invoer, Charset charset, CodingErrorAction codingErrorAction) gooit IOException {CharsetDecoder charsetDecoder = charset.newDecoder (); charsetDecoder.onMalformedInput (codingErrorAction); retourneer nieuwe BufferedReader (nieuwe InputStreamReader (nieuwe ByteArrayInputStream (input.getBytes ()), charsetDecoder)). readLine (); }

Dus als we decoderen "Het gevelpatroon is een softwareontwerppatroon." met US_ASCII, zou de output voor elke strategie anders zijn. Ten eerste gebruiken we CodingErrorAction.IGNORE die illegale tekens overslaat:

Assertions.assertEquals ("Het gevelpatroon is een softwareontwerppatroon.", CharacterEncodingExamples.decodeText ("Het gevelpatroon is een softwareontwerppatroon.", StandardCharsets.US_ASCII, CodingErrorAction.IGNORE));

Voor de tweede test gebruiken we CodingErrorAction.REPLACE dat plaatst in plaats van de illegale karakters:

Assertions.assertEquals ("Het façadepatroon is een softwareontwerppatroon.", CharacterEncodingExamples.decodeText ("Het façadepatroon is een softwareontwerppatroon.", StandardCharsets.US_ASCII, CodingErrorAction.REPLACE));

Voor de derde test gebruiken we CodingErrorAction.REPORT wat leidt tot gooien MalformedInputException:

Assertions.assertThrows (MalformedInputException.class, () -> CharacterEncodingExamples.decodeText ("Het façadepatroon is een softwareontwerppatroon.", StandardCharsets.US_ASCII, CodingErrorAction.REPORT));

7. Andere plaatsen waar codering belangrijk is

Bij het programmeren hoeven we niet alleen rekening te houden met tekencodering. Teksten kunnen op veel andere plaatsen terminaal fout gaan.

De De meest voorkomende oorzaak van problemen in deze gevallen is de conversie van tekst van het ene coderingsschema naar het andere, waardoor mogelijk gegevensverlies ontstaat.

Laten we snel een paar plaatsen doornemen waar we problemen kunnen tegenkomen bij het coderen of decoderen van tekst.

7.1. Teksteditors

In de meeste gevallen is een teksteditor de oorsprong van teksten. Er zijn talloze teksteditors in populaire keuze, waaronder vi, Kladblok en MS Word. Bij de meeste van deze teksteditors kunnen we het coderingsschema selecteren. Daarom moeten we er altijd voor zorgen dat ze geschikt zijn voor de tekst die we behandelen.

7.2. Bestandssysteem

Nadat we teksten in een editor hebben gemaakt, moeten we ze in een bestandssysteem opslaan. Het bestandssysteem is afhankelijk van het besturingssysteem waarop het wordt uitgevoerd. De meeste besturingssystemen hebben inherente ondersteuning voor meerdere coderingsschema's. Er kunnen echter nog steeds gevallen zijn waarin een coderingsconversie tot gegevensverlies leidt.

7.3. Netwerk

Teksten die via een netwerk worden verzonden met behulp van een protocol zoals File Transfer Protocol (FTP), omvatten ook conversie tussen tekencoderingen. Voor alles dat in Unicode is gecodeerd, is het het veiligst om over te dragen als binair om het risico van conversieverlies te minimaliseren. Het overbrengen van tekst via een netwerk is echter een van de minder voorkomende oorzaken van gegevensbeschadiging.

7.4. Databases

De meeste populaire databases zoals Oracle en MySQL ondersteunen de keuze van het tekencoderingsschema bij de installatie of het maken van databases. We moeten dit kiezen in overeenstemming met de teksten die we verwachten op te slaan in de database. Dit is een van de meest voorkomende plaatsen waar de beschadiging van tekstgegevens optreedt als gevolg van coderingsconversies.

7.5. Browsers

Ten slotte maken we in de meeste webapplicaties teksten en sturen deze door verschillende lagen met de bedoeling om ze in een gebruikersinterface, zoals een browser, te bekijken. Ook hier is het voor ons absoluut noodzakelijk om de juiste tekencodering te kiezen die de tekens correct kan weergeven. Bij de meeste populaire browsers zoals Chrome en Edge kunt u de tekencodering kiezen via hun instellingen.

8. Conclusie

In dit artikel hebben we besproken hoe codering een probleem kan zijn tijdens het programmeren.

We hebben verder de basisprincipes besproken, waaronder codering en tekensets. Bovendien hebben we verschillende coderingsschema's en hun gebruik doorgenomen.

We hebben ook een voorbeeld opgepikt van onjuist gebruik van tekencodering in Java en hebben gezien hoe we dat goed konden doen. Ten slotte hebben we enkele andere veelvoorkomende foutscenario's besproken met betrekking tot tekencodering.

Zoals altijd is de code voor de voorbeelden beschikbaar op GitHub.