Anleitung zur Zeichenkodierung

Anleitung zur Zeichenkodierung

1. Überblick

In diesem Tutorial werden die Grundlagen der Zeichenkodierung und deren Umgang mit Java erläutert.

2. Bedeutung der Zeichenkodierung

Wir haben es oft mit Texten zu tun, die zu mehreren Sprachen gehören, mit unterschiedlichen Schriften wie Latein oder Arabisch. Jedes Zeichen in jeder Sprache muss irgendwie einer Reihe von Einsen und Nullen zugeordnet werden. Es ist wirklich ein Wunder, dass Computer alle unsere Sprachen korrekt verarbeiten können.

Um dies richtig zu machen, kannwe need to think about character encoding. Nichtbeachtung häufig zu Datenverlust und sogar zu Sicherheitslücken führen.

Um dies besser zu verstehen, definieren wir eine Methode zum Dekodieren eines Textes in Java:

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

Beachten Sie, dass der hier eingegebene Text die Standard-Plattformcodierung verwendet.

If we run this method with input as “The façade pattern is a software design pattern.” and encoding as “US-ASCII” wird ausgegeben:

The fa��ade pattern is a software design pattern.

Nun, nicht genau das, was wir erwartet hatten.

Was hätte schief gehen können? Wir werden versuchen, dies im Rest dieses Tutorials zu verstehen und zu korrigieren.

3. Grundlage

Bevor wir jedoch tiefer graben, lassen Sie uns kurz drei Begriffe überprüfen:encoding,charsets undcode point.

3.1. Codierung

Computer können nur binäre Darstellungen wie1 und0 verstehen. Für die Verarbeitung anderer Elemente ist eine Art Zuordnung vom realen Text zu seiner binären Darstellung erforderlich. This mapping is what we know as character encoding or simply just as encoding.

Zum Beispiel der erste Buchstabe in unserer Nachricht "T" in US-ASCIIencodes to "01010100".

3.2. Zeichensätze

Die Zuordnung von Zeichen zu ihren binären Darstellungen kann in Bezug auf die Zeichen, die sie enthalten, sehr unterschiedlich sein. Die Anzahl der Zeichen, die in einer Zuordnung enthalten sind, kann von wenigen bis zu allen Zeichen variieren, die in der Praxis verwendet werden. The set of characters that are included in a mapping definition is formally called a charset.

3.3. Codepunkt

Ein Codepunkt ist eine Abstraktion, die ein Zeichen von seiner eigentlichen Codierung trennt. A code point is an integer reference to a particular character.

Wir können die ganze Zahl selbst in einfachen Dezimal- oder alternativen Basen wie Hexadezimal oder Oktal darstellen. Wir verwenden alternative Basen, um die Verweisung großer Zahlen zu vereinfachen.

Zum Beispiel hat der erste Buchstabe unserer Nachricht, T, in Unicode einen Codepunkt "U + 0054" (oder 84 in Dezimal).

4. Grundlegendes zu Codierungsschemata

Eine Zeichenkodierung kann abhängig von der Anzahl der Zeichen, die sie kodiert, verschiedene Formen annehmen.

Die Anzahl der codierten Zeichen steht in direkter Beziehung zur Länge jeder Darstellung, die typischerweise als Anzahl der Bytes gemessen wird. Having more characters to encode essentially means needing lengthier binary representations.

Lassen Sie uns einige der heute in der Praxis gängigen Codierungsschemata durchgehen.

4.1. Einzelbyte-Codierung

Eines der frühesten Codierungsschemata, das als ASCII (American Standard Code for Information Exchange) bezeichnet wird, verwendet ein Einzelbyte-Codierungsschema. Dies bedeutet im Wesentlichen, dasseach character in ASCII is represented with seven-bit binary numbers. Dies lässt immer noch ein Bit in jedem Byte frei!

Der 128-Zeichen-Satz von ASCII umfasst englische Alphabete in Klein- und Großbuchstaben, Ziffern sowie einige Sonder- und Steuerzeichen.

Definieren wir eine einfache Methode in Java, um die Binärdarstellung für ein Zeichen unter einem bestimmten Codierungsschema anzuzeigen:

String convertToBinary(String input, String encoding)
      throws 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(" "));
}

