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