Bitte warten...

Python: Binärdaten

► Python-Dokumentation: Bytes and Bytearray Operations

Alle digitalen Daten – sowohl die während der Laufzeit eines Programms verarbeiteten Daten als auch im Dateisystem oder als BLOB in einer Datenbank gespeicherte Dateien – liegen in binärer Form vor. Das bedeutet, dass die kleinste Speichereinheit, das Bit, einen von zwei Zuständen annehmen kann: je nach Kontext 1 oder 0, an oder aus, ja oder nein, wahr oder falsch usw. Daten werden nun als Kette von individuellen Bits gespeichert und verarbeitet. Um mehr als zwei Zustände speichern zu können, werden einzelne Bits zu Bytes zusammengefasst, in der Regel sind das acht Bits, mit denen daher 2⁸ = 256 verschiedenen Zustände gespeichert werden können (s. Dualsystem). Um noch mehr unterschiedliche Zustände speichern zu können, können Bytes kombiniert werden.

Bei Textdateien werden die Werte der einzelnen Bytes anhand einer Zeichenkodierung interpretiert, wobei ein oder mehrere Bytes einem konkreten Zeichen entsprechen, das entsprechend der Zeichenkodierung erkannt und ausgegeben wird. Auf diese Weise kann Text unterschiedlicher Schriftsysteme bequem als Ein- und Ausgabeformat verwendet werden. In anderen Fällen, beispielsweise bei Grafik- oder Multimediaformaten werden komplexere Algorithmen benötigt, die die vorliegenden Daten sinnvoll verwerten können. Solche Daten können nur mit einer entsprechenden Software dargestellt werden. Das zugrunde liegende Schema der Speicherung als Bytes ist aber nicht anders als wie bei Textdateien.

Wie Daten auf der Ebene von Bytes und Bits mit Python verarbeitet werden können, wird auf dieser Seite erklärt.

Binärdatei lesen

► Python-Dokumentation: open read

Als Beispiel für eine Binärdatei diene hier diese BMP-Grafikdatei:
Emoji: crazy
Die Datei ist 32 × 32 Pixel sowie 4234 Bytes groß. Dieser Wert ergibt sich aus 138 Bytes für die Metadaten und spezielle Bildinformationen sowie 32 × 32 × 4 Bytes für die eigentlichen Bilddaten (jedes Pixel wird mit 32 Bits = 4 Bytes kodiert).

Um die Datei zum binären Lesen zu öffnen (und nicht als Textdatei), wird der Funktion open() als zweites Argument der Wert rb übergeben, was für read binary steht.

Code kopieren
import os
path = os.path.dirname(__file__) + "/"

with open(path + "emoji.bmp", "rb") as f:
    binary_data = f.read()

# Datentyp ermitteln (= "bytes")
print(type(binary_data))

# Größe der Datei anzeigen:
print("Größe:", len(binary_data))

# Wert eines spezifischen Bytes anzeigen:
print("Byte 10:", binary_data[10])

# Wert jedes einzelnen Bytes anzeigen:
for byte in binary_data:
    print(byte)

Einzelne Bytes oder Bytesequenzen können lesend über deren Index angesprochen werden. In dieser Beziehung verhalten sich binäre Daten ähnlich wie Strings, Listen oder Tupel.

Code kopieren
byte = binary_data[10]  # Wert des Bytes 10
sequence = binary_data[10:20]  # Sequenz von Byte 10 bis 19

► Python-Dokumentation: find

Mit der Methode find() kann eine Bytesequenz (oder auch ein einzelnes Byte) innerhalb eines Datenpakets gefunden werden, sofern diese existiert. Der Rückgabewert ist der Index bzw. die Position des erstens Bytes der Sequenz innerhalb der durchsuchten Daten. In diesem Beispiel wird aus den Daten zunächst eine Sequenz extrahiert, nach der dann gesucht wird.

Code kopieren
sequence = binary_data[40:100]  # 100 Bytes ab einschließlich Byte 40
print(binary_data.find(sequence))  # 40

► Python-Dokumentation: seek

Möchte man nicht eine ganze Datei einlesen, sondern nur einen Teil, so kann die Startposition zunächst mit der Funktion seek() festgelegt werden. Anschließend wird die Größe der zu lesenden Daten an die Funktion read() übergeben.

Code kopieren
offset = binary_data[10]  # Beginn der Grafikdaten
with open(path + "emoji.bmp", "rb") as f:
    f.seek(offset)  # lies ab Byte 138
    image_data = f.read(32 * 32 * 4)  # lies 4096 Bytes

Binärdatei schreiben

► Python-Dokumentation: open write

Um die Datei zum binären Schreiben zu öffnen (und nicht als Textdatei), wird der Funktion open() als zweites Argument der Wert wb übergeben, was für write binary steht.

In diesem Beispiel wird der Inhalt der Datei emoji.bmp in die neu erstellte Datei emoji2.bmp geschrieben, also eine Kopie erzeugt.

