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...
perl -n -e "warn length if length > 1234" blabla.alg
perl -p -e "s/^\x01/\x01abcd/" blabla.alg > foobar.ald
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//;
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
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!)
Der folgende Einzeiler ändert alle Dateinamen im aktuellen Verzeichnis in die kleingeschriebene Form um, sofern sie Großbuchstaben enthalten:
perl -e "foreach (glob qq(*)) {($t=$_)=~tr/A-Z/a-z/ && rename $_, $t}"
perl -e 'foreach (glob qq(*)) {($t=$_)=~tr/A-Z/a-z/ && rename $_, $t}'
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).
perl -n -e "/\x1E025z/ and print" dateibzw., 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" dateioder, 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:
perl -0035 -n -e "/\x1E501 / and /\bSpiel\b/ and print" datei(Alle Sätze mit Fußnote, in denen irgendwo "Spiel" als Wort vorkommt).
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)
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.
[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.
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...
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
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
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.outdie 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
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
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:
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.
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:
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
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:
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.
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
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.mabNoch 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
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.)
Identnummer <tab> Zeilennummeroder
Identnummer <tab> Byteoffsetund 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.
perl -n -e "BEGIN{open(NUM, \"numbers.txt\");
@n = <NUM>
}"
-e "if ($. == $n[0]) {print;
shift @n;
exit unless @n
}"
bigfile.mab
perl -n -e "BEGIN{open(MAB, \"bigfile.mab\")}"
-e "seek(MAB,$_,0);
$_=<MAB>
print"
offsets.txt
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:
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.
grep "Sigel" lokal.mab > sellok.mabOder, 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/ ...!
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.
sort titids.tmp > titsort.tmp sort -m -u titsort.tmp > titids.txtHat 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.
perl -p -e "BEGIN{open(KNK, \"titind.tab)\";local $/;%knk=split(/\W/,)}"
-e "s/^(\w+)/$knk{$1}/e"
titids.txt > titoffs.txt
sort -n titoffs.txt > titoffs.sorMit 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.
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?
perl -e "$/=undef;
%list = map{($_,1)} split(/\n/, <ARGV>);
print join(\"\n\", sort {$a <=> $b} keys %list)"
titoffs2.txt ;> titoffs.sor
Weil wir aber die Konkordanz Identnummern zu seek'ablen Offsets
benutzen, kommt definitiv ein Schritt hinzu, und weil wir die
Reihenfolge der Titeldatei "extern" (durch den Sortierschritt
5.) herstellen müssen (und sie uns nicht dadurch geschenkt wird,
daß wir das Original von Anfang bis Ende durchsuchen), ein
weiterer.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.