Kolumne: C# im Fokus

Encoding von Zeichenketten
Kommentare

Die letzte Ausgabe der Kolumne beschäftigte sich mit der Verarbeitung von Zeichen und Zeichenketten im Speicher. Oft müssen aber Zeichenketten aus unterschiedlichen Orten, wie zum Beispiel Dateien oder Webdiensten, abgerufen und verarbeitet werden. Dabei ist die Verwendung eines korrekten Encodings wichtig.

Solange Zeichenketten lediglich im Speicher verarbeitet werden, spielt das Thema Encoding noch keine Rolle. Innerhalb des .NET Frameworks wird für die interne Speicherung in der Common Language Runtime (CLR) Unicode verwendet. Genauer gesagt handelt es sich hierbei um das Unicode Transformation Format 16, kurz UTF-16; also um 16 Bit breite Zeichen [1]. Alle internen Zeichenketten setzen sich somit aus UTF-16 kodierten Zeichen zusammen. Dies hat den wesentlichen Vorteil, dass nahezu alle Zeichen – auch Sonderzeichen – abgebildet werden können. Somit wird die interne Verarbeitung von Zeichenketten erheblich vereinfacht. Oft besteht aber die Notwendigkeit, Zeichenketten über ein Netzwerk zu versenden oder Text aus vorhandenen Dateien einzulesen. Gerade bei dem Zugriff auf (Text-)Dateien, die von älteren Anwendungssystemen (automatisch) erstellt wurden, kann man nicht davon ausgehen, dass diese in einem Unicode-Format gespeichert sind. Bei der Versendung von Text über das Netzwerk kann durch die richtige Anwendung von Encoding und Decoding die zu übertragende Textmenge reduziert werden. Soll zum Beispiel ein Text übertragen werden, der lediglich Buchstaben und Zahlen beinhaltet, können die 16-Bit-Werte zunächst per Encode in ein komprimiertes Byte-Array überführt werden. Anschließend kann dieses per Decode wieder in die ursprüngliche Form überführt werden.

Codepages und Unicode

In den frühen Anfängen des Personals Computers wurden Zeichen im ASCII-Format gespeichert [2]. Hierbei handelt es sich um eine 7 Bit breite Speicherung, d. h., es können insgesamt 128 Zeichen kodiert werden. Innerhalb der ASCII-Kodierung wird jedem Zeichen eine Nummer zugeordnet, zum Beispiel ist das A unter der Nummer 65 abgelegt. Somit konnten alle allgemeinen Zeichen, zum Beispiel A-Z, a-z, 0-9 sowie einige Sonderzeichen, innerhalb der 7m Bits kodiert werden. Da die nächste Speichergröße jedoch ein Byte beträgt, blieb zunächst ein Bit unbenutzt. Auf Basis des verbleibenden Bits entwickelten sich im Lauf der Zeit verschiedene Codepages [3]. Die freien zusätzlichen Nummern wurden dazu verwendet länderspezifische Zeichen zu hinterlegen. So enthält die Codepage 850 (DOS-Latin-1) an der Position 142 den Buchstaben Ä und die Codepage 855 (Kyrillisch) den Buchstaben j. Mithilfe der Codepages ist es somit möglich geworden, länderspezifische Zeichen zu verwenden. Allerdings reichen die verbleibenden 127 Zeichen nicht aus, um alle länderspezifischen Zeichen zu kodieren. Deutlich wird dieses Problem bei dem Versuch, alle chinesischen Zeichen in ein Byte zu kodieren. Für die Ablage dieser Zeichen werden mindestens zwei Bytes benötigt. Zunächst wurde dafür das nicht standardisierte Double Byte Character Set (DBCS) verwendet [4]. Erst mit der Einführung von Unicode wurde ein standardisiertes System umgesetzt, das in der Lage ist, alle möglichen Zeichen unabhängig von der Schriftart zu verwalten. Innerhalb von Unicode werden Zeichen über einen so genannten Code Point adressiert. Ein typischer Code Point lautet zum Beispiel U+0639.

