gymel  >> Tools  >> Einzeiler

Perl-Einzeiler

Manchmal lohnt es sich nicht, ein Skript in einer Datei abzulegen, denn es wäre zu kompliziert, sich Namen und Ort der Datei zu merken...

Die folgenden Beispiele stammen aus der Praxis und sind daher fallweise für allegro-, MAB-, oder MARC-Daten. Letztendlich sind aber alle bibliothekarischen Daten Tabellen mit Zeilen- und Spaltentrennern (Satz- und Feldenden). Im Gegensatz zu Tabellendaten aus relationalen Datenbanksystemen bestimmt aber nicht die Spaltennummer eines Elements seine Bedeutung, sondern jedes Element beginnt mit einer Feldnummer, die je nach Format auch innerhalb eines Records mehrfach vorkommen kann. Felder können je nach Format auch durch Teilfelder substrukturiert sein, das folgt dann aber eigentlich demselben Schema.

Die Beispiele funktionieren unter Windows NT. Perl selber ist natürlich plattformunabhängig, im Wesen der Einzeiler liegt es aber, daß ihr Perl-Code in der Kommandozeile angegeben ist. Hier gibt es dann zwischen Kommandointerpreter und Perl einen Konflikt, was Anführungszeichen etc. angeht. Genauer: Perl bekommt nur das zu sehen, was der Interpreter übrig lässt. In der Praxis bedeutet dies, daß das eigentliche Skript hinter dem Schalter -e in Doppelanführungszeichen (") eingeschlossen sein muß, alle im Skript eigentlich gemeinten "einfachen" Doppelanführungszeichen müssen darum entweder mittels "\" escape'd oder als qq/.../ umformuliert werden. Dies macht leider gerade die "Einzeiler" kryptischer als sie eigentlich sind.

Unter U**X ist die Situation eher umgekehrt: "$" ist auch für die Shells ein aktives Zeichen, die hinter -e notierten Perl-Anweisungen sind daher typischerweise in einfache Anführungszeichen (') einzuschliessen, mit allen Konsequenzen für im Inneren dann auftauchende einfache Anführungszeichen...

^Top

Inhalt

lange Datensätze finden
.cLG- in .cLD-Dateien umwandeln
.cLD- in .cLG-Dateien umwandeln
hierarchische Datensätze zählen
"Aufbohr"-Byte in der .TBL-Datei setzen
Kleinschreibung von Dateinamen erzwingen
MAB-Band nach MAB-Diskette umwandeln
MAB-Sätze durchsuchen
Große Dateien splitten
MARC-Sätze durchsuchen
ISO-2709-Struktur (von MARC-Sätzen) auflösen
Länge in MAB-Sätzen eintragen
(Records mit) bestimmte(n) Felder(n) selektieren
.cLG-Dateien nach Inhalt einer Kategorie sortieren
Feldübersicht herstellen
Umrechnung Dezimal <-> Hexadezimal
Datei nach Liste von Feldinhalten ausgeben
Konkordanz Identnummer zu Zeilennummer aufbauen
Konkordanzen Identnummer zu Zeilennummer ausnutzen
Liste von Zeilen nach Zeilennummer selektieren
Liste von Zeilen nach Byteoffsets selektieren
Komplexes Beispiel

^Top

lange Datensätze finden

Aus blabla.alg sollen die Positionen aller Sätze ausgeworfen werden, die mehr als 1234 Zeichen haben:
perl -n -e "warn length if length > 1234" blabla.alg

^Top

.cLG- in .cLD-Dateien umwandeln

Aus blabla.alg soll foobar.ald werden (hinter dem ersten Zeichen müssen also vier Platzhalterzeichen für die Satznummer eingefügt werden):
perl -p -e "s/^\x01/\x01abcd/" blabla.alg > foobar.ald

^Top

.cLD- in .cLG-Dateien umwandeln

Aus foo_1.ald soll bar.alg werden (hinter dem ersten Zeichen muß also die vier Zeichen lange Satznummer eliminiert werden sowie die Füllzeichen am Ende des Datensatzes). Knifflig wird die Angelegenheit deshalb, weil die Satznummer rein binär ist und daher auch Zeilenvorschübe und vor allem Dateiendezeichen enthalten kann:
perl -p -e "BEGIN {binmode STDIN};
            $_ .= <> while length($_) < 8;
            substr($_, 1, 4) = '';
            s/^\x08/\x01/;
            s/\x00\xdb+\x00/\x00/;"
    < foo_1.ald > bar.alg

(Zeilenumbrüche und Spatien sind nur wegen der Lesbarkeit eingefügt, für den Aufruf auf der Kommandozeile sind die Zeilenumbrüche zu entfernen und ggfls. auch soviele Leerzeichen, daß die Kommandozeile nicht zu lang wird...)

Gelöschte Datensätze werden mitexportiert, ergänzen Sie bei Bedarf also noch

s/^\x09/\x01u1 @@@@@\x00/
oder
s/^\x09.*\n//;

^Top

hierarchische Datensätze zählen

Allegro-Grunddateien enthalten pro Zeile genau einen Datensatz, dieser beginnt mit dem Zeichen 1 (\x01). Um die Anzahl der Datensätze in einer solchen Datei zu zählen, können wir entweder die Anzahl der Zeilen der Datei ermitteln oder aber, wie häfig das Zeichen \x01 vorkommt. Hierarchische Untersätze stehen alle in der Zeile des zugehörigen Hauptsatzes, sie sind durch das Zeichen 2 (\x02) vom vorangehenden (Haupt- oder Unter-)Satz abgetrennt. Untersätze tieferer Hierarchiestufen folgen im Kontext der jeweils übergeordneten Stufe hinter Zeichen 3 bis 6 (\x03-x06). Kategorien kann man zählen, wenn man berücksichtigt, daß jede Kategorie von einem Zeichen 0 (\x00) abgeschlossen wird: Auch hier korrespondiert die Anzahl der Zeichen \x00 mit der Anzahl der Kategorien.

Folgendes zählt Zeilen, selbst wenn wir einen Modifier g an den regulären Ausdruck hängen würden (der tut in skalarem Kontext näich etwas anderes als zu zählen), das Resultat ist also die Anzahl der Datensätze mit hierarchischen Untersätzen, nicht die Anzahl der Untersätze:
perl -n -e "$i += /\x02/; END{print $i,' mehrbaendige'}" foo.alg
Um die Anzahl der Zeichen \x02 zu ermitteln, haben wir zwei Möglichkeiten: Entweder wir erzwingen Listen-Kontext und addieren die Läge der Liste, indem wir sie in ein anonymes Array verwandeln und das in skalaren Kontext zurückzwingen:
perl -n -e "$i += @{[/\x02/g]}; END{print $i,' Hauptbaende'}" foo.alg
Oder wir machen uns zunutze, daß Ersetzungsoperatoren in skalarem Kontext die Anzahl der durchgeführten Ersetzungen liefern. Das ist sogar effizienter als die obige Methode mit dem anonymen Array zum Zählen, allerdings wird die gelesene Zeile verändern, was zu bedenken ist, wenn man noch mehr damit machen möchte):
perl -n -e "$i += tr/\x00//d; END{print $i,' Kategorien'}" foo.alg
Folgendes zählt demnach alle "Einheiten" (Haupt- und Unteraufnahmen):
perl -n -e "$i += @{[/[\x01-\x06]/g]}; END{print $i,' Einheiten'}" foo.alg
bzw.
perl -n -e "$i += tr/\x01-\x06//d; END{print $i,' Einheiten'}" foo.alg

^Top

"Aufbohr"-Byte in der .TBL-Datei setzen

Manchmal bemerkt man erst nach dem Transport auf den Webserver, daß eine allegro-Datenbank mit alten Programmen erzeugt wurde, der Server jedoch das 2. Byte in der .TBL-Datei xyz.tbl als Aufbohrfaktor interpretiert, dies muß dann adhoc auf den Wert 3 gesetzt werden:

perl -e "open(TBL,'+<xyz.tbl')&&seek(TBL,1,0)&&print TBL \"\03\""
(Vorher unbedingt eine Sicherungskopie der xyz.tbl anlegen!)

^Top

Kleinschreibung von Dateinamen erzwingen

Der folgende Einzeiler ändert alle Dateinamen im aktuellen Verzeichnis in die kleingeschriebene Form um, sofern sie Großbuchstaben enthalten:

NT:
perl -e "foreach (glob qq(*)) {($t=$_)=~tr/A-Z/a-z/ && rename $_, $t}"
U**X:
perl -e 'foreach (glob qq(*)) {($t=$_)=~tr/A-Z/a-z/ && rename $_, $t}'
Das Verzeichnis darf keine Dateien mit Leerzeichen im Namen enthalten!

^Top

MAB-Band nach MAB-Diskette umwandeln

In MAB-Band sind Feld- und Satzenden über die Zeichen 0x1E (ASCII 30) und 0x1D (ASCII 29) realisiert, es hat sich allerdings eingebürgert, daß hinter dem Satzende noch ein (Unix-)Zeilenumbruch steht. Ungünstig ist vor allem, daß vor dem ersten Datenfeld kein Feldendezeichen steht, es ist also nur über seine Position ermittelbar, nicht durch einen Suchbefehl. Ansonsten ist dieses Format (pro Datensatz eine Zeile) sehr günstig für Filteraktionen mittels grep und anderen Schnellschüssen.

In MAB-Diskette hingegen ist der Header als Pseudofeld "###" realisiert, Feldende ist (DOS-)Zeilenumbruch, Satzende eine Leerzeile. Dies ist einerseits Editierfreundlicher, andererseits gibt es keine Probleme mehr mit dem Herauspflücken des ersten Feldes

Damit die Verarbeitung immer noch "zeilenweise" funktioniert, wird mit dem Schalter -o der Input Record Separator auf 0x1E (Okatal 035) gesetzt. Zeichenkonversion MAB-Band nach MAB-Diskette findet übrigens nicht statt.
perl -0035 -p -e "tr/\x1E\x1D\x0D\x0A/\n\n/d;
                  substr($_,24,0)=\"\n\";
                  substr($_,0,0)='### '"
     Datei

(Nach dem letzten Datensatz gibt es übrigens eine Fehlermeldung: Strenggenommen ist ja das Convenience-Zeilenbruchzeichen hinter dem MAB-Satzende des letzten Satzes bereits das erste Zeichen eines neuen Datensatzes).

^Top

MAB-Sätze durchsuchen

Wir suchen nach allen Sätzen in einer Datei, die Feld 025z (ZDB-Nummer) enthalten (das Beispiel funktioniert fuer alle Felder, ausser dem ersten Feld im Datensatz). Der einfachste Fall ist MAB-Band (MAB2): Die Daten sind zeilenweise organisiert:
perl -n -e "/\x1E025z/ and print" datei
bzw., wenn es sich um "reines" MAB ohne zusätzliche Zeilenumbrüche handelt müssen wir mittels des Schalters -0 (Null) gefolgt von einer Oktalzahl das Zeilenendezeichen einstellen:
perl -0035 -n -e "/\x1E025z/ and print" datei
oder, wenn es sich um Daten in MAB-Diskette handelt, wo der Zeilenumbruch der Feldtrenner ist und das Satzende durch eine Leerzeile angegeben wird ("00" ist ein spezieller Wert, der das Einlesen von Perl in den "Absatzmodus" einstellt):
perl -000 -n -e "/\n025z/ and print" datei
(Beachte jedoch die Bemerkung zur Verarbeitung von MAB-Disketten-Daten unter Unix)

Variante Suchbegriffe:

Kombinierte Suchbegriffe:
  1. Wenn die Reihenfolge nicht klar ist, ist es am besten, die zwei verschiedenen Begriffe durch einen einzelnen Regulären Ausdruck anzugeben:
    perl -0035 -n -e "/\x1E501 / and /\bSpiel\b/ and print" datei
    
    (Alle Sätze mit Fußnote, in denen irgendwo "Spiel" als Wort vorkommt).
  2. Man kann auch versuchen, mit einem Ausdruck eine komplexe Abfrage zu formulieren:
    perl -0035 -n -e "/x1E5[^\x1E]*http:.*\x1E65[345]/ and print" datei
    
    ("http:" kommt in einem Fußnotenfeld MAB 5xx vor und es gibt eine "elektronische" Fußnote MAB 653, 654 oder 655)

^Top

Große Dateien splitten

Oft möchte man eine große Datei mit Datensätzen in kleinere Portionen mit gleich vielen Zeilen zerlegen. Wenn die Daten zeilenweise organsiert sind (eine Zeile pro Datensatz, etwa MAB-Band), benutzt man traditionell das das Unix-Utiliy split für diese Aufgabe (es kann Dateien in Portionen mit gleich vielen Zeichen oder gleich vielen Zeilen zerlegen). Bestehen Datensätze jedoch aus mehreren, unterschiedlich vielen Zeilen, so wird split im Allgemeinen mitten in einem Datensatz eine neue Datei beginnen, was oft nicht erwünscht ist (außer man zerlegt die Datei nur zum Transport um sie hinterher wieder zusammenzusetzen, dann ist es natürlich egal).

Der folgende Einzeiler benutzt den Input Record Separator $/ von Perl, um die MAB-Diskette-Datei xyz.mab absatzweise einzulesen (ein Absatz ist eine Folge von Zeilen, die von einer Leerzeile abgeschlossen werden). Alle 1234 Zeilen wird hierbei eine neue Datei nach dem Muster xxx.000 ... xxx.nnn angefangen.

perl -000 -p -e "open (STDOUT, sprintf(qw(>xxx.%03u), $i++)) 
           unless ($.-1) % 1234 xyz.mab

Der Wert 00 für den Schalter -0 ist dabei eine Abkürzung für die Belegung von $/ mit dem leeren String, was wiederum die spezielle Bedeutung "Absatzweise einlesen" hat. Allgemeiner könnten wir also schreiben:

perl -p -e "BEGIN{$/=''};
       open (STDOUT, sprintf(qw(>xxx.%03u), $i++)) 
           unless ($.-1) % 1234 xyz.mab
Obiges Beispiel funktioniert so nur unter Win32, denn nur dort ist der (DOS-)Zeilenumbruch von MAB-Diskette-Daten (CRLF) identisch mit dem von Perl unterstellten Standard-Zeilenumbruch des Betriebssystems (als \n). Unter Unix sind die Eingangsdateien daher entweder mit u2d vorzubehandeln (dann sind sie natuerlich kein MAB mehr) oder aber die "Magie" von \n, die auch bei $/='' bzw. dem Schalter -000 wirkt, darf nicht genutzt werden, vielmehr müssen wir explizit angeben, welche Bytes unser Satztrenner sein sollen:
perl -p -e "BEGIN{$/=qq(\x0D\x0A)};
       open (STDOUT, sprintf(qw(>xxx.%03u), $i++)) 
           unless ($.-1) % 1234 xyz.mab
Je nach Beschaffenheit der Daten (vgl. nächster Einzeiler) sind in dieser Form auch andere Werte (und vor allem längere Zeichenketten) als Datensatztrenner einstellbar.

^Top

MARC-Sätze durchsuchen

[Nicht-Einzeiler] Es gibt ein Perl-Modul MARC.pm (http://marcpm.sourceforge.net/) für die Manipulation von MARC-Daten.

Zumindest im mir vorliegenden Fall waren die MARC-Sätze in etwa analog zu MAB-Diskette strukturiert: Pro Feld eine Zeile, Datensatzgrenzen daran erkennbar, daß jeweils eine mit "FMT" beginnende Zeile vorkam. Dieses Format ist für Menschen (und ihre Editoren) relativ gut lesbar, dafür ist das Filtern mit zeilenweise arbeitenden Werkzeugen nicht so einfach: Man möchte ja Records mit bestimmten Eigenschaften herausfiltern, und nicht nur das Feld, in dem diese Eigenschaft vorkommt. Bzw. man möchte -- etwa durch ein nachgeschaltetes grep -- die Identnummer von Records mit dieser Eigenschaft, die für die Selektion zu nutzende Eigenschaft steht aber in diesem Fall nicht in der Zeile mit der Identnummer, dort steht ja eben nur die Feldnummer der Identnummer und die Identnummer selber.

Realisiert wird die Suche mittels Umdefinition des Perl-Record-Separators. Im vorigen Beispiel war dieser mittels des -0-Schalters auf einen Oktalwert gesetzt worden, in diesem Beispiel weisen wir im BEGIN-Abschnitt der Variable $/ die Zeichenkette "<Zeilenumbruch>FMT" zu. Der Export ist ein wenig unsauber, weil "FMT" eigentlich der Start des Records ist. Daher müsste eigentlich "FMT" am Anfang ergänzt werden und ein evtl. folgendes "FMT" am Ende entfernt... Bis auf den ersten und den letzten Satz gleichen sich die Effekte aber aus, so daß also dem ersten Ergebnissatz das "FMT" fehlt (außer er war der erste der Input-Datei) und hinter dem letzten ein überzähliges "FMT" folgt (außer er war der letzte der Input-Datei).

Selektiert werden sollen übrigens alle Sätze aus foo.marc, die die Zeichenkette "nac--" enthalten:

perl -ne "BEGIN{$/=\"\nFMT\"} /nac--/ and print" foo.marc
und dies wäre wiederum kombinierbar mit
| grep "^SYS"
um nur die SYS-Nummern der Records zu erhalten.

Im Beispiel war "nac--" sehr signifikant. Die folgende Verfeinerung sucht "Max" nur im Feld 100:

perl -ne "BEGIN{$/=\"\nFMT\"} /\n100.*Max/ and print" foo.marc
Hinweis: ".*" trifft von sich aus alle Zeichen außer dem Zeilenumbruch!

Folgendes Beispiel sucht Records mit Feld 245, die nicht Feld 100 haben:

perl -ne "BEGIN{$/=\"\nFMT\"} /\n245/ and ! /\n100/ and print" foo.marc

Angenommen, wir haben kein grep zur Verfügung und wollen aus Perl heraus statt des kompletten Records nur den Inhalt des Feldes SYS exportieren:

perl -ne "BEGIN{$/=\"\nFMT\"} /\n100.*Max/ and /\nSYS\s*(.+)/ and print \"$1\n\"" foo.marc
Der zweite Reguläre Ausdruck /\nSYS\s*(.+)/ weist alles aus der (ersten) mit "SYS" und einer unbestimmten Zahl von Leerzeichen beginnenden Zeile (".*" trifft von sich aus alle Zeichen außer dem Zeilenumbruch!) der Sondervariablen $1 zu, die dann anschließend von einem Zeilenumbruch gefolgt ausgegeben wird.

^Top

Länge in MAB-Sätzen eintragen

MAB-Datensätze haben einen Header von 24 Bytes, die ersten fünf Bytes davon enthalten die Länge des Datensatzes (inklusive Header). Das Nicht-Standard-Zeilenbruchzeichen zwischen den Datensätzen wird dabei traditionell nicht mitgezählt. Aufgrund von Zeichenumsetzungen oder als Nachbearbeitung eines Rohexports mag es nötig sein, diese Läge zu ermitteln und an der richtigen Stelle einzusetzen:

perl -p -e "substr($_, 0, 5) = sprintf('%05u', length($_) -1)" foo.mab
(Die Korrektur -1 ist für den Zeilenumbruch).

In Datensätzen im Format MAB-Diskette scheint folkloristischerweise stets eine Datensatznummer statt der Satzlänge auf diesen Positionen zu stehen, daher weiß ich nicht, ob die Einleitung "### " mitzuzählen ist, ob die Zeilenumbrüche zwischen den Feldern ein- oder zweifach zählen und ob der Durchschuss zwischen den Datensätzen nicht, einfach, zweifach oder vierfach zählen sollte...

MARC-ISO2709-Directory auflösen

MARC21-Daten liegen im Original in ISO-2709-Directory-Struktur vor, d.h. die Feldnummern sind am Satzanfang zusammengefasst, jeweils mit Angabe der Feldlänge und dem Offset innerhalb des Datensatzes, an dem sich der Feldinhalt befindet. Dies ist für Selektionen nicht sehr praktisch. Der folgende Einzeiler wandelt daher solche Daten in zeilenweise Daten mit vorangestellter Feldnummer um, entsprechend dem "Tagged View":

perl -0035 -ne "($_,$c)=split(qq(\x1E),$_,2);
               print '###'.substr($_,0,24,'').qq(\n);
               while (substr($_,0,12,'')=~/(\d{3})(\d{4})(\d{5})/) {
                   print $1.substr($c,$3,$2-1).qq(\n)}
               print qq(\n)" usmarc.001

^Top

(Records mit) bestimmte(n) Felder(n) selektieren

MAB-Datensätze, die Feld 100ff (Person) belegt haben, sollen selektiert werden (in den Beispielen werden nur die ersten vier Personen berücksichtigt):

perl -ne "print if /\x1E1(00|04|08|12)/" bla.mab

Falls die Sätze nur gezählt werden sollen (aber warum nicht nach wc pipen?):

perl -ne "$i++ if /\x1E1(00|04|08|12)/; END{print $i}" bla.mab

Jetzt soll die Anzahl der Felder ermittelt werden (036 ist die oktale Notation von \x1E, der Schalter -0 setzt den Record-Separator):

perl -0036 -ne "$i++ if /^1(00|04|08|12)/; END{print $i}" bla.mab

^Top

.cLG-Dateien nach Inhalt einer Kategorie sortieren

Der folgende Einzeiler benutzt die beliebte »Schwartzian transform«.

Sortierbegriff sei der Inhalt von #kkf bzw. "zzz", falls die Kategorie nicht vorhanden ist. Groß- und Kleinschreibung zählt, Umlaute werden nicht einsortiert!

perl -e "@a = <>; 
         print map {$_->[0]} 
               sort {$a->[1] cmp $b->[1]} 
               map {[$_, /[\x01\x00]kkf([^\x00]+)/ ? $1 : "zzz"]}
               @a;"
      blabla.alg

(Zeilenumbrüche und Spatien sind nur wegen der Lesbarkeit eingefügt, für den Aufruf auf der Kommandozeile sind die Zeilenumbrüche zu entfernen und ggfls. auch soviele Leerzeichen, daß die Kommandozeile nicht zu lang wird...)

Tip: machen Sie aus obigem Einzeiler eine .bat-Datei (etwa psort.bat) und ersetzen darin "kkf" durch "%1" und "blabla.alg" durch "%2" und hängen am besten noch " > %3" an:
perl -e "@a = <>; print map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, /[\x01\x00]%1([^\x00]+)/ ? $1 : "zzz"]} @a;" %2 > %3
Dann können Sie durch einen Aufruf

psort 81h otto.alg otto.out
die Datei otto.alg sortiert nach Kategorie #81h in die Datei otto.out überführen. Hat die Kategorie keinen Folgebuchstaben, etwa #91, setzen Sie für das Spatium "\s" in den Suchbegriff:
psort 81\s otto.alg otto.out

^Top

Feldübersicht herstellen

In einer Datei test.kat stehen Daten mit durch vierstellige Feldnummern eingeleiteten Datenfeldern. Der folgende Einzeiler gibt eine nach Feldnummern sortierte Ausgabe der Häufigkeiten (also insbesondere der vorkommenden Felder) in der Datei test.kat:

perl -n 
     -e "/^(\d{4})/ and ($fld{$1}++);"
     -e "END {
          foreach (sort keys %fld) {
            print \"\#$_: $fld{$_}\n\"}
        }"
     test.kat

Erläterung: Die beiden -e-Argumente lassen sich auch zusammenfassen. Das erste wird (wegen des Schalters -n) für jede Zeile einmal durchlaufen und zählt die Felder im Hash %fld. Beim Beenden des Programms wird dann der Block END {...} abgearbeitet, der Schlüssel (Feldnummern) und Werte (Häufigkeiten) des Hashs sortiert ausgibt.

Das ganze etwas unübersichtlicher:
perl -ne "/^(\d{4})/ and ($fld{$1}++);END{foreach (sort keys %fld) {print \"$_: $fld{$_}\n\"}}" test.kat

^Top

Umrechnung Dezimal <-> Hexadezimal

Hexadezimalzahlen 0xnnn sollen in ihr dezimales Äquivalent nnn umgerechnet werden oder umgekehrt. Dies soll daran erkannt werden, daß die Eingabe mit 0x beginnt oder nicht.

perl -n 
     -e "/(0x)?(.*)/;
         print $1 ? hex($2).qq(\n)
                  : sprintf(\"0x%x\n\", $2);
        " 
Erläuterung: Das ganze zu einer Zeile zusammengefasst:
perl -ne "/(0x)?(.*)/ and print $1?hex($2).qq(\n):sprintf(\"0x%x\n\",$2)"
Falls Sie diese Zeile in eine .BAT-Datei übernehmen, müssen Sie darauf achten, daß das Prozentzeichen in %x durch die Schreibung %%x geschützt wird.

^Top

Datei nach Liste von Feldinhalten ausgeben

Wir haben zwei Dateien mit zeilenweise organisierten Records, diejenigen der einen Datei enthalten im Sinne einer n:1-Relation Pointer auf Records in der anderen.

In einem konkreten Beispiel haben wir zwei Dateien im MAB-Format, titel.mab mit Titeldaten und lokal.mab mit Lokaldaten. Alle Datensätze haben ihre Identnummer in Feld 001, die Lokalsätze enthalten im Feld 012 die Identnummer des zugehörigen Titelsatzes.

Die Dateien seien im MAB2-Bandformat, d.h. Feldtrenner ist also Hex 1E ("\0x1E", ASCII 30), Satzende ist theoretisch Hex 1D ("\0x1D", ASCII 29), wir nutzen aber aus, daß MAB2-Dateien normalerweise nach jedem Satzende noch einen (Unix-)Zeilenumbruch Hex 0A enthalten, den Perl als Zeilenumbruch erkennt (Macintosh nicht getestet).

Zunächst einmal der Fall, daß wir ein Selektionskriterium für Titelätze haben und die zugehörigen Lokalsätze "nachziehen" wollen:

1. Bestimmen der Titelidentnummern
Wir durchsuchen die Datei und geben bei Treffern die jeweilige Identnummer aus:
perl -n -e "/String/ and (/001 (\w+)\x1E/, print qq($1\n))" 
           titel.mab > titid.txt

Wir haben nun also eine Datei titid.txt die pro Zeile eine Identnummer aus der Titeldatei enthält

2. Selektieren der zugehörigen Lokalsätze
Im BEGIN-Block des Skripts bauen wir aus titid.txt einen Hash auf, der die Treffer enthält, in der impliziten Schleife testen wir dann jeden Datensatz aus lokal.mab daraufhin ab, ob sein zugehöriger Titelsatz ein Treffer war und geben ihn in diesem Fall aus.
perl -n -e "BEGIN{open(HIT, \"titid.txt\");
                  local $/;
                  %treff=map{($_,1)} split(/\W/,<HIT>)}"
        -e "print if /\x1E012 (\w+)\x1E/ and $treff{$1}"
           lokal.mab > lokhit.mab

Technische Bemerkung: Es wird also die gesamte Trefferliste in den Speicher geladen ($/ ist durch das Lokalisieren gleichzeitig auf undef gesetzt, dadurch liefert die Leseoperation <HIT> die gesamte Datei).

Alle Lokalsätze, die mit den in titid.txt hinterlegten Identnummern unserer Selektion verknüpft sind, befinden sich nun in lokhit.mab.

Der umgekehrte Weg: Unser "Trefferkriterium" gilt für Lokalätze und wir brauchen die zugehörigen Titelsätze:

1. Bestimmen der Titelidentnummern
Diesmal suchen wir im Lokalsatz und werfen den Inhalt von MAB 012 ("Verknüpfungsfeld") aus:
perl -n -e "/String/ and (/012 (\w+)\x1E/, print qq($1\n))" 
           lokal.mab > titid.txt

Wir haben wieder eine Datei titid.txt mit Titelidentnummern erhalten, wegen der Richtung der Relation sind diese nicht mehr unbedingt eindeutig, das stört uns aber nicht.

2. Selektion der zugehörigen Titelsätze
Im Vergleich mit dem vorigen Schritt 2 erfolgt der Test nur anhand MAB 001 (und in einer anderen Datei):
perl -n -e "BEGIN{open(HIT, \"titid.txt)\";
                  local $/;
                  %treff=map{($_,1)} split(/\W/,<HIT>)
                 }"
        -e "print if /\x1E001 (\w+)\x1E/ and exists $treff{$1}"
           titel.mab > tithit.mab

Alle Titelsätze, mit denen die im ersten Schritt in getroffenen Lokalsätze verknüpft sind, befinden sich nun in tithit.mab.

Beide Selektionen brauchen jeweils einen vollen Durchlauf sowohl durch die Titel- als auch durch die Lokaldatei. Das Einlesen des Zwischenergebnisses in den jeweiligen Schritten 2 ist natürlich vom Aufwand etwa proportional zur Anzahl der Treffer, diese Datei ist aber vergleichsweise klein, weil sie nur die Identnummern (und Zeilenvorschuübe) umfasst.

Die folgenden Einzeiler versuchen, dieses Laufzeitverhalten durch Indizierung der Daten noch zu verbessern

^Top

Konkordanz Identnummer zu Zeilennummer aufbauen

Eine MAB-Datei enthält pro Zeile einen Datensatz, Feld nnn enthält eine (innerhalb dieser Datei) eindeutige Identnummer. Wir wollen eine Tabelle aufbauen, die in einer Spalte die Identnummer, in einer anderen Spalte die Zeilennummer enthält, damit wir später durch Sortierung einen effizienteren Zugriff durch vorsortierte Begriffe haben (wenn wir aus einer Datei mit 10.000 Saetzen später 10 Zeilen selektieren wollen, ist es geschickt, dies mit einer einzigen Volltextsuche, naemlich der nach aufsteigend geordneten Zeilennummern, auszuführen).
perl -n -e "/\x1Ennn (\w+)\x1E/ and print qq($1\t$.\n)" 
           bigfile.mab
Eine Variante ist nötig, weil typischerweise nnn das Feld MAB 001 ist, welches fast immer unmittelbar auf den Header folgt und daher das Suchmuster /\x1E001 .../ nicht funkioniert. In diesem Fall dann also (mit Vorsicht):
perl -n -e "/001 (\w+)\x1E/ and print qq($1\t$.\n)" bigfile.mab
Noch eine Variante ist, statt der Zeilennummern die Byteoffsets relativ zum Anfang der Datei zu vermerken, so daß wir später mittels seek() direkt auf die Datensätze zugreifen können. Dies ist umso effizienter, je grösser die Eingangsdatei ist und je weniger Sätze wir tatsächlich selektieren werden (etwa via CGI-Skript einen Satz aus einer gigantischen Datei, wo vor dem Ende einer Volltextsuche bereits ein Timeout stattfinden würde).
perl -n -e "BEGIN{$pos=0} /\x1Ennn (\w+)\x1E/ and print qq($1\t$pos\n);$pos=tell" monster.mab
Die Angelegenheit ist wegen ihrer Einzeilerhaftigkeit unnötig kompliziert: Durch das implizite Lesen (Schalter "-n") liefert uns tell() immer nur die Position am Ende der Zeile, die wir dann mittels $pos für den nächsten Durchlauf retten müssen.

Unter Windows haben wir ein anderes Problem: Die implizite Schleife bewirkt, daß die typischerweise vorliegenden Unix-Zeilenenden wie DOS-Zeilenenden interpretiert werden (gut) aber daher beim tell() auch immer als 2 Bytes zählen (schlecht). In diesem Fall müssen wir leider ganz anders vorgehen, nämlich auf die implizite Schleife verzichten:

perl -e "BEGIN{$pos=0;
               open(MAB, pop @ARGV);
               binmode MAB}"
     -e "while(<MAB>){
                   /\x1Ennn (\w+)\x1E/ and print qq($1\t$pos\n);
                   $pos=tell}"
        monster.mab
(hierfür braucht es dann cmd.exe, damit die Aufrufzeile nicht zu lang ist.)

^Top

Konkordanzen Identnummer zu Zeilennummer ausnutzen

Angenommen, wir haben eine Datei index.tab mit einer Konkordanztabelle der Form
Identnummer <tab> Zeilennummer
oder
Identnummer <tab> Byteoffset
und eine weitere Datei idnums.txt mit Identnummern. Folgender Einzeiler liefert uns die zu den Identnummern korrespondierenden Zeilennummern bzw. Byteoffsets:
perl -p -e "BEGIN{open(KNK, \"index.tab\");
                  local $/;
                  %knk=split(/\W/,<KNK>)}"
        -e "s/^(\w+)/$knk{$1}/e"
     idnums.txt
(Achten Sie wie immer darauf, "%knk" als "%%knk" zu schreiben, wenn diese Zeile in einer DOS-Stapeldatei steht...).

Bemerkung: Wie im 2. Schritt des obigen Beispiels einer Selektion über zwei Dateien wird im BEGIN-Block ein Hash aufgebaut, dessen Schlüssel sind wiederum die Identnummern, die Werte sind allerdings nicht mehr "1" wie Treffer sondern die jeweiligen Zeilennummern bzw. Offsets.

Sollte die Konkordanztabelle selbst mehrere Megabytes groß sein, so kann auch dieses Laden zu einem Performanceproblem führen. Sind die nachzuschlagenden Identnummern wenige im Vergleich zur Gesamtmenge, ist eine irgendwie geartete Suche in der Konkordanztabelle angemessener, ohne sie komplett einzulesen.

