Kapitel 4. Unicode Text Versus Bytes

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Menschen verwenden Text. Computer sprechen Bytes.

Esther Nam und Travis Fischer, "Zeichenkodierung und Unicode in Python"1

Mit Python 3 wurde eine scharfe Unterscheidung zwischen Zeichenketten aus menschlichem Text und Sequenzen aus rohen Bytes eingeführt. Die implizite Konvertierung von Bytefolgen in Unicode-Text gehört der Vergangenheit an. In diesem Kapitel geht es um Unicode-Strings, binäre Sequenzen und die Kodierungen, die zur Konvertierung zwischen ihnen verwendet werden.

Je nachdem, welche Art von Arbeit du mit Python machst, denkst du vielleicht, dass es nicht wichtig ist, Unicode zu verstehen. Das ist zwar unwahrscheinlich, aber trotzdem gibt es keine Möglichkeit, die Kluft zwischen str und byte zu überwinden. Als Bonus wirst du feststellen, dass die spezialisierten binären Sequenztypen Funktionen bieten, die der "Allzweck"-Typ Python 2 str nicht hatte.

Unter werden wir in diesem Kapitel die folgenden Themen besuchen:

  • Zeichen, Codepunkte und Byte-Darstellungen

  • Einzigartige Merkmale von binären Sequenzen: bytes, bytearray, und memoryview

  • Kodierungen für alle Unicode- und Legacy-Zeichensätze

  • Vermeidung von und Umgang mit Kodierungsfehlern

  • Bewährte Methoden beim Umgang mit Textdateien

  • Die Standard-Kodierungsfalle und Standard-E/A-Probleme

  • Sichere Unicode-Textvergleiche mit Normalisierung

  • Hilfsfunktionen für die Normalisierung, Groß- und Kleinschreibung und die rohe Entfernung von diakritischen Zeichen

  • Richtiges Sortieren von Unicode-Text mit locale und der pyuca-Bibliothek

  • Zeichen-Metadaten in der Unicode-Datenbank

  • Dual-Mode-APIs, die str und bytes

Was ist neu in diesem Kapitel?

Die Unterstützung für Unicode in Python 3 ist umfassend und stabil. Die bemerkenswerteste Ergänzung ist "Finding Characters by Name", das ein Dienstprogramm zum Durchsuchen der Unicode-Datenbank beschreibt - eine großartige Möglichkeit, um eingekreiste Ziffern und lächelnde Katzen über die Kommandozeile zu finden.

Eine kleine Änderung, die erwähnenswert ist, ist die Unicode-Unterstützung unter Windows, die seit Python 3.6 besser und einfacher ist, wie wir in "Vorsicht vor den Encoding Defaults" sehen werden .

Beginnen wir mit den nicht ganz so neuen, aber grundlegenden Konzepten von Zeichen, Codepunkten und Bytes.

Hinweis

Für, die zweite Ausgabe, habe ich den Abschnitt über das Modul struct erweitert und online unter"Parsing binary records with struct" auf der fluentpython.com-Begleitseite veröffentlicht.

Unter findest du auch denAbschnitt "Emojis mit mehreren Zeichen erstellen", in dem beschrieben wird, wie du Länderflaggen, Regenbogenflaggen, Menschen mit verschiedenen Hautfarben und verschiedene Familiensymbole durch die Kombination von Unicode-Zeichen erstellen kannst.

Charakter-Fragen

Das Konzept von ist ganz einfach: Eine Zeichenkette ist eine Folge von Zeichen. Das Problem liegt in der Definition von "Zeichen".

Im Jahr 2021 ist die beste Definition von "Zeichen", die wir haben, ein Unicode-Zeichen. Dementsprechend sind die Elemente, die wir aus einem Python 3 str erhalten, Unicode-Zeichen, genau wie die Elemente eines unicode Objekts in Python 2 und nicht die rohen Bytes, die wir aus einem Python 2 str erhalten.

Der Unicode-Standard trennt ausdrücklich die Identität der Zeichen von den spezifischen Byte-Darstellungen:

  • Die Identität eines Zeichens - sein Codepunkt - isteine Zahl von 0 bis 1.114.111 (Basis 10), die im Unicode-Standard als 4 bis 6 Hexadezimalziffern mit einem "U+"-Präfix von U+0000 bis U+10FFFF angegeben wird. Der Codepunkt für den Buchstaben A ist zum Beispiel U+0041, das Euro-Zeichen ist U+20AC und das Musiksymbol G-Schlüssel ist dem Codepunkt U+1D11E zugeordnet. Etwa 13 % der gültigen Codepunkte sind in Unicode 13.0.0, dem in Python 3.10.0b4 verwendeten Standard, Zeichen zugewiesen.

  • Die tatsächlichen Bytes, die ein Zeichen darstellen, hängen von der verwendeten Kodierung ab. Eine Kodierung ist ein Algorithmus, der Codepunkte in Bytefolgen umwandelt und umgekehrt. Der Codepunkt für den Buchstaben A (U+0041) wird in der UTF-8-Kodierung als einzelnes Byte \x41 kodiert, in der UTF-16LE-Kodierung als Bytes \x41\x00. Ein weiteres Beispiel: UTF-8 benötigt drei Bytes -\xe2\x82\xac- um das Euro-Zeichen (U+20AC) zu kodieren, aber in UTF-16LE wird derselbe Codepunkt als zwei Bytes kodiert: \xac\x20.

Die Umwandlung von Codepunkten in Bytes ist die Kodierung; die Umwandlung von Bytes in Codepunkte ist die Dekodierung. Siehe Beispiel 4-1.

Beispiel 4-1. Kodierung und Dekodierung
>>> s = 'café'
>>> len(s)  1
4
>>> b = s.encode('utf8')  2
>>> b
b'caf\xc3\xa9'  3
>>> len(b)  4
5
>>> b.decode('utf8')  5
'café'
1

Die str 'café' hat vier Unicode-Zeichen.

2

Kodiere str auf bytes mit UTF-8 Kodierung.

3

bytes Literale haben ein b Präfix.

4

bytes b hat fünf Bytes (der Codepunkt für "é" wird in UTF-8 als zwei Bytes kodiert).

5

Dekodiere bytes auf str mit UTF-8 Kodierung.

Tipp

Wenn du eine Gedächtnisstütze brauchst, um .decode() von .encode() zu unterscheiden, überzeuge dich selbst davon, dass Bytefolgen kryptische Maschinenkernabfälle sein können, während Unicode str Objekte "menschlicher" Text sind. Deshalb macht es Sinn, dass wir bytes zu str dekodieren, um menschlich lesbaren Text zu erhalten, und wir str zu bytes für die Speicherung oder Übertragung kodieren.

Obwohl der Python 3 str so ziemlich der Python 2 unicode -Typ mit einem neuen Namen ist, ist der Python 3 bytes nicht einfach der alte str -Typ mit neuem Namen, und es gibt auch den eng verwandten bytearray -Typ. Es lohnt sich also, einen Blick auf die binären Sequenztypen zu werfen, bevor man sich mit Fragen der Kodierung/Dekodierung beschäftigt.

Byte Essentials

Die neuen Binärsequenztypen unterscheiden sich in vielerlei Hinsicht von den Typen str in Python 2. Zunächst muss man wissen, dass es zwei eingebaute Grundtypen für Binärsequenzen gibt: den unveränderlichen Typ bytes, der in Python 3 eingeführt wurde, und den veränderlichen Typ bytearray, der bereits in Python 2.6 hinzugefügt wurde.2 In der Python-Dokumentation wird manchmal der allgemeine Begriff "Byte-String" verwendet, um sowohl bytes als auch bytearray zu bezeichnen. Ich vermeide diesen verwirrenden Begriff.

Jedes Element in bytes oder bytearray ist eine ganze Zahl von 0 bis 255 und keine einstellige Zeichenkette wie in Python 2 str. Ein Slice einer binären Sequenz erzeugt jedoch immer eine binäre Sequenz desselben Typs - einschließlich Slices der Länge 1. Siehe Beispiel 4-2.

Beispiel 4-2. Eine Fünf-Byte-Sequenz als bytes und als bytearray
>>> cafe = bytes('café', encoding='utf_8')  1
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0]  2
99
>>> cafe[:1]  3
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr  4
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:]  5
bytearray(b'\xa9')
1

bytes kann aus einer str erstellt werden.

2

Jedes Element ist eine ganze Zahl in range(256).

3

Scheiben von bytes sind auch bytes- sogar Scheiben von einem einzigen Byte.

4

Für bytearray gibt es keine Literal-Syntax: Sie werden als bytearray() mit einem bytes Literal als Argument angezeigt.

5

Ein Stück von bytearray ist auch ein bytearray.

Warnung

Die Tatsache, dass my_bytes[0] eine int abruft, my_bytes[:1] aber eine bytes Sequenz der Länge 1 zurückgibt, ist nur deshalb überraschend, weil wir an den Python-Typ str gewöhnt sind, bei dem s[0] == s[:1]. Für alle anderen Sequenztypen in Python ist 1 Item nicht dasselbe wie ein Slice derLänge 1.

Obwohl Binärsequenzen eigentlich Folgen von ganzen Zahlen sind, spiegelt ihre wörtliche Schreibweise die Tatsache wider, dass in ihnen oft ASCII-Text eingebettet ist. Deshalb werden je nach Byte-Wert vier verschiedene Darstellungen verwendet:

  • Für Bytes mit den Dezimalcodes 32 bis 126 - von Leerzeichen bis ~ (Tilde) - wird das ASCII-Zeichen selbst verwendet.

  • Für Bytes, die Tabulator, Newline, Carriage Return und \ entsprechen, werden die Escape-Sequenzen \t, \n, \r und \\ verwendet.

  • Wenn die beiden Stringbegrenzer ' und " in der Bytefolge vorkommen, wird die gesamte Folge durch ' begrenzt, und alle ' darin werden als \' escaped.3

  • Für andere Byte-Werte wird eine hexadezimale Escape-Sequenz verwendet (z. B. ist \x00 das Null-Byte).

Deshalb siehst du in Beispiel 4-2 b'caf\xc3\xa9': Die ersten drei Bytes b'caf' liegen im druckbaren ASCII-Bereich, die letzten beiden nicht.

Sowohl bytes als auch bytearray unterstützen alle str Methoden mit Ausnahme derer, die eine Formatierung vornehmen (format, format_map) und derer, die von Unicode-Daten abhängen, einschließlich casefold, isdecimal, isidentifier, isnumeric, isprintable und encode. Das bedeutet, dass du bekannte String-Methoden wie endswith, replace, strip, translate, upper und Dutzende andere mit binären Sequenzen verwenden kannst - nur mit bytes und nicht mit str Argumenten. Außerdem funktionieren die Funktionen für reguläre Ausdrücke im Modul re auch mit binären Sequenzen, wenn der Regex aus einer binären Sequenz anstelle einer str kompiliert wird. Seit Python 3.5 funktioniert der Operator % wieder mit binären Sequenzen.4

Binäre Sequenzen haben eine Klassenmethode, die str nicht hat. Sie heißt fromhex und baut eine binäre Sequenz auf, indem sie Paare von Hex-Ziffern analysiert, die optional durch Leerzeichen getrennt sind:

>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'

Die anderen Möglichkeiten, bytes oder bytearray Instanzen zu erstellen, sind der Aufruf ihrer Konstruktoren mit:

  • Ein str und ein encoding Schlüsselwort-Argument

  • Eine Iterable, die Elemente mit Werten von 0 bis 255 bereitstellt

  • Ein Objekt, das das Pufferprotokoll implementiert (z. B. bytes, bytearray, memoryview, array.array), das die Bytes aus dem Quellobjekt in die neu erstellte Binärsequenz kopiert

Warnung

Bis Python 3.5 war es auch möglich, bytes oder bytearray mit einer einzelnen Ganzzahl aufzurufen, um eine binäre Sequenz dieser Größe zu erstellen, die mit Null-Bytes initialisiert wurde. Diese Signatur wurde in Python 3.5 veraltet und in Python 3.6 entfernt. Siehe PEP 467-Minor API improvements for binary sequences.

Der Aufbau einer binären Sequenz aus einem pufferähnlichen Objekt ist eine Low-Level-Operation, die ein Typ-Casting beinhalten kann. Eine Demonstration findest du in Beispiel 4-3.

Beispiel 4-3. Initialisierung von Bytes aus den Rohdaten eines Arrays
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])  1
>>> octets = bytes(numbers)  2
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'  3
1

Typecode 'h' erstellt eine array aus kurzen Ganzzahlen (16 Bit).