UTF-Formate

Wie eingangs erwähnt wurde, speichert die CLR intern alle Zeichenketten im UTF-16-Format ab. Das heißt, jedes Zeichen wird innerhalb eines zwei Byte großen Speicherbereichs abgelegt. Neben UTF-16 existieren noch die Formate UTF-7, UTF-8 und UTF-32. Aus den Zahlen geht auch hier jeweils die Bit-Tiefe hervor. UTF-8 kodiert die Zeichen abhängig von deren Wertigkeit. Zeichen, die sich gemäß ASCII-Code unterhalb von 128 befinden, werden komprimiert in einem Byte gespeichert. Zeichen zwischen 128 und 2047 werden in zwei Bytes gespeichert. Für Zeichen über 2047 werden drei Bytes benötigt. Reichen die zwei bzw. drei Bytes für die Speicherung nicht aus, werden so genannte Surrogates innerhalb von vier Bytes gespeichert. Die UTF-8-Kodierung ist sehr effizient, da Zeichen ihrem tatsächlichen Speicherplatzbedarf gemäß abgelegt werden. UTF-32 dagegen kodiert jedes Zeichen mit vier Byte und benötigt daher wesentlich mehr Speicherplatz. UTF-32 eignet sich daher nicht optimal für die Speicherung von Daten oder für die Versendung von Daten über das Netzwerk. Das Format eignet sich eher für Algorithmen, die einzelne Zeichen einer Zeichenkette analysieren müssen. Bei dem Format kann einfach von Zeichen zu Zeichen gesprungen werden, da jedes Zeichen eine feste Bit-Breite besitzt. UTF-7 wurde speziell für die Übertragung von Daten über das Netzwerk entworfen, hat sich aber aufgrund der Verbreitung der Base64-Kodierung nicht durchsetzen können und gilt mittlerweile als veraltet.

ASCII

Das .NET Framework unterstützt weiterhin die ASCII-Zeichenkodierung. Innerhalb von ASCII steht ein Byte für die Kodierung der einzelnen Zeichen zur Verfügung, wobei lediglich sieben Bits genutzt werden. D. h., wird eine UTF-16 kodierte Zeichenkette in das ASCII-Format überführt, gehen alle Zeichen über 127 (0x007F) verloren. Listing 1 zeigt dazu ein Beispiel. Die Zeichenkette „ABC ÖÄÜ“, die intern als Unicode gespeichert ist, wird per ASCII encodiert. Anschließend wird das kodierte Byte-Array decodiert und das Ergebnis auf der Konsole ausgegeben. Abbildung 1 zeigt die Ausgabe. Wie erkennbar ist, könnten die Umlaute nicht kodiert werden und gehen somit verloren. Aufgrund des geringeren Speicherplatzbedarfs pro Zeichen eignet sich das ASCII-Format sehr gut, wenn lediglich die unterstützten Zeichen zum Einsatz kommen.

Listing 1

public static void EncodeToASCII()
{
    string sample = "ABC ÖÄÜ";
    Encoding ascii = Encoding.ASCII;
    Byte[] byteASCII = ascii.GetBytes(sample);
    Console.WriteLine("Dekodierte Zeichenkette: {0}", 
                           ascii.GetString(byteASCII));

}  

Abb. 1: ASCII Encodierung mit Sonderzeichen
Abb. 1: ASCII Encodierung mit Sonderzeichen
Encoding mit C#

