UCS-2, UTF-8, UTF-16, UTF-32, WTF?



  • Hallo zusammen,

    in unserer Software setzen wir weitestgehend std::string ein, was jetzt langsam zu Problemen führt, wenn Sprachen mit exotischen Schriftzeichen benutzt werden. Die Software ist alt, gewachsen und stammt aus einer Zeit, in der Texte aus der GUI als 8bit-codierter Text behandelt wurden. Inzwischen spricht das GUI 16bittig und das wird anhand der aktuellen Codepage als 8bit kodiert und auf gleichem Weg wieder zurück. Um dieses Problem möchten wir uns jetzt kümmern und überlegen gerade, welche Stringrepräsentation da wohl geignet ist. Bisher dachte ich, dass Windows intern UCS-2 benutzt und alle Zeichen als 16bit Wert abgelegt werden. Das ist ab Windows 2000 nicht mehr so, ab da wird UTF-16 mit zwei oder vier Byte pro Zeichen gearbeitet. Wir stellen uns gerade die Frage, welche Kodierung sinnvoll ist. Sowohl bei UTF-8 als auch bei UTF-16 hat man jetzt das Problem, dass ein einfaches Ersetzen von Zeichen kompliziert wird, weil ein einzelnes Zeichen plötzlich durch ein bis fünf Byte (UTF-8) oder zwei/vier Byte (UTF-16) ersetzt werden kann. Oder man kann nicht mehr nach 'Ä' suchen, weil es durch zwei Byte repräsentiert wird. UTF-32 hat diese Probleme nicht, verbraucht aber generell 4 Byte pro Zeichen.
    Hat da jemand Erfahrungen und kann mir was empfehlen?

    PS:
    Wir sind ausschließlich unter Windows unterwegs.


  • Mod

    @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Sowohl bei UTF-8 als auch bei UTF-16 hat man jetzt das Problem, dass ein einfaches Ersetzen von Zeichen kompliziert wird, weil ein einzelnes Zeichen plötzlich durch ein bis fünf Byte (UTF-8) oder zwei/vier Byte (UTF-16) ersetzt werden kann. Oder man kann nicht mehr nach 'Ä' suchen, weil es durch zwei Byte repräsentiert wird.

    Sind das für dich denn solch unüberwindbare Probleme? Wenn du ein ganzes Wort Suchen oder Ersetzen wolltest, wäre das doch das gleiche, und da würde niemand drüber jammern.

    Für Kommunikation nach außen nimm auf jeden Fall immer UTF-8. D.h. beispielsweise speicher niemals UTF-16 o.Ä. in einer Textdatei. Sonst kann niemand deine Kommunikation verstehen. Und wenn du das akzeptierst, bietet es sich auch an, dass man es als internes Format benutzt, damit man keinen Bruch hat zwischen der internen und der externen Darstellung. std::string ist schon gar nicht so schlecht für die Verarbeitung von UTF-8, für viele einfache Operationen geht das direkt von Haus aus.



  • Wenn du mit 'Ä' meinst dass es aus einem 8Bit (UTF-8), 16Bit (UTF-16) oder 32Bit (UTF-32) "block" besteht, dann wirst du selbst bei UTF-32 nicht einfach danach suchen können.
    Bzw. es als ein Codepoint betrachten können

    Denn in unicode können Zeichen auch aus mehreren codepoints zusammengesetzt sein (Stichwort Decomposition).

    z.b. das Ä hat den codepoint: "U+00C4"
    Aber kann auch durch die codepoints "U+0041" (A) und "U+0308" (Die beiden Punkte über dem A) kodiert werden.

    Was man noch beachten sollte ist dass es UTF-16/32 in zwei Varianten gibt: LE und BE. (In welcher reihenfolge die bytes innerhalb des 16/32Bit "blocks" angeordnet sind.

    Welches format man am besten nimmt, bezogen auf den speicherverbrauch, ist abhängig in welcher "Sprache" die meisten Texte im Programm vorkommen können.

    Wenn zum großteil nur Latin-1 verwendet wird dann ist UTF-8 die beste wahl da hier am wenigsten Speicher benötigt wird da großteil der Zeichen dann in 8Bit kodiert sind.



  • @SeppJ sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Sind das für dich denn solch unüberwindbare Probleme? Wenn du ein ganzes Wort Suchen oder Ersetzen wolltest, wäre das doch das gleiche, und da würde niemand drüber jammern.

    Ich versuche mir grade über die Konsequenzen klar zu werden, bevor wir was entscheiden. Zugriffe auf einzelne Zeichen sind schon relativ häufig, allein die Tatsache, dass ein std::string::back() das letzte Byte eines Codepoints zurückgeben könnte kann schon zu Problemen führen.

    UTF-8 nach außen habe ich mir auch schon überlegt, damit müssten wir unsere Datenformate nicht ändern, sondern nur wissen, ob es sich bei einer Datei um eine ANSI oder UTF-8 Datei handelt. Ist zwar etwas Arbeit, aber überschaubar.



  • Ich bin ein Fan von https://utf8everywhere.org/ - die haben auch einen Abschnitt für Windows: https://utf8everywhere.org/#windows



  • @firefly sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Wenn du mit 'Ä' meinst dass es aus einem 8Bit (UTF-8), 16Bit (UTF-16) oder 32Bit (UTF-32) "block" besteht, dann wirst du selbst bei UTF-32 nicht einfach danach suchen können.
    Bzw. es als ein Codepoint betrachten können

    Doch, schon. Die UTF-32 Kodierung sieht immer 4 Byte pro Zeichen vor.

    Edit:
    Ächz... hast recht, auf wikipedia kann man lesen, dass z.B. bestimmte vietnamesische Zeichen auch mit vier Byte nicht auskommen.



  • @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    @firefly sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Wenn du mit 'Ä' meinst dass es aus einem 8Bit (UTF-8), 16Bit (UTF-16) oder 32Bit (UTF-32) "block" besteht, dann wirst du selbst bei UTF-32 nicht einfach danach suchen können.
    Bzw. es als ein Codepoint betrachten können

    Doch, schon. Die UTF-32 Kodierung sieht immer 4 Byte pro Zeichen vor.

    Nein es sind keine Zeichen sondern codepoints!
    Und ein codepoint kann ein zeichen sein. Oder mehrere codepoints können ein Zeichen darstellen.

    siehe z.b. https://www.fileformat.info/info/unicode/char/00c4/index.htm Punkt "Decomposition"


  • Mod

    @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Ich versuche mir grade über die Konsequenzen klar zu werden, bevor wir was entscheiden. Zugriffe auf einzelne Zeichen sind schon relativ häufig, allein die Tatsache, dass ein std::string::back() das letzte Byte eines Codepoints zurückgeben könnte kann schon zu Problemen führen.

    Von dieser Einzelzeichensichtweise müssen du und deine Anwendung halt weg. Das ist aber auch unabhängig von der Alternativcodierung, denn diese Sichtweise funktioniert halt nur bei bei Codepagecodierungen wie ASCII.



  • Nur eine kurze Anmerkung zu UTF-8: du kannst Teilstrings in UTF-8 mit nem normalen strstr suchen. Das funktioniert, weil UTF-8 so aufgebaut ist dass sämtliche Werte die für das 1. Byte einer Sequenz erlaubt sind nicht für die weiteren Bytes erlaubt sind.
    Dadurch kann man z.B. auch einfach das letzte Zeichen eines Strings finden: einfach vom Ende des Strings rückwärts das erste "lead byte" suchen - dort beginnt dann das letzte Zeichen.

    Aber: um was für eine Art Software geht es denn? Ist das irgendwas in Richtung Textverarbeitung?



  • @hustbaer
    Ja, das ist mir schon klar, die UTF-8 Kodierung kenne ich. Nur haben wir an vielen Stellen Überprüfungen wie somestring[a] == 'b' oder auto pos = somestring.find( 'a' ). Solange das alles ASCII ist macht das keine Probleme, aber sobald nach einem nicht-ASCII Zeichen gesucht wird kann´s halt Probleme geben. Um ehrlich zu sein weiß ich nicht mal genau, ob dieser Fall in unserer Software überhaupt auftritt, aber ich dachte, ich frage mal nach, wie andere das lösen.

    Wir machen Messsoftware, wo viele Dinge per Text einstellbar sind und oft Platzhalter Ersetzungen stattfinden. Da dürfte nix passieren, weil unsere Platzhalter alle ASCII sind.



  • @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Nur haben wir an vielen Stellen Überprüfungen wie somestring[a] == 'b' oder auto pos = somestring.find( 'a' ). Solange das alles ASCII ist macht das keine Probleme

    Richtig, das funktioniert aber mit UTF-8 auch noch.

    aber sobald nach einem nicht-ASCII Zeichen gesucht wird kann´s halt Probleme geben.

    Dann musst du halt nach dem String "ä" oder "𝄞" suchen und nicht nach 'ä' und '𝄞'. Wenn dein Source dann auch in UTF-8 ist und du eine Konvention hast, dass all deine Strings UTF-8 sind, dann ist das kein Problem.



  • @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    @hustbaer
    Ja, das ist mir schon klar, die UTF-8 Kodierung kenne ich. Nur haben wir an vielen Stellen Überprüfungen wie somestring[a] == 'b' oder auto pos = somestring.find( 'a' ). Solange das alles ASCII ist macht das keine Probleme, aber sobald nach einem nicht-ASCII Zeichen gesucht wird kann´s halt Probleme geben. Um ehrlich zu sein weiß ich nicht mal genau, ob dieser Fall in unserer Software überhaupt auftritt, aber ich dachte, ich frage mal nach, wie andere das lösen.

    Da Windows mittlerweile UTF-16 nutzt, wirst Du wohl oder übel den Code zu umstellen müssen, dass mit Substrings anstatt Zeichen als Suchmuster gearbeitet wird. Dieser Code funktioniert dann auch mit UTF-8, falls ihr jemals auf Linux oder Gtk, Qt portieren wolltet.

    Noch eine Anmerkung zu Länge des kodierten Texts. Die ursprüngliche Version von UTF-8 erlaubte bis zu 6 Zeichen für die Kodierung eines UCS-4 Zeichens. Unicode hat mittlerweile den erlaubten Bereich wieder eingeschränkt, so dass man mit 4 Zeichen auskommen kann. Ob es aber dabei dauerhaft bleiben wird sei einmal dahin gestellt.



  • @DocShoe
    Klingt für mich so als ob man gut mit sowohl UTF-8 als auch UTF-16 arbeiten könnte.
    UTF-16 ist auf Windows einfacher, aber je nachdem wie viele OS Funktionen man wirklich benötigt, ist UTF-8 auch kein so grosses Problem. Und es würde eine eventuelle Portierung einfacher machen.



  • // deleted



  • Vielen Dank für eure Antworten, die haben mir gezeigt, dass ich mit meinen Überlegungen richtig liege. Von der Idee, Zeichen als einzelne Byte zu behandeln muss man sich dann wohl trennen, wenn man Unicode verwendet. Ich muss jetzt mal prüfen, welche Stringliterale unser Compiler unterstützt und die Entscheidung davon abhängig machen. Es bedeutet aber auf jeden Fall, dass sämtliche WinAPI Aufrufe auf die W-Version geändert werden müssen.



  • @DocShoe

    UTF-8 nach außen habe ich mir auch schon überlegt, damit müssten wir unsere Datenformate nicht ändern, sondern nur wissen, ob es sich bei einer Datei um eine ANSI oder UTF-8 Datei handelt. Ist zwar etwas Arbeit, aber überschaubar.

    Bei solchen Fragen muss man auch die älteren Versionen im Umlauf mit berücksichtigen.

    • Was passiert wenn beispielsweise Kunde XYZ von der alten auf die neue Version aktualisiert und dieser Daten im alten Format (Konfigurationen, Dateien) laden möchte?
    • Müssen Daten bzw. Dateien (z.B. Rechnungsdaten) aufwärtskompatibel bleiben?
    • Was passiert wenn der Kunde mit alter Programmversion Daten bzw. Dateien im neueren Format laden möchte? Absturz?

    Dadurch muss evt. deine Software Daten bzw. Dateien im alten und neuen Format einlesen können. Denn UTF-8 hin oder her, gibt der Kunde beispielsweise in der Rechnungsadresse Tüpfelhyänenöhrchenstraße 13 an, so wird nach der UTF-8 Interpretation Tüpfelhy䮥nöhrchenstraߥ 13 daraus.



  • @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Von der Idee, Zeichen als einzelne Byte zu behandeln muss man sich dann wohl trennen, wenn man Unicode verwendet.

    Jein, man kann natürlich intern mit wchar_t arbeiten und muss so nicht den Programmcode abändern, aber man konvertiert dann ständig zwischen Programm interner Kodierung (UCS-4) und API Kodierung (UTF-8 bzw. UTF-16). Ob das so sinnvoll ist?

    Da es letztlich im Forum die Frage nach ncurses gab (hat natürlich nichts mit Windows zu tun), habe ich mir die ncursesw Library angeschaut. Da wird immer wchar_t an die Library übergeben und in cchar_t konvertiert (wchar_t plus Attribute fürs Terminal: Farbe, Stil …). D.h. es gibt auch APIs, die mit wchar_t arbeiten. Weshalb Windows ausgerechnet UTF-16 verwendet ist mir persönlich schleierhaft, weil es weder die Vorzüge einer gleichen API bietet wie UTF-8 aber nicht problemlos mit allen Unicodezeichen funktioniert wie UCS-4. Windows erschien mir noch nie logisch durchdacht zu sein.



  • @Quiche-Lorraine:
    Unsere Dateien tragen alle eine Versionsnummer, dann muss man strings beim Einlesen abhängig von der Versionsnummer behandeln.

    @john-0
    Ne, wchar_t hilft da nicht weiter, weil man bei UTF-16 ähnliche Probleme wie bei UTF-8 hat, nämlich dass ein sich Zeichen aus bis zu zwei wchar_t zusammensetzen darf. Bis Windows2000 hat Microsoft UCS-2 benutzt, dabei wurde jedes Zeichen mit genau 2 Byte kodiert.



  • @DocShoe sagte in UCS-2, UTF-8, UTF-16, UTF-32, WTF?:

    Unsere Dateien tragen alle eine Versionsnummer, dann muss man strings beim Einlesen abhängig von der Versionsnummer behandeln.

    Wird dann spannend die Versionsnummer zu lesen wenn man noch nicht weiss welches Encoding das File verwendet. Ich würde aber empfehlen für Files auf jeden Fall UTF-8 zu verwenden - selbst wenn ihr euch entschiesst im Speicher UTF-16 zu verwenden. Dann ist das mit Versionsnummer lesen vermutlich auch kein grosses Problem, weil UTF-8 ja ASCII kompatibel ist. (Gut, zu prüfen ob ein Textfile welches z.B. garantiert irgendwo auch ASCII Zeichen enthält UTF-16 ist, ist jetzt auch nicht so schwer. Aber wer will schon UTF-16 Textfiles verwenden?)

    Von der Idee, Zeichen als einzelne Byte zu behandeln muss man sich dann wohl trennen, wenn man Unicode verwendet.

    Das kommt drauf an was man mit den Zeichen machen will, und um welche Zeichen es sich handelt. Ein str[0] == '#' oder so funktioniert auch ohne Probleme mit UTF-8 oder UTF-16. Genau so ein str.back() == '?'. Mit UTF-8 geht sogar noch strchr (und mit UTF-16 wcschr`) so lange man nur die Position im Speicher braucht und nicht wissen muss um den wievielten Codepoint im String es sich handelt.

    Wenn man dagegen etwas ala str[0] == userDefinedCharacter hat, dann ja, dann wird das eine Spur komplizierter. Dann muss da ein begins_with(str, userDefinedCharacter) statt dessen hin.



  • @hustbaer
    Das sind keine Textdateien, sondern Binärdateien, die unter anderem auch Text enthalten. Du bist vermutlich noch bei deiner Textverarbeitung. Der Dateiheader hat eine ASCII Signatur und unter anderem 4 uint für Major, Minor, Release und Build Version. Den Header kann ich also immer richtig lesen und anhand der Versionsnummer weiter entscheiden, wie Text behandelt wird (ANSI/UTF-8). Seh da kein Problem.


Anmelden zum Antworten