2

octets enthält eine Kopie der Bytes, aus denen numbers besteht.

3

Dies sind die 10 Bytes, die die 5 kurzen Ganzzahlen darstellen.

Wenn du ein bytes oder bytearray Objekt aus einer beliebigen pufferähnlichen Quelle erstellst, werden die Bytes immer kopiert. Im Gegensatz dazu kannst du mit memoryview Objekten den Speicher zwischen binären Datenstrukturen teilen, wie wir in "Speicheransichten" gesehen haben .

Nach dieser grundlegenden Erkundung der binären Sequenztypen in Python wollen wir sehen, wie sie in/aus Strings umgewandelt werden.

Grundlegende Kodierer/Dekodierer

Die Python-Distribution bündelt mehr als 100 Codecs (Encoder/Decoder) für die Umwandlung von Text in Bytes und umgekehrt. Jeder Codec hat einen Namen, wie 'utf_8', und oft auch Aliase, wie 'utf8', 'utf-8' und 'U8', die du als encoding Argument in Funktionen wieopen(), str.encode(), bytes.decode(), usw. verwenden kannst.Beispiel 4-4 zeigt denselben Text, der als drei verschiedene Bytefolgen kodiert wurde.

Beispiel 4-4. Die Zeichenfolge "El Niño" kodiert mit drei Codecs, die sehr unterschiedliche Bytefolgen erzeugen
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
...     print(codec, 'El Niño'.encode(codec), sep='\t')
...
latin_1 b'El Ni\xf1o'
utf_8   b'El Ni\xc3\xb1o'
utf_16  b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

Abbildung 4-1 zeigt eine Reihe von Codecs, die Bytes aus Zeichen wie dem Buchstaben "A" und dem Musiksymbol G-clef erzeugen. Die letzten drei Codierungen sind Multibyte-Codierungen mit variabler Länge.

Encodings demonstration table
Abbildung 4-1. Zwölf Zeichen, ihre Codepunkte und ihre Byte-Darstellung (in Hex) in 7 verschiedenen Kodierungen (Sternchen zeigen an, dass das Zeichen in dieser Kodierung nicht dargestellt werden kann).

Die Sternchen in Abbildung 4-1 machen deutlich, dass einige Kodierungen wie ASCII und sogar die Multibyte-Kodierung GB2312 nicht alle Unicode-Zeichen darstellen können. Die UTF-Kodierungen sind jedoch so konzipiert, dass sie jeden Unicode-Codepunkt verarbeiten können.

Die in Abbildung 4-1 gezeigten Kodierungen wurden als repräsentative Beispiele ausgewählt:

latin1 auch bekannt als... iso8859_1

Das ist wichtig, weil es die Grundlage für andere Kodierungen ist, wie cp1252 und Unicode selbst (beachte, wie die latin1 Byte-Werte in den cp1252 Bytes und sogar in den Code-Punkten erscheinen).

cp1252

Eine nützliche latin1 Obermenge, die von Microsoft entwickelt wurde und nützliche Symbole wie geschweifte Anführungszeichen und € (Euro) hinzufügt; einige Windows-Anwendungen nennen sie "ANSI", aber sie war nie ein echter ANSI-Standard.

cp437

Der ursprüngliche Zeichensatz des IBM PC, mit Kastenzeichen. Inkompatibel mit latin1, das später erschien.

gb2312

Älterer Standard zur Kodierung der vereinfachten chinesischen Ideogramme, die auf dem chinesischen Festland verwendet werden; eine von mehreren weit verbreiteten Multibyte-Kodierungen für asiatische Sprachen.

utf-8

Die mit Abstand häufigste 8-Bit-Kodierung im Web. Laut"W3Techs: Usage statistics of character encodings for websites"vom Juli 2021 verwenden 97 % der Websites UTF-8, gegenüber 81,4 %, als ich diesen Absatz in der ersten Ausgabe dieses Buches im September 2014 schrieb.

utf-16le

Eine Form des UTF-16-Bit-Kodierungsschemas; alle UTF-16-Kodierungen unterstützen Codepunkte jenseits von U+FFFF durch Escape-Sequenzen, die "Surrogatpaare" genannt werden.

Warnung

UTF-16 hat die ursprüngliche 16-Bit-Kodierung von Unicode 1.0 - UCS-2 - bereits 1996 abgelöst. UCS-2 wird immer noch in vielen Systemen verwendet, obwohl es seit dem letzten Jahrhundert veraltet ist, weil es nur Codepunkte bis U+FFFF unterstützt. 2021 liegen mehr als 57 % der zugewiesenen Codepunkte über U+FFFF, einschließlich der wichtigen Emojis.

Nach diesem Überblick über gängige Kodierungen kommen wir nun zur Behandlung von Problemen bei Kodierungs- und Dekodierungsvorgängen.

Verstehen von Kodierungs-/Dekodierungsproblemen

Obwohl eine allgemeine UnicodeError Ausnahme ist, ist der von Python gemeldete Fehler normalerweise spezifischer: entweder ein UnicodeEncodeError (bei der Konvertierung von str in binäre Sequenzen) oder ein UnicodeDecodeError (beim Einlesen von binären Sequenzen in str). Das Laden von Python-Modulen kann auch SyntaxError auslösen, wenn die Quellkodierung unerwartet ist. Wir werden in den nächsten Abschnitten zeigen, wie man all diese Fehler behandelt.

Tipp

Wenn du einen Unicode-Fehler bekommst, musst du zuerst den genauen Typ der Ausnahme feststellen. Ist es ein UnicodeEncodeError, ein UnicodeDecodeError oder ein anderer Fehler (z. B. SyntaxError), der auf ein Kodierungsproblem hinweist? Um das Problem zu lösen, musst du es zuerst verstehen.

Umgang mit UnicodeEncodeError

Die meisten Nicht-UTF-Codecs verarbeiten nur eine kleine Teilmenge der Unicode-Zeichen. Wenn bei der Konvertierung von Text in Bytes ein Zeichen nicht in der Zielcodierung definiert ist, wirdUnicodeEncodeError ausgelöst, es sei denn, es wird eine besondere Behandlung vorgesehen, indem ein errors Argument an die Codierungsmethode oder -funktion übergeben wird. Das Verhalten der Fehlerbehandlungsprogramme ist in Beispiel 4-5 dargestellt.

Beispiel 4-5. Kodierung in Bytes: Erfolg und Fehlerbehandlung
>>> city = 'São Paulo'
>>> city.encode('utf_8')  1
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1')  2
b'S\xe3o Paulo'
>>> city.encode('cp437')  3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore')  4
b'So Paulo'
>>> city.encode('cp437', errors='replace')  5
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace')  6
b'S&#227;o Paulo'
1

Die UTF-Kodierungen verarbeiten alle str.

2

iso8859_1 funktioniert auch für den String 'São Paulo'.

3

cp437 kann das 'ã' ("a" mit Tilde) nicht kodieren. Der Standard-Fehlerhandler -'strict'- löst UnicodeEncodeError aus.

4

Der error='ignore' Handler überspringt Zeichen, die nicht kodiert werden können; das ist in der Regel eine sehr schlechte Idee und führt zu stillem Datenverlust.

5

Bei der Kodierung ersetzt error='replace' nicht kodierbare Zeichen durch '?'. Die Daten gehen ebenfalls verloren, aber die Benutzer bekommen einen Hinweis, dass etwas nicht stimmt.

6

'xmlcharrefreplace' ersetzt nicht kodierbare Zeichen durch eine XML-Entität. Wenn du kein UTF verwenden kannst und es dir nicht leisten kannst, Daten zu verlieren, ist dies die einzige Option.

Hinweis

Die codecs Fehlerbehandlung ist erweiterbar. Du kannst zusätzliche Zeichenketten für das errors Argument registrieren, indem du einen Namen und eine Fehlerbehandlungsfunktion an die codecs.register_error Funktion übergibst. Siehe die codecs.register_error Dokumentation.

ASCII ist eine gemeinsame Untermenge für alle mir bekannten Kodierungen, daher sollte die Kodierung immer funktionieren, wenn der Text ausschließlich aus ASCII-Zeichen besteht. Mit Python 3.7 wurde eine neue boolesche Methode str.isascii() hinzugefügt, mit der du überprüfen kannst, ob dein Unicode-Text zu 100% aus ASCII-Zeichen besteht. Wenn das der Fall ist, solltest du in der Lage sein, ihn in jeder beliebigen Kodierung in Bytes zu kodieren, ohne UnicodeEncodeError auszulösen.

Umgang mit UnicodeDecodeError

Nicht jedes Byte enthält ein gültiges ASCII-Zeichen, und nicht jede Bytefolge ist ein gültiges UTF-8 oder UTF-16; wenn du also bei der Konvertierung einer binären Folge in Text eine dieser Kodierungen annimmst, erhältst du eine UnicodeDecodeError, wenn unerwartete Bytes gefunden werden.

Auf der anderen Seite sind viele ältere 8-Bit-Kodierungen wie 'cp1252', 'iso8859_1' und 'koi8_r'in der Lage, jeden Bytestrom zu dekodieren, auch zufälliges Rauschen, ohne Fehler zu melden. Wenn dein Programm also die falsche 8-Bit-Kodierung annimmt, wird es stillschweigend Müll dekodieren.

Tipp

Unleserliche Zeichen werden als Gremlins oder Mojibake (文字化け-Japanisch für "umgewandelter Text") bezeichnet.

Beispiel 4-6 veranschaulicht, wie die Verwendung des falschen Codecs zu Gremlins oder einemUnicodeDecodeError.

Beispiel 4-6. Dekodierung von str in Bytes: Erfolg und Fehlerbehandlung
>>> octets = b'Montr\xe9al'  1
>>> octets.decode('cp1252')  2
'Montréal'
>>> octets.decode('iso8859_7')  3
'Montrιal'
>>> octets.decode('koi8_r')  4
'MontrИal'
>>> octets.decode('utf_8')  5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace')  6
'Montr�al'
1

Das Wort "Montréal" verschlüsselt als latin1; '\xe9' ist das Byte für "é".

2

Die Dekodierung mit Windows 1252 funktioniert, weil es eine Obermenge von latin1 ist.

3

ISO-8859-7 ist für Griechisch gedacht, daher wird das Byte '\xe9' falsch interpretiert und es wird kein Fehler ausgegeben.

4

KOI8-R steht für Russisch. Jetzt steht '\xe9' für den kyrillischen Buchstaben "И".

5

Der 'utf_8' Codec erkennt, dass octets kein gültiges UTF-8 ist, und gibt UnicodeDecodeError aus.

6

Mit der 'replace' Fehlerbehandlung wird \xe9 durch "�" (CodepunktU+FFFD) ersetzt, dem offiziellen Unicode REPLACEMENT CHARACTER, der für unbekannte Zeichen gedacht ist.

SyntaxError beim Laden von Modulen mit unerwarteter Kodierung

UTF-8 ist die Standardkodierung für Python 3, so wie ASCII die Standardkodierung für Python 2 war. Wenn du ein .py-Modul lädst, das keine UTF-8-Daten und keine Kodierungsdeklaration enthält, erhältst du eine Meldung wie diese:

SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line
  1, but no encoding declared; see https://python.org/dev/peps/pep-0263/
  for details

Da UTF-8 in GNU/Linux- und macOS-Systemen weit verbreitet ist, ist ein wahrscheinliches Szenario das Öffnen einer .py-Datei, die unter Windows mit cp1252 erstellt wurde. Beachte, dass dieser Fehler auch in Python für Windows auftritt, da die Standardkodierung für Python 3-Quellen auf allen Plattformen UTF-8 ist.

Um dieses Problem zu lösen, füge einen magischen coding Kommentar am Anfang der Datei ein, wie in Beispiel 4-7 gezeigt.

Beispiel 4-7. ola.py: "Hallo, Welt!" auf Portugiesisch
# coding: cp1252

print('Olá, Mundo!')
Tipp

Da der Quellcode von Python 3 nun nicht mehr auf ASCII beschränkt ist, sondern standardmäßig in der hervorragenden UTF-8-Kodierung vorliegt, ist die beste "Lösung" für Quellcode in alten Kodierungen wie 'cp1252', ihn bereits in UTF-8 zu konvertieren und sich nicht um die coding Kommentare zu kümmern.

Angenommen, du hast eine Textdatei, sei es Quellcode oder Gedichte, aber du kennst ihre Kodierung nicht. Wie kannst du die tatsächliche Kodierung erkennen? Die Antworten findest du im nächsten Abschnitt.

Wie man die Kodierung einer Bytesequenz herausfindet