C# enthält viele Möglichkeiten, Text in verschiedene Formate zu encodieren (encoding) und zu decodieren (decoding). Eine Encodierung findet zum Beispiel immer dann statt, wenn ein Datenstrom (Data Stream) über das Netzwerk versendet werden soll. Typischerweise kommen dabei die Klassen System.IO.BinaryWriter und System.IO.StreamWriter zum Einsatz. Auf der Empfängerseite findet dann mithilfe der korrespondierenden Klassen System.IO.StreamReader und System.IO.BinaryReader eine Decodierung des Datenstroms statt. Wird kein Encodierungs-Format vorgegeben, verwenden die genannten Klassen UTF-8. Listing 2 zeigt dazu ein Beispiel. Zunächst wird eine Textdatei erstellt und unter Verwendung einer ASCII-Kodierung gespeichert. Anschließend wird die gespeicherte Datei unter Verwendung einer Unicode-Kodierung wieder eingelesen und der Inhalt, wie in Abbildung 2 zu sehen ist, ausgegeben. Wie deutlich wird, kann der Inhalt der Datei nicht ordnungsgemäß wiedergegeben werden, da die verwendeten Kodierungen nicht kompatibel zueinander sind. Um die Datei richtig einlesen zu können, muss ebenfalls die ASCII-Kodierung verwendet werden. In diesem Beispiel ist dies noch offensichtlich, greift man allerdings auf fremde Datenquellen zu, ist die Kodierung oft nicht bekannt. Oft bedeutet dies dann zunächst, verschiedene Kodierungen zu testen.

Listing 2

public static void SaveTextToFile()
{
    string[] content = { "Keine Sonderzeichen", "Einfacher plain Text", 
                             "Nun mit Zeichen über 7-Bit: öäü" };
    File.WriteAllLines(@"C:ASCIIFile.txt", content, Encoding.ASCII);
    content = File.ReadAllLines(@"C:ASCIIFile.txt", Encoding.ASCII);
    foreach (string line in content)
        Console.WriteLine(line);
}  

Abb. 2: Einlesen von Inhalten mit falscher Encodierung
Abb. 2: Einlesen von Inhalten mit falscher Encodierung
Codepages

Wie weiter oben schon erläutert wurde, wurde das achte freie Bit bei dem ASCII-Format für länderspezifische Zeichen verwendet. Im Lauf der Zeit entstanden eine Menge unterschiedlicher Codepages. Genau wie bei der Kodierung und Decodierung ist es wichtig, jeweils kompatible Codepages zu verwenden. Geschieht dies nicht, kann der Inhalt nicht korrekt interpretiert werden. Das .NET Framework ermöglicht über die statische Methode GetEncoding die Kodierung nach verschiedenen Codepages. Dazu gibt Listing 3 ein Beispiel. Zunächst wird eine Zeichenkette gemäß der englischen Codepage 437 kodiert. Danach werden die Zeichen wieder mit der gleichen Codepage decodiert. Anschließend erfolgt eine Decodierung mit der nicht kompatiblen, türkischen Codepage 857. Das Ergebnis ist in Abbildung 3 zu sehen. Wie zu erkennen ist, wird das letzte Sonderzeichen falsch dargestellt.

Listing 3

public static void SaveToASCII()
{
    string sample = "ABC ÖäÜ⌐";
    Encoding cpEN = Encoding.GetEncoding(437);
    Byte[] byteCodePageEnglish = cpEN.GetBytes(sample);
    Console.WriteLine("Dekodierte Zeichenkette: {0}", 
                          cpEN.GetString(byteCodePageEnglish));
    Encoding cptk = Encoding.GetEncoding(857);
    Console.WriteLine("Dekodierte Zeichenkette: {0}", 
                          cptk.GetString(byteCodePageEnglish));
}  

Abb. 3: Auswirkungen einer falschen Codepage
Abb. 3: Auswirkungen einer falschen Codepage
Zusammenfassung

Die heutige Kolumne hat einen ersten Einstieg in das Thema Textkodierung gegeben. Wie deutlich wurde, muss die verwendete Kodierung bekannt sein, damit Inhalte ordnungsgemäß eingelesen und verarbeitet werden können. Die nächste Ausgabe der Kolumne geht vertiefender auf einige Kodierungsaspekte ein.

Marc André Zhou ist als freiberuflicher .NET-Technologieberater tätig. Seine Schwerpunkte sind die .NET-Entwicklung, SharePoint, Softwarearchitekturen und Frameworks. Sie erreichen ihn unter zhou@dev-sky.net.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -