Unicode mit Perl

Unicode unter Perl funktioniert meistens einfach. Über das warum und wieso muss man sich meistens keine sorgen machen. Wenn man denn trotzdem mit Zeichensätzen spielen möchte, so muss man hinter die Kulissen blicken.

Ausgangssituation

Zuerst einmal die Ausgangssituation: Meine mp3-Sammlung. Sie hat im Laufe ihres Lebens diverse male ihr Speichermedium gewechselt und ist weiter gewachsen. Bei jedem Kopiervorgang einer Datei wird eine neue Datei mit dem gleichen Dateinamen und dem gleichen Inhalt angelegt. Bei den Dateinamen fangen die Probleme an. Was bedeutet „gleich“. Heutzutage werden auf jedem vernünftigen Betriebssystem die Dateinamen als UTF gespeichert (meistens UTF-8). Jedoch sind auch Zeichensätze der iso-8859 (iso-8859-1 oder iso-8859-15 im europäischen Raum) oder gar Codepages (wie cp1250, cp850, cp437) auf Windows-Systemen im Einsatz.

ASCII

Das ist alles kein Problem, solange sich alle Zeichen eines Dateinamens im ASCII-Zeichensatz befinden. Der ASCII-Zeichensatz wurde von Amerikanern erfunden, die der Meinung waren, dass die ganze Welt nur die Zeichen A-Z, a-z, 0-9, Sonderzeichen wie !“$&\|[](){}=*+-/%~#’_.:,?!; (nicht vollständig) und verschiedene Leer- und Steuerzeichen gebraucht. Es wurde jedoch bald erkannt, dass diese 128 verschiedenen Zeichen (\x00-\x7F) nicht der Weisheit letzter Schluss sind. Also entstanden genau so kurzsichtig diverse Definitionen, die den übrigen 128 Zuständen eines Bytes (\x80-\xFF) ein Bildchen verpassten. Somit waren die ASCII-Erweiterungen (iso-8859-1 bis iso-8859-15 und einige Codepages) geboren. Am Beispiel einer Tabelle einiger Zeichensätze auf Wikipedia werde ich mein Problem verdeutlichen.

Kopieren

Ich habe es wie auch immer geschafft, eine Datei mit dem Zeichen ´ (\xB4) auszustatten. Das ist der Akzent, der aus einem Cafe einen Café macht. Jedenfalls hieß die Datei dann „Salt´n´Pepper – Push It.mp3“. Abgesehen davon, dass die Band falsche geschrieben ist (Salt-N-Pepa), sind da nun noch zwei nicht-ASCII-Zeichen versteckt. Es ergab sich, dass dieser Dateiname in der Windows Codepage 1252 (oder 1250? – ich weiß es nicht mehr) gespeichert wurde. Die Festplatte unter Linux angesprochen hat die Bytekette, die den Dateinamen repräsentiert nun aber als iso-8859-15 interpretiert, und daher für das Zeichen Ž (\x017D) gehalten. Das ganze dann als UTF-8 neu abgespeichert ergibt „SaltŽnŽPepper – Push It.mp3“.

Fast jede Datei hat so ihre Geschichte. Die meisten bestehen zum Glück nur aus ASCII-zeichen. Und viele sind einfach UTF-8 interpretierte, aber iso-8859-15 kodierte Dateinamen. Ich wollte diese jetzt alle korrekt nach UTF-8 konvertieren, da mir dieser Zeichensatz zukunftssicher erscheint.

Repräsentation in Perl

Perl speichert Strings in Scalaren (z.B. $file). Ein Scalar enthält Zeichen und einen Perl-internen Overhead der diverse Flags und die Länge enthält. Damit hat man aber eigentlich nichts am Hut. Seit Perl Version 5.6 wird für jedes Zeichen eines Scalares ein Wide-Character (2 Bytes) benutzt. Vorher galt ein Byte pro Zeichen. Erstmal ändert sich dadurch nichts. Ein UTF-8-kodierter String (z.B. aus einer Datei, einem UTF-8 kodierten Perl-Script oder dem Terminal als Argument) wird nach wie vor in einem Haufen von Zeichen mit einem dezimalen Wert von 0-244 gespeichert. (Die Kombinationen \xF5-\xFF, sowie \xC0 und \xC1 kommen in gültigen UTF-8 Sequenzen nicht vor.) Wenn er wieder geschrieben wird, so bleibt er UTF-8 kodiert. Ein m/ü/ in einem script (UTF-8) ist das gleiche wie m/\xC3\xBC/ (ü = \xFC (iso-8859-1) = \xC3\xBC (UTF-8)). Das ist schlecht, wenn Script und Daten in verschiedenen Zeichensätzen kodiert sind.