^Top

Liste von Zeilen nach Zeilennummer selektieren

Wir haben eine Datei numbers.txt mit aufsteigend geordneten Zeilennummern und eine andere Datei bigfile.mab, aus der wir die in der ersten Datei enthaltenen Zeilen selektieren möchten.
perl -n -e "BEGIN{open(NUM, \"numbers.txt\");
                  @n = <NUM>
                 }"
        -e "if ($. == $n[0]) {print;
                              shift @n;
                              exit unless @n
                             }"
        bigfile.mab

^Top

Liste von Zeilen nach Byteoffsets selektieren

Wir haben eine Datei offsets.txt mit aufsteigend geordneten Byteoffsets und eine andere Datei bigfile.mab, aus der wir die durch die in der ersten Datei spezifizierten Zeilen selektieren möchten.
perl -n -e "BEGIN{open(MAB, \"bigfile.mab\")}"
        -e "seek(MAB,$_,0);
            $_=<MAB>
            print"
        offsets.txt

^Top

Komplexes Beispiel:

Die Ausgangslage ist wie bei der einfachen Selektion weiter oben: Aus zwei zusammengehörenden Dateien (Titel- und Lokalsätze, wobei die Lokalsätze Links auf die Titel enthalten) ist ein Paar von "Auszugsdateien" zu erstellen: Eine Teildatei der Lokaldatei entsprechend einem Suchkriterium und die Teildatei der Titeldatei, die von der Teil-Lokaldatei "getroffen" wird.

