fietur: UTF-8 und Windows

Hallo, mal wieder vermutlich ein Evergreen-Klassiker:

ich binde auf meiner Seite Text aus einer DB an. Da HTML5 als Codierung UTF-8 verlangt, habe ich den Editor (ich benutze Eclipse) von Windows-1252 auf UTF-8 umgestellt, auch weil ich keine Lust habe, mich immer wieder mit den diversen Inkompatibilitäten herumzuschlagen.

Im Anschluss zeigten dann die aus der DB geladenen Inhalte auf der mit <meta charset="UTF-8"> ausgezeichneten Seite den typischen Umlaute-Müll.

Zunächst dachte ich, das läge an den Inhalten der DB-Tabelle, da diese im Windows CP 1252 in die Tabelle importiert wurden. Als Hotfix für die Ausgabe war utf8-encode() *) erstmal wirksam.

Ich habe in der DB eine neue Tabelle angelegt, die explizit UTF-8-codiert ist; auch die Kollation der Spalten wurde angepasst von latin1_german1_ci zu utf8_general_ci.

Überraschenderweise landen die DB-Texte, wenn man utf8-encode() weglässt, wieder nur mit Umlaute-Müll auf der Webseite. Texte, die nicht aus der DB geladen werden, kommen übrigens korrekt an, auch wenn ich keine HTML entities verwende (inklusive Eurozeichen).

Was läuft da schief und wie behebe ich das?