Jetzt hat das Zeichen "T" in US-ASCII einen Codepunkt von 84 (ASCII wird in Java als US-ASCII bezeichnet).

Und wenn wir unsere Utility-Methode verwenden, können wir ihre binäre Darstellung sehen:

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

Dies ist erwartungsgemäß eine Sieben-Bit-Binärdarstellung für das Zeichen 'T'.

The original ASCII left the most significant bit of every byte unused. Gleichzeitig hatte ASCII eine ganze Reihe von Zeichen nicht dargestellt, insbesondere für nicht englische Sprachen.

Dies führte zu dem Versuch, dieses nicht verwendete Bit zu verwenden und zusätzliche 128 Zeichen einzuschließen.

There were several variations of the ASCII encoding scheme proposed and adopted over the time. Diese wurden lose als "ASCII-Erweiterungen" bezeichnet.

Viele der ASCII-Erweiterungen hatten unterschiedliche Erfolgsniveaus, aber dies war offensichtlich nicht gut genug für eine breitere Anwendung, da immer noch viele Zeichen nicht dargestellt wurden.

One of the more popular ASCII extensions was ISO-8859-1, auch als „ISO Latin 1“ bezeichnet.

4.2. Multi-Byte-Codierung

Mit dem wachsenden Bedarf an Platz für immer mehr Zeichen waren Einzelbyte-Codierungsschemata wie ASCII nicht nachhaltig.

Dies führte zu Mehrbyte-Codierungsschemata, die eine viel bessere Kapazität aufweisen, wenn auch auf Kosten eines erhöhten Platzbedarfs.

BIG5 und SHIFT-JIS sind Beispiele fürmulti-byte character encoding schemes which started to use one as well as two bytes to represent wider charsets. Die meisten davon wurden für die Darstellung chinesischer und ähnlicher Schriften mit einer deutlich höheren Anzahl von Zeichen erstellt.

Nennen wir nun die MethodeconvertToBinary mitinput als "語", einem chinesischen Schriftzeichen, undencoding als "Big5":

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

Die obige Ausgabe zeigt, dass die Big5-Codierung zwei Bytes verwendet, um das Zeichen "語" darzustellen.

Eincomprehensive list der Zeichenkodierungen wird zusammen mit ihren Aliasnamen von der International Number Authority verwaltet.

5. Unicode

Es ist nicht schwer zu verstehen, dass Kodierung wichtig ist, Dekodierung jedoch ebenso wichtig ist, um die Darstellungen zu verstehen. This is only possible in practice if a consistent or compatible encoding scheme is used widely.

Verschiedene Codierungsschemata, die isoliert entwickelt und in lokalen Regionen praktiziert wurden, wurden zu einer Herausforderung.

Diese Herausforderung führte zua singular encoding standard called Unicode which has the capacity for every possible character in the world. Dies schließt die verwendeten Zeichen und sogar die nicht mehr verwendeten Zeichen ein!

Nun, das muss mehrere Bytes erfordern, um jedes Zeichen zu speichern? Ehrlich gesagt ja, aber Unicode hat eine geniale Lösung.

Unicode as a standard defines code points for every possible character in the world. Der Codepunkt für das Zeichen 'T' in Unicode ist 84 in Dezimalzahl. Wir bezeichnen dies im Allgemeinen als "U + 0054" in Unicode, was nichts anderes als U + gefolgt von der Hexadezimalzahl ist.

Wir verwenden Hexadezimal als Basis für Codepunkte in Unicode, da es 1.114.112 Punkte gibt, eine ziemlich große Zahl, um bequem dezimal zu kommunizieren!

How these code points are encoded into bits is left to specific encoding schemes within Unicode. Wir werden einige dieser Codierungsschemata in den folgenden Unterabschnitten behandeln.

5.1. UTF-32

UTF-32 istan encoding scheme for Unicode that employs four bytes to represent every code point, definiert durch Unicode. Offensichtlich ist es ineffizient, vier Bytes für jedes Zeichen zu verwenden.

Mal sehen, wie ein einfaches Zeichen wie "T" in UTF-32 dargestellt wird. Wir werden die zuvor eingeführte MethodeconvertToBinary verwenden:

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

Die obige Ausgabe zeigt die Verwendung von vier Bytes, um das Zeichen "T" darzustellen, wobei die ersten drei Bytes nur verschwendeten Speicherplatz darstellen.