Konkreter: titel.mab und lokal.mab enthalten die Titel- rsp. Lokalsätze. Der "Teilbestand", den wir extrahieren wollen ist definiert durch "Selektiere alle Lokalsätze zu einem bestimmten Sigel und die zugehörigen Titelsätze".

Feldtrenner ist Hex 1E ("\0x1E", ASCII 30), Satzende ist theoretisch Hex 1D ("\0x1D", ASCII 29), wir nutzen aber aus, daß MAB2-Dateien normalerweise hinter dem Satzende noch einen (Unix-)Zeilenumbruch Hex 0A enthalten, den Perl als Zeilenumbruch erkennt (Macintosh nicht getestet).

Wir gehen dazu in mehreren Schritten vor und benutzten dafür vier Einzeiler, falls wir grep und ein genügend mächtiges Sortierprogramm haben, sonst sieben Einzeiler:

0. Erstellung einer Indizierung der Titeldatei
Wir nutzen dafür die Byte-Offset-Variante des Indexierungs-Einzeilers von oben:
perl -n -e "BEGIN{$pos=0} /\x1E001 (\w+)\x1E/ and print qq($1\t$pos\n);$pos=tell"
     titel.mab > titind.tab
bzw. die Variante, die berücksichtigt, daß das Identnummernfeld MAB 001 stets das erste hinter den Headern ist und daß wir eine MAB-Datei mit (den typischen, eingeschobenen) Unix-Zeilenumbrüchen auf einer Maschine mit DOS-Zeilenumbrüchen verarbeiten:
perl -e "BEGIN{$pos=0;open(MAB, pop @ARGV);binmode MAB}"
     -e "while(){/001 (\w+)\x1E/ and print qq($1\t$pos\n);$pos=tell}"
     titel.mab > titind.tab

