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
, undmemoryview
-
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
undbytes
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
)
4
>>
>
b
=
s
.
encode
(
'
utf8
'
)
>>
>
b
b
'
caf
\xc3
\xa9
'
>>
>
len
(
b
)
5
>>
>
b
.
decode
(
'
utf8
'
)
'
café
'
Die
str
'café'
hat vier Unicode-Zeichen.Kodiere
str
aufbytes
mit UTF-8 Kodierung.bytes
Literale haben einb
Präfix.bytes
b
hat fünf Bytes (der Codepunkt für "é" wird in UTF-8 als zwei Bytes kodiert).Dekodiere
bytes
aufstr
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
'
)
>>>
cafe
b'caf\xc3\xa9'
>>>
cafe
[
0
]
99
>>>
cafe
[
:
1
]
b'c'
>>>
cafe_arr
=
bytearray
(
cafe
)
>>>
cafe_arr
bytearray(b'caf\xc3\xa9')
>>>
cafe_arr
[
-
1
:
]
bytearray(b'\xa9')
bytes
kann aus einerstr
erstellt werden.Jedes Element ist eine ganze Zahl in
range(256)
.Scheiben von
bytes
sind auchbytes
- sogar Scheiben von einem einzigen Byte.Für
bytearray
gibt es keine Literal-Syntax: Sie werden alsbytearray()
mit einembytes
Literal als Argument angezeigt.Ein Stück von
bytearray
ist auch einbytearray
.
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 einencoding
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
]
)
>>>
octets
=
bytes
(
numbers
)
>>>
octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
Typecode
'h'
erstellt einearray
aus kurzen Ganzzahlen (16 Bit).octets
enthält eine Kopie der Bytes, aus denennumbers
besteht.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'
]:
...
(
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.
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 dielatin1
Byte-Werte in dencp1252
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
'
)
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
'
)
b'S\xe3o Paulo'
>>>
city
.
encode
(
'
cp437
'
)
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
'
)
b'So Paulo'
>>>
city
.
encode
(
'
cp437
'
,
errors
=
'
replace
'
)
b'S?o Paulo'
>>>
city
.
encode
(
'
cp437
'
,
errors
=
'
xmlcharrefreplace
'
)
b'São Paulo'
Die UTF-Kodierungen verarbeiten alle
str
.iso8859_1
funktioniert auch für den String'São Paulo'
.cp437
kann das'ã'
("a" mit Tilde) nicht kodieren. Der Standard-Fehlerhandler -'strict'
- löstUnicodeEncodeError
aus.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.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.'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
\xe9
al
'
>>>
octets
.
decode
(
'
cp1252
'
)
'Montréal'
>>>
octets
.
decode
(
'
iso8859_7
'
)
'Montrιal'
>>>
octets
.
decode
(
'
koi8_r
'
)
'MontrИal'
>>>
octets
.
decode
(
'
utf_8
'
)
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
'
)
'Montr�al'
Das Wort "Montréal" verschlüsselt als
latin1
;'\xe9'
ist das Byte für "é".Die Dekodierung mit Windows 1252 funktioniert, weil es eine Obermenge von
latin1
ist.ISO-8859-7 ist für Griechisch gedacht, daher wird das Byte
'\xe9'
falsch interpretiert und es wird kein Fehler ausgegeben.KOI8-R steht für Russisch. Jetzt steht
'\xe9'
für den kyrillischen Buchstaben "И".Der
'utf_8'
Codec erkennt, dassoctets
kein gültiges UTF-8 ist, und gibtUnicodeDecodeError
aus.Mit der
'replace'
Fehlerbehandlung wird\xe9
durch "�" (CodepunktU+FFFD) ersetzt, dem offiziellen UnicodeREPLACEMENT 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
(
'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:
$ chardetect04
-text-byte.asciidoc04
-text-byte.asciidoc: utf-8 with confidence0
.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."
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.
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
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>>
fp
.
write
(
'
café
'
)
4
>>>
fp
.
close
(
)
>>>
import
os
>>>
os
.
stat
(
'
cafe.txt
'
)
.
st_size
5
>>>
fp2
=
open
(
'
cafe.txt
'
)
>>>
fp2
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>>
fp2
.
encoding
'cp1252'
>>>
fp2
.
read
(
)
'café'
>>>
fp3
=
open
(
'
cafe.txt
'
,
encoding
=
'
utf_8
'
)
>>>
fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>>
fp3
.
read
(
)
'café'
>>>
fp4
=
open
(
'
cafe.txt
'
,
'
rb
'
)
>>>
fp4
<_io.BufferedReader name='cafe.txt'>
>>>
fp4
.
read
(
)
b'caf\xc3\xa9'
Standardmäßig verwendet
open
den Textmodus und gibt einTextIOWrapper
Objekt mit einer bestimmten Kodierung zurück.Die Methode
write
aufTextIOWrapper
gibt die Anzahl der geschriebenen Unicode-Zeichen zurück.os.stat
sagt, dass die Datei 5 Bytes hat; UTF-8 kodiert'é'
als 2 Bytes, 0xc3 und 0xa9.Wenn du eine Textdatei ohne explizite Kodierung öffnest, erhältst du eine
TextIOWrapper
mit einer Standardkodierung aus dem Gebietsschema.Ein
TextIOWrapper
Objekt hat ein Kodierungsattribut, das du einsehen kannst:cp1252
in diesem Fall.In der Windows-Kodierung
cp1252
ist das Byte 0xc3 ein "Ã" (A mit Tilde), und 0xa9 ist das Copyright-Zeichen.Öffnen der gleichen Datei mit der richtigen Kodierung.
Das erwartete Ergebnis: die gleichen vier Unicode-Zeichen für
'café'
.Das
'rb'
Flag öffnet eine Datei zum Lesen im Binärmodus.Das zurückgegebene Objekt ist ein
BufferedReader
und nicht einTextIOWrapper
.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
)
(
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
Active
code
page:
437
>
python
default_encodings.py
locale.getpreferredencoding
(
)
->
'cp1252'
type
(
my_file
)
->
<
class
'_io.TextIOWrapper'
>
my_file.encoding
->
'cp1252'
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'
chcp
zeigt die aktive Codeseite für die Konsole an:437
.Ausführen von default_encodings.py mit Ausgabe auf der Konsole.
locale.getpreferredencoding()
ist die wichtigste Einstellung.Textdateien verwenden standardmäßig
locale.getpreferredencoding()
.Die Ausgabe geht auf die Konsole, also ist
sys.stdout.isatty()
True
.Nun ist
sys.stdout.encoding
nicht dasselbe wie die Konsolencodeseite, die vonchcp
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
(
sys
.
version
)
()
(
'sys.stdout.isatty():'
,
sys
.
stdout
.
isatty
())
(
'sys.stdout.encoding:'
,
sys
.
stdout
.
encoding
)
()
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
:
(
f
'Trying to output {name(char)}:'
)
(
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.
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.
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 durchlocale.getpreferredencoding()
('cp1252'
in Beispiel 4-11) gegeben. -
Die Kodierung von
sys.stdout|stdin|stderr
wurde vor Python 3.6 durch diePYTHONIOENCODING
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 vonlocale.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/vonstr
verwendet. Eine Änderung dieser Einstellung wird nicht unterstützt. -
sys.getfilesystemencoding()
wird zum Kodieren/Dekodieren von Dateinamen (nicht von Dateiinhalten) verwendet. Er wird verwendet, wennopen()
einstr
Argument für den Dateinamen erhält; wenn der Dateiname alsbytes
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}
'
>>>
(
half
)
½
>>>
normalize
(
'NFKC'
,
half
)
'1⁄2'
>>>
for
char
in
normalize
(
'NFKC'
,
half
):
...
(
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
)
shaved
=
'
'
.
join
(
c
for
c
in
norm_txt
if
not
unicodedata
.
combining
(
c
)
)
return
unicodedata
.
normalize
(
'
NFC
'
,
shaved
)
Zerlege alle Zeichen in Basiszeichen und Kombinationszeichen.
Filtere alle kombinierenden Zeichen heraus.
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.”'
>>>
Greek
=
'
Ζέφυρος, Zéfiro
'
>>>
shave_marks
(
Greek
)
'Ζεφυρος, Zefiro'
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
)
latin_base
=
False
preserve
=
[
]
for
c
in
norm_txt
:
if
unicodedata
.
combining
(
c
)
and
latin_base
:
continue
# ignore diacritic on Latin base char
preserve
.
append
(
c
)
# if it isn't a combining char, it's a new base char
if
not
unicodedata
.
combining
(
c
)
:
latin_base
=
c
in
string
.
ascii_letters
shaved
=
'
'
.
join
(
preserve
)
return
unicodedata
.
normalize
(
'
NFC
'
,
shaved
)
Zerlege alle Zeichen in Basiszeichen und Kombinationszeichen.
Überspringe Kombinationszeichen, wenn das Basiszeichen lateinisch ist.
Andernfalls behalte den aktuellen Charakter bei.
Erkenne neue Basiszeichen und stelle fest, ob sie lateinisch sind.
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
(
"""
‚ƒ„ˆ‹‘’“”•–—˜›
"""
,
"""'f"^<''""---~>"""
)
multi_map
=
str
.
maketrans
(
{
'
€
'
:
'
EUR
'
,
'
…
'
:
'
...
'
,
'
Æ
'
:
'
AE
'
,
'
æ
'
:
'
ae
'
,
'
Œ
'
:
'
OE
'
,
'
œ
'
:
'
oe
'
,
'
™
'
:
'
(TM)
'
,
'
‰
'
:
'
<per mille>
'
,
'
†
'
:
'
**
'
,
'
‡
'
:
'
***
'
,
}
)
multi_map
.
update
(
single_map
)
def
dewinize
(
txt
)
:
"""Replace Win1252 symbols with ASCII chars or sequences"""
return
txt
.
translate
(
multi_map
)
def
asciize
(
txt
)
:
no_marks
=
shave_marks_latin
(
dewinize
(
txt
)
)
no_marks
=
no_marks
.
replace
(
'
ß
'
,
'
ss
'
)
return
unicodedata
.
normalize
(
'
NFKC
'
,
no_marks
)
Erstelle eine Zuordnungstabelle für die Ersetzung von Zeichen zu Zeichen.
Erstelle eine Mapping-Tabelle für die Ersetzung von Zeichen durch Strings.
Führe Kartierungstabellen zusammen.
dewinize
wirkt sich nicht auf den TextASCII
oderlatin1
aus, sondern nur auf die Microsoft-Zusätze zulatin1
incp1252
.Wende
dewinize
an und entferne diakritische Zeichen.Ersetze das Eszett durch "ss" (wir verwenden hier keine Groß- und Kleinschreibung, weil wir die Großschreibung beibehalten wollen).
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í."'
>>>
asciize
(
order
)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."'
dewinize
ersetzt geschweifte Anführungszeichen, Aufzählungszeichen und ™ (Markensymbol).asciize
wendetdewinize
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.
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'
)
(
my_locale
)
fruits
=
[
'caju'
,
'atemoia'
,
'cajá'
,
'açaí'
,
'acerola'
]
sorted_fruits
=
sorted
(
fruits
,
key
=
locale
.
strxfrm
)
(
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
einelocale.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. Abersorted(fruits, key=locale.strxfrm)
lieferte das gleiche falsche Ergebnis wiesorted(fruits)
. Ich habe auch die Localesfr_FR
,es_ES
undde_DE
unter macOS ausprobiert, aberlocale.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 label
zu 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
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.
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
def
find
(
*
query_words
,
start
=
START
,
end
=
END
)
:
query
=
{
w
.
upper
(
)
for
w
in
query_words
}
for
code
in
range
(
start
,
end
)
:
char
=
chr
(
code
)
name
=
unicodedata
.
name
(
char
,
None
)
if
name
and
query
.
issubset
(
name
.
split
(
)
)
:
(
f
'
U+
{code:04X}
\t
{char}
\t
{name}
'
)
def
main
(
words
)
:
if
words
:
find
(
*
words
)
else
:
(
'
Please provide words to find.
'
)
if
__name__
==
'
__main__
'
:
main
(
sys
.
argv
[
1
:
]
)
Lege die Standardwerte für den Bereich der zu durchsuchenden Codepunkte fest.
find
akzeptiertquery_words
und optionale Schlüsselwort-Argumente, um den Bereich der Suche einzuschränken und das Testen zu erleichtern.Konvertiere
query_words
in eine Reihe von Zeichenketten in Großbuchstaben.Hol dir das Unicode-Zeichen für
code
.Ermittelt den Namen des Zeichens, oder
None
, wenn der Codepunkt nicht zugewiesen ist.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.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
:
(
f
'
U+{ord(char):04x}
'
,
char
.
center
(
6
)
,
'
re_dig
'
if
re_digit
.
match
(
char
)
else
'
-
'
,
'
isdig
'
if
char
.
isdigit
(
)
else
'
-
'
,
'
isnum
'
if
char
.
isnumeric
(
)
else
'
-
'
,
f
'
{unicodedata.numeric(char):5.2f}
'
,
unicodedata
.
name
(
char
)
,
sep
=
'
\t
'
)
Codepunkt im Format
U+0000
.Zeichen in der Mitte einer
str
der Länge 6.Zeige
re_dig
an, wenn das Zeichen mit derr'\d'
regex übereinstimmt.Zeige
isdig
, wennchar.isdigit()
True
ist.Zeige
isnum
, wennchar.isnumeric()
True
ist.Numerischer Wert, der mit der Breite 5 und 2 Dezimalstellen formatiert ist.
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.
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+
'
)
re_words_str
=
re
.
compile
(
r
'
\
w+
'
)
re_numbers_bytes
=
re
.
compile
(
rb
'
\
d+
'
)
re_words_bytes
=
re
.
compile
(
rb
'
\
w+
'
)
text_str
=
(
"
Ramanujan saw
\u0be7
\u0bed
\u0be8
\u0bef
"
"
as 1729 = 1³ + 12³ = 9³ + 10³.
"
)
text_bytes
=
text_str
.
encode
(
'
utf_8
'
)
(
f
'
Text
\n
{text_str!r}
'
)
(
'
Numbers
'
)
(
'
str :
'
,
re_numbers_str
.
findall
(
text_str
)
)
(
'
bytes:
'
,
re_numbers_bytes
.
findall
(
text_bytes
)
)
(
'
Words
'
)
(
'
str :
'
,
re_words_str
.
findall
(
text_str
)
)
(
'
bytes:
'
,
re_words_bytes
.
findall
(
text_bytes
)
)
Die ersten beiden regulären Ausdrücke sind vom Typ
str
.Die letzten beiden sind vom Typ
bytes
.Zu durchsuchender Unicode-Text, der die tamilischen Ziffern für
1729
enthält (die logische Zeile geht bis zum rechten Klammerzeichen weiter).Diese Zeichenkette wird zur Kompilierzeit mit der vorherigen verbunden (siehe "2.4.2. String-Literal-Verkettung" in The Python Language Reference).
Für die Suche mit den regulären Ausdrücken von
bytes
wird einebytes
Zeichenfolge benötigt.Das Muster
str
r'\d+'
entspricht den tamilischen und ASCII-Ziffern.Das Muster
bytes
rb'\d+'
entspricht nur den ASCII-Bytes für Ziffern.Das Muster
str
r'\w+'
entspricht den Buchstaben, Hochkommata, Tamil undASCII-Ziffern.Das Muster
bytes
rb'\w+'
entspricht nur den ASCII-Bytes für Buchstaben und Ziffern.
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
(
'
.
'
)
['abc.txt', 'digits-of-π.txt']
>>>
os
.
listdir
(
b
'
.
'
)
[b'abc.txt', b'digits-of-\xcf\x80.txt']
Der zweite Dateiname ist "digits-of-π.txt" (mit dem griechischen Buchstaben pi).
Mit dem Argument
byte
gibtlistdir
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
, undmemoryview
haben 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.