5.2. UTF-8

UTF-8 istanother encoding scheme for Unicode which employs a variable length of bytes to encode. Während im Allgemeinen ein einzelnes Byte zum Codieren von Zeichen verwendet wird, kann bei Bedarf eine höhere Anzahl von Bytes verwendet werden, wodurch Platz gespart wird.

Rufen wir noch einmal die MethodeconvertToBinary mit der Eingabe als "T" und der Codierung als "UTF-8" auf:

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

Die Ausgabe ist mit nur einem Byte genau wie bei ASCII. Tatsächlich ist UTF-8 vollständig abwärtskompatibel mit ASCII.

Rufen wir noch einmal die MethodeconvertToBinary mit der Eingabe als "語" und der Codierung als "UTF-8" auf:

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

Wie wir hier sehen können, verwendet UTF-8 drei Bytes, um das Zeichen "語" darzustellen. This is known as variable-width encoding.

UTF-8 ist aufgrund seiner Speicherplatzeffizienz die im Web am häufigsten verwendete Codierung.

6. Codierungsunterstützung in Java

Java unterstützt eine Vielzahl von Codierungen und deren Konvertierungen ineinander. Die KlasseCharset definiert einset of standard encodings, zu dessen Unterstützung jede Implementierung der Java-Plattform verpflichtet ist.

Dies umfasst unter anderem US-ASCII, ISO-8859-1, UTF-8 und UTF-16. A particular implementation of Java may optionally support additional encodings.

Es gibt einige Feinheiten in der Art und Weise, wie Java einen Zeichensatz aufnimmt, mit dem gearbeitet werden soll. Lassen Sie uns sie genauer durchgehen.

6.1. Standardzeichensatz

Die Java-Plattform hängt stark von einer Eigenschaft namensthe default charset ab. The Java Virtual Machine (JVM) determines the default charset during start-up.

Dies hängt vom Gebietsschema und dem Zeichensatz des zugrunde liegenden Betriebssystems ab, auf dem JVM ausgeführt wird. Unter MacOS ist der Standardzeichensatz beispielsweise UTF-8.

Mal sehen, wie wir den Standardzeichensatz bestimmen können:

Charset.defaultCharset().displayName();

Wenn wir diesen Codeausschnitt auf einem Windows-Computer ausführen, erhalten wir folgende Ausgabe:

windows-1252

Jetzt ist "windows-1252" der Standardzeichensatz der Windows-Plattform auf Englisch, der in diesem Fall den Standardzeichensatz von JVM bestimmt hat, der unter Windows ausgeführt wird.

6.2. Wer verwendet den Standardzeichensatz?

Viele der Java-APIs verwenden den von der JVM festgelegten Standardzeichensatz. Um ein paar zu nennen:

Wenn wir also unser Beispiel ausführen, ohne den Zeichensatz anzugeben, bedeutet dies:

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

dann würde es den Standardzeichensatz verwenden, um es zu dekodieren.

Und es gibt mehrere APIs, die standardmäßig dieselbe Auswahl treffen.

Der Standardzeichensatz nimmt daher eine Bedeutung an, die wir nicht sicher ignorieren können.

6.3. Probleme mit dem Standardzeichensatz

Wie wir gesehen haben, wird der Standardzeichensatz in Java dynamisch bestimmt, wenn die JVM gestartet wird. Dies macht die Plattform weniger zuverlässig oder fehleranfällig, wenn sie unter verschiedenen Betriebssystemen verwendet wird.

Zum Beispiel, wenn wir rennen

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

Unter MacOS wird UTF-8 verwendet.

Wenn wir dasselbe Snippet unter Windows versuchen, wird Windows-1252 verwendet, um denselben Text zu dekodieren.

Oder stellen Sie sich vor, Sie schreiben eine Datei auf einem MacOS und lesen dieselbe Datei unter Windows.

Es ist nicht schwer zu verstehen, dass dies aufgrund unterschiedlicher Codierungsschemata zu Datenverlust oder Beschädigung führen kann.

6.4. Können wir den Standardzeichensatz überschreiben?

