Binär-I/O mit Perl

Lesen und Schreiben von binären Dateien

Binär-Dateien können Bytes mit beliebigen Werten (0..255) enthalten, nicht nur lesbare Zeichen wie Text-Dateien.
Typische Binär-Dateien sind Grafik-, Video- und Sound-Dateien, z.B. *.gif, *.jpeg, *.mp3 usw.
Auf dieser Seite finden sie Hinweise und Beispiele zum Lesen und Schreiben von Binär-Dateien mit Perl.
Perl Scripts für CGI und Systemverwaltung
Datei In/Out Lesen und schreiben von Dateien mit Perl
Binär lesen Binär-Dateien lesen und verarbeiten
Binär schreiben Binär-Dateien schreiben
Random Access An beliebigen Datei-Adressen Lesen oder schreiben
Hilfsprogramm Dump (Hex-Ausgabe eines Binär-Strings)
Verwandte Themen:
Text lesen Text-Dateien lesen und verarbeiten
Text schreiben Text-Dateien schreiben
Webseiten lesen Live-Webseiten lesen und Informationen entnehmen
Perl Module Zusatz-Module zum Lesen & Schreiben von Dateien
Datei-Baum Durchforsten eines ganzen Datei-Baums (tree)

Binär-Dateien lesen und verarbeiten

Verwenden sie zum Test z.B. eine kleine Grafik-Datei (*.gif, *.jpg) oder noch besser die Datei d_0_255.dat, deren Herstellung im nächsten Kapitel demonstriert wird.

Es ist sinnvoll, aber nicht unbedingt notwendig, vor dem Lesen die Länge der Datei festzustellen.

Das Öffnen erfolgt mit open() genauso wie bei allen anderen Dateien.
Unmittelbar darauf wird mit binmode verhindert, dass zufällige Byte-Kombinationen (Zeilen-Ende, Datei-Ende) das Lesen vorzeitig beenden. Dieser Befehl wird derzeit nur für Windows benötigt, schadet aber bei allen anderen Systemen nicht.

Mit read() wird versucht, die Anzahl $soll Bytes von der Datei in die Variable $result zu lesen. Das Ergebnis $br gibt die Anzahl tatsächlich gelesener Bytes an.

Anschließend folgt normalerweise die Verarbeitung. Das wird hier durch die Ausgabe der gelesenen Daten mit hex_dump() simuliert.
Zur Decodierung (z.B. von binär codierten Ganzen Zahlen oder Gleitkomma-Zahlen) verwendet man vorteilhaft die → Funktion unpack()
$inpath = 'c:/test/test.png';
$len1 = -s $inpath;
print "Laenge($inpath)=$len1\n";

# open und binmode
$ok = open(BINREAD,$inpath);
binmode BINREAD;
print "open($inpath) = $ok\n";
if(!$ok) {die;}

# Datei lesen
$soll = 1000;
$br = read(BINREAD,$result,$soll);
close BINREAD;
print "$br Bytes gelesen\n";

# Daten verarbeiten
hex_dump($result);

Lese-Schleife

Für längere Dateien oder Dateien unbekannter Länge ist diese einfache Form nicht brauchbar. In diesem Fall liest man nicht die gesamte Datei in eine Variable, sondern in einer Schleife jeweils eine bestimmte Anzahl Bytes, die meist sofort verarbeitet werden.
Dabei werden normalerweise je $br=$soll Bytes von der Datei gelesen. Lediglich am Ende der Datei kann $br<$soll auftreten.
$br = 1;
$soll = 256;
while($br) {
$br = read(BINREAD,$result,$soll);
# $result hier verarbeiten
}

Sequential Access

Mit jedem Lese-Zugriff wird ein Zeiger (Zähler, file pointer) auf die Datei weitergesetzt. So wird gewährleistet, dass mit aufeinander folgenden Lese-Befehlen auch genau aufeinander folgende Daten gelesen werden.
Eine Alternative ist der gezielte Zugriff (↓ Random Access) auf eine bestimmte Adresse der Datei.
Ein weiteres Beispiel auf einer anderen Webseite:
Verarbeitung von Text-Dateien mit → Datensätzen fixer Länge zu SQL-Anweisungen.

Binär-Dateien schreiben

Das Schreiben einer Binär-Datei unterscheidet sich nur wenig vom Schreiben einer Text-Datei.
In eine Binär-Datei kann man auch beliebige nicht-druckbare Zeichen schreiben.
Im Beispiel wird eine Datei erzeugt, die 256 Bytes mit allen möglichen Werten enthält, also fortlaufend dezimal 0-255, Hex 00-FF.

Dazu werden in einer Schleife die notwendigen Bytes im Binär-String $daten zusammengestellt: chr() erzeugt ein Byte aus dem angegebenen Wert (hier $i).

Die Verwendung von binmode unmittelbar nach dem Öffnen der Datei stellt sicher, dass die Daten unverändert geschrieben werden (sonst wird auf Win-System jedes Zeichen 0A in die Folge 0D0A umgewandelt.
Zur Codierung von Variablen (z.B. Ganze oder Gleitkomma-Zahlen) zu binären Daten verwendet man vorteilhaft die → Funktion pack()
Die Daten werden mit print() in die Datei geschrieben.
Zuletzt wird die Länge der erzeugten Datei kontrolliert.
$outpath = 'c:/test/d_0_255.dat';
#$outpath = '/home/test/d_0_255.dat';
$daten="";
for($i=0;$i<256;$i++) {
$daten.=chr($i);
}
$len3 = length($daten);
print "Laenge(daten) = $len3\n";

# Datei Schreiben
$ok = open(BINWRITE, "> $outpath");
binmode BINWRITE;
print "open($outpath) = $ok\n";
print BINWRITE $result;
close BINWRITE;

# Kontrolle
$len3 = -s $outpath;
print "Laenge($outpath) = $len3\n";
Den Inhalt der erzeugten Binärdatei können sie mit dem Beispiel ↑ 'Binärdatei Lesen' dieser Seite kontrollieren, alternativ auch mit jedem Hex-Editor Programm.
Bei Verzicht auf die ersten 32 (Steuer)-Zeichen ($i=32;$i<256;$i++) wird eine Text-Datei erzeugt, die man mit jedem Text-Editor lesen kann.
Längere Dateien werden nicht mit einer einzigen print() -Anweisung geschrieben, sondern mit print() in einer Schleife.

Random Access

File-Pointer lesen

Zur Organisation des Zugriffs auf eine Datei verwendet Perl einen Zeiger (file pointer). Der Wert des Zeigers ist gleichbedeutend mit den Bgeriffen Adresse oder Offset.
Der Pointer wird beim Öffnen jeder Datei an den Anfang (Adresse=0) gesetzt. Mit jedem Lese- oder Schreib-Zugriff wird der Pointer um genau die Anzahl der gelesenen oder geschriebenen Bytes weitergesetzt.
Mit Funktion tell() kann man den Wert des Zeigers jederzeit lesen (Beispiel rechts).
open(BIN,$path);
binmode BIN;
# Anfangs ist $fp=0
$fp = tell(BIN);
print "fp = $fp\n";

$soll=100;
$br = read(BIN,$r,$soll);
# Nun ist $fp=100
$fp = tell(BIN);
print "fp = $fp\n";

File-Pointer setzen

Mit Funktion seek() aus dem Modul Fcntl kann man den Wert des Datei-Zeigers auf jeden beliebigen Wert zwischen 0 und der Datei-Größe in Byte setzen.

Die Funktion seek gibt bei korrekter Ausführung den Wert=1 zurück, das ist allerdings unbrauchbar: Wenn man den Zeiger auf Werte>Datei-Größe setzt, wird auch 1 zurückgegeben. In diesem Fall analysiert man die Anzahl gelesener oder geschriebener Bytes eines nachfolgenden Zugriffs-Befehls.

Im Beispiel rechts werden die ersten 400 Bytes der Datei übersprungen, es wird direkt der Bereich 400..500 gelesen.
Wenn man den Zeiger danach nicht mehr ändert, werden mit jeder weiteren read()-Anweisung die jeweils folgenden Daten gelesen.

use Fcntl;
open(BIN,$path);
binmode BIN;

# Bytes 400..500 lesen:
$soll = 100;
$adr = 400;
seek(BIN,$adr,0);
read(BIN,$r,$soll);

# Auswertung
hex_dump($r);

Absolute oder relative Bewegung

Der Datei-Zeiger kann absolut bewegt werden, d.h. durch Angabe der Byte-Adresse relativ zum Anfang der Datei. In diesem Fall wird wird das 3. Argument der Funktion seek() auf den Wert=0 oder SEEK_SET gesetzt.
Alternativ wird der Zeiger relativ zu seiner aktuellen Position bewegt, d.h. um so viele Bytes wie angegeben - vor oder zurück. Das 3. Argument der Funktioin seek() wird auf den Wert=1 oder SEEK_CUR gesetzt.
Die Konstanten SEEK_SET und SEEK_CUR werden im Modul IO::Seekable definiert. Wenn man die konstanten Werte 0,1 verwendet, braucht man dieses Modul nicht zu laden.
use IO::Seekable;
# Start auf Byte 333 der Datei:
seek(BIN,333,0);
# tell(BIN) ergibt 333
# Um +7 Bytes vor:
seek(BIN,7,1);
# tell(BIN) ergibt 340
# Um -40 Bytes vor:
seek(BIN,-40,SEEK_CUR);

Fixed Length Records

In der IT-Urzeit wurden größere Mengen von Daten häufig in Dateien und diese auf Magnetbändern gespeichert. Das Auffinden und Lesen eines bestimmten Datensatzes war sehr zeitraubend. Um diesen Vorgang zu beschleunigen, wurden die Daten in Datensätzen fixer Länge (fixed length records) gespeichert.

Wenn man die Datensatz-Nummer kennt, dann lässt sich daraus seine Adresse berechnen, wie rechts demonstriert wird.
Danach zeigt der Pointer auf den nächsten Datensatz. Wenn der gleiche Datensatz (geändert und danach wieder) geschrieben werden soll, muss man den Pointer um die Länge eines Datensatzes zurücksetzen.

$rec_len = 100;

# Lese Datensatz 23:
$adr = 23*$rec_len;
seek(BIN,$adr,SEEK_SET);
read(BIN,$rec,$rec_len);

# Schreibe den gleichen Datensatz
seek(BIN,-$rec_len;SEEK_CUR);
print BIN,$newrec;

Auch für Text-Dateien

Der File-Pointer wird auch beim Lesen oder Schreiben von Text-Dateien verwendet. Das ist lediglich weniger bekannt, weil man sich dort meist an Zeilen orientiert.

Die Funktionen tell() und seek() funktionieren analog bei Zugriff auf Text-Dateien.

Alte Datenbank-Dateien

In der IT-Urzeit wurden größere Mengen von Daten in Dateien auf Magnetbändern verwaltet. Diese Dateien waren in Datensätze (Zeilen, records) fixer Länge organisiert.
In Datensätzen fixer Länge haben auch alle Teile (Felder, Spalten) eine fixe Länge und werden mit Null-Bytes oder Leerzeichen aufgefüllt.

Beispiel: Eine Personen-Datei kann in Datensätzen fixer Länge von 100 Byte organisiert sein. Bytes 0..9 sind für eine Personen-Nummer reserviert, Bytes 10..19 für den Vornamen, Bytes 20..40 für den Zunamen usw.
Die Felder können lesbaren Text oder binär codierte Daten (Zahlen) enthalten.
Trennzeichen (z.B. CR oder LF) sind nicht üblich, d.h. die Bytes der Datensätze folgen unmittelbar aufeinander.

Heute organisiert man größere Dateien in Daten-Banken, die von Datenbank Server-Programmen verwaltet werden. Kleinere Dateien werden in Text-Dateien variabler Länge organisiert: Felder und Datensätze sind jeweils genauso lang wie benötigt und voneinander durch Trennzeichen getrennt.
Lediglich auf Groß-Computern trifft man noch gelegentlich auf Dateien mit Datensätzen fixer Länge.

Datei-Standards

Das gezielte Setzen des Datei-Zeigers braucht man u.a. bei der Verarbeitung strukturierter Binär-Dateien.
Fast alle heute verwendeten Binär-Dateien (GIF, JPEG, PNG, MP3, ...) sind intern nach dem jeweiligen Standard strukturiert.
Damit man raschen Zugang zu den verschiedenen Teilen hat, wird für jeden Teil dessen absolute oder relative Adresse (Offset) gespeichert.

Zur Auswertung liest man zunächst nur die Adresse eines Daten-Blocks (1..4 Byte), die mit Funktion unpack decodiert wird. Achten sie dabei auf die Reihenfolge der Bytes (Little endian oder Big endian).
Danach setzt man den Datei-Zeiger an die berechnete Adresse und liest die gesuchten Daten. Die einzelnen Elemente größerer Daten-Blocks können ihrerseits fixe oder variable Länge haben.

Typische Beispiele: Bei der Auswertung von Grafik- oder Audio-Daten setzt man die hier beschriebenen Methoden ein. Mit etwas Geschick kann man Perl-Programme zum Lesen oder Ändern auch jener Meta-Daten erstellen, die sonst nur schwer zugänglich sind.

Viele Grafik-Dateien (JPEG) enthalten zusätzlich zum Bild auch beschreibende Meta-Daten, z.B. Datum & Zeit der Aufnahme, Kamera (Modell, Einstellungen), Daten der verwendeten Software (z.B. Seriennummer) usw, manchmal auch eine verkleinerte Kopie des Bildes (Thumbnail, Preview).

Viele Audio-Dateien (MP3) enthalten zusätzlich beschreibende Meta-Daten, z.B. Künstler, Werk, Jahr, manchmal sogar ganze (JPEG)-Bilder.

Hilfs-Programme

Dump

Das Sub hex_dump() eignet sich zur raschen Analyse eines Binär-Strings, z.B. von Daten, die von einer Binär-Datei gelesen wurden.

Typische Anwendung:
$bs = 'ABC'.chr(50).chr(51);
hex_dump($bs);
Das Programm gibt die Werte aller Bytes des Strings hexadezimal und - wenn möglich - als → ASCII-Zeichen aus, ähnlich wie mit üblichen Hex-Editoren.
Je 16 Bytes werden in einer Zeile angezeigt. Angefangene Zeilen werden mit Null-Bytes aufgefüllt.

Die Ausgabe ist für die Konsole eingerichtet. Für Ausgabe auf Webseiten ersetzen sie jedes Zeichen "\n" durch "<br />\n"

Es gibt keine Gewähr für die korrekte Funktion, die Anwendung erfolgt auf eigenes Risiko.
sub hex_dump {
my ($dat)=@_;
my $datlen=length($dat);
my($i,$j,$k,$bn)=0;
my($b,$t);
$t='....';
$t.=' .0 .1 .2 .3 .4 .5 .6 .7';
$t.=' .8 .9 .A .B .C .D .E .F';
$t.=' ASCII';
print "$t\n";
for($i=0;$i<$datlen;$i+=16) {
printf('%04X ',$i);
$t='';
for($j=0;$j<16;$j++) {
$b=substr($dat,$i+$j,1);
$bn=ord($b);
printf(' %02X',$bn);
if($j==7) {print ' ';}
if($bn>31 && $bn<127) {$t.=$b;}
else {$t.='.';}
}
print(" $t\n");
}
}