Trick 58: Strategie
der kleinsten Schritte (Datei Byte für Byte lesen und
verarbeiten)
Aufgabe: Schnell mal eben eine
Binärdatei durchsehen und schauen, ob
eine bestimmte Codefolge darin vorkommt und wie oft.
Schön wäre, die Vorkommnisse auch angezeigt zu bekommen,
aber mit 30 Zeichen oder so links und rechts davon. Und
auch die Position innerhalb der Datei (sog. "Offset")!
Warum: Es gibt Dateien ohne
Zeilenstruktur, und zwar nicht wenige.
Oft steht kein lesbarer Text drin, sondern codierte Daten.
Man nennt sie oft "Binärdateien", obwohl JEDE Datei aus Bytes
und jedes Byte aus Bits (= binary digits) besteht und somit
ALLES binär gespeichert ist.
Neben den Programmdateien (.EXE u.a.), von denen man besser
die Finger läßt, gibt es Bild- und Videodateien (.JPG. .AVI
u.a.), mit denen aus Sicht von allegro-Anwendungen wohl auch
nichts gemacht werden muß, aber es ist z.B. auch möglich,
daß eine XML- oder HTML-Datei keine Zeilentrenner enthält!
Das trifft auch auf MARC-Originaldateien zu. Und schließlich
sind die allegro-Dateien der Typen .ALD/.ALG nicht zeilenweise
strukturiert und enthalten neben Text- auch Steuerzeichen. Das
bequeme Einlesen einer Zeile mit "get iV" geht jedenfalls dann
nicht, weil die Zeilen-Steuerzeichen 13 10 fehlen.
Anm.: Das Durchsuchen und
Verarbeiten von .ALD und .ALG-Dateien geht
natürlich am allerbequemsten mit der Volltextsuche, wenn man
Zeichenfolgen sucht, die in den Datenfeldern vorkommen.
Will man sich nicht auf Dateien
unterhalb einer bestimmten Länge
beschränken, sondern
prinzipiell in der Lage sein, beliebig lange
Dateien durchzusehen, dann hilft
nur die Verarbeitung Byte für
Byte. Ist das nicht furchtbar
langsam? Es geht.
Zwar ist auch das Einlesen von
Blöcken fester Länge
möglich (z.B. fetch 1000),
aber wenn die zu suchende Zeichenfolge
genau auf einer Blockgrenze liegt
(z.B. auf Position 999 beginnt),
gibt es ein Problem...
Lösung:
Ein einzelnes Byte einlesen, das kann man auf zwei Arten machen:
fetch
1
holt ein Byte in die iV, wobei
Steuercodes unterhalb
32 ersetzt werden durch ^ und einen nachfolgenden Buchstaben,
und zwar A für 1 usw., @ für 0, ^Z für 26
Mit if "1" ... prüft man, ob das Byte die Ziffer
1 ist
Mit if "^A" ... dagegen, ob es der Bytecode 01 ist.
fetch
b
holt das nächste Byte als
Dezimalzahl; statt A also 65
und statt a die Zahl 97, statt Ziffer 1 die 49.
Mit if 49 ... prüft man, ob das Byte die Ziffer 1 ist
Mit if 1 ... dagegen, ob es der Bytecode 01 ist.
Beide Befehle eignen sich für
den hier in Rede stehenden Zweck.
(Umcodiert wird hierbei
übrigens nie!)
Nur der erste eignet sich, wenn man
das gelesene Byte hernach mit
"write ^" wieder korrekt in eine
andere Datei hinausschreiben will.
Der erste Trick besteht nun darin,
Zeichen für Zeichen mit einem der
beiden Befehle zu lesen und jeweils
mit dem ersten Zeichen der
gesuchten Folge zu vergleichen. Nur
bei Gleichheit geht es dann
weiter mit dem Lesen des
nächsten Zeichens und Vergleich mit dem
zweiten Zeichen der Folge usw.,
sonst braucht das zweite Zeichen
ja nicht mehr verglichen zu werden.
Der zweite Trick ist, bei einer
Übereinstimmung des ersten Zeichens
dessen Position in der Datei
mit fetch p zu bestimmen und zu sichern.
Bei Nichtübereinstimmung des
zweiten, dritten ... Zeichens wird dann
zu der gesicherten Position
zurückgekehrt (mit fetch m) und das
nächste Zeichen geholt. Nur so
kann man, wenn z.B. nach der Folge
'121' gesucht wird, zwei Treffer
ermitteln, wenn in der Datei die
Folge '12121' auftritt, d.h.
die gesuchte Folge innerhalb ihrer
selbst neu beginnt.
Beispiel
========
Es soll festgestellt werden, ob und
wie oft in der Datei abc.xyz
die Zeichenfolge '121' auftritt.
(Leider muß man an mehreren Stellen
eingreifen, wenn es eine andere
Folge sein soll, siehe ACHTUNG...
Enorm elegant ist diese Lösung
also nicht, zugegeben.)
------------------------
MUSTER ------------------------
Die Datei öffnen
open abc.xyz
Protokolldatei öffnen
open x ergebnis.txt
Zähler für die
Vorkommnisse
z=0
^^^^^^^^^^^^^^^^^ Beginn
der Schleife
:GLOOP
naechstes Zeichen lesen,
als dezimale Bytezahl
fet b
war denn noch eins da?
Sonst Ende
if can jump GLEND
ein gelesenes Zeichen
steht in der iV als Zahl
***************************************************
Hier ist der Platz zum
Manipulieren!
Erstes Zeichen
vergleichen: (Ziffer 1 = Byte 49)
if =49 jump MATCH //
<- ACHTUNG: anpassen
***************************************************
Das erste Zeichen wurde
noch nicht gefunden
jump GLOOP
erstes Zeichen gefunden,
die weiteren einzeln vergleichen
:MATCH
Offset-Position hinter dem
ersten Zeichen in $pos vermerken
fet p
ins $pos
jetzt einzeln lesen und
vergleichen, bei Ungleichheit -> :NEXTP
fet b
ACHTUNG: hier ebenfalls anpassen für die weiteren Bytes
if not =50 jump NEXTP
// Ziffer 2
fet b
if not =49 jump NEXTP
// Ziffer 1
... hier evtl. noch
weitere Bytes in dieser Weise behandeln
Treffer! Zähler
erhöhen
z+1
Meldung in die
Ergebnisdatei
wri "Pos. " $pos ": "
Umgebung 30 Zeichen links
und rechts abgreifen
eval $pos -30
if <0 var "0"
Pos. 30 Byte nach links
setzen
fet m
50 Zeichen holen
fet 60
ins $umg
und mit ausgeben
write "..." $umg "..." n
:NEXTP
Zur gemerkten Position
zurück
var $pos
fet m
und dort weitermachen
jump GLOOP
:GLEND
^^^^^^^^^^ Ende der
Schleife
Datei schliessen
close
Zähler ausgeben
(ACHTUNG: Wert "121" anpassen)
wri n "121 wurde " z "mal
gefunden"
close x
Ergebnisdatei zeigen
help ergebnis.txt
---------------------
MUSTER ENDE ------------------------
Teil 2:
Binäre Dateien Byte für Byte verarbeiten, z.B. auch XML
Brauchen kann man das immer dann, wenn eine Datei u.U. keine
Zeilentrenner hat, wie z.B. bei XML. Dann ist vor allem "get" zum
Einlesen der nächsten Zeile nicht anwendbar.
Im Kern gibt es dabei zwei Tricks:
1. Die Sequenz
fetch 1
write ^
(Sonderfall: Zeichen ^ wird intern zu ^~)
liest zuerst ein Byte als Zeichen, wobei Steuercodes wie z.B. 13 10
in der Form ^M^J in die iV gesetzt werden (^ und M getrennt!).
Der zweite Befehl schreibt den Inhalt der iV in entsprechender
Weise in die Ausgabedatei, d.h. aus ^M wird wieder der Code 13.
Zwischen diesen beiden Zeilen kann man mit dem eingelesenen Zeichen
natürlich anstellen, was immer man will. Statt des zweiten Befehls
kann man auch ganz andere Dinge machen, schließlich muß nicht für
jedes Zeichen wieder genau ein Zeichen ausgegeben werden.
2. Mit dem Befehl
fetch c
schaut man nach, was das nächste Zeichen ist, ohne daß schon der
Dateizeiger weitergerückt würde. Das braucht man, um nach dem
Einlesen eines '<' schon mal zu spicken, ob als nächstes
ein '/' kommt.
Als Beispiel nehmen wir die Aufgabe, schnell mal eben eine XML-Datei
ein ganz klein wenig leichter lesbar darzustellen:
1. Jedes Tag soll auf neuer Zeile beginnen, schließende Tags aber nicht
2. Die echten Daten, also was zwischen den Tags steht, sollen fett
angezeigt werden.
[Man braucht das nicht wirklich, es gibt ja mächtige XML-Werkzeuge,
angefangen bei notepad++. Es geht nur um das Schema der VerFLEXung
dieser Aufgabe. Anwendbar auch auf HTML.]
So sieht der fertige FLEX aus:
----------------------------------------------------------------
XMLSHOW.FLX : XML-Datei anzeigen, Feldinhalte fett
2008-12-09 : Es wird zunaechst eine RTF-Datei draus gemacht
Aufruf: X xmlshow dateiname
Dateiname steht in iV, oeffnen
open
if no mes Die Datei gibt's nicht;end
Datei zum Schreiben oeffnen
open x xmlshow.rtf
Dateikopf, damit rtf-Anzeige dann klappt
wri Flisthead.rtf
Schleife verarbeitet die Datei zeichenweise
:LOOP
naechstes Zeichen holen
fetch 1
Dateiende? -> :dende
if cancel jump dende
Ist es < oder > ? Dann Sonderbehandlung
if "<" perf k;jump LOOP
if ">" perf g;jump LOOP
normal: Zeichen einfach wieder rausschreiben
wri ^
jump LOOP
:dende
close
Abschluss der rtf-Datei
wri "}}}"
close x
und anzeigen
help xmlshow
end
:k // UPro fuer Zeichen <
welches Zeichen kommt hinter < ?
fetch c
Falls nicht </, dann auch neue Zeile
if not ="47" wri "\\b0 \\par " n "<";return
sonst nur Fett abschalten
wri "\\b0 <"
return
:g // UPro fuer Zeichen >
Fett einschalten, es kommt (vielleicht) ein Inhalt
wri ">\\b "
return