Wie findest du die Kodierung einer Bytefolge? Kurze Antwort: Du kannst es nicht. Man muss es dir sagen.

Einige Kommunikationsprotokolle und Dateiformate wie HTTP und XML enthalten Header, die ausdrücklich angeben, wie der Inhalt kodiert ist. Du kannst sicher sein, dass einige Byteströme nicht ASCII sind, weil sie Bytewerte über 127 enthalten, und die Art und Weise, wie UTF-8 und UTF-16 aufgebaut sind, schränkt die möglichen Bytefolgen ebenfalls ein.

Wenn man jedoch bedenkt, dass auch menschliche Sprachen ihre Regeln und Einschränkungen haben, kann man, sobald man annimmt, dass ein Bytestrom menschlicher Klartext ist, seine Kodierung mithilfe von Heuristiken und Statistiken herausfinden. Wenn zum Beispiel b'\x00' häufig vorkommt, handelt es sich wahrscheinlich um eine 16- oder 32-Bit-Kodierung und nicht um ein 8-Bit-Schema, denn Nullzeichen im Klartext sind Fehler. Wenn die Bytefolge b'\x20\x00' häufig vorkommt, ist es wahrscheinlicher, dass es sich um das Leerzeichen (U+0020) in einer UTF-16LE-Kodierung handelt und nicht um das obskure Zeichen U+2000 EN QUAD - was auch immer das ist.

So arbeitet das Paket "Chardet-The Universal Character Encoding Detector", um eine von mehr als 30 unterstützten Kodierungen zu erraten.Chardet ist eine Python-Bibliothek, die du in deinen Programmen verwenden kannst, enthält aber auch ein Kommandozeilenprogramm, chardetect. Hier ist, was es über die Quelldatei dieses Kapitels berichtet:

$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99

Obwohl binäre Sequenzen von kodiertem Text normalerweise keine expliziten Hinweise auf ihre Kodierung enthalten, können die UTF-Formate dem Textinhalt eine Byte-Order-Markierung voranstellen. Das wird im Folgenden erklärt.

BOM: Ein nützlicher Gremlin

In Beispiel 4-4 hast du vielleicht ein paar zusätzliche Bytes am Anfang einer UTF-16 kodierten Sequenz bemerkt. Hier sind sie wieder:

>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

Die Bytes sind b'\xff\xfe'. Das ist eine BOM-Byte-Order-Markierung, die die "Little-Endian"-Byte-Order der Intel-CPU angibt, auf der die Kodierung durchgeführt wurde.

Auf einem Little-Endian-Rechner kommt für jeden Codepunkt das niederwertigste Byte zuerst: Der Buchstabe 'E', Codepunkt U+0045 (dezimal 69), wird in den Byte-Offsets 2 und 3 als 69 und 0 kodiert:

>>> list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]

Auf einer Big-Endian-CPU würde die Kodierung umgekehrt werden; 'E' würde als 0 und 69 kodiert werden.

Um Verwirrung zu vermeiden, stellt die UTF-16-Kodierung dem zu kodierenden Text das unsichtbare Sonderzeichen ZERO WIDTH NO-BREAK SPACE (U+FEFF) voran. In einem Little-Endian-System wird es als b'\xff\xfe' (dezimal 255, 254) kodiert. Da es in Unicode kein U+FFFE-Zeichen gibt, muss die Bytefolge b'\xff\xfe'in einer Little-Endian-Kodierung ZERO WIDTH NO-BREAK SPACE bedeuten, damit der Codec weiß, welche Bytereihenfolge er verwenden muss.

Es gibt eine Variante von UTF-16 - UTF-16LE -, die explizit Little-Endian ist, und eine andere, die explizit Big-Endian ist, UTF-16BE. Wenn du sie verwendest, wird kein BOMerzeugt:

>>> u16le = 'El Niño'.encode('utf_16le')
>>> list(u16le)
[69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
>>> u16be = 'El Niño'.encode('utf_16be')
>>> list(u16be)
[0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]

Wenn es vorhanden ist, soll das BOM vom UTF-16 Codec gefiltert werden, sodass du nur den eigentlichen Textinhalt der Datei ohne das führende ZERO WIDTH NO-BREAK SPACE erhältst. Der Unicode-Standard besagt, dass eine Datei, die UTF-16 ist und kein BOM hat, als UTF-16BE (Big-Endian) angesehen werden sollte. Die Intel x86-Architektur ist jedoch Little-Endian, sodass es in der freien Wildbahn viel Little-Endian UTF-16 ohne BOM gibt.

Das Problem der Endianness betrifft nur Kodierungen, die Wörter mit mehr als einem Byte verwenden, wie UTF-16 und UTF-32. Ein großer Vorteil von UTF-8 ist, dass es unabhängig von der Endianness des Rechners die gleiche Bytefolge erzeugt, sodass kein BOM benötigt wird. Dennoch fügen einige Windows-Anwendungen (vor allem Notepad) das BOM zu UTF-8-Dateien hinzu - und Excel ist auf das BOM angewiesen, um eine UTF-8-Datei zu erkennen, da es sonst annimmt, dass der Inhalt mit einer Windows-Codepage kodiert ist. Diese UTF-8-Kodierung mit BOM wird in Pythons Codec-Registry UTF-8-SIG genannt. Das in UTF-8-SIG kodierte Zeichen U+FEFF ist die Drei-Byte-Sequenz b'\xef\xbb\xbf'. Wenn eine Datei mit diesen drei Bytes beginnt, handelt es sich wahrscheinlich um eine UTF-8-Datei mit BOM.

Caleb's Tipp über UTF-8-SIG

Caleb Hattingh - einer der technischen Prüfer - empfiehlt, beim Lesen von UTF-8-Dateien immer den UTF-8-SIG-Codec zu verwenden. Das ist harmlos, weil UTF-8-SIG Dateien mit oder ohne BOM korrekt liest und das BOM selbst nicht zurückgibt. Beim Schreiben empfehle ich, UTF-8 für die allgemeine Interoperabilität zu verwenden. Python-Skripte können zum Beispiel unter Unix-Systemen ausführbar gemacht werden, wenn sie mit dem Kommentar #!/usr/bin/env python3 beginnen. Die ersten beiden Bytes der Datei müssen b'#!' sein, damit das funktioniert, aber das BOM bricht diese Konvention. Wenn du Daten an Anwendungen exportieren musst, die das BOM benötigen, verwende UTF-8-SIG, aber beachte, dass in derPython-Codecs-Dokumentation steht: "In UTF-8 wird von der Verwendung des BOM abgeraten und es sollte generell vermieden werden."

Wir gehen nun zum Umgang mit Textdateien in Python 3 über.

Umgang mit Textdateien

Die bewährte Methode für die Handhabung von Text-E/A ist das "Unicode-Sandwich"(Abbildung 4-2).5 Das bedeutet, dass bytes bei der Eingabe so früh wie möglich nach str dekodiert werden sollte (z. B. beim Öffnen einer Datei zum Lesen). Die "Füllung" des Sandwichs ist die Geschäftslogik deines Programms, bei der die Textverarbeitung ausschließlich auf str Objekten erfolgt. Du solltest niemals mitten in einer anderen Verarbeitung kodieren oder dekodieren. Bei der Ausgabe werden die str so spät wie möglich nach bytes kodiert. Die meisten Web-Frameworks funktionieren so, und wir berühren bytes nur selten, wenn wir sie benutzen. In Django zum Beispiel sollten deine Views Unicode str ausgeben; Django selbst kümmert sich um die Kodierung der Antwort nach bytes und verwendet standardmäßig UTF-8.

Python 3 macht es einfacher, den Rat des Unicode-Sandwichs zu befolgen, denn die open() built-in übernimmt die notwendige Dekodierung beim Lesen und die Kodierung beimSchreiben von Dateien im Textmodus, so dass alles, was du von my_file.read() erhältst und an my_file.write(text) weitergibst, str Objekte sind.

Deshalb ist es scheinbar einfach, Textdateien zu verwenden. Aber wenn du dich auf die Standardkodierungen verlässt, wirst du gebissen werden.

Unicode sandwich diagram
Abbildung 4-2. Unicode-Sandwich: die derzeit bewährte Methode für die Textverarbeitung.

Schau dir die Konsolensitzung in Beispiel 4-8 an. Kannst du den Fehler erkennen?

Beispiel 4-8. Ein Problem mit der Plattformkodierung (wenn du das auf deinem Rechner ausprobierst, kannst du das Problem sehen oder auch nicht)
>>> open('cafe.txt', 'w', encoding='utf_8').write('café')
4
>>> open('cafe.txt').read()
'café'

Der Fehler: Ich habe beim Schreiben der Datei die UTF-8-Kodierung angegeben, aber beim Lesen fehlgeschlagen, so dass Python die Standard-Dateikodierung von Windows annahm - Seite 1252 - und die letzten Bytes in der Datei als Zeichen 'é' statt 'é' dekodiert wurden.

Ich habe Beispiel 4-8 mit Python 3.8.1, 64 Bits, unter Windows 10 (Build 18363) ausgeführt. Die gleichen Anweisungen funktionieren unter neueren GNU/Linux- oder macOS-Betriebssystemen einwandfrei, da deren Standardkodierung UTF-8 ist, was den falschen Eindruck erweckt, dass alles in Ordnung ist. Wenn das Argument "Kodierung" beim Öffnen der zu schreibenden Datei weggelassen würde, würde die Standardkodierung des Gebietsschemas verwendet und wir würden die Datei mit der gleichen Kodierung korrekt lesen. Aber dann würde dieses Skript Dateien mit unterschiedlichen Byte-Inhalten je nach Plattform oder sogar je nach Gebietsschema-Einstellungen auf derselben Plattform erzeugen, was zu Kompatibilitätsproblemen führen würde.

Tipp

Code, der auf mehreren Rechnern oder bei mehreren Gelegenheiten ausgeführt werden muss, sollte sich nie auf die Standardkodierung verlassen. Übergib immer ein explizites encoding= Argument, wenn du Textdateien öffnest, denn die Standardeinstellung kann sich von einem Rechner zum nächsten oder von einem Tag zum nächsten ändern.

Ein merkwürdiges Detail in Beispiel 4-8 ist, dass die Funktion write in der ersten Anweisung meldet, dass vier Zeichen geschrieben wurden, aber in der nächsten Zeile werden fünf Zeichen gelesen.Beispiel 4-9 ist eine erweiterte Version von Beispiel 4-8, in der dieses und andere Details erklärt werden.

Beispiel 4-9. Wenn du dir Beispiel 4-8 unter Windows genauer ansiehst, erkennst du den Fehler und weißt, wie du ihn beheben kannst
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp  1
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café')  2
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size  3
5
>>> fp2 = open('cafe.txt')
>>> fp2  4
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding  5
'cp1252'
>>> fp2.read() 6
'café'
>>> fp3 = open('cafe.txt', encoding='utf_8')  7
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read() 8
'café'
>>> fp4 = open('cafe.txt', 'rb')  9
>>> fp4                           10
<_io.BufferedReader name='cafe.txt'>
>>> fp4.read()  11
b'caf\xc3\xa9'
1

Standardmäßig verwendet open den Textmodus und gibt ein TextIOWrapper Objekt mit einer bestimmten Kodierung zurück.

2

Die Methode write auf TextIOWrapper gibt die Anzahl der geschriebenen Unicode-Zeichen zurück.

3

os.stat sagt, dass die Datei 5 Bytes hat; UTF-8 kodiert 'é' als 2 Bytes, 0xc3 und 0xa9.

4

Wenn du eine Textdatei ohne explizite Kodierung öffnest, erhältst du eine TextIOWrapper mit einer Standardkodierung aus dem Gebietsschema.

5

Ein TextIOWrapper Objekt hat ein Kodierungsattribut, das du einsehen kannst: cp1252 in diesem Fall.

6

In der Windows-Kodierung cp1252 ist das Byte 0xc3 ein "Ã" (A mit Tilde), und 0xa9 ist das Copyright-Zeichen.

7

Öffnen der gleichen Datei mit der richtigen Kodierung.

8

Das erwartete Ergebnis: die gleichen vier Unicode-Zeichen für 'café'.

9

Das 'rb' Flag öffnet eine Datei zum Lesen im Binärmodus.

10

Das zurückgegebene Objekt ist ein BufferedReader und nicht ein TextIOWrapper.

11

Das Lesen liefert erwartungsgemäß Bytes.

Tipp

Öffne keine Textdateien im Binärmodus, es sei denn, du musst den Inhalt der Datei analysieren, um die Kodierung zu bestimmen - selbst dann solltest du Chardet verwenden, anstatt das Rad neu zu erfinden (siehe "Wie man die Kodierung einer Bytefolge herausfindet"). Gewöhnlicher Code sollte den Binärmodus nur verwenden, um Binärdateien wie Rasterbilder zu öffnen.

Das Problem in Beispiel 4-9 hat damit zu tun, dass du dich beim Öffnen einer Textdatei auf eine Standardeinstellung verlassen musst. Es gibt mehrere Quellen für solche Standardeinstellungen, wie der nächste Abschnitt zeigt.

Vorsicht vor Kodierungsvorgaben

Mehrere Einstellungen auf beeinflussen die Kodierungsvorgaben für E/A in Python. Siehe das Skript default_encodings.py in Beispiel 4-10.

Beispiel 4-10. Erkunden der Kodierungsvorgaben
import locale
import sys

expressions = """
        locale.getpreferredencoding()
        type(my_file)
        my_file.encoding
        sys.stdout.isatty()
        sys.stdout.encoding
        sys.stdin.isatty()
        sys.stdin.encoding
        sys.stderr.isatty()
        sys.stderr.encoding
        sys.getdefaultencoding()
        sys.getfilesystemencoding()
    """

my_file = open('dummy', 'w')

for expression in expressions.split():
    value = eval(expression)
    print(f'{expression:>30} -> {value!r}')

Die Ausgabe von Beispiel 4-10 auf GNU/Linux (Ubuntu 14.04 bis 19.10) und macOS (10.9 bis 10.14) ist identisch, was zeigt, dass UTF-8 überall auf diesen Systemen verwendet wird:

$ python3 default_encodings.py
 locale.getpreferredencoding() -> 'UTF-8'
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'UTF-8'
           sys.stdout.isatty() -> True
           sys.stdout.encoding -> 'utf-8'
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'utf-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'utf-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'

Unter Windows sieht die Ausgabe jedoch wie in Beispiel 4-11 aus.

Beispiel 4-11. Standardkodierungen in der Windows 10 PowerShell (die Ausgabe ist dieselbe wie in cmd.exe)
> chcp  1
Active code page: 437
> python default_encodings.py  2
 locale.getpreferredencoding() -> 'cp1252'  3
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'cp1252'  4
           sys.stdout.isatty() -> True      5
           sys.stdout.encoding -> 'utf-8'   6
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'utf-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'utf-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'
1

chcp zeigt die aktive Codeseite für die Konsole an: 437.

2

Ausführen von default_encodings.py mit Ausgabe auf der Konsole.

3

locale.getpreferredencoding() ist die wichtigste Einstellung.

4

Textdateien verwenden standardmäßig locale.getpreferredencoding().

5

Die Ausgabe geht auf die Konsole, also ist sys.stdout.isatty() True .

6

Nun ist sys.stdout.encoding nicht dasselbe wie die Konsolencodeseite, die von chcp gemeldet wird!

Die Unicode-Unterstützung in Windows selbst und in Python für Windows wurde verbessert, seit ich die erste Ausgabe dieses Buches geschrieben habe.Beispiel 4-11 meldete früher vier verschiedene Kodierungen in Python 3.4 unter Windows 7. Die Kodierungen für stdout, stdin und stderr waren früher dieselben wie die aktive Codepage, die vom Befehl chcp gemeldet wurde, aber jetzt sind sie alle utf-8 dankPEP 528-Change Windows console encoding to UTF-8, das in Python 3.6 implementiert wurde, und der Unicode-Unterstützung in der PowerShell in cmd.exe (seit Windows 1809 vom Oktober 2018).6 Es ist seltsam, dass chcp und sys.stdout.encoding unterschiedliche Dinge sagen, wenn stdout in die Konsole schreibt, aber es ist toll, dass wir jetzt Unicode-Zeichenfolgen ohne Kodierungsfehler unter Windows ausgeben können - es sei denn, der Benutzer leitet die Ausgabe in eine Datei um, wie wir gleich sehen werden. Das bedeutet nicht, dass alle deine Lieblings-Emojis in der Konsole erscheinen: Das hängt auch von der Schriftart ab, die die Konsole verwendet.

Eine weitere Änderung war PEP 529-Change Windows filesystem encoding to UTF-8, die ebenfalls in Python 3.6 implementiert wurde und die Dateisystemkodierung (die zur Darstellung von Verzeichnis- und Dateinamen verwendet wird) von Microsofts proprietärem MBCS auf UTF-8 änderte.

Wenn die Ausgabe von Beispiel 4-10 jedoch in eine Datei umgeleitet wird, wie hier:

Z:\>python default_encodings.py > encodings.log

dann wird der Wert von sys.stdout.isatty() zu False, und sys.stdout.encoding wird durch locale.getpreferredencoding(),'cp1252' in dieser Maschine gesetzt - aber sys.stdin.encoding und sys.stderr.encoding bleiben utf-8.

Tipp

In Beispiel 4-12 verwende ich den '\N{}' Escape für Unicode-Literale, bei dem wir den offiziellen Namen des Zeichens in das \N{} schreiben. Das ist zwar ziemlich langatmig, aber eindeutig und sicher: Python meldet SyntaxError, wenn der Name nicht existiert - viel besser als eine Hexadezimalzahl zu schreiben, die falsch sein könnte, was du aber erst viel später herausfindest. Du würdest wahrscheinlich sowieso einen Kommentar schreiben wollen, der die Zeichencodes erklärt, also ist die Ausführlichkeit von \N{} leicht zu akzeptieren.

Das bedeutet, dass ein Skript wie Beispiel 4-12 funktioniert, wenn es auf der Konsole ausgegeben wird, aber möglicherweise abbricht, wenn die Ausgabe in eine Datei umgeleitet wird.

Beispiel 4-12. stdout_check.py
import sys
from unicodedata import name

print(sys.version)
print()
print('sys.stdout.isatty():', sys.stdout.isatty())
print('sys.stdout.encoding:', sys.stdout.encoding)
print()

test_chars = [
    '\N{HORIZONTAL ELLIPSIS}',       # exists in cp1252, not in cp437
    '\N{INFINITY}',                  # exists in cp437, not in cp1252
    '\N{CIRCLED NUMBER FORTY TWO}',  # not in cp437 or in cp1252
]

for char in test_chars:
    print(f'Trying to output {name(char)}:')
    print(char)

Beispiel 4-12 zeigt das Ergebnis von sys.stdout.isatty(), den Wert von sys.​stdout.encoding und diese drei Zeichen:

  • '…' HORIZONTAL ELLIPSIS-gibt es in CP 1252, aber nicht in CP 437.

  • '∞' INFINITY-gibt es in CP 437, aber nicht in CP 1252.

  • '㊷' CIRCLED NUMBER FORTY TWO-gibt es weder in CP 1252 noch in CP 437.

Wenn ich stdout_check.py mit PowerShell oder cmd.exe ausführe, funktioniert es wie in Abbildung 4-3 dargestellt.

Screen capture of `stdout_check.py` on PowerShell
Abbildung 4-3. Ausführen von stdout_check.py auf der PowerShell.

Obwohl chcp den aktiven Code als 437 meldet, ist sys.stdout.encoding UTF-8, sodass HORIZONTAL ELLIPSIS und INFINITY beide korrekt ausgegeben werden. CIRCLED NUMBER FORTY TWO wird durch ein Rechteck ersetzt, aber es wird kein Fehler gemeldet. Vermutlich wird es als gültiges Zeichen erkannt, aber die Konsolenschriftart hat nicht die Glyphe, um es darzustellen.

Wenn ich jedoch die Ausgabe von stdout_check.py in eine Datei umleite, erhalte ich Abbildung 4-4.

Screen capture of `stdout_check.py` on PowerShell, redirecting output
Abbildung 4-4. Ausführen von stdout_check.py auf der PowerShell, Umleitung der Ausgabe.

Das erste Problem, das in Abbildung 4-4 gezeigt wird, ist das UnicodeEncodeError erwähnende Zeichen '\u221e', denn sys.stdout.encoding ist 'cp1252'- eine Codepage, die das Zeichen INFINITY nicht hat.

Das Lesen von out.txt mit dem Befehl type - oder einem Windows-Editor wie VS Code oder Sublime Text - zeigt, dass ich statt HORIZONTAL ELLIPSIS 'à' (LATIN SMALL LETTER A WITH GRAVE) erhalten habe. Wie sich herausstellt, bedeutet der Byte-Wert 0x85 in CP 1252 '…', aber in CP 437 steht derselbe Byte-Wert für 'à'. Es scheint also, dass die aktive Codepage eine Rolle spielt, nicht in einer sinnvollen oder nützlichen Weise, sondern als Teilerklärung für eine schlechte Unicode-Erfahrung.

Hinweis

Für diese Experimente habe ich einen für den US-Markt konfigurierten Laptop mit Windows 10 OEM verwendet. Für andere Länder lokalisierte Windows-Versionen können andere Kodierungskonfigurationen haben. In Brasilien zum Beispiel verwendet die Windows-Konsole standardmäßig die Codepage 850 und nicht 437.

Um dieses verrückte Thema der Standardkodierungen abzuschließen, lass uns einen letzten Blick auf die verschiedenen Kodierungen in Beispiel 4-11 werfen:

  • Wenn du das Argument encoding beim Öffnen einer Datei weglässt, wird die Vorgabe durch locale.getpreferredencoding() ('cp1252' in Beispiel 4-11) gegeben.

  • Die Kodierung von sys.stdout|stdin|stderr wurde vor Python 3.6 durch die PYTHONIOENCODING Umgebungsvariable festgelegt - jetzt wird diese Variable ignoriert, es sei denn, sie ist auf einen nicht leeren String gesetzt. PYTHONLEGACYWINDOWSSTDIO Andernfalls ist die Kodierung für Standard-E/A UTF-8 für interaktive E/A oder wird von locale.getpreferredencoding() definiert, wenn die Ausgabe/Eingabe in/aus einer Datei umgeleitet wird.

  • sys.getdefaultencoding() wird von Python intern für implizite Konvertierungen von Binärdaten nach/von str verwendet. Eine Änderung dieser Einstellung wird nicht unterstützt.

  • sys.getfilesystemencoding() wird zum Kodieren/Dekodieren von Dateinamen (nicht von Dateiinhalten) verwendet. Er wird verwendet, wenn open() ein str Argument für den Dateinamen erhält; wenn der Dateiname als bytes Argument angegeben wird, wird er unverändert an die OS-API weitergegeben.

Hinweis

Unter GNU/Linux und macOS sind alle diese Kodierungen standardmäßig auf UTF-8 eingestellt, und das schon seit mehreren Jahren, so dass I/O alle Unicode-Zeichen verarbeiten kann. Unter Windows werden nicht nur verschiedene Kodierungen im selben System verwendet, sondern es handelt sich in der Regel um Codepages wie 'cp850' oder 'cp1252', die nur ASCII unterstützen, mit 127 zusätzlichen Zeichen, die von einer Kodierung zur anderen nicht gleich sind. Daher ist die Wahrscheinlichkeit, dass Windows-Benutzer mit Kodierungsfehlern konfrontiert werden, viel größer, wenn sie nicht besonders vorsichtig sind.

Zusammenfassend lässt sich sagen, dass die wichtigste Kodierungseinstellung die ist, die von locale.getpreferredencoding() zurückgegeben wird: Sie ist die Standardeinstellung für das Öffnen von Textdateien und für sys.stdout/stdin/stderr, wenn sie auf Dateien umgeleitet werden. In der Dokumentation heißt es jedoch (teilweise):

locale.getpreferredencoding(do_setlocale=True)

Gibt die Kodierung zurück, die für Textdaten gemäß den Benutzerpräferenzen verwendet wird. Die Benutzerpräferenzen werden auf verschiedenen Systemen unterschiedlich ausgedrückt und sind auf einigen Systemen möglicherweise nicht programmatisch verfügbar, daher gibt diese Funktion nur eine Vermutung zurück. [...]

Daher lautet der beste Ratschlag zu Kodierungsvorgaben: Verlasse dich nicht auf sie.

Du kannst dir viel Ärger ersparen, wenn du die Ratschläge des Unicode-Sandwichs befolgst und die Kodierungen in deinen Programmen immer explizit angibst. Leider ist Unicode schmerzhaft, selbst wenn du deine bytes korrekt in str umwandelst. Die nächsten beiden Abschnitte behandeln Themen, die im ASCII-Land einfach sind, aber auf dem Planeten Unicode ziemlich komplex werden: Textnormalisierung (d.h. die Umwandlung von Text in eine einheitliche Darstellung fürVergleiche) und Sortierung.

Normalisierung von Unicode für verlässliche Vergleiche

String-Vergleiche werden dadurch erschwert, dass es in Unicode kombinierende Zeichen gibt: diakritische Zeichen und andere Markierungen, die an das vorangehende Zeichen angehängt werden und beim Druck als ein Zeichen erscheinen.

Das Wort "Café" kann zum Beispiel auf zwei Arten zusammengesetzt werden, mit vier oder fünf Codepunkten, aber das Ergebnis sieht genau gleich aus:

>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False

Wenn du COMBINING ACUTE ACCENT (U+0301) nach "e" einfügst, wird "é" daraus. Im Unicode-Standard werden Sequenzen wie 'é' und 'e\u0301' als "kanonische Äquivalente" bezeichnet, und die Anwendungen sollen sie als gleichwertig behandeln. Python sieht jedoch zwei unterschiedliche Folgen von Codepunkten und betrachtet sie als nicht gleichwertig.

Die Lösung lautet unicodedata.normalize(). Das erste Argument dieser Funktion ist eine von vier Zeichenketten: 'NFC', 'NFD', 'NFKC' und 'NFKD'. Beginnen wir mit den ersten beiden.

Normalisierungsform C (NFC) setzt die Codepunkte so zusammen, dass die kürzeste äquivalente Zeichenfolge entsteht, während NFD die zusammengesetzten Zeichen in Basiszeichen und separate Kombinationszeichen zerlegt. Bei beiden Normalisierungen funktionieren Vergleiche wie erwartet, wie das nächste Beispiel zeigt:

>>> from unicodedata import normalize
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) == normalize('NFC', s2)
True
>>> normalize('NFD', s1) == normalize('NFD', s2)
True

