Grundlagenwissen Zeichenkodierung - wie aus Bits und Bytes lesbarer Test wird
- Autor: PGD
Computer arbeiten auf Basis des Binärsystems und daher werden Daten in Form von langen Ketten von Nullen und Einsen gespeichert. Da alle Daten auf diese Weise gespeichert werden ist es essentiell wie diese binären Daten interpretiert werden. Sehen wir uns dazu ein Beispiel in der Python-Shell an:
>>> binary = "01010101"
>>> int(binary, 2)
85
>>> hex(int(binary, 2))
'0x55'
>>> chr(int(binary, 2))
'U'
Zuerst definieren wir den String
binary mit dem Wert
01010101. Hierbei ergeben
8 sogenannte Bits (
eine Null oder eine Eins) ein Byte. Ein Byte kann also 2
8 Werte annehmen - das entspeicht dem Bereich von
0 (
00000000) bis
255 (
11111111). Mit
int(binary, 2) rechnen wir den String mit der Binärzahl
01010101 in eine Dezimalzahl (
Basis 10) um und erhalten
85 als Ergebnis.
Interpretieren wir dies nun als Hexadezimalzahl mit
hex(int(binary, 2)) dann erhalten wir
0x55. Hierbei weißt das
0x vor der Zahl darauf hin, dass es sich um die hexadezimale Schreibweise handelt. Das Hexadezimalsystem basiert auf
16 und nutzt die Zahlen
0 -
9 sowie
A -
F um die Dezimalzahlen
10 -
15 darzustellen. Somit steht
0x55 für 5 x 16 + 5 also die
85 im Dezimalsystem. Das Hexadezimalsystem wird gern genutzt um binäre Daten kompakter darzustellen da sich damit die Zahlen
0 -
255 als
0x00 -
0xFF darstellen lassen was deutlich kompater und einfache zu lesen ist als
0b00000000 -
0b11111111! Mit dem
0b vor der Zahl kann zB dem Python-Interpreter mitgeteilt werden, dass es sich um eine Binärzahl handelt.
Mit
chr(int(binary, 2)) erhalten wir dann das Zeichen, dass sich hinter der Dezimalzahl
85 verbirgt und genau hier kommen Zeichenkodierungen zum tragen...
Das Problem mit den verschiedenen Zeichenkodierungen:
Python kennt dutzende Zeichenkodierungen anhand derer Text interpretiert wird. Eine Liste der verschiednen Kodierungen findet sich unter
https://docs.python.org/3/library/codecs.html#standard-encodings. Sehen wir uns dazu ein einfaches praktisches Beispiel an:
#!/usr/bin/env python3
with open("text", "w", encoding="iso-8859-1") as f:
f.write("Test! äöü")
for i in range(1, 17):
if i == 12:
continue
try:
with open("text", "r", encoding="iso-8859-" + str(i)) as f:
print(f"ISO-8859-{i:<2} == {f.read()}")
except UnicodeDecodeError:
print(f"ISO-8859-{i:<2} == UnicodeDecodeError")
Zuerst öffnen wir die Datei
text zum schreiben und verwenden dazu die Zeichenkodierung
iso-8859-1. Mit
f.write(...) schreiben wir dann den Text
Test! äöü in diese Datei. Da wir in Python3-Dateien UFT-8 als Zeichenkodierung verwenden kümmert sich der Interpreter hierbei darum, dass der Text "
Test! äöü" von UFT-8 zu ISO-8859-1 kodiert wird und dann die entsprechenden binären Daten in dieser Datei landen.
Danach versuchen wir die Datei in allen ISO-Kodierungen zu lesen und geben uns das Ergebnis aus:
ISO-8859-1 == Test! äöü
ISO-8859-2 == Test! äöü
ISO-8859-3 == Test! äöü
ISO-8859-4 == Test! äöü
ISO-8859-5 == Test! фіќ
ISO-8859-6 == UnicodeDecodeError
ISO-8859-7 == Test! δφό
ISO-8859-8 == UnicodeDecodeError
ISO-8859-9 == Test! äöü
ISO-8859-10 == Test! äöü
ISO-8859-11 == UnicodeDecodeError
ISO-8859-13 == Test! äöü
ISO-8859-14 == Test! äöü
ISO-8859-15 == Test! äöü
ISO-8859-16 == Test! äöü
Hierbei sehen wir beispielsweise, dass ISO-8859-1 bis ISO-8859-4 sowohl mit den Zeichen
Test! als auch den Umlauten
äöü klarkommen. Bei ISO-8859-5 und ISO-8859-7 werden die Umlaute als etwas anderes interpretiert (
фіќ bzw. δφό) und bei ISO-8859-6 / 8 / 11 kommt es zu einer
UnicodeDecodeError Exception und das Programm könnte die Daten gar nicht erst lesen.
Noch schlimmer wird es bei folgendem Beispiel:
#!/usr/bin/env python3
with open("text", "w", encoding="iso-8859-1") as f:
f.write("Test! äöü")
with open("text", "r", encoding="cp273") as f:
print(f"cp273 == {f.read()}")
Hier erhalten wir:
cp273 == èÁËÈU6]
Wir können also festhalten, dass wir im besten Fall falsche Daten bekommen und im schlechtesten Fall das Programm abstürzt wenn wir Daten mit einer anderen Zeichenkodierung einlesen als derjenigen mit der die Daten geschrieben wurden. Dieses Problem tritt sowohl bei Textdateien auf als auch bei Emails, Webseiten und vielen anderen Dingen. Da dies immer wieder zu Schwierigkeiten, Fehlern und Problemen führte wurden die Unicode-Zeichenkodierungen der UFT-Familie geschaffen. Heute ist UTF-8 die am weitensten verbreitete Zeichenkodierung.
UTF-8 der Retter in der Not:
Damit wird es nicht mehr nötig für die verschiedenen Sprachen unterschiedliche Zeichenkodierungen zu verwenden denn UTF-8 vereint die Zeichen verschiedener Sprachen in einer einzigen Zeichenkodierung. So können das lateinische Alphabet genau so wie das kyrillische oder griechische Alphabet und auch chinesische und viele weitere Schriftzeichen in der gleichen Zeichenkodierung enthalten sein. Das macht das "Wirrwarr" der verschiedenen Zeichenkodierungen für die verschiedensten Sprachen großteils obsolet. Möglich wird das durch einen einfachen "Trick":
#!/usr/bin/env python3
with open("text", "w", encoding="utf-8") as f:
f.write("Test! äöü")
with open("text", "rb") as f:
print(f.read())
Dieses Programm liefert:
b'Test! \xc3\xa4\xc3\xb6\xc3\xbc'
Hierbei deutet das
b vor dem Sting an, dass es sich um ein Bytearray handelt. Dieses wird zwar von Python beinahe wie ein String dargestellt aber in Wahrheit ist es nur eine für den User aufbereitete Darstellung. Uns fällt allerdings auf, dass einige Werte beispielsweise als
\xc3 dargestellt werden. Dies ist der hexadezimale Wert von Sonderzeichen. Weiters fällt uns auf, dass wir die Umlaute
äöü in die Datei geschrieben haben aber wir erhalten die folgenden sechs Zeichen:
\xc3\xa4\xc3\xb6\xc3\xbc
Das liegt daran, dass man mit "nur" 256 möglichen Werten (
die Werte 0 - 255 die mit einem Byte darstellbar wären) nicht alle Zeichen aller Sprachen abbilden kann. Daher verwendet UTF-8 ein Zeichen für die latainischen Buchstaben und Satzzeichen und nutzt mehrere Bytes um Sonderzeichen und Buchstaben anderer Alphabete darzustellen. Somit entspricht zB
\xc3\xa4 dem
ä, usw.
Verändern wird das Programm ein Wenig um die hexadezimalen Werte der einzelnen Bytes zu sehen:
#!/usr/bin/env python3
with open("text", "w", encoding="utf-8") as f:
f.write("Test! äöü")
with open("text", "rb") as f:
data = f.read()
for byte in data:
print(hex(byte))
Dann erhalten wir:
0x54
0x65
0x73
0x74
0x21
0x20
0xc3
0xa4
0xc3
0xb6
0xc3
0xbc
Lesen wir nun wieder die Datei in jeder ISO-Kodierung ein:
#!/usr/bin/env python3
with open("text", "w", encoding="utf-8") as f:
f.write("Test! äöü")
for i in range(1, 17):
if i == 12:
continue
try:
with open("text", "r", encoding="iso-8859-" + str(i)) as f:
print(f"ISO-8859-{i:<2} == {f.read()}")
except UnicodeDecodeError:
print(f"ISO-8859-{i:<2} == UnicodeDecodeError")
Und wir erhalten:
ISO-8859-1 == Test! äöü
ISO-8859-2 == Test! äÜß
ISO-8859-3 == UnicodeDecodeError
ISO-8859-4 == Test! äÃļÃŧ
ISO-8859-5 == Test! УЄУЖУМ
ISO-8859-6 == UnicodeDecodeError
ISO-8859-7 == Test! Àâü
ISO-8859-8 == UnicodeDecodeError
ISO-8859-9 == Test! äöü
ISO-8859-10 == Test! ÃĪÃķÞ
ISO-8859-11 == Test! รครถรผ
ISO-8859-13 == Test! Ć¤Ć¶Ć¼
ISO-8859-14 == Test! ÃĊöÃỳ
ISO-8859-15 == Test! ÀöÌ
ISO-8859-16 == Test! Ă€Ă¶ĂŒ
Sehen wir uns nun die Ausgabe von
ISO-8859-1 genauer an und dekodieren diese von Hand mit der folgenden Tabelle:
|
…0 |
…1 |
…2 |
…3 |
…4 |
…5 |
…6 |
…7 |
…8 |
…9 |
…A |
…B |
…C |
…D |
…E |
…F |
0… |
nicht belegt |
1… |
2… |
SP |
! |
" |
# |
$ |
% |
& |
' |
( |
) |
* |
+ |
, |
- |
. |
/ |
3… |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
: |
; |
< |
= |
> |
? |
4… |
@ |
A |
B |
C |
D |
E |
F |
G |
H |
I |
J |
K |
L |
M |
N |
O |
5… |
P |
Q |
R |
S |
T |
U |
V |
W |
X |
Y |
Z |
[ |
\ |
] |
^ |
_ |
6… |
` |
a |
b |
c |
d |
e |
f |
g |
h |
i |
j |
k |
l |
m |
n |
o |
7… |
p |
q |
r |
s |
t |
u |
v |
w |
x |
y |
z |
{ |
| |
} |
~ |
|
8… |
nicht belegt |
9… |
A… |
NBSP |
¡ |
¢ |
£ |
¤ |
¥ |
¦ |
§ |
¨ |
© |
ª |
« |
¬ |
SHY |
® |
¯ |
B… |
° |
± |
² |
³ |
´ |
µ |
¶ |
· |
¸ |
¹ |
º |
» |
¼ |
½ |
¾ |
¿ |
C… |
À |
Á |
 |
à |
Ä |
Å |
Æ |
Ç |
È |
É |
Ê |
Ë |
Ì |
Í |
Î |
Ï |
D… |
Ð |
Ñ |
Ò |
Ó |
Ô |
Õ |
Ö |
× |
Ø |
Ù |
Ú |
Û |
Ü |
Ý |
Þ |
ß |
E… |
à |
á |
â |
ã |
ä |
å |
æ |
ç |
è |
é |
ê |
ë |
ì |
í |
î |
ï |
F… |
ð |
ñ |
ò |
ó |
ô |
õ |
ö |
÷ |
ø |
ù |
ú |
û |
ü |
ý |
þ |
ÿ |
Dann sehen wir, dass zB
0x54 für das
T steht oder
0xc3 für das
à (
in Gelb hervorgehoben)! Eine Zeichenkodierung ist also nichts weiter als eine Tabelle mit der die einzelnen Werte der binären Bytes einem Buchstaben oder Schriftzeichen zugeordnet werden.
Betrachten wird die
UnicodeDecodeError Exception bei
ISO-8859-3 und die dazugehörige Tablle unter
https://en.wikipedia.org/wiki/ISO/IEC_8859-3 dann wird uns gleich auffallen, dass
0xc3 in dieser Zeichenkodierung nicht genutzt wird und daher auch nicht zu einem Zeichen aufgelöst werden kann. Darum kommt es zu der
UnicodeDecodeError Exception!
Die nicht belegten Teile in den gezeigten Tabellen sind mit Steuerzeichen und sogenannten Whitespaces (
Zeilenschaltung, Tabulator, Leerzeichen, ...) belegt die aus der ASCII-Tabelle stammen.