UTF-8, ISO-8859-1, og slike ting


lørdag 30. mai 2015 C# Historie

Pope embraces unicode

In the beginning, there was ASCII, and things were simple. But they weren't good, for no one could write in Cyrillic or Thai. So there exploded a proliferation of character encodings .. UTF-8: The Secret of Character Encoding

Tegnsett og encoding er vanskelig! Eller egentlig ikke, det er ganske elementært, men av en eller annen grunn nekter liksom hjernen å ta vare på kunnskap om ulike tegnsett, encoding, konvertering og slike ting. Jeg vet ikke hvorfor, men jeg kjenner flere som har det sånn.

La oss leke litt med det, gjøre det ufarlig, og se om vi kan lære noe. Vi skal ikke gjøre noe avansert, bare helt grunnleggende ting som de fleste har bruk for.

NB: Jeg kommer til å bruke ordet "tegnkoding", og mener da det som på engelsk heter "character encoding". Dette er IKKE det samme som "tegnsett" (character set), men disse begrepene blandes mye (og har endret betydning gjennom historien). Et tegnsett definerer hvilke tegn man har tilgjengelige, mens en tegnkoding definerer hvordan tegnene representeres - i vårt tilfelle hvilke binære koder som representerer hvilke tegn.

Ok, la oss begynne med litt data...

Hva er dette?

01010100 01101111 01110010 01100010
01101010 11111000 01110010 01101110 

Dette er åtte bytes. Eller octets om du vil. Om jeg konverterer dem til titallsystemet får vi:

84 111 114 98 106 248 114 110 

Eller vi kan representere dem i 16-tallsystemet, altså som hexadesimaler, som også er ganske vanlig:

54 6F 72 62 6A F8 72 6E

ISO-8859-1

En tegnkoding bestemmer som sagt hvilke bokstaver disse bytene representerer. En veldig vanlig tegnkoding er ISO-8859-1, også kalt latin1. Det ble definert av ECMA i 1985, og var basert på et tegnsett brukt av den populære terminalen VT220 fra Digital Equipment Corporation (DEC). Samme år valgte Commodore denne tegnkodingen for sitt nye AmigaOS. ISO-8859-1 ble også valgt som en del av http-standarden. Default encoding for alle MIME-typer som begynner på text/ skal nemlig i følge standarden være 8859-1.

Ved å slå opp i ISO-8859-1 kan vi se hva bytene betyr:

84  111 114 98  106 248 114 110 
54  6F  72  62  6A  F8  72  6E
T   o   r   b   j   ø   r   n

Akkurat, ja, det var navnet mitt dette her :)

UTF-8

UTS-8's vekst

ISO-8859-1 er derimot ikke lenger den tegnkodingen man støter på oftest. Verden har de senere årene gått mer og mer over til UTF-8, som kan representere alle tegn i Unicode. UTF-8 ble oppfunnet av Ken Thompson, som er aller mest kjent for å ha designet og implementert operativsystemet Unix. Sammen med Rob Pike implementerte han UTF-8 i 1992.

Yngre utviklere vil kanskje også kjenne igjen disse to navnene; både Rob og Ken jobber for Google i dag, og har vært delaktige i utviklingen av programmeringsspråket Go.

Regel nummer 1 når man skal forholde seg til tegnkoding er at tekst ikke inneholder informasjon om hvilken koding som er brukt. Normalt sett da - her er det unntak, men la oss holde det enkelt. Tekst er bare en rekke bytes, og du må selv vite hvilken tegnkoding du skal bruke for å tolke dem. En fil som inneholder teksten Torbjørn og er kodet med ISO-8859-1 inneholder åtte bytes - de bytene jeg startet denne posten med.

Om filen derimot er kodet med UTF-8 vil den inneholde ni bytes! UTF-8 bruker et variablet antall bytes for å representere et tegn; én byte for de vanligste tegnene, men opp til fire bytes (og i teorien enda mer) for mer uvanlige tegn.

Som Joel Spolsky sa det:

It does not make sense to have a string without knowing what encoding it uses.

Navnet mitt kodet i UTF-8, representert som hex, er:

54 6F 72 62 6A C3 B8 72 6E

Som du ser er dette ganske likt ISO-8859-1:

ISO:  54 6F 72 62 6A F8    72 6E
UTF8: 54 6F 72 62 6A C3 B8 72 6E

Forskjellen er ikke uventet representasjonen av ø, som for de fleste mennesker er et ukjent eller i alle fall uvanlig tegn.

Å bytte tegnkoding

Tekststrenger i for eksempel C# (og mange andre språk) er i utgangspunktet UTF-8, og da er alt greit. Men om man henter inn tekst fra et annet sted - for eksempel en fil - og denne teksten ikke er UTF-8, så har man et problem. Da må man typisk fortelle hvilken encoding som skal benyttes, eller konvertere til UTF-8.

La oss anta vi har to filer som inneholder "Torbjørn" i de to tegnkodingene:

string f1 = @"c:\temp\name.utf8.txt";
string f2 = @"c:\temp\name.latin1.txt";

Om jeg leser den første file og skriver ut innholdet så går det fint:

Console.WriteLine(System.IO.File.ReadAllText(f1));

Men om jeg gjør det samme med f2 så blir ø'en feiltolket. Da må jeg spesifisere encoding:

Encoding iso = Encoding.GetEncoding("ISO-8859-1");
Console.WriteLine(System.IO.File.ReadAllText(f2, iso));

Vil jeg se på de faktiske bytene kan jeg lese dem direkte, uten å spesifisere noen encoding:

byte[] data1 = System.IO.File.ReadAllBytes(f1);
byte[] data2 = System.IO.File.ReadAllBytes(f2);

Og så har vi en fin verktøykasse som heter BitConverter som jeg kan bruke til å skrive ut bytene om jeg vil:

Console.WriteLine(BitConverter.ToString(data1));
Console.WriteLine(BitConverter.ToString(data2));
// Skriver ut
// 54-6F-72-62-6A-C3-B8-72-6E
// 54-6F-72-62-6A-F8-72-6E

I et prosjekt jeg gjorde nylig leste og behandlet jeg eksportfiler fra et regnskapssystem, og de var i ISO-8859-1. Men jeg skulle også bruke dataene til å generere noen UTF-8-kodede XML-filer. Da fant jeg ut det var greit å ha en metode som konverterer en streng fra ISO til UTF-8:

public static string Iso88591ToUtf8(string text)
{
    Encoding iso = Encoding.GetEncoding("ISO-8859-1");
    Encoding utf8 = Encoding.UTF8;
    byte[] isoBytes = iso.GetBytes(text);
    byte[] utfBytes = Encoding.Convert(iso, utf8, isoBytes);
    return utf8.GetString(utfBytes);
}

I Iso88591ToUtf8() ser du hvordan du generelt bruker Encoding til å konvertere string til bytes, bytes til string, og fra bytes i en tegnkoding til bytes i en annen tegnkoding.

Phhhuu, det er jammen vanskelig å si "tegnkoding" hele tiden - jeg er vandt til å si tegnsett, men om du ikke tenker på det som to ulike ting så er det lett å bli forvirret.

Likte du ideen bak denne bloggposten, men synes den var dårlig gjennomført, kan du i stedet lese den klassiske posten The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets av Joel Spolsky.


comments powered by Disqus