Unicode <-----> UTF-X

Alles, was nicht direkt mit Python-Problemen zu tun hat. Dies ist auch der perfekte Platz für Jobangebote.
Antworten
pudeldestodes
User
Beiträge: 65
Registriert: Samstag 9. Juni 2007, 23:45

Ich wollte noch einmal versuchen den Unterschied zwischen Unicode und UTF-X deutlich zu machen (und hoffentlich verzapfe ich hier jetzt keinen totalen Müll). An anderer Stelle wurde das imho häufig mit anderen python-spezifischen Sachen vermischt (encoding cookie, gewisse Zeichen auf Konsole ausgeben), bevor die Grundlagen klar sind.

Das folgende ist also mein Versuch den Unterschied zwischen Unicode und UTF-X aus Leonidas' Folien, Wikipedia und dem Python-Wiki ein wenig auf die spezifische Fragestellung zu komprimieren.
Was ist Unicode?
Unicode ordnet jedem Zeichen aus der Unicode-Zeichenmenge (http://www.unicode.org/charts/) eine Zahl zu. Diese Zahl wird auch Codepoint genannt. Diese Zuordnung samt Zeichen- und Zahlenmenge nennt man Unicode.
Längere Erläuterung:
ß wird zum Beispiel die 223 zugeordnet. Anstatt durch die Dezimalzahl wird der Codepoint im Allgemeinen durch ein vorangestelltes "U+" gefolgt von der Hexadezimalenzahl kenntlich gemacht. Das ergäbe dann: U+DF (laut Wikipedia U+00DF, offensichtlich werden da noch Füllnullen eingebaut).
Python 2.6:

Code: Alles auswählen

>>> a = u'ß'
>>> type(a)
<type 'unicode'>
>>> a
u'\xdf' # df in Hexschreibweise entspricht der Dezimalschreibweise 223 '
Also alles wie erwartet.

Was ist das Encoding?

Jeder Unicodezahl (Codepoint) wird eine Bytefolge zugeordnet, die so dann auch auf der Festplatte oder sonstwo gespeichert werden kann. Der Vorgang der Umwandlung von Codepoint -> Bytefolge nennt sich Enkodieren, der umgekehrte Weg heißt Dekodieren. Für den Kodierungsvorgang gibt es jetzt verschiedene Ansätze. Hier jetzt beispielsweise UTF-8: UTF-8 weißt jedem Codepoint ("Unicode-Zahl") eine Bytefolge variabler Länge zu. Die 2 lässt sich ganz gut mit einem Byte kodieren, die 25698 sicherlich nicht mehr(1 Byte = 8 Bit = 2^8 = 256 Möglichkeiten) und wird entsprechen mit mehr Bytes kodiert (wobei auch die 128 schon mit zwei Bytes kodiert wird - warum auch immer. Edit: die Erklärung hierfür findet sich im folgenden Beitrag von Lunar).

Code: Alles auswählen

              ß     <---------------------------------> U+00DF (223)   <----------------------> 11000011:10011111 (in UTF-8)
Zeichen aus Unicodezeichemenge <-------------------> Codepoint (Zahl) <----------------------> Abfolge von Bytes 
|--------------------------------Unicode-----------------------|
                                                               |--------------------Kodierung (z.B. UTF-8)-------------------|
                                                               ------------------enkodieren-------->       
                                                               <-------------dekodieren-----------   

In Python 2.x ist der Datentyp string 'str' eben eine Bytefolge, also ganz rechts in obiger Abbildung.
Test:

Code: Alles auswählen

>>> a = u'ß'
>>> type(a)
<type 'unicode'>
>>> b = a.encode('utf-8')
>>> b
'\xc3\x9f' # also c39f
>>> type(b)
<type 'str'>'
c39f in Binärdarstellung ergibt 1100001110011111 und das entspricht der Darstellung eines UTF-8 kodierten 'ß', wie man hier auch unter "UTF-8 (binary)" nachschauen kann.

Kurz:
'unicode'-Objekte in Python 2.x enthalten im Endeffekt den/die Codepoint/s des/der Zeichen/s, 'str'-Objekte sind eine spezielle Bytedarstellung des/r Zeichen/s.
Zuletzt geändert von pudeldestodes am Mittwoch 29. Dezember 2010, 21:20, insgesamt 2-mal geändert.
lunar

Ist das am weitesten links stehende Bit eines Oktetts gesetzt, so bedeutet dies in UTF-8, dass dieses Oktett Bestandteile einer Folge aus mehreren Oktetten ist, die zusammen ein Zeichen darstellen. Das erste Oktett dieser Folge beginnt dabei mit so vielen gesetzten Bits, wie insgesamt Oktette in der Folge enthalten sind (mindestens also "11" für einen durch zwei Oktette dargestellten Codepunkt). Alle folgenden Oktette beginnen immer mit der Folge "10". Somit kann man durch Blick auf das erste Bit erkennen, ob es sich um ein ein einzeln stehendes Oktett handelt oder um eine Folge mehrerer zusammengehöriger Oktette, außerdem vermeidet diese einfache Regel viele Mehrdeutigkeiten und somit wesentlich kompliziertere (und damit aufwendiger und weniger effizient zu implementierende) Regeln zur Auflösung solcher Mehrdeutigkeiten.

Deswegen muss der Codepunkt 128 bereits durch zwei Oktette dargestellt werden, da 128 in binärer Darstellung durch ein Oktett mit gesetztem ersten Bit dargestellt wird.
pudeldestodes
User
Beiträge: 65
Registriert: Samstag 9. Juni 2007, 23:45

Ah, so wird ein Schuh draus. Vielen Dank für die Erklärung.
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

pudeldestodes hat geschrieben:Kurz:
'unicode'-Objekte in Python 2.x enthalten im Endeffekt den/die Codepoint/s des/der Zeichen/s, 'str'-Objekte sind eine spezielle Bytedarstellung des/r Zeichen/s.
Jein. Unicode-Objekte sind in Python entweder Array von 2 oder 4 byte großen Elementen (wie groß wird beim Kompilieren bestimmt) und kein Array von Codepoints. Das macht sich leider dadurch bemerkbar, dass Unicode-Zeichen die durch vier bytes dargestellt werden müssten bei einer Python-Version die mit 2-byte großen Elementen arbeitet auch durch zwei Elemente dieses Arrays dargestellt werden. Dadurch erscheint ein Zeichen wie zwei Zeichen.

Code: Alles auswählen

>>> print len(u'\U0001d11e')
2
Halleluja.
lunar

Nun, genau genommen enthält ein Unicode-Objekt weder direkt die Codepoints selbst noch „Elemente“ irgendwelcher Art, sondern eben Zeichen in einer bestimmten Codierung. Python bietet zur Übersetzungszeit die Wahl zwischen UCS-2 und UCS-4. UCS-2 kann nun mal nur die BMP kodieren, es sollte daher nicht überraschen, dass in diesem Falle Codepoints außerhalb der BMP "nicht funktionieren".

Sicherlich ist das demonstrierte, im Übrigen wahrscheinlich nicht einmal klar definierte, Verhalten nicht ideal, man hätte vielleicht auch eine Ausnahme auslösen können. Doch sollte man je in der Verlegenheit kommen, Gotisch darstellen zu müssen, lässt sich der höchstmögliche Codepoint auch mit "sys.maxunicode" abfragen, so dass man solche Situationen auch abfangen und im Rahmen des Möglichen korrekt behandeln kann.

Außerdem ist UCS-2 für die große Mehrheit der Anwendungen auch vollkommen ausreichend, da die wenigsten Anwendungen jemals die wohldefinierte Sicherheit der BMP verlassen werden. Auch kompilieren die meisten Linux-Distributionen Python mittlerweile mit UCS-4, und im Falle von Windows kann man ja – sollte man UCS-4 tatsächlich wirklich benötigen – noch immer eine selbst übersetzte Python-Variante nutzen, und mittels pyinstaller oder py2exe mitliefern.

Auch ist sind Probleme mit Zeichen außerhalb der BMP keine Eigenheit von Python. Java beispielsweise kann solche Literale auch nicht direkt verarbeiten. Insofern ist das sarkastische „Halleluja“ der Situation nicht wirklich angemessen, die Welt ist nun mal kompliziert, und wirklich perfekt hat Kodierungen noch keiner implementiert.
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

Lunar hat geschrieben:Nun, genau genommen enthält ein Unicode-Objekt weder direkt die Codepoints selbst noch „Elemente“ irgendwelcher Art, sondern eben Zeichen in einer bestimmten Codierung.
Würden Unicode-Objekte wirklich Zeichen enthalten, könnte man sie nicht durch slicing zerrupfen und normale Indexzugriffe würden Zeichen zurückgeben und nicht wieder Unicode-Objekte. Das zwei- oder vier-Byte-Array scheint leider deutlich durch.
Sicherlich ist das demonstrierte, im Übrigen wahrscheinlich nicht einmal klar definierte, Verhalten nicht ideal, man hätte vielleicht auch eine Ausnahme auslösen können.
Ja du hast recht. Es ist tatsächlich nirgends in der Doku definiert, was ein unicode-Objekt enthält, geschweige denn was __len__ eigentlich genau misst. Also muss man es mit dem naheliegenden Versuchen und hoffen dass das klappt.
Doch sollte man je in der Verlegenheit kommen, Gotisch darstellen zu müssen, lässt sich der höchstmögliche Codepoint auch mit "sys.maxunicode" abfragen, so dass man solche Situationen auch abfangen und im Rahmen des Möglichen korrekt behandeln kann.
Das Problem ist, dass das nicht passieren wird. Niemand rechnet damit, dass jemand Gotisch braucht. Deswegen wird das Programm dann im Zweifelsfall einfach einen Bug und keine spezielle Fallbehandlung haben.
Auch ist sind Probleme mit Zeichen außerhalb der BMP keine Eigenheit von Python. Java beispielsweise kann solche Literale auch nicht direkt verarbeiten.
Nur weil Unicode in Java auch kaputt ist, ist das keine Entschuldigung für Python. Das Arbeiten mit Unicode ist nicht einfach, dass die len() das falsche Ergebnis ausgibt macht es nicht einfacher. Ist aber wenigstens das deutlichste Indiz, dass man Unicode-Strings niemals einfach so zerteilen sollte.
und wirklich perfekt hat Kodierungen noch keiner implementiert.
Jap, alles außerhalb von ASCII ist auch im Jahre 2011 noch potentiell gefährlich. Deswegen ist mein sarkastisches Halleluja mehr als angemessen. Wenn es nämlich jeder ein bisschen falsch macht, dann muss man sich nicht wundern, wenn wir in zehn Jahren immer noch dieselben Probleme mit Zeichenkodierungen haben.
lunar

@Darii: Du hast mich falsch verstanden: Die Python-Dokumentation definiert durchaus, wenn auch recht beiläufig, was eine Unicode-Zeichenkette enthält, nämlich der Sprachreferenz, Abschnitt 2.4.1, "string literals":
Unicode strings use the Unicode character set as defined by the Unicode Consortium and ISO 10646.
"len()" zählt die Anzahl aller einzelnen Zeichen, Slicing liefert einzelne Zeichen, wobei Zeichen im Sinne des Unicode-Standards zu verstehen ist und nicht im Sinne irgendeines Schriftsystems. Demnach verhalten sich Slicing, "len()" und alle die anderen Operationen auf Unicode-Zeichenketten durchaus korrekt. Wiewohl Unicode manche Zeichenketten als äquivalent definiert, sind es auch gemäß des Standards deswegen noch lange nicht dieselben Zeichenketten. So stehen "\u006f\u0308" und "\u00f6" beide für "ö" und sind gemäß des Unicode-Standards kanonisch äquivalent (canonical equivalent), aber ganz offensichtlich sind es eben nicht dieselben Zeichenketten. Und man kann "\u006f\u0308" problemlos zerteilen, und erhält wiederum gültige Zeichenketten.

Ich möchte also klar stellen, dass das Verhalten von Python in diesem Fall keineswegs wahlfrei ist, sondern eigentlich klar definiert und auch korrekt. Natürlich birgt es wie jede Implementierung, wie jedes Programm auch seine Überraschungen. Deswegen muss man dieses Verhalten kennen, ebenso wie man die Semantik jeder Sprache und jeder Bibliothek kennen muss, die man verwendet. Dann kann man mit den Mitteln der Standardbibliothek auch mit Situationen umgehen, in denen man beispielsweise die Anzahl der semantisch äquivalenten Zeichen in einer Zeichenkette benötigt, in denen ganz allgemein die Semantik eines "unicode"-Objekts nicht passt, oder nicht der Gewünschten entspricht.

Dir mag dieses Verhalten nicht gefallen, aber es gibt nichts besseres, also lerne leben, mit dem was Du hast. Es ist ja auch nicht so, als würden alle bei der täglichen Arbeit dauernd über Kodierungsprobleme aufgrund irgendwelcher Probleme in irgendwelchen Unicode-Implementierungen stolpern. Natürlich ist mir klar, dass es auch tatsächliche Probleme aufgrund der Mehrdeutigkeiten im Unicode-Standard und in Unicode-Implementierungen gibt, nur in Anbetracht der Komplexität dieses Thema funktioniert Unicode im Großen und Ganzen recht gut. Vor allem aber gibt es wie gesagt nichts besseres, insofern ist mir meine Zeit auch zu schade, lange darüber zu diskutieren, ob Pythons Unicode-Implementierung tatsächlich „kaputt(TM)“ ist, ob man "unicode" nicht besser auf Basis kanonischer Äquivalenz hätte implementieren sollen, und ob Unicode vielleicht nicht selbst gar „kaputt(TM)“ ist.
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

lunar hat geschrieben:Demnach verhalten sich Slicing, "len()" und alle die anderen Operationen auf Unicode-Zeichenketten durchaus korrekt. Wiewohl Unicode manche Zeichenketten als äquivalent definiert, sind es auch gemäß des Standards deswegen noch lange nicht dieselben Zeichenketten. So stehen "\u006f\u0308" und "\u00f6" beide für "ö" und sind gemäß des Unicode-Standards kanonisch äquivalent (canonical equivalent), aber ganz offensichtlich sind es eben nicht dieselben Zeichenketten. Und man kann "\u006f\u0308" problemlos zerteilen, und erhält wiederum gültige Zeichenketten.
Das ist mir klar (führt übrigens zu massiven Problemen bei Dateinamen). Was hat das jetzt damit zu tun, dass man einzelne Unicode-Zeichen zerteilen kann?

Code: Alles auswählen

In [1]: s = u'\U0001d11e'

In [2]: s[0], s[1]

Out[2]: (u'\ud834', u'\udd1e')
Deine Ironie (kaputt(TM))gegen Ende hättst du dir übrigens auch sparen können. Wenn du das nicht willst, dann gestehe anderen Leuten wenigsten ihren Sarkasmus zu. Ist übrigens Pech/Glück, dass ™ zufällig im BMP ist und der Notenschlüssel um den es gerade geht, nicht.

Edit:
Allgemeiner Fehler
SQL ERROR [ mysqli ]

Incorrect string value: '\xF0\x9D\x84\x9E u...' for column 'post_text' at row 1 [1366]

Beim Laden der Seite ist ein SQL-Fehler aufgetreten. Bitte kontaktiere die Board-Administration, falls dieses Problem fortlaufend auftritt.
Muhahaha (sorry fürs lachen, musste jetzt sein, hab nur versucht den Notenschlüssen einzufügen)
lunar

"\u006f\u0308" sind zwei Unicode-Zeichen, nicht eins. Sie sind lediglich äquivalent zu einem einzelnen Zeichen. Insofern ist es durchaus akzeptabel, dass man diese Zeichenfolge in der Mitte zerteilen kann.

Das von Dir zum zweiten Mal demonstrierte Verhalten ist natürlich eher weniger akzeptabel, doch das habe ich bereits gesagt. Alternativen wie das Auslösen einer Ausnahme wären vielleicht besser, wahrscheinlich aber auch wesentlich langsamer gewesen. Aber dieses Verhalten betrifft auch nur UCS-2-Builds, sprich hauptsächlich die Windows-Installer. Linux-Distributionen liefern durchweg UCS-4-Builds aus, die sich bei Codepoints außerhalb der BMP korrekt verhalten.

Das PHP am Notenschlüssel ebenfalls scheitert, ist zwar amüsant, aber doch irgendwo auch ohne Belang ;)
Antworten