Tastaturtreiber erzeugen in der Regel zusammengesetzte Zeichen, so dass der von den Nutzern eingegebene Text standardmäßig in NFC vorliegt. Um sicherzugehen, kann es jedoch sinnvoll sein, Strings vor dem Speichern mit normalize('NFC', user_text) zu normalisieren. NFC ist auch die vom W3C in"Character Model for the World Wide Web" empfohlene Normalisierungsform:String Matching and Searching".

Einige einzelne Zeichen werden von NFC in ein anderes einzelnes Zeichen normalisiert. Das Symbol für das Ohm (Ω), die Einheit des elektrischen Widerstands, wird auf das griechische Großbuchstaben-Omega normalisiert. Optisch sind sie identisch, aber im Vergleich sind sie ungleich, daher ist es wichtig, zu normalisieren, um Überraschungen zu vermeiden:

>>> from unicodedata import normalize, name
>>> ohm = '\u2126'
>>> name(ohm)
'OHM SIGN'
>>> ohm_c = normalize('NFC', ohm)
>>> name(ohm_c)
'GREEK CAPITAL LETTER OMEGA'
>>> ohm == ohm_c
False
>>> normalize('NFC', ohm) == normalize('NFC', ohm_c)
True

Die anderen beiden Normalisierungsformen sind NFKC und NFKD, wobei der Buchstabe K für "Kompatibilität" steht. Dies sind stärkere Formen der Normalisierung, die die sogenannten "Kompatibilitätszeichen" betreffen. Obwohl ein Ziel von Unicode darin besteht, für jedes Zeichen einen einzigen "kanonischen" Codepunkt zu haben, tauchen einige Zeichen ausKompatibilitätsgründen mit bereits bestehenden Standards mehr als einmal auf. Zum Beispiel wurde das MICRO SIGN, µ (U+00B5), zu Unicode hinzugefügt, um die Umwandlung in latin1 zu unterstützen, wo es enthalten ist, obwohl dasselbe Zeichen im griechischen Alphabet mit dem Codepunkt U+03BC (GREEK SMALL LETTER MU) enthalten ist. Das Mikrozeichen wird also als "Kompatibilitätszeichen" betrachtet.

In den NFKC- und NFKD-Formen wird jedes Kompatibilitätszeichen durch eine "Kompatibilitätszerlegung" eines oder mehrerer Zeichen ersetzt, die als "bevorzugte" Darstellung gelten, auch wenn es zu einem gewissen Formatierungsverlust kommt - im Idealfall sollte die Formatierung in der Verantwortung externer Auszeichner liegen und nicht Teil von Unicode sein. Ein Beispiel: DieKompatibilitätszerlegung des Halbbruchs '½' (U+00BD) ist die Folge von drei Zeichen '1/2', und die Kompatibilitätszerlegung des Mikrozeichens 'µ' (U+00B5) ist das Kleinbuchstaben-Mu 'μ' (U+03BC).7

So funktioniert der NFKC in der Praxis:

>>> from unicodedata import normalize, name
>>> half = '\N{VULGAR FRACTION ONE HALF}'
>>> print(half)
½
>>> normalize('NFKC', half)
'1⁄2'
>>> for char in normalize('NFKC', half):
...     print(char, name(char), sep='\t')
...
1	DIGIT ONE
⁄	FRACTION SLASH
2	DIGIT TWO
>>> four_squared = '4²'
>>> normalize('NFKC', four_squared)
'42'
>>> micro = 'µ'
>>> micro_kc = normalize('NFKC', micro)
>>> micro, micro_kc
('µ', 'μ')
>>> ord(micro), ord(micro_kc)
(181, 956)
>>> name(micro), name(micro_kc)
('MICRO SIGN', 'GREEK SMALL LETTER MU')

Obwohl '1⁄2' ein vernünftiger Ersatz für '½' ist und das Mikrozeichen in Wirklichkeit ein klein geschriebenes griechisches mu ist, ändert die Umwandlung von '4²' in '42' die Bedeutung. Eine Anwendung könnte '4²' als '4<sup>2</sup>' speichern, aber die Funktion normalize weiß nichts über die Formatierung. Daher können NFKC oder NFKD Informationen verlieren oder verzerren,aber sie können bequeme Zwischendarstellungen für die Suche undIndizierung erzeugen.

Leider ist bei Unicode immer alles komplizierter, als es auf den ersten Blick scheint. Für VULGAR FRACTION ONE HALF ergab die NFKC-Normalisierung 1 und 2, die durch FRACTION SLASH verbunden sind, statt durch SOLIDUS, auch bekannt als "Schrägstrich" - das bekannte Zeichen mit dem ASCII-Code Dezimal 47. Daher würde die Suche nach der dreistelligen ASCII-Sequenz '1/2' nicht die normalisierte Unicode-Sequenz finden.

Warnung

NFKC- und NFKD-Normalisierung führen zu Datenverlusten und sollten nur in besonderen Fällen wie der Suche und Indizierung und nicht für die dauerhafte Speicherung von Text angewendet werden.

Bei der Vorbereitung von Texten für die Suche oder Indizierung ist eine weitere Operation nützlich: die Groß-/Kleinschreibung, unser nächstes Thema.

Koffer falten

Groß- und Kleinschreibung bedeutet im Wesentlichen, dass der gesamte Text in Kleinbuchstaben umgewandelt wird, mit einigen zusätzlichen Umwandlungen. Sie wird von der Methode str.casefold() unterstützt.

Für jede Zeichenkette s, die nur latin1 Zeichen enthält, ergibt s.casefold() das gleiche Ergebnis wie s.lower(), mit nur zwei Ausnahmen - das Mikrozeichen 'µ' wird in das griechische Kleinbuchstaben mu (das in den meisten Schriftarten gleich aussieht) und das deutsche Eszett oder "scharfe s" (ß) wird zu "ss":

>>> micro = 'µ'
>>> name(micro)
'MICRO SIGN'
>>> micro_cf = micro.casefold()
>>> name(micro_cf)
'GREEK SMALL LETTER MU'
>>> micro, micro_cf
('µ', 'μ')
>>> eszett = 'ß'
>>> name(eszett)
'LATIN SMALL LETTER SHARP S'
>>> eszett_cf = eszett.casefold()
>>> eszett, eszett_cf
('ß', 'ss')

Es gibt fast 300 Codepunkte, für die str.casefold() und str.lower() unterschiedliche Ergebnisse liefern.

Wie bei allem, was mit Unicode zu tun hat, ist die Groß- und Kleinschreibung ein schwieriges Thema mit vielen sprachlichen Sonderfällen, aber das Python-Kernteam hat sich bemüht, eine Lösung zu finden, die hoffentlich für die meisten Benutzer funktioniert.

In den nächsten Abschnitten werden wir unser Wissen über Normalisierung bei der Entwicklung von Hilfsfunktionen anwenden.

Utility-Funktionen für den normalisierten Textabgleich

Wie wir auf gesehen haben, sind NFC und NFD sicher in der Anwendung und ermöglichen sinnvolle Vergleiche zwischen Unicode-Strings. NFC ist die beste normalisierte Form für die meisten Anwendungen. str.casefold() ist der richtige Weg für Vergleiche, bei denen Groß- und Kleinschreibung keine Rolle spielen.

Wenn du mit Text in vielen Sprachen arbeitest, sind Funktionen wie nfc_equal und fold_equal in Beispiel 4-13 nützliche Ergänzungen für deinen Werkzeugkasten.

Beispiel 4-13. normeq.py: normalisierter Unicode-Stringvergleich
"""
Utility functions for normalized Unicode string comparison.

Using Normal Form C, case sensitive:

    >>> s1 = 'café'
    >>> s2 = 'cafe\u0301'
    >>> s1 == s2
    False
    >>> nfc_equal(s1, s2)
    True
    >>> nfc_equal('A', 'a')
    False

Using Normal Form C with case folding:

    >>> s3 = 'Straße'
    >>> s4 = 'strasse'
    >>> s3 == s4
    False
    >>> nfc_equal(s3, s4)
    False
    >>> fold_equal(s3, s4)
    True
    >>> fold_equal(s1, s2)
    True
    >>> fold_equal('A', 'a')
    True

"""

from unicodedata import normalize

def nfc_equal(str1, str2):
    return normalize('NFC', str1) == normalize('NFC', str2)

def fold_equal(str1, str2):
    return (normalize('NFC', str1).casefold() ==
            normalize('NFC', str2).casefold())

Über die Unicode-Normalisierung und die Groß- und Kleinschreibung hinaus - die beide Teil des Unicode-Standards sind - ist es manchmal sinnvoll, tiefer gehende Umwandlungen vorzunehmen, z. B. 'café' in 'cafe' zu ändern. Wir werden im nächsten Abschnitt sehen, wann und wie.

Extreme "Normalisierung": Diakritische Zeichen herausnehmen

Die Geheimsauce der Google-Suche beinhaltet viele Tricks, aber einer davon ist offenbar das Ignorieren von diakritischen Zeichen (z. B. Akzente, Zedern usw.), zumindest in einigen Zusammenhängen. Das Entfernen von diakritischen Zeichen ist keine angemessene Form der Normalisierung, da es oft die Bedeutung von Wörtern verändert und bei der Suche zu falsch positiven Ergebnissen führen kann. Aber es hilft, mit einigen Tatsachen des Lebens fertig zu werden: Menschen sind manchmal faul oder unwissend, was die korrekte Verwendung von diakritischen Zeichen angeht, und Rechtschreibregeln ändern sich im Laufe der Zeit, was bedeutet, dass Akzente in lebenden Sprachen kommen und gehen.

Abgesehen von der Suche sorgt die Abschaffung der diakritischen Zeichen auch für besser lesbare URLs, zumindest in lateinischen Sprachen. Sieh dir die URL für den Wikipedia-Artikel über die Stadt São Paulo an:

https://en.wikipedia.org/wiki/S%C3%A3o_Paulo

Der Teil %C3%A3 ist die URL-escapte, UTF-8-Wiedergabe des einzelnen Buchstabens "ã" ("a" mit Tilde). Das Folgende ist viel einfacher zu erkennen, auch wenn es nicht die richtigeSchreibweise ist:

https://en.wikipedia.org/wiki/Sao_Paulo

Um alle diakritischen Zeichen aus einer str zu entfernen, kannst du eine Funktion wie Beispiel 4-14 verwenden.

Beispiel 4-14. simplify.py: Funktion zum Entfernen aller Kombinationszeichen
import unicodedata
import string


def shave_marks(txt):
    """Remove all diacritic marks"""
    norm_txt = unicodedata.normalize('NFD', txt)  1
    shaved = ''.join(c for c in norm_txt
                     if not unicodedata.combining(c))  2
    return unicodedata.normalize('NFC', shaved)  3
1

Zerlege alle Zeichen in Basiszeichen und Kombinationszeichen.

2