Nun gibt es aber für jeden $scalar ein UTF8-Flag. Perl merkt sich damit, ob es in den Wide-Characters des $scalars direkt den code point gespeichert hat anstatt nur die kodierte Form. Der code point ist eine eindeutige Zahl, die von der ISO-10646 jedem Zeichen zugeordnet ist – für ASCII-Zeichen ist er äquivalent.

Umwandlung in Perl

Wie bekommt man nun Perl dazu, den Inhalt eines $scalars als Zeichen zu interpretieren und in Codepoints zu dekodieren?

  • use utf8; $scalar=“äöü“; wenn der Inhalt von $scalar im script steht.
  • binmode(FILEHANDLE, „:utf8“); $scalar=; wenn der Inhalt von $scalar aus einem file handle kommt.
  • use Encode; $scalar2=decode(„utf8“,$scalar1); falls $scalar1 bereits UTF-8 kodierten Inhalt enthält.

Das Flag lässt sich mit $utf_encoded = Encode::is_utf8($scalar); abfragen. Anstatt „utf8“ kann man auch „iso-8859-1“ oder ähnliches verwenden. Ich bin so faul und erlaube mir für weitere Informationen auf die Dokumentation von Encode, binmode, utf8 und perlunicode zu verweisen.

Konvertierungs-Script

Mein Script nimmt eine Liste von Datei- und Verzeichnisnamen entgegen benennt alle Dateien in den Verzeichnissen und Unterverzeichnissen in UTF-8 um. Zudem ändert es auch die Dateinamen in .md5 und .sfv Dateien. Es beachtet dabei auch mehrfache falsch-Konvertierungen wie Eingangs beschrieben, indem es sich an Hand einer Zeichen-Bewertung die schönste Konvertierung heraus sucht. Die Präferenzen, was schön ist und was nicht muss von Nation zu Nation angepasst werden. Für Deutsche ist im Dateinamen ein Umlaut schöner als ein ´ schöner als ein Ž. Kroaten mögen andere Zeichensätze bevorzugen und das Ž höher als Umlaute und ´ bewerten.

Es bleibt jedem überlassen, wie er sich das Script anpasst. Getestet werden sollte es allerdings erstmal in einem kleinen Verzeichnis von dem man ein Backup angelegt hat.
Herunterladen kann man es unter

http://download.entropie.li/perl/recode.pl.txt

4 Responses to “Unicode mit Perl”

  1. danho sagt:

    Hi!
    Sehr feines skript!
    Ich war ewig auf der Suche, wie ich meine umlaute mp3’s in den Griff bekomme. Ich wollte schon einfach alle Umlaute rekursiv eliminieren. Ich komme aber an dem Punkt nicht weiter, wenn ich rename(A,B) einen Umlaute Dateinamen übergebe…
    Dein Skript wird bei mir (XP+Activeperl 5.8.8) nicht ausgeführt:
    –snip
    Number found where operator expected at recode.pl line 170, near „verbose 2“
    (Do you need to predeclare verbose?)
    …etc
    syntax error at recode.pl line 170, near „verbose 2“
    …etc
    –snap
    Den verwendeten Konstrukt mit verbose kenne ich nicht. Anscheinend mein Perl auch nicht…

    LG
    danho

  2. entropie sagt:

    Ich hatte eine subroutine namens verbose im script vorgesehen:

    sub verbose { print STDERR @_ if shift >= $verbose }

    Wenn du diese Zeile am Anfang einfügst, sollte nicht mehr gemeckert werden. Ansonsten kannst du die verbose-Zeilen auch einfach auskommentieren. Das script habe ich bisher aber nur unter Linux in einem UTF-8 ext3-dateisystem benutzt.
    Es greift unter anderem auch auf das Programm „file“ zu, welches Informationen zum Dateiinhalt liefert – keine Ahnung ob es sowas auch unter Windows gibt.

  3. danho sagt:

    Kay, läuft jetzt.
    Funzt aber unter NTFS nicht; mal schaun, was NTFS alles können sollte.
    Meine mp3’s liegen eh auf einer FAT32, da machts wahrschenlich sowiso keinen Sinn, Energie rein zu stecken…

  4. entropie sagt:

    fat32 ist ein recht einfaches Dateisystem. Ich kann es für Archivierungszwecke empfehlen: Es kann von den meisten Betriebssystemen gelesen und beschrieben werden und funktioniert auch noch auf sehr großen Platten.
    Die Interpretation des Zeichensatzes der Dateinamen liegt nach wie vor beim Betriebssystem.

    PS: Mittlerweile setze ich ext3 zur HDD-Archivierung ein, da es robuster ist und es auch Treiber für WinXP gibt, um derart formatierte Festplatten ins System einzubinden.