Wir haben jetzt also in titind.tab eine Tabelle mit zwei Spalten: Identnummer und Byteoffset des zur Identummer gehörenden Satzes in titel.mab.

1. Selektion der Lokaldaten
Wir wollen ja auch eine Datei sellok.mab mit den selektierten Lokaldaten haben:
grep "Sigel" lokal.mab > sellok.mab
Oder, wenn kein grep vorhanden:
perl -n -e "/Sigel/ and print" lokal.mab > sellok.mab

Achten Sie darauf, Sonderzeichen im konkreten Sigel korrekt zu escapen, also etwa -e "/Kn 41\/41/ ...!

2. Selektion der benötigten Titelidentnummern
Lokalsätze enthalten in MAB 012 die Identnummer des zugehörigen Titelsatzes, diese müssen wir jetzt extrahieren und in eine Datei wegschreiben:
perl -n -e "/\x1E012 (\w+)\x1E/ and print \"$1\n\"" sellok.mab > titids.tmp

Die Ausgabedatei titids.tmp enthält jetzt auf jeder Zeile die Identnummer eines Datensatzes aus der Titeldatei. Es ist allerdings so, daß eine Datei mit Lokaldaten typischerweise mehrere Lokalsätze zu einem Titel enthält, wir können also davon ausgehen, daß Identnummern in titids.tmp mehrfach vorkommen.