Filtere alle kombinierenden Zeichen heraus.

3

Setze alle Zeichen neu zusammen.

Beispiel 4-15 zeigt eine Reihe von Anwendungen von shave_marks.

Beispiel 4-15. Zwei Beispiele mit shave_marks aus Beispiel 4-14
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> shave_marks(order)
'“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”'  1
>>> Greek = 'Ζέφυρος, Zéfiro'
>>> shave_marks(Greek)
'Ζεφυρος, Zefiro'  2
1

Nur die Buchstaben "è", "ç" und "í" wurden ersetzt.

2

Sowohl "έ" als auch "é" wurden ersetzt.

Die Funktion shave_marks aus Beispiel 4-14 funktioniert ganz gut, aber vielleicht geht sie zu weit. Der Grund für das Entfernen von diakritischen Zeichen ist oft, dass lateinischer Text in reines ASCII umgewandelt werden soll, aber shave_marks ändert auch nicht-lateinische Zeichen wie griechische Buchstaben, die niemals zu ASCII werden, nur weil sie ihre Akzente verlieren. Daher ist es sinnvoll, jedes Basiszeichen zu analysieren und angehängte Zeichen nur dann zu entfernen, wenn das Basiszeichen ein Buchstabe des lateinischen Alphabets ist. Genau das macht Beispiel 4-16.

Beispiel 4-16. Funktion zum Entfernen von Kombinationszeichen aus lateinischen Zeichen (Import-Anweisungen werden weggelassen, da dies Teil des simplify.py-Moduls aus Beispiel 4-14 ist)
def shave_marks_latin(txt):
    """Remove all diacritic marks from Latin base characters"""
    norm_txt = unicodedata.normalize('NFD', txt)  1
    latin_base = False
    preserve = []
    for c in norm_txt:
        if unicodedata.combining(c) and latin_base:   2
            continue  # ignore diacritic on Latin base char
        preserve.append(c)                            3
        # if it isn't a combining char, it's a new base char
        if not unicodedata.combining(c):              4
            latin_base = c in string.ascii_letters
    shaved = ''.join(preserve)
    return unicodedata.normalize('NFC', shaved)   5
1

Zerlege alle Zeichen in Basiszeichen und Kombinationszeichen.

2

Überspringe Kombinationszeichen, wenn das Basiszeichen lateinisch ist.

3

Andernfalls behalte den aktuellen Charakter bei.

4

Erkenne neue Basiszeichen und stelle fest, ob sie lateinisch sind.

5

Setze alle Zeichen neu zusammen.

Ein noch radikalerer Schritt wäre es, gängige Symbole in westlichen Texten (z. B. geschweifte Anführungszeichen, em-Striche, Aufzählungszeichen usw.) durch ASCII zu ersetzen. Genau das macht die Funktion asciize in Beispiel 4-17.

Beispiel 4-17. Einige westliche typografische Symbole in ASCII umwandeln (dieses Snippet ist auch Teil von simplify.py aus Beispiel 4-14)
single_map = str.maketrans("""‚ƒ„ˆ‹‘’“”•–—˜›""",  1
                           """'f"^<''""---~>""")

multi_map = str.maketrans({  2
    '': 'EUR',
    '': '...',
    'Æ': 'AE',
    'æ': 'ae',
    'Œ': 'OE',
    'œ': 'oe',
    '': '(TM)',
    '': '<per mille>',
    '': '**',
    '': '***',
})

multi_map.update(single_map)  3


def dewinize(txt):
    """Replace Win1252 symbols with ASCII chars or sequences"""
    return txt.translate(multi_map)  4


def asciize(txt):
    no_marks = shave_marks_latin(dewinize(txt))     5
    no_marks = no_marks.replace('ß', 'ss')          6
    return unicodedata.normalize('NFKC', no_marks)  7
1

Erstelle eine Zuordnungstabelle für die Ersetzung von Zeichen zu Zeichen.

2

Erstelle eine Mapping-Tabelle für die Ersetzung von Zeichen durch Strings.

3

Führe Kartierungstabellen zusammen.

4

dewinize wirkt sich nicht auf den Text ASCII oder latin1 aus, sondern nur auf die Microsoft-Zusätze zu latin1 in cp1252.

5

Wende dewinize an und entferne diakritische Zeichen.

6

Ersetze das Eszett durch "ss" (wir verwenden hier keine Groß- und Kleinschreibung, weil wir die Großschreibung beibehalten wollen).

7

Wende die NFKC-Normalisierung an, um Zeichen mit ihren Kompatibilitätscodepunkten zusammenzusetzen.

Beispiel 4-18 zeigt asciize im Einsatz.

Beispiel 4-18. Zwei Beispiele mit asciize aus Beispiel 4-17
>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> dewinize(order)
'"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."'  1
>>> asciize(order)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."'  2
1

dewinize ersetzt geschweifte Anführungszeichen, Aufzählungszeichen und ™ (Markensymbol).

2

asciize wendet dewinize an, lässt die diakritischen Zeichen weg und ersetzt das 'ß'.

Warnung

Verschiedene Sprachen haben ihre eigenen Regeln zum Entfernen von diakritischen Zeichen. Im Deutschen wird zum Beispiel aus dem 'ü' ein 'ue'. Unsere Funktion asciize ist nicht so ausgefeilt, daher kann sie für deine Sprache geeignet sein oder auch nicht. Für Portugiesisch funktioniert sie aber ganz gut.

Zusammenfassend lässt sich sagen, dass die Funktionen in simplify.py weit über die Standardnormalisierung hinausgehen und tiefgreifende Eingriffe in den Text vornehmen, bei denen die Wahrscheinlichkeit groß ist, dass sie seine Bedeutung verändern. Nur du kannst entscheiden, ob du so weit gehen willst, wenn du die Zielsprache und deine Nutzer kennst und weißt, wie der umgewandelte Text verwendet werden soll.

Damit ist unsere Diskussion über die Normalisierung von Unicode-Text abgeschlossen.

Jetzt wollen wir uns um die Unicode-Sortierung kümmern.

Sortieren von Unicode-Text

Python sortiert Sequenzen beliebigen Typs, indem es die Elemente in jeder Sequenz eins nach dem anderen vergleicht. Bei Zeichenketten bedeutet das, dass die Codepunkte verglichen werden. Leider führt dies zu inakzeptablen Ergebnissen für alle, die Nicht-ASCII-Zeichen verwenden.

Sortiere eine Liste von Früchten, die in Brasilien angebaut werden:

>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']

Die Sortierregeln variieren von Land zu Land, aber im Portugiesischen und in vielen anderen Sprachen, die das lateinische Alphabet verwenden, machen Akzente und Cedillen beim Sortieren selten einen Unterschied.8 So wird "cajá" als "caja" sortiert und muss vor "caju" stehen.

Die sortierte Liste fruits sollte lauten:

['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Die Standardmethode, um Nicht-ASCII-Text in Python zu sortieren, ist die Funktion locale.strxfrm, die laut der Dokumentation des Modulslocale "eine Zeichenkette in eine Zeichenkette umwandelt, die in lokalisierten Vergleichen verwendet werden kann".

Um locale.strxfrm zu aktivieren, musst du zunächst ein passendes Gebietsschema für deine Anwendung festlegen und hoffen, dass das Betriebssystem es unterstützt. Die Befehlsfolge in Beispiel 4-19 könnte für dich funktionieren.

Beispiel 4-19. locale_sort.py: Verwendung der Funktion locale.strxfrm als Sortierschlüssel
import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale)
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)

Wenn ich Beispiel 4-19 auf GNU/Linux (Ubuntu 19.10) mit dem pt_BR.UTF-8 Gebietsschema ausführe, erhalte ich das richtige Ergebnis:

'pt_BR.UTF-8'
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Du musst also setlocale(LC_COLLATE, «your_locale») aufrufen, bevor du locale.strxfrm als Schlüssel beim Sortieren verwendest.

Es gibt jedoch einige Vorbehalte:

  • Da die Locale-Einstellungen global sind, wird der Aufruf von setlocale in einer Bibliothek nicht empfohlen. Deine Anwendung oder dein Framework sollte das Gebietsschema beim Start des Prozesses festlegen und es danach nicht mehr ändern.

  • Das Gebietsschema muss auf dem Betriebssystem installiert sein, sonst löst setlocale eine locale.Error: unsupported locale setting Ausnahme aus.

  • Du musst wissen, wie man den Namen des Gebietsschemas buchstabiert.

  • Das Gebietsschema muss von den Herstellern des Betriebssystems korrekt implementiert sein. Unter Ubuntu 19.10 war ich erfolgreich, aber nicht unter macOS 10.14. Unter macOS gibt der Aufruf setlocale(LC_COLLATE, 'pt_BR.UTF-8') die Zeichenkette 'pt_BR.UTF-8' ohne Beanstandungen zurück. Aber sorted(fruits, key=locale.strxfrm) lieferte das gleiche falsche Ergebnis wie sorted(fruits). Ich habe auch die Locales fr_FR, es_ES und de_DE unter macOS ausprobiert, aber locale.strxfrm hat seine Arbeit nicht gemacht.9

Die Lösung der Standardbibliothek für die internationalisierte Sortierung funktioniert also, scheint aber nur unter GNU/Linux gut unterstützt zu werden (vielleicht auch unter Windows, wenn du ein Experte bist). Und selbst dann hängt sie von den Locale-Einstellungen ab, was zu Problemen bei der Bereitstellung führt.

Zum Glück gibt es eine einfachere Lösung: die pyuca-Bibliothek, die auf PyPI verfügbar ist.

Sortieren mit dem Unicode-Sortieralgorithmus

James Tauber, der viel zu Django beigetragen hat, muss den Schmerz gespürt haben und hat pyuca entwickelt, eine reine Python-Implementierung des Unicode Collation Algorithm (UCA). Beispiel 4-20 zeigt, wie einfach es zu benutzen ist.

Beispiel 4-20. Verwendung der Methode pyuca.Collator.sort_key
>>> import pyuca
>>> coll = pyuca.Collator()
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=coll.sort_key)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Das ist einfach und funktioniert unter GNU/Linux, macOS und Windows, zumindest bei meinem kleinen Beispiel.

pyuca berücksichtigt das Gebietsschema nicht. Wenn du die Sortierung anpassen möchtest, kannst du dem Collator() Konstruktor den Pfad zu einer benutzerdefinierten Sortiertabelle übergeben. Standardmäßig wird die Datei allkeys.txt verwendet, die mit dem Projekt mitgeliefert wird. Das ist nur eine Kopie der Default Unicode Collation Element Table von Unicode.org.

PyICU: Miros Empfehlung für die Unicode-Sortierung

(Der Tech-Reviewer Miroslav Šedivý ist Polyglott und Unicode-Experte. Das hat er über pyuca geschrieben).

pyuca hat einen Sortieralgorithmus, der die Sortierreihenfolge in den einzelnen Sprachen nicht beachtet. Zum Beispiel steht Ä im Deutschen zwischen A und B, während es im Schwedischen nach Z kommt. Sieh dirPyICUan, das wie locale funktioniert, ohne die Locale des Prozesses zu ändern. Es wird auch benötigt, wenn du die Groß-/Kleinschreibung von iİ/ıI im Türkischen ändern willst. PyICU enthält eine Erweiterung, die kompiliert werden muss, daher kann es auf manchen Systemen schwieriger zu installieren sein als p yuca, das nur Python ist.

Diese Sortierungstabelle ist übrigens eine der vielen Datendateien, aus denen die Unicode-Datenbank besteht, unser nächstes Thema.

Die Unicode-Datenbank

Der Unicode-Standard stellt eine ganze Datenbank in Form mehrerer strukturierter Textdateien zur Verfügung, die nicht nur die Tabelle enthält, die die Codepunkte den Zeichennamen zuordnet, sondern auch Metadaten über die einzelnen Zeichen und ihre Beziehung zueinander. In der Unicode-Datenbank wird zum Beispiel festgehalten, ob ein Zeichen druckbar, ein Buchstabe, eine Dezimalziffer oder ein anderes numerisches Symbol ist. So funktionieren die Methoden str isalpha , isprintable, isdecimal und isnumeric.str.casefold verwendet ebenfalls Informationen aus einer Unicode-Tabelle.

Hinweis

Die Funktion unicodedata.category(char) gibt die Zwei-Buchstaben-Kategorie von char aus der Unicode-Datenbank zurück. Die übergeordneten Methoden str sind einfacher zu verwenden. Zum Beispiel,label.isalpha() True zurück, wenn jedes Zeichen in labelzu einer der folgenden Kategorien gehört: Lm, Lt, Lu, Ll oder Lo. Was diese Codes bedeuten, erfährst du unter"Allgemeine Kategorie"imArtikel "Unicode-Zeicheneigenschaften" in der englischen Wikipedia.