*) Das PHP-Manual weist darauf hin, dass utf8-encode() eigentlich ein Konverter aus einem ISO-Format in UTF-8 ist, und nicht für die Windows-1252-Konvertierung. Das Euro-Zeichen beispielsweise wird nicht erfasst. Natürlich kann ich dem Vorschlag folgen, und utf8_encode() um die entsprechenden Zeichen erweitern, aber das ist wieder nur ein Workaround für das Problem, dass das UTF-8 irgendwo offenbar doch wieder nur Windows-codiert durchläuft. Dennoch wäre es als zweite Frage interessant zu wissen, ob es eine bessere Möglichkeit gibt, als die Funktion zu erweitern.

  1. Was läuft da schief

    Scheinbar hast Du die Inhalte in der DB nicht in UTF-8 codiert stehen, sondern in Windows-1252. Was die Datenbank über den Inhalt der Tabelle behauptet interessiert Sortierer und dergleichen… aber nicht beim Einlesen und bei der Ausgabe der Daten an sich.

    und wie behebe ich das?

    Möglicherweise beherrscht Deine Datenbank (Du erwähnst diese nicht...) eine Methode der Recodierung. Dann kann es mit einem Update in SQL gehen.

    Auslesen könnte so gehen

    SELECT CONVERT(CAST(column as BINARY) USING utf8) as column FROM table 
    

    Update könnte so gehen:

    UPDATE table SET column = CONVERT(CAST(column as BINARY) USING utf8)
    
  2. Hallo,

    Im Anschluss zeigten dann die aus der DB geladenen Inhalte auf der mit <meta charset="UTF-8"> ausgezeichneten Seite den typischen Umlaute-Müll.

    Zunächst dachte ich, das läge an den Inhalten der DB-Tabelle, da diese im Windows CP 1252 in die Tabelle importiert wurden. Als Hotfix für die Ausgabe war utf8-encode() *) erstmal wirksam.

    also war deine Vermutung wohl richtig.

    Ich habe in der DB eine neue Tabelle angelegt, die explizit UTF-8-codiert ist; auch die Kollation der Spalten wurde angepasst von latin1_german1_ci zu utf8_general_ci.

    Und hast du die Tabelleninhalte dabei nur kopiert? Oder auch wirklich umcodiert? Denn einfach nur Kopieren ist so, als ob du den Senf aus dem Senfglas in ein anderes Glas umfüllst, auf dem "Honig" steht. Dadurch wird der Senf noch nicht zu Honig.

    Überraschenderweise landen die DB-Texte, wenn man utf8-encode() weglässt, wieder nur mit Umlaute-Müll auf der Webseite.

    Also vermutlich wirklich nur kopiert. 😟

    Was läuft da schief und wie behebe ich das?

    Ich kenne mich nicht gut genug mit SQL aus, um dir eine datenbank-interne Lösung aufzuzeigen, bin aber sehr zuversichtlich, dass es eine solche gibt. Ohne ausdrückliches Umcodieren geht's jedenfalls nicht.

    Live long and pros healthy,
     Martin

    --
    Lieber heimlich schlau als unheimlich blöd.
  3. Tach!

    Im Anschluss zeigten dann die aus der DB geladenen Inhalte auf der mit <meta charset="UTF-8"> ausgezeichneten Seite den typischen Umlaute-Müll.

    Damit Daten über die gesamte Kette der Systeme korrekt verarbeitet und dargestellt werden können, muss man zwei grundlegende Dinge beachten:

    • Ein Empfänger muss wissen, in welcher Kodierung die Daten kommen.
    • Ein System muss bei der Verarbeitung mit der Kodierung der Daten umgehen können, oder sie nur 1:1 durchreichen.

    Mir scheint, dass bei dir der erste Punkt nicht klar ist. Du erwartest anders kodierte Daten als gesendet werden.

    Zunächst dachte ich, das läge an den Inhalten der DB-Tabelle, da diese im Windows CP 1252 in die Tabelle importiert wurden. Als Hotfix für die Ausgabe war utf8-encode() *) erstmal wirksam.

    Um zu schauen, ob die Inhalte korrekt gespeichert sind, helfen Tools, die beide genannte Punkte beachten. Du kannst nicht direkt das Speicherformat der Daten im DBMS kontrollieren. Wann immer du sie dir ansehen möchtest, braucht es ein Programm, dass die Daten abfragt und darstellt. Damit haben wir schon zwei Systeme in der Kette, das DBMS und das Anzeigeprogramm. Letzteres muss mit dem DBMS ausgehandelt haben, in welcher Kodierung die Daten zu senden sind. Das DBMS muss daraufhin die Daten liefern und sie gegebenenfalls umkodieren, wenn sie intern anders kodiert gespeichert sind.

    Damit die Daten nun korrekt angezeigt werden können, ist es eine Voraussetzung, dass damals beim Eintragen ins DBMS ebenfalls die beiden Grundregeln beachtet wurden. Konkret heißt das, dass das Eingabeprogramm die Daten korrekt erfasst hat, und beim Senden an das DBMS mit diesem ausgehandelt hat, welche Kodierung zu verwenden sei. Auch hier wieder muss das DBMS umkodieren, wenn die Daten intern in einer anderen Kodierung gespeichert werden.

    Was läuft da schief und wie behebe ich das?

    Vermutlich hast du den ersten Punkt nicht beachtet, und nicht mit dem DBMS ausgehandelt, welche Kodierung beim Austausch der Daten zu verwenden sei. Das DBMS verwendet daraufhin einen Default-Wert, der wohl nicht zu deinen Erwartungen passt.

    Ich habe in der DB eine neue Tabelle angelegt, die explizit UTF-8-codiert ist; auch die Kollation der Spalten wurde angepasst von latin1_german1_ci zu utf8_general_ci.

    Du erwähnst nicht, welches DBMS zum Einsatz kommt. Aus den Kollationsnamen vermute ich, es ist MySQL/MariaDB. Wenn ja, dann siehe https://wiki.selfhtml.org/wiki/Zeichencodierung/MySQL.

    dedlfix.

    1. Ich muss zugeben, dass mir das Ganze ziemlich schleierhaft ist.

      Ich habe die (MySQL) Datenbank nicht einfach nur kopiert, sondern die Tabelle tatsächlich neu importiert, wobei das importierte File auf jeden Fall UTF-8 codiert war. Das glaube ich zumindest, weil ich das ja als Grund für den Fehler bei der Umstellung auf UTF-8 angenommen habe, und daher darauf geachtet habe.

      Dabei könnte natürlich etwas schiefgelaufen sein, aber das sollte sich kontrollieren lassen. Wo und wie könnte ich denn nachsehen, welche Kodierung an welcher Stelle verwendet wird?

      Eben habe ich einen phpMyAdmin SQL Dump der Tabelle angefertigt, darin steht für die alte Version:

      CREATE TABLE `tabelle` (
        `Datum` varchar(10) COLLATE latin1_german1_ci NOT NULL,
        ... 
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci;
      

      Hier wurden die Daten damals beim Import im Format Windows-1251 eingelesen. Ein neuer Import (aus einem File, das in UTF-8 vorlag), der mit der Einstellung des Formats utf8 im Interface vorgenommen wurde, zeigt stattdessen:

      CREATE TABLE `tabelle_utf` (
        `Datum` varchar(10) NOT NULL,
        ...
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      

      Laienhaft würde ich dann davon ausgehen, dass das Speicherformat in der neuen Version dann utf8 ist und dieses auch bei Abfragen - falls nicht eine Konvertierung angegeben/eingestellt ist - Verwendung findet.

      1. Laienhaft würde ich dann davon ausgehen, dass das Speicherformat in der neuen Version dann utf8 ist und dieses auch bei Abfragen - falls nicht eine Konvertierung angegeben/eingestellt ist - Verwendung findet.

        Nein. Wenn Du CP1252-Daten einträgst, dann glaubt MySQL nur, dass es UTF-8 sei und gibt das treudoof auch in dem Glaube aus …

      2. Tach!

        Wo und wie könnte ich denn nachsehen, welche Kodierung an welcher Stelle verwendet wird?

        Es gibt Default-Werte - und ein großes Kapitel zur Thematik in der Dokumentation. Ansonsten musst du selbst angeben, welche Kodierung verwendet werden soll. Es gibt keinen zuverlässigen Mechanismus, Kodierungen zu erraten und deswegen ist das auch meist nicht vorgesehen. Jedenfalls, wenn du ein Import-Tool verwendest, muss das irgendeine Option haben, die Kodierung der Datei anzugeben. Und beim Export uss es auch eine Option geben, die die zu erzeugende Kodierung angibt.

        Eben habe ich einen phpMyAdmin SQL Dump der Tabelle angefertigt, darin steht für die alte Version:

        CREATE TABLE `tabelle` (
          `Datum` varchar(10) COLLATE latin1_german1_ci NOT NULL,
          ... 
        ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci;
        

        Hier wurden die Daten damals beim Import im Format Windows-1251 eingelesen.

        Die Angabe für das Feld sagt nur aus, wie MySQL das intern ablegt. Was du auf der Verbindung zum MySQL-Server für eine Kodierung verwendest, ist davon unabhängig. MySQL konvertiert das, wenn die angegebene Verbindungskodierung nicht mit der jeweiligen Feldkodierung übereinstimmt. Jedes Feld kann auch individuell konfiguriert sein. Zu beachten ist dabei, dass natürlich nicht alles in jede Kodierung umkodiert werden kann. Da kann es zu Datenverlust kommtn, wenn die Zielkodierung bestimmte Zeichen nicht kennt.

        Ein neuer Import (aus einem File, das in UTF-8 vorlag), der mit der Einstellung des Formats utf8 im Interface vorgenommen wurde, zeigt stattdessen:

        CREATE TABLE `tabelle_utf` (
          `Datum` varchar(10) NOT NULL,
          ...
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
        

        Laienhaft würde ich dann davon ausgehen, dass das Speicherformat in der neuen Version dann utf8 ist und dieses auch bei Abfragen - falls nicht eine Konvertierung angegeben/eingestellt ist - Verwendung findet.

        Wenn keine Feldkodierung angegeben wurde, nimmt MySQL die Angabe für die Tabelle als Default-Wert. Ist da nichts angegeben, gibts einen für die Datenbank. Und dessen Defaultwert ist eine Servereinstellung, wenn ich mich recht erinnere. Das steht aber alles gut dokumentiert im MySQL-Handbuch.

        dedlfix.

        1. So langsam dringt Licht durchs Dickicht. Wobei @Raketenrecodierer das schon im zweiten Satz so gesagt hat: Die Angaben in der DB bezeichnen nicht die Codierungen des Inhalts, sondern die Art, wie mit dem Inhalt umgegangen werden soll: bei Sortierreihenfolgen und Ähnlichem.

          Und davon abweichend gibt es die eigentliche Codierung des Inhalts (unerheblich, wie diese realisiert ist, solange sie nur die verlustfreie Konvertierung ermöglicht), wobei deren Ein- und Ausgabe bestimmt werden kann, aber eben nicht so, wie von mir fälschlich vermutet. Anstelle des gewünschten - und vermeintlich eingestelten "utf-8" gilt weiterhin der default "latin1". Letzteres laut Handbuch, das ich auf der Suche nach einem Menu für die Konfiguration dann endlich gefunden habe.

          Den default jetzt für die ganze DB zu ändern, ist aber nicht die Lösung. Denn mit der Umstellung würde das laufende System vermutlich Probleme bekommen.

          Also lautet die Lösung:

          SET NAMES 'utf8';
          

          als erster Befehl nach Herstellung der Verbindung zur DB in allen neuen Skripten, während die alten nach wie vor ohne Umstellung funktionieren.

          Vielen Dank für eure Hilfe, als Lohn drohe ich weitere Fragen an.

          1. Vielleicht noch einen Nachsatz: Mit dem MYSQL Befehl SET NAMES funktioniert auch die alte Tabelle jetzt wieder - ohne PHP-Behandlung mit utf_encode().

          2. Tach!

            So langsam dringt Licht durchs Dickicht. Wobei @Raketenrecodierer das schon im zweiten Satz so gesagt hat: Die Angaben in der DB bezeichnen nicht die Codierungen des Inhalts, sondern die Art, wie mit dem Inhalt umgegangen werden soll: bei Sortierreihenfolgen und Ähnlichem.

            Jein. Genauer gesagt besteht eine Kollationsangabe aus drei Teilen: Kodierung, Kollation und Case Sensitivity. Die Kodierung gibt wirklich an, wie die Daten gespeichert sind. Die anderen beiden sind die genannten Verarbeitungshinweise zum Vergleichen und Sortieren.

            Und davon abweichend gibt es die eigentliche Codierung des Inhalts (unerheblich, wie diese realisiert ist, solange sie nur die verlustfreie Konvertierung ermöglicht), wobei deren Ein- und Ausgabe bestimmt werden kann, aber eben nicht so, wie von mir fälschlich vermutet. Anstelle des gewünschten - und vermeintlich eingestelten "utf-8" gilt weiterhin der default "latin1". Letzteres laut Handbuch, das ich auf der Suche nach einem Menu für die Konfiguration dann endlich gefunden habe.

            Nein, wenn die Daten tatsächlich generell in latin1 (aka Win-1252) gespeichert wären, gäbe es Verlust, wenn man versuchen würde, die restlichen Unicode-Zeichen zu speichern. Die Kodierung eines Feldes entspricht stattdessen schon der jeweils dort angegebenen. Zusätzlich dazu gibt es weitere Kodierungen - wovon die wichtigste diejenige ist, die auf der Client-Verbindung verwendet wird - und MySQL konvertiert gegebenenfalls hin und her.

            Dass verlustfrei konvertiert werden kann (oder es am besten gar nicht werden muss), dafür musst du sorgen. Also immer UTF-8 verwenden und das auch so angeben. Wobei man noch prüfen muss, ob der "Dialekt" utf8mb4 benötigt wird.

            Den default jetzt für die ganze DB zu ändern, ist aber nicht die Lösung. Denn mit der Umstellung würde das laufende System vermutlich Probleme bekommen.

            Nein, eigentlich nicht, denn die Default-Werte wurden kopiert. Die Felder bleiben in ihrer derzeitigen Kodierung. Nur Neuanlagen bekommen die neue Kodierung, wenn nichts angegeben wurde.

            Also lautet die Lösung:

            SET NAMES 'utf8';
            

            als erster Befehl nach Herstellung der Verbindung zur DB in allen neuen Skripten, während die alten nach wie vor ohne Umstellung funktionieren.

            Im Prinzip ja, und es gibt keinen praktischen Nachteil, aber nimm doch lieber die dafür vorgesehenen ..._set_charset-Funktionen, statt ein Statement abzusetzen.

            dedlfix.