3. Reduktion von Identnummerndubletten
Dies sollte man eigentlich durch eine Sortierung und anschliessende Eliminierung dubletter Zeilen bewerkstelligen, wie es etwa GNU sort kann:
sort titids.tmp > titsort.tmp
sort -m -u titsort.tmp > titids.txt
Hat man kein geeignetes Sortierprogramm, aber etwas Hauptspeicher, kann man es auch mit Perl machen:
perl -n -e "$merk{$_}=1" -e "END{print keys %merk}" titids.tmp > titids.txt

Zum Verständnis: Das Zeilenende hinter den Identnummern wird nicht eliminiert, ist also Bestandteil der Zugriffsschlüssel im Hash %merk, und wird daher mit ausgegeben. Daher hat die (nicht sortierte!) Ausgabedatei titids.txt auch wieder Zeilen mit jeweils einer Identnummer.

4. Bestimmen der Byteoffsets über die Konkordanz
Mittels des obigen Einzeilers zum Ersetzen einer Liste entsprechend Konkordanz erzeugen wir aus titids.txt und der im Schritt 0 erzeugten Konkordanzdatei titind.tab eine Datei titoffs.txt mit den Byteoffsets der gewüschten Datensätze in der Titeldatei:
perl -p -e "BEGIN{open(KNK, \"titind.tab)\";local $/;%knk=split(/\W/,)}"
        -e "s/^(\w+)/$knk{$1}/e" 
        titids.txt > titoffs.txt