Charaktere nach Namen finden

Das Modul unicodedata verfügt über Funktionen zum Abrufen von Zeichen-Metadaten, darunter unicodedata.name(), die den offiziellen Namen eines Zeichens im Standard zurückgibt.Abbildung 4-5 zeigt diese Funktion.10

Exploring unicodedata.name in the Python console
Abbildung 4-5. Erkundung von unicodedata.name() in der Python-Konsole.

Du kannst die Funktion name() verwenden, um Anwendungen zu erstellen, mit denen du Zeichen nach ihrem Namen suchen kannst.Abbildung 4-6 zeigt das Befehlszeilenskript cf.py, das ein oder mehrere Wörter als Argumente annimmt und die Zeichen auflistet, die diese Wörter in ihrem offiziellen Unicode-Namen haben. Der vollständige Quellcode für cf.py ist in Beispiel 4-21 zu finden.

Using cf.py to find smiling cats.
Abbildung 4-6. Verwendung von cf.py, um lächelnde Katzen zu finden.
Warnung

Die Unterstützung für Emoji variiert stark zwischen den verschiedenen Betriebssystemen und Apps. In den letzten Jahren bietet das macOS-Terminal die beste Unterstützung für Emojis, gefolgt von modernen GNU/Linux-Grafikterminals. Windows cmd.exe und PowerShell unterstützen jetzt die Unicode-Ausgabe, aber während ich diesen Abschnitt im Januar 2020 schreibe, zeigen sie immer noch keine Emojis an - zumindest nicht "out of the box". Tech-Reviewer Leonardo Rochael erzählte mir von einem neuen, quelloffenen Windows-Terminal von Microsoft, das möglicherweise eine bessere Unicode-Unterstützung hat als die älteren Microsoft-Konsolen. Ich hatte leider keine Zeit, es auszuprobieren.

Beachte in Beispiel 4-21 die Anweisung if in der Funktion find, die die Methode .issubset() verwendet, um schnell zu prüfen, ob alle Wörter in der Menge query in der aus dem Namen des Zeichens gebildeten Wortliste vorkommen. Dank Pythons Rich-Set-API brauchen wir keine verschachtelte for -Schleife und keine weitere if, um diese Prüfung durchzuführen.

Beispiel 4-21. cf.py: das Zeichenfindungsprogramm
#!/usr/bin/env python3
import sys
import unicodedata

START, END = ord(' '), sys.maxunicode + 1           1

def find(*query_words, start=START, end=END):       2
    query = {w.upper() for w in query_words}        3
    for code in range(start, end):
        char = chr(code)                            4
        name = unicodedata.name(char, None)         5
        if name and query.issubset(name.split()):   6
            print(f'U+{code:04X}\t{char}\t{name}')  7

def main(words):
    if words:
        find(*words)
    else:
        print('Please provide words to find.')

if __name__ == '__main__':
    main(sys.argv[1:])
1

Lege die Standardwerte für den Bereich der zu durchsuchenden Codepunkte fest.

2

find akzeptiert query_words und optionale Schlüsselwort-Argumente, um den Bereich der Suche einzuschränken und das Testen zu erleichtern.

3

Konvertiere query_words in eine Reihe von Zeichenketten in Großbuchstaben.

4

Hol dir das Unicode-Zeichen für code.

5

Ermittelt den Namen des Zeichens, oder None, wenn der Codepunkt nicht zugewiesen ist.

6

Wenn es einen Namen gibt, zerlege ihn in eine Liste von Wörtern und prüfe dann, ob die Menge query eine Teilmenge dieser Liste ist.

7

Drucke die Zeile mit dem Codepunkt im Format U+9999, das Zeichen und seinen Namen aus.

Das Modul unicodedata hat weitere interessante Funktionen. Als Nächstes sehen wir uns ein paar an, die mit dem Abrufen von Informationen aus Zeichen mit numerischer Bedeutung zu tun haben.

Numerische Bedeutung der Zeichen

Das Modul unicodedata enthält Funktionen, mit denen geprüft werden kann, ob ein Unicode-Zeichen eine Zahl darstellt und wenn ja, welchen numerischen Wert es für Menschen hat - im Gegensatz zu seiner Codepunktnummer.Beispiel 4-22 zeigt die Verwendung von unicodedata.name() und unicodedata.numeric() sowie die Methoden .isdecimal() und .isnumeric() von str.

Beispiel 4-22. Demo der numerischen Zeichen-Metadaten der Unicode-Datenbank (Callouts beschreiben jede Spalte in der Ausgabe)
import unicodedata
import re

re_digit = re.compile(r'\d')

sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'

for char in sample:
    print(f'U+{ord(char):04x}',                       1
          char.center(6),                             2
          're_dig' if re_digit.match(char) else '-',  3
          'isdig' if char.isdigit() else '-',         4
          'isnum' if char.isnumeric() else '-',       5
          f'{unicodedata.numeric(char):5.2f}',        6
          unicodedata.name(char),                     7
          sep='\t')
1

Codepunkt im Format U+0000.

2

Zeichen in der Mitte einer str der Länge 6.

3

Zeige re_dig an, wenn das Zeichen mit der r'\d' regex übereinstimmt.

4

Zeige isdig, wenn char.isdigit() True ist.

5

Zeige isnum, wenn char.isnumeric() True ist.

6

Numerischer Wert, der mit der Breite 5 und 2 Dezimalstellen formatiert ist.

7

Name des Unicode-Zeichens.

Wenn du Beispiel 4-22 ausführst, erhältst du Abbildung 4-7, wenn deine Terminalschriftart alle diese Zeichen enthält.

Numeric characters screenshot
Abbildung 4-7. macOS Terminal zeigt numerische Zeichen und Metadaten dazu an; re_dig bedeutet, dass das Zeichen dem regulären Ausdruck r'\d' entspricht.

Die sechste Spalte in Abbildung 4-7 ist das Ergebnis des Aufrufs von unicodedata.numeric(char) für das Zeichen. Sie zeigt, dass Unicode den numerischen Wert von Symbolen kennt, die Zahlen darstellen. Wenn du also eine Tabellenkalkulationsanwendung erstellen willst, die tamilische Ziffern oder römische Zahlen unterstützt, dann leg los!

Abbildung 4-7 zeigt, dass der reguläre Ausdruck r'\d' auf die Ziffer "1" und die Devanagari-Ziffer 3 passt, aber nicht auf einige andere Zeichen, die von der Funktion isdigit als Ziffern angesehen werden. Das Modul re ist nicht so versiert in Sachen Unicode, wie es sein könnte. Das neue Modul regex, das auf PyPI verfügbar ist, wurde entwickelt, um re zu ersetzen und bietet eine bessere Unicode-Unterstützung.11 Wir werden im nächsten Abschnitt auf das Modul re zurückkommen.

In diesem Kapitel haben wir einige Funktionen von unicodedata verwendet, aber es gibt noch viele weitere, die wir nicht behandelt haben. Siehe die Dokumentation der Standardbibliothek für das Modulunicodedata .

Als Nächstes werfen wir einen kurzen Blick auf Dual-Mode-APIs, die Funktionen anbieten, die str oder bytes Argumente akzeptieren und je nach Typ besonders behandelt werden.

Dual-Mode str und bytes APIs

Die Standardbibliothek von Python hat Funktionen, die str oder bytes Argumente akzeptieren und sich je nach Typ unterschiedlich verhalten. Einige Beispiele findest du in denModulen re und os.

str vs. bytes in regulären Ausdrücken

Wenn du einen regulären Ausdruck mit bytes bildest, passen Muster wie \d und \w nur auf ASCII-Zeichen; wenn diese Muster dagegen als str angegeben werden, passen sie auf Unicode-Ziffern oder Buchstaben jenseits von ASCII. Beispiel 4-23 und Abbildung 4-8 vergleichen, wie Buchstaben, ASCII-Ziffern, hochgestellte Ziffern und tamilische Ziffern von den Mustern str und bytes erkannt werden.

Beispiel 4-23. ramanujan.py: Vergleich des Verhaltens von einfachen str und bytes regulären Ausdrücken
import re

re_numbers_str = re.compile(r'\d+')     1
re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+')  2
re_words_bytes = re.compile(rb'\w+')

text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef"  3
            " as 1729 = 1³ + 12³ = 9³ + 10³.")        4

text_bytes = text_str.encode('utf_8')  5

print(f'Text\n  {text_str!r}')
print('Numbers')
print('  str  :', re_numbers_str.findall(text_str))      6
print('  bytes:', re_numbers_bytes.findall(text_bytes))  7
print('Words')
print('  str  :', re_words_str.findall(text_str))        8
print('  bytes:', re_words_bytes.findall(text_bytes))    9
1

Die ersten beiden regulären Ausdrücke sind vom Typ str.

2

Die letzten beiden sind vom Typ bytes.

3

Zu durchsuchender Unicode-Text, der die tamilischen Ziffern für 1729 enthält (die logische Zeile geht bis zum rechten Klammerzeichen weiter).

4

Diese Zeichenkette wird zur Kompilierzeit mit der vorherigen verbunden (siehe "2.4.2. String-Literal-Verkettung" in The Python Language Reference).

5

Für die Suche mit den regulären Ausdrücken von bytes wird eine bytes Zeichenfolge benötigt.

6

Das Muster str r'\d+' entspricht den tamilischen und ASCII-Ziffern.

7

Das Muster bytes rb'\d+' entspricht nur den ASCII-Bytes für Ziffern.

8

Das Muster str r'\w+' entspricht den Buchstaben, Hochkommata, Tamil undASCII-Ziffern.

9

Das Muster bytes rb'\w+' entspricht nur den ASCII-Bytes für Buchstaben und Ziffern.

Output of ramanujan.py
Abbildung 4-8. Screenshot der Ausführung von ramanujan.py aus Beispiel 4-23.

Beispiel 4-23 ist ein triviales Beispiel, um einen Punkt zu verdeutlichen: Du kannst reguläre Ausdrücke auf str und bytes anwenden, aber im zweiten Fall werden Bytes außerhalb des ASCII-Bereichs als Nicht-Ziffern und Nicht-Wortzeichen behandelt.

Für str reguläre Ausdrücke gibt es ein re.ASCII Flag, mit dem \w, \W, \b, \B, \d, \D, \s und \S einen reinen ASCII-Abgleich durchführen. In der Dokumentation des Moduls re findest du alle Einzelheiten.

Ein weiteres wichtiges Dual-Mode-Modul ist os.

str Versus bytes in os Funktionen

Der GNU/Linux-Kernel kennt sich nicht mit Unicode aus, so dass du in der realen Welt Dateinamen finden kannst, die aus Bytefolgen bestehen, die in keinem vernünftigen Kodierungsschema gültig sind und nicht nach str dekodiert werden können. Dateiserver mit Clients, die eine Vielzahl von Betriebssystemen verwenden, sind für dieses Problem besonders anfällig.

Um dieses Problem zu umgehen, nehmen alle Funktionen des Moduls os, die Datei- oder Pfadnamen akzeptieren, die Argumente str oder bytes entgegen. Wird eine solche Funktion mit einem str Argument aufgerufen, wird das Argument automatisch mit dem von sys.getfilesystemencoding() genannten Codec konvertiert und die Antwort des Betriebssystems wird mit demselben Codec dekodiert. Das ist fast immer erwünscht und entspricht der bewährten Methode von Unicode Sandwich.

Wenn du aber mit Dateinamen umgehen (und sie vielleicht reparieren) musst, die auf diese Weise nicht behandelt werden können, kannst du bytes Argumente an die os Funktionen übergeben, um bytes Rückgabewerte zu erhalten. Mit dieser Funktion kannst du mit jeder Datei oder jedem Pfadnamen umgehen, egal wie viele Gremlins du findest. Siehe Beispiel 4-24.

Beispiel 4-24. listdir mit str und bytes Argumenten und Ergebnissen
>>> os.listdir('.')  1
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.')  2
[b'abc.txt', b'digits-of-\xcf\x80.txt']
1

Der zweite Dateiname ist "digits-of-π.txt" (mit dem griechischen Buchstaben pi).

2

Mit dem Argument byte gibt listdir die Dateinamen als Bytes zurück: b'\xcf\x80' ist die UTF-8-Kodierung des griechischen Buchstabens pi.

Um den manuellen Umgang mit str oder bytes Sequenzen zu erleichtern, bei denen es sich um Dateinamen oder Pfadnamen handelt, bietet das Modul os spezielle Kodierungs- und Dekodierungsfunktionen os.fsencode(name_or_path) und os.fsdecode(name_or_path). Beide Funktionen akzeptieren ein Argument vom Typ str, bytes oder ein Objekt, das seit Python 3.6 die Schnittstelle os.PathLike implementiert.

Unicode ist ein tiefes Kaninchenloch. Es ist Zeit, unsere Erkundung von str und bytes abzuschließen.

Kapitel Zusammenfassung

Wir haben das Kapitel damit begonnen, dass wir die Idee von 1 character == 1 byte verworfen haben. Da die Welt Unicode annimmt, müssen wir das Konzept der Textstrings von den binären Sequenzen, die sie in Dateien repräsentieren, getrennt halten, und Python 3 erzwingt diese Trennung.

Nach einem kurzen Überblick über die binären Sequenzdatentypen -bytes, bytearray, undmemoryviewhaben wir uns mit der Kodierung und Dekodierung befasst, mit einer Auswahl wichtiger Codecs, gefolgt von Ansätzen zur Vermeidung oder zum Umgang mit den berüchtigten UnicodeEncodeError, UnicodeDecodeError und SyntaxError, die durch falsche Kodierung in Python-Quelldateien verursacht werden.

Dann haben wir uns mit der Theorie und Praxis der Kodierungserkennung ohne Metadaten beschäftigt: Theoretisch ist es nicht möglich, aber in der Praxis gelingt es dem Chardet-Paket für eine Reihe von gängigen Kodierungen recht gut. Anschließend wurden Byte Order Marks als einziger Hinweis auf die Kodierung vorgestellt, der häufig in UTF-16- und UTF-32-Dateien zu finden ist - manchmal auch in UTF-8-Dateien.

Im nächsten Abschnitt haben wir das Öffnen von Textdateien demonstriert, eine einfache Aufgabe, bis auf einen Fallstrick: Das Argument des Schlüsselworts encoding= ist nicht zwingend erforderlich, wenn du eine Textdatei öffnest, sollte es aber sein. Wenn du die Kodierung nicht angibst, landest du bei einem Programm, das aufgrund von widersprüchlichen Standardkodierungen plattformübergreifend inkompatiblen "Klartext" erzeugt. Anschließend haben wir die verschiedenen Kodierungseinstellungen vorgestellt, die Python als Standard verwendet, und wie man sie erkennt. Eine traurige Erkenntnis für Windows-Benutzer ist, dass diese Einstellungen auf ein und demselben Rechner oft unterschiedliche Werte haben, die nicht miteinander kompatibel sind. GNU/Linux- und macOS-Benutzer hingegen leben in einer glücklichen Welt, in der UTF-8 so gut wie überall die Standardeinstellung ist.

Unicode bietet mehrere Möglichkeiten, einige Zeichen darzustellen, daher ist die Normalisierung eine Voraussetzung für den Textabgleich. Wir haben nicht nur die Normalisierung und die Groß- und Kleinschreibung erklärt, sondern auch einige Hilfsfunktionen vorgestellt, die du an deine Bedürfnisse anpassen kannst, darunter auch drastische Umwandlungen wie das Entfernen aller Akzente. Dann haben wir gesehen, wie man Unicode-Text korrekt sortiert, indem man das Standardmodul locale nutzt - mit einigen Einschränkungen - und eine Alternative, die nicht von komplizierten Locale-Konfigurationen abhängt: das externe pyuca-Paket.

Wir nutzten die Unicode-Datenbank, um ein Kommandozeilenprogramm zu programmieren, mit dem wir dank der Leistungsfähigkeit von Python in 28 Codezeilen nach Zeichen suchen können. Wir warfen einen Blick auf andere Unicode-Metadaten und bekamen einen kurzen Überblick über Dual-Mode-APIs, bei denen einige Funktionen mit str oder bytes Argumenten aufgerufen werden können und unterschiedliche Ergebnisse liefern.

Weitere Lektüre

Ned Batchelders 2012 PyCon US-Vortrag "Pragmatic Unicode, or, How Do I Stop the Pain?" war hervorragend. Ned ist so professionell, dass er neben den Folien und dem Video auch eine vollständige Abschrift des Vortrags zur Verfügung stellt.

"Zeichenkodierung und Unicode in Python: How to (╯°□°)╯︵ ┻━┻ with dignity"(Folien, Video) war der exzellente PyCon 2014-Vortrag von Esther Nam und Travis Fischer, in dem ich den prägnanten Epigraphen dieses Kapitels fand: "Menschen benutzen Text. Computer sprechen Bytes."

Lennart Regebro - einer der technischen Prüfer für die erste Ausgabe dieses Buches - stellt sein "Useful Mental Model of Unicode (UMMU)" in dem kurzen Beitrag "Unconfusing Unicode" vor : Unicode ist ein komplexer Standard, daher ist Lennarts UMMU ein wirklich nützlicher Ausgangspunkt.

Das offizielle "Unicode HOWTO" in den Python-Dokumenten behandelt das Thema aus verschiedenen Blickwinkeln, von einer guten historischen Einführung über Syntaxdetails, Codecs, reguläre Ausdrücke und Dateinamen bis hin zu bewährten Methoden für Unicode-fähige E/A (d.h. das Unicode-Sandwich), mit vielen zusätzlichen Links aus jedem Abschnitt. Kapitel 4, "Strings", in Mark Pilgrims großartigem Buch Dive into Python 3 (Apress) bietet ebenfalls eine sehr gute Einführung in die Unicode-Unterstützung in Python 3. Im selben Buch wird in Kapitel 15 beschrieben, wie die Chardet-Bibliothek von Python 2 auf Python 3 portiert wurde. Dies ist eine wertvolle Fallstudie, denn der Wechsel von der alten str zur neuen bytes ist die Ursache für die meisten Migrationsschwierigkeiten, und das ist ein zentrales Anliegen bei einer Bibliothek, die Kodierungen erkennen soll.

Wenn du Python 2 kennst, aber neu in Python 3 bist, findest du in Guido van Rossums "What's New in Python 3.0" 15 Punkte, die zusammenfassen, was sich geändert hat, mit vielen Links. Guido beginnt mit der unverblümten Aussage: "Alles, was du über Binärdaten und Unicode zuwissen glaubtest, hat sich geändert." Armin Ronachers Blogbeitrag "The Updated Guide to Unicode on Python" ist tiefgründig undzeigt einige der Fallstricke von Unicode in Python 3auf (Armin ist kein großer Fan vonPython 3).

Kapitel 2, "Strings and Text", des Python Cookbook, 3rd ed. (O'Reilly), von David Beazley und Brian K. Jones, enthält mehrere Rezepte, die sich mit der Unicode-Normalisierung, der Bereinigung von Text und der Durchführung von textorientierten Operationen auf Bytefolgen befassen. Kapitel 5 befasst sich mit Dateien und E/A und enthält "Rezept 5.17. Schreiben von Bytes in eine Textdatei", das zeigt, dass hinter jeder Textdatei immer ein Binärstrom liegt, auf den bei Bedarf direkt zugegriffen werden kann. Im weiteren Verlauf des Kochbuchs wird das Modul struct in "Rezept 6.11. Binäre Arrays von Strukturen lesen und schreiben".

Im Blog "Python Notes" von Nick Coghlan gibt es zwei Beiträge, die für dieses Kapitel sehr wichtig sind: "Python 3 and ASCII Compatible Binary Protocols" und "Processing Text Files in Python 3". Äußerst empfehlenswert.

Eine Liste der von Python unterstützten Kodierungen findest du unter "Standard Encodings" in der Dokumentation des Moduls codecs. Wenn du diese Liste programmatisch abrufen willst, kannst du das im Skript/Tools/unicode/listcodecs.pytun, das im CPython-Quellcode enthalten ist.

Die Bücher Unicode Explained von Jukka K. Korpela (O'Reilly) und Unicode Demystified von Richard Gillam (Addison-Wesley) sind nicht Python-spezifisch, waren aber sehr hilfreich, als ich die Unicode-Konzepte studierte. Programming with Unicode von Victor Stinner ist ein kostenloses, selbstveröffentlichtes Buch (CreativeCommons BY-SA), das Unicode im Allgemeinen sowie Tools und APIs im Zusammenhang mit den wichtigsten Betriebssystemen und einigen Programmiersprachen, darunter Python, behandelt.

Die W3C-Seiten "Case Folding: An Introduction" und "Character Model for the World Wide Web": String Matching" befassen sich mit Normalisierungskonzepten, wobei erstere eine sanfte Einführung ist und letztere eine Arbeitsgruppennotiz, die in trockener Standardsprache verfasst ist - im gleichen Ton wie der "Unicode Standard Annex #15-Unicode Normalization Forms". Der Abschnitt "Frequently Asked Questions, Normalization" von Unicode.org ist besser lesbar, ebenso wie die "NFC FAQ" von Mark Davis - Autor mehrerer Unicode-Algorithmen und Präsident des Unicode-Konsortiums zum Zeitpunkt der Erstellung dieses Artikels.

2016 hat das Museum of Modern Art (MoMA) in New York ( )das Original-Emoji in seine Sammlung aufgenommen: die 176 Emojis, die Shigetaka Kurita 1999 für den japanischen Mobilfunkanbieter NTT DOCOMO entworfen hat. Noch weiter zurück in der Geschichte geht Emojipedia mit der Veröffentlichung"Correcting the Record on the First Emoji Set", in der der japanischen SoftBank das erste bekannte Emoji-Set zugeschrieben wird, das 1997 in Mobiltelefonen verwendet wurde. Das SoftBank-Set ist die Quelle von 90 Emojis, die heute im Unicode enthalten sind, darunter U+1F4A9 (PILE OF POO). Matthew Rothenbergs emojitracker.comist ein Live-Dashboard, das die Emoji-Nutzung auf Twitter in Echtzeit anzeigt. Während ich dies schreibe,ist FACE WITH TEARS OF JOY (U+1F602)das beliebteste Emoji auf Twitter, mit mehr als 3.313.667.315 registriertenVorkommen.

1 Folie 12 des PyCon 2014 Vortrags "Character Encoding and Unicode in Python"(Folien, Video).

2 Python 2.6 und 2.7 hatten auch bytes, aber das war nur ein Alias für den Typ str.

3 Wissenswertes: Das ASCII-Zeichen "einfaches Anführungszeichen", das Python standardmäßig als String-Begrenzer verwendet, heißt im Unicode-Standard eigentlich APOSTROPHE. Die echten einfachen Anführungszeichen sind asymmetrisch: links ist U+2018 und rechts ist U+2019.

4 In Python 3.0 bis 3.4 funktionierte sie nicht, was für Entwickler, die mit binären Daten arbeiten, sehr schmerzhaft war. Die Umkehrung ist in PEP 461-Adding % formatting to bytes and bytearray dokumentiert.

5 Ich habe den Begriff "Unicode-Sandwich" zum ersten Mal in Ned Batchelders ausgezeichnetem Vortrag "Pragmatic Unicode" auf der US PyCon 2012 gesehen.

6 Quelle: "Windows Befehlszeile: Unicode und UTF-8 Output Text Buffer".

7 Seltsamerweise gilt das Mikrozeichen als "Kompatibilitätszeichen", das Ohm-Symbol jedoch nicht. Das Ergebnis ist, dass NFC das Mikrozeichen nicht berührt, aber das Ohm-Symbol in ein großes Omega verwandelt, während NFKC und NFKD sowohl das Ohm als auch das Mikro in griechische Zeichen verwandeln.

8 Diakritische Zeichen beeinflussen die Sortierung nur in dem seltenen Fall, dass sie der einzige Unterschied zwischen zwei Wörtern sind - in diesem Fall wird das Wort mit dem diakritischen Zeichen nach dem normalen Wort sortiert.

9 Auch hier konnte ich keine Lösung finden, aber ich habe andere Leute gefunden, die das gleiche Problem gemeldet haben. Alex Martelli, einer der Tech-Reviewer, hatte keine Probleme mit setlocale und locale.strxfrm auf seinem Macintosh mit macOS 10.9. Zusammenfassend lässt sich sagen: Je nachdem, wie du dich entscheidest.

10 Das ist ein Bild und kein Code-Listing, denn Emojis werden von O'Reillys Toolchain für digitales Publizieren nicht gut unterstützt, während ich dies schreibe.

11 Obwohl sie in dieser speziellen Stichprobe nicht besser als re war, um Ziffern zu erkennen.

Get Fließendes Python, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.