Code kopieren
with open(path + "emoji2.bmp", "wb") as f:
    f.write(binary_data)

► Python-Dokumentation: seek

Analog zum Lesen einer Datei ist es auch möglich, Daten ab einer bestimmten Position zu schreiben. Diese Position wird mit der Funktion seek() festgelegt. In der Funktion open() muss das Argument r+b übergeben werden, damit nicht der gesamte ursprüngliche Inhalt überschrieben wird, sondern nur der gewünschte Abschnitt.

Code kopieren
offset = binary_data[10]  # Beginn der Grafikdaten
with open(path + "emoji3.bmp", "r+b") as f:
    f.seek(offset)  # schreibe ab Byte 138
    f.write(image_data)

Bytes überschreiben

► Python-Dokumentation: bytearray

Um Bytes sinnvoll zu verändern, muss man natürlich das zugrunde liegende Dateiformat verstehen. Das Format der hier verwendeten BMP-Grafik ist vergleichsweise einfach aufgebaut. Wie oben bereits beschrieben, besteht die Datei aus Metadaten sowie aus den Farbwerten der einzelnen Pixel der Grafik. Die Größe der Metadaten kann variieren. Der Beginn der Grafikdaten ist allerdings in Byte 10 der Metadaten gespeichert. So können aus diesem Wert und den bekannten Maßen der Grafik die Farbwerte für jedes beliebige Pixel ermittelt und auch verändert werden. Die BMP-Grafik in diesem Beispiel besitzt eine Farbtiefe von 32 Bits, das bedeutet, dass jeder Farbkanal 8 Bits groß ist. Die Farbkanäle sind in diesem Beispiel von links nach rechts: Blau, Grün, Rot und Alphakanal (Opazität). Da jeder Kanal 8 Bits groß ist, kann er 256 verschiedene Zustände annehmen. Die Pixel werden entgegen der Darstellung Zeile für Zeile vom unteren Bildrand beginnend gespeichert.

Im folgenden Beispiel werden nun mit diesen Informationen die Daten des oberen linken Viertels der Grafik ausgelesen und mit diesen das untere rechte Viertel ersetzt. Die resultierende Grafik wird dann in eine neue Datei geschrieben.

Dabei ist zu beachten, dass auf Daten des Typs bytes nur lesend zugegriffen werden kann. Sie verhalten sich daher hier wie Tupel. Um sie auch überschreiben zu können, müssen sie aus diesem Grunde zunächst mit der Funktion bytearray() in den entsprechenden Datentyp umgewandelt werden (s. Zeile 6)! Dann verhalten sie sich wie eine Liste.

Code kopieren
import os
path = os.path.dirname(__file__) + "/"

# Datei binär einlesen
with open(path + "emoji.bmp", "rb") as f:
    binary_data = bytearray(f.read())
print(type(binary_data))

width  = 32  # Breite der Grafik
height = 32  # Höhe der Grafik
bpp = 4  # Bytes pro Pixel
offset = binary_data[10]  # Beginn der Grafikdaten

# Grafikdaten lesen und ersetzen
new_data = binary_data
for i in range(0, int(height / 2)):  # Schleife über die halbe Höhe der Grafik
    source = offset + (bpp * width * height / 2) + (i * bpp * width)
    target = offset + (bpp * width / 2         ) + (i * bpp * width)
    new_data[int(target):int(target + bpp * width / 2)] = binary_data[int(source):int(source + bpp * width / 2)]

# Veränderte Daten in neue Datei schreiben
with open(path + "emoji3.bmp", "wb") as f:
    f.write(new_data)

In der neu erstellten Grafik lässt sich das Ergebnis dann überprüfen:
Emoji: crazy

Bytes löschen

Bytes können wie die Elemente einer Liste mit del (delete) aus einem Bytearray gelöscht werden. In diesem Beispiel werden alle Pixel der linken Hälfte der Grafik gelöscht. Zusätzlich muss die neue Breite der Grafik in den Metadaten der Datei eingetragen werden.

Code kopieren
# Bytes löschen
new_data = binary_data
for i in range(0, height):  # Schleife über die halbe Höhe der Grafik
    lineoffset = offset + (32 - i) * bpp * width
    del new_data[int(lineoffset):int(lineoffset + bpp * width / 2)]
    
# Neue Breite in Metadaten eintragen
new_data[18] = 16

# Veränderte Daten in neue Datei schreiben
with open(path + "emoji4.bmp", "wb") as f:
    f.write(new_data)

In der neu erstellten Grafik lässt sich das Ergebnis dann überprüfen:
Emoji: crazy

Bytes ergänzen

In diesem Beispiel werden 10 Zeilen mit roten Pixeln (bei 50 % Opazität) in die Mitte der Grafik eingefügt. Auch hier muss die neue Höhe der Grafik in den Metadaten geändert werden.

Code kopieren
# Bytes ergänzen
new_data  = binary_data[:offset + 16 * bpp * width]
new_data += 10 * width * bytearray([0, 0, 255, 127])
new_data += binary_data[offset + 16 * bpp * width:]
    
# Neue Höhe in Metadaten eintragen
new_data[22] = height + 10

# Veränderte Daten in neue Datei schreiben
with open(path + "emoji5.bmp", "wb") as f:
    f.write(new_data)

In der neu erstellten Grafik lässt sich das Ergebnis dann überprüfen:
Emoji: crazy

Bits ein- und ausschalten

► Python-Dokumentation: Binary bitwise operations

Unter Umständen möchte man nicht ein ganzes Byte, sondern nur einzelne Bits gezielt ändern. Natürlich ist es möglich, das betroffene Byte auszulesen, den dezimalen Wert dieses Bytes nach der gewünschten Änderung einzelner Bits zu berechnen und das Byte dann mit diesem Wert zu überschreiben. Python bietet hier aber komfortablere Operationen an, die mit weniger Aufwand verbunden sind.

Zunächst existieren hier die logischen Operatoren & (Und), | (Oder) sowie ^ (exklusives Oder), um einzelne Bytes miteinander zu verrechnen. Die durchführbaren Operationen entsprechen jenen mit den Wahrheitswerten True und False. Im Gegensatz zu gewöhnlichem Oder ist das Ergebnis von 1 ^ 1 == 0 („das eine oder das andere Bit, aber nicht beide“).

Hierbei gelten folgende Zusammenhänge, die für jeden Operator die vier möglichen Kombinationen abdecken:

0011 & 0101 == 0001
0011 | 0101 == 0111
0011 ^ 0101 == 0110

Im folgenden Beispiel wird demonstriert, wie sich einzelne Bits eines Bytes (in diesem Fall die letzten beiden) an- und ausschalten bzw. invertieren lassen, ohne dass die anderen Bits verändert werden.

Im ersten Fall wird das Byte durch den Operator | mit einem Wert verknüpft, bei dem die Bits, die eingeschaltet werden sollen (unabhängig von ihrem aktuellen Zustand), durch 1 repräsentiert werden, alle anderen Bits durch 0.

Im zweiten Fall wird das Byte durch den Operator & mit einem Wert verknüpft, bei dem die Bits, die ausgeschaltet werden sollen (unabhängig von ihrem aktuellen Zustand), durch 0 repräsentiert werden, alle anderen Bits durch 1.

Im dritten Fall wird das Byte durch den Operator ^ mit einem Wert verknüpft, bei dem die Bits, die invertiert werden sollen, durch 1 repräsentiert werden, alle anderen Bits durch 0.

Das Präfix 0b vor der eigentlichen Zahl gibt an, dass es sich um eine Binärzahl handelt.

Code kopieren
byte = 0b10101010
print(bin(byte | 0b00000011))  # einschalten -> 10101011
print(bin(byte & 0b11111100))  # ausschalten -> 10101000
print(bin(byte ^ 0b00000011))  # invertieren -> 10101001

Für unser Grafikbeispiel lässt sich dies nun nutzen, um beispielsweise die Farben einzelner Pixel zu verändern. Es werden alle Farben im oberen linken Viertel invertiert, wobei der Alphakanal unangetastet bleibt:

Code kopieren
# Grafikdaten lesen und ersetzen
new_data = binary_data
for i in range(0, int(height / 2)):  # Schleife über die halbe Höhe der Grafik
    for p in range(0, int(width / 2)):  # Schleife über die halbe Breite der Grafik
        for byte in range(0, 3):  # Schleife über die einzelnen Bytes (Alphakanal in Byte 4 wird ignoriert)
            pos = int(offset + (bpp * width * height / 2) + (i * bpp * width) + (p * bpp) + byte)
            new_data[pos] = binary_data[pos] ^ 0b11111111

# Veränderte Daten in neue Datei schreiben
with open(path + "emoji6.bmp", "wb") as f:
    f.write(new_data)

In der neu erstellten Grafik lässt sich das Ergebnis dann überprüfen:
Emoji: crazy

Bits verschieben

► Python-Dokumentation: Shifting operations

Außerdem existieren Operatoren zum Verschieben der Bits innerhalb einer Zahl um n Bits. Diese Zahl kann größer als acht BIts sein, beispielsweise, um auf mehrere Bytes aufgeteilt zu werden. Der Operator << verschiebt die Bits nach links (Multiplikation mit 2n), der Operator >> nach rechts (Division durch 2n).

Im Beispiel wird um 3 Bits verschoben. Beim Verschieben nach links werden die letzten Bits (rechts) durch 0 ersetzt. Beim Verschieben nach rechts werden die letzten Bits (rechts) abgetrennt.

Code kopieren
binary_value = 0b1111110000111111
print(bin(binary_value))       # ursprüngliche Zahl      ->    1111110000111111
print(bin(binary_value << 3))  # verschieben nach links  -> 1111110000111111000
print(bin(binary_value >> 3))  # verschieben nach rechts ->       1111110000111