5. Sortieren der Byteoffsets
Wir wollen gewärleisten, dass die Ergebnisdatei der Titelsätze die Sätze genau in der Reihenfolge enthält, wie sie in der Originaldatei vorkommen, auch ist die Hoffnung, daß die Selektion im folgenden Schritt dann vom Betriebssystem optimiert werden kann. Gnu sort kann numerisch sortieren:
sort -n titoffs.txt > titoffs.sor
Mit Perl können wir das Verhalten emulieren:
perl -e "$/=undef; @list = split(/\n/, <ARGV>); print join(\"\n\", sort {$a <=> $b} @list)"
        titoffs.txt ;> titoffs.sor

Wir haben nun also in titoffs.sor eine aufsteigend sortierte Liste der Anfangspositionen der von uns benötigten Zeilen (= Sätze) aus titel.mab.

6. Extrahieren der Titelsätze
Dieser abschließende Schritt besteht aus der Anwendung des Einzeilers Zeilen nach Byteoffset selektieren:
perl -n -e "BEGIN{open(MAB, \"titel.mab\")}"
        -e "seek(MAB,$_,0);$_ = ;print"
        titoffs.sor > seltit.mab

Damit haben wir also nun die aus lokal.mab selektierten Sätze in sellok.mab (Schritt 1) und die dazu gehörenden Titelsätze in seltit.mab