Die Bestimmung des Standardzeichensatzes in Java führt zu zwei Systemeigenschaften:

  • file.encoding: Der Wert dieser Systemeigenschaft ist der Name des Standardzeichensatzes

  • sun.jnu.encoding: Der Wert dieser Systemeigenschaft ist der Name des Zeichensatzes, der beim Codieren / Decodieren von Dateipfaden verwendet wird

Es ist jetzt intuitiv, diese Systemeigenschaften über Befehlszeilenargumente zu überschreiben:

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

Es ist jedoch wichtig zu beachten, dass diese Eigenschaften in Java schreibgeschützt sind. Their usage as above is not present in the documentation. Das Überschreiben dieser Systemeigenschaften hat möglicherweise kein gewünschtes oder vorhersehbares Verhalten.

Daher istwe should avoid overriding the default charset in Java.

6.5. Warum löst Java dies nicht?

Es gibt einJava Enhancement Proposal (JEP) which prescribes using “UTF-8” as the default charset in Java, anstatt es auf das Gebietsschema und den Zeichensatz des Betriebssystems zu stützen.

Dieses JEP befindet sich ab sofort im Entwurfszustand, und wenn es (hoffentlich!) Durchläuft, werden die meisten Probleme, die wir zuvor besprochen haben, gelöst.

Beachten Sie, dass die neueren APIs wie die injava.nio.file.Files nicht den Standardzeichensatz verwenden. Die Methoden in diesen APIs lesen oder schreiben Zeichenströme mit Zeichensatz als UTF-8 und nicht mit dem Standardzeichensatz.

6.6. Lösung dieses Problems in unseren Programmen

Wir sollten normalerweisechoose to specify a charset when dealing with text instead of relying on the default settings. Wir können explizit die Codierung deklarieren, die wir in Klassen verwenden möchten, die sich mit Zeichen-zu-Byte-Konvertierungen befassen.

Zum Glück spezifiziert unser Beispiel bereits den Zeichensatz. We just need to select the right one and let Java do the rest.

Wir sollten jetzt erkennen, dass akzentuierte Zeichen wie „ç“ im Codierungsschema ASCII nicht vorhanden sind und daher eine Codierung benötigen, die sie enthält. Vielleicht UTF-8?

Versuchen wir das, wir werden jetzt die MethodedecodeText mit derselben Eingabe ausführen, aber mit der Codierung als "UTF-8":

The façade pattern is a software-design pattern.

Bingo! Wir können die Ergebnisse sehen, die wir uns jetzt erhofft hatten.

Hier haben wir die Codierung festgelegt, die unserer Meinung nach am besten zu unseren Anforderungen im Konstruktor vonInputStreamReader passt. Dies ist normalerweise die sicherste Methode für den Umgang mit Zeichen und Bytekonvertierungen in Java.

In ähnlicher Weise unterstützenOutputStreamWriter und viele andere APIs das Festlegen eines Codierungsschemas über ihren Konstruktor.

6.7. MalformedInputException

Wenn wir eine Byte-Sequenz dekodieren, gibt es Fälle, in denen sie für die angegebenenCharset nicht zulässig ist, oder es handelt sich nicht um einen zulässigen 16-Bit-Unicode. Mit anderen Worten, die gegebene Bytesequenz hat keine Zuordnung in den angegebenenCharset.

Es gibt drei vordefinierte Strategien (oderCodingErrorAction), wenn die Eingabesequenz eine fehlerhafte Eingabe aufweist:

  • IGNORE ignoriert fehlerhafte Zeichen und setzt den Codierungsvorgang fort

  • REPLACE ersetzt die fehlerhaften Zeichen im Ausgabepuffer und setzt den Codierungsvorgang fort

  • REPORT wirftMalformedInputException

Der StandardwertmalformedInputAction fürCharsetDecoder is REPORT, und der StandardwertmalformedInputAction des Standarddecoders inInputStreamReader istREPLACE.

Definieren wir eine Decodierungsfunktion, die einen bestimmtenCharset, einenCodingErrorAction-Typ und eine zu decodierende Zeichenfolge empfängt:

String decodeText(String input, Charset charset,
  CodingErrorAction codingErrorAction) throws IOException {
    CharsetDecoder charsetDecoder = charset.newDecoder();
    charsetDecoder.onMalformedInput(codingErrorAction);
    return new BufferedReader(
      new InputStreamReader(
        new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}

Wenn wir also dekodieren: "Das Fassadenmuster ist ein Software-Entwurfsmuster." MitUS_ASCII wäre die Ausgabe für jede Strategie unterschiedlich. Zuerst verwenden wirCodingErrorAction.IGNORE, die illegale Zeichen überspringen:

Assertions.assertEquals(
  "The faade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.IGNORE));

Für den zweiten Test verwenden wirCodingErrorAction.REPLACE, bei denen � anstelle der unzulässigen Zeichen gesetzt wird:

Assertions.assertEquals(
  "The fa��ade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.REPLACE));

Für den dritten Test verwenden wirCodingErrorAction.REPORT, was zum Werfen vonMalformedInputException: führt

Assertions.assertThrows(
  MalformedInputException.class,
    () -> CharacterEncodingExamples.decodeText(
      "The façade pattern is a software design pattern.",
      StandardCharsets.US_ASCII,
      CodingErrorAction.REPORT));

7. Andere Orte, an denen die Codierung wichtig ist

Wir müssen beim Programmieren nicht nur die Zeichenkodierung berücksichtigen. Texte können an vielen anderen Stellen tödlich schief gehen.

Diemost common cause of problems in these cases is the conversion of text from one encoding scheme to another, wodurch möglicherweise Datenverlust verursacht wird.

Lassen Sie uns schnell einige Stellen durchgehen, an denen beim Codieren oder Decodieren von Text Probleme auftreten können.

7.1. Texteditoren

In den meisten Fällen stammen die Texte aus einem Texteditor. Es gibt zahlreiche Texteditoren, darunter vi, Notepad und MS Word. In den meisten dieser Texteditoren können wir das Codierungsschema auswählen. Daher sollten wir immer sicherstellen, dass sie für den von uns behandelten Text geeignet sind.

7.2. Dateisystem

Nachdem wir Texte in einem Editor erstellt haben, müssen wir sie in einem Dateisystem speichern. Das Dateisystem hängt vom Betriebssystem ab, auf dem es ausgeführt wird. Die meisten Betriebssysteme unterstützen inhärent mehrere Kodierungsschemata. Es kann jedoch immer noch Fälle geben, in denen eine Codierungskonvertierung zu Datenverlust führt.

7.3. Netzwerk

Bei der Übertragung von Texten über ein Netzwerk mit einem Protokoll wie File Transfer Protocol (FTP) werden auch Zeichenkodierungen konvertiert. Für alles, was in Unicode codiert ist, ist es am sichersten, als Binärdatei zu übertragen, um das Risiko eines Konvertierungsverlusts zu minimieren. Die Übertragung von Text über ein Netzwerk ist jedoch eine der selteneren Ursachen für Datenkorruption.

7.4. Datenbanken

Die meisten gängigen Datenbanken wie Oracle und MySQL unterstützen die Auswahl des Zeichencodierungsschemas bei der Installation oder Erstellung von Datenbanken. Wir müssen dies in Übereinstimmung mit den Texten wählen, die wir in der Datenbank speichern wollen. Dies ist einer der häufigsten Orte, an denen die Korruption von Textdaten aufgrund von Codierungskonvertierungen auftritt.

7.5. Browser

Schließlich erstellen wir in den meisten Webanwendungen Texte und leiten sie durch verschiedene Ebenen, um sie auf einer Benutzeroberfläche wie einem Browser anzuzeigen. Auch hier ist es unerlässlich, die richtige Zeichenkodierung zu wählen, die die Zeichen korrekt darstellen kann. In den meisten gängigen Browsern wie Chrome und Edge können Sie die Zeichenkodierung über die Einstellungen auswählen.

8. Fazit

In diesem Artikel wurde erläutert, wie das Codieren beim Programmieren zu Problemen führen kann.

Wir haben die Grundlagen einschließlich Codierung und Zeichensätze weiter besprochen. Darüber hinaus haben wir verschiedene Kodierungsschemata und deren Verwendung durchlaufen.

Wir haben auch ein Beispiel für eine falsche Zeichencodierung in Java aufgegriffen und gesehen, wie man das richtig macht. Schließlich haben wir einige andere häufige Fehlerszenarien im Zusammenhang mit der Zeichencodierung erörtert.

Wie immer ist der Code für die Beispieleover on GitHub verfügbar.