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