.

Nimmt man mehrere Selektionen vor, muß Schritt 0, also der Aufbau eines Index zur Titeldatei, nur einmal durchgeführt werden. Die Volltextsuche zur Selektion der Lokalsätze erfolgt aber jedesmal, das kann sehr ineffizient werden, wenn man viele Selektionen vornimmt...

Diskussion: Wir haben nun sieben Schritte für etwas gebraucht, was weiter oben (Daten aus zwei Tabellen selektieren bereits in zwei Schritten realisiert wurde. Warum?

Weitere Optimierungen (die dann zu noch mehr Schritten führen) gehen dann in die Richtung, daß man die Lokaldatei nach den möglichen Suchbegriffen indiziert (dafür braucht man dann auch wieder eine Konkordanztabelle mit Zeilennummern oder Byteoffsets) und daß man Verfahren findet, die das Einlesen der gesamten Konkordanztabellen ersparen, indem man diese sortiert und mit festen Zeilenlängen versieht, was binäres Suchen ermöglicht, oder man muß sie als Binärbaum auf der Platte ablegen. Mittels Tied Hashes kann man viel der unterliegenden Komplexität verbergen, so daß die Lösungen immer noch Einzeiler sind.

Alle Anstrengungen in diese Richtung dienen aber immer einer Verlagerung des Rechenaufwands in die Arbeitsvorbereitung, der "Quantensprung" des einfachen Beispiels, mit zwei Volltextsuchen auszukommen, egal wieviele Resultate man für seine Suchanfrage hat, wird durch das komplexe Beispiel erst dann wieder erreicht, wenn man viele verschiedene Suchen nacheinander durchführt (und Schritt 0 nicht mehr ins Gewicht fällt). Wenn diese Suchanfragen allerdings alle gleichzeitig bekannt sind, kann man möglicherweise die Kaprizierung auf Einzeiler verlassen und Teile der Schritte 1 bis 6 auch simultan in einem Perlskript durchführen: Schritt 1 empfiehlt sich unbedingt, damit die große Lokaldatei nur einmal durchsucht werden muß. Ein noch besserer Kandidat für optimierende Zusammenfassungen ist Schritt 4, wo das Einlesen der umfangreiche Konkordanztabelle dann nur einmal und simultan für alle Recherchen passieren braucht.

Mit trickreichen Codierungen wird man auch mit den Einzeiler-Lösungen viele Suchen simultan durchführen, es kommt dann nur noch als 7. und 8. Schritt ein Auffächern der Ergebnissdateien aus 1. und 6. hinzu.


Letzte Aktualisierung: 30.06.2001
submit more here