Manche Zeitschriftenartikel gibt es einfach nicht Online. Das heutige Perlskript archiviert sie im PDF-Format und nutzt eine Datenbank für spätere Zugriffe.
Ob ein gelungenes 100-Fragen-Interview im Magazin der Süddeutschen Zeitung steht oder eine Max-Goldt-Kolumne in der Titanic: Beide liest man immer wieder gerne und möchte sie vielleicht archivieren. Leider kommen diese Blätter und Autoren altmodisch daher und haben noch nicht den Sprung ins Internet geschafft. Das geht auf Kosten Vieler, denn gedruckte Zeitschriften nehmen unnötig Platz weg. Kellerräume müssen angemietet, knallrote Ordner aus bretthartem Karton gekauft werden. Dabei interessiert doch nur hier und da ein Artikelchen, ganze Zeitschriften liest nach zwei Jahren kein Mensch mehr.
Statt dessen wäre es viel praktischer, die interessanten Passagen
mit einem Scanner zu digitalisieren und als PDF-Datei
auf der Festplatte abzuspeichern. Damit der Archivar in der
so ständig wachsenden Bibliothek nicht den Überblick verliert,
verwaltet das heute vorgestellte Perlskript magsafe
eine
Datenbank mit den eingescannten Druckerzeugnissen.
Zwei, drei Seiten sind schnell eingescannt, das hervorragende
GUI-Programm xsane
aus dem Sane
-Projekt arbeitet über
den Sane-Backend mit allen bekannten Scannern zusammen. Sowohl
der Epson-Fotoscanner als auch der HP-All-In-One-Officejet
aus dem Perlmeister-Testlabor
ticken problemlos unter Linux. Die Einzelseiten werden gescannt
und als Bilder im PNG-Format abgelegt. Üblicherweise reichen 200 dpi,
damit der Text lesbar bleibt und ein Drucker später eventuell
gerade noch akzeptable Qualität produziert.
Mehrere Seiten zu einem PDF-Dokument zusammenzufassen, das
erledigt die convert
Utility nach einem aus [2] geklauten Trick:
convert -density 200 -quality 95 \ -resize "1600x1600>" *.png archive.pdf
Der Aufruf sammelt alle im gegenwärtigen Verzeichnis liegenden
*.png
-Dateien ein, begrenzt sowohl Höhe als auch Breite auf 1600
Pixels. Kleinere Bilder bleiben wegen des >
-Zeichens unverändert.
convert
bündelt die Einzelseiten zu einer mehrseitigen PDF-Datei mit
200 dpi Auflösung zusammen. Die PNGs werden in PDF zu JPG komprimiert,
mit der angegebenen 95%igen Qualität.
Abbildung 1: Der Titanic-Artikel wird mit xsane digitalisiert. |
Liegt der zweiseitige Artikel von Max Goldt dann in goldt.pdf
, verfrachtet
ihn folgender Aufruf ins Archiv:
magsafe -m Titanic -a "Max Goldt" -t "Tropfen, Klingeln und die üble Weiterleiterei" -i 2005/03 -p 44 -d goldt.pdf
Dies legt einen Datensatz mit dem Titel der Zeitschrift (``Titanic''), einem Dokumententitel (``Tropfen, Klingeln und ...''), der Ausgabe (``2005/03'') und der Anfangsseite (44) an.
Die Daten liegen in einer waschechten Datenbank mit
SQL-Abfragemöglichkeit. Der Datenbankmotor SQLite
kam im Snapshot
schon öfter zum Einsatz, denn er ist einfach unschlagbar einfach zu
installieren. Wie der Abschnitt ``Installation'' zeigen wird, sind
lediglich ein paar CPAN-Module zu laden, und das Skript
erledigt den Rest. Kein Dämon ist zu starten, keine Datebank oder
Tabellen zu definieren, das Skript erledigt alles selbst.
Das Skript kopiert das PDF-Dokument goldt.pdf
nicht in eine Datenbank, sondern in ein Verzeichnis, in
dem alle Dokumente unter durchnumerierten Dateinamen (000001, 000002, ...)
stehen. War die Datenbank bislang leer, hat der Befehl oben ein neues
Dokument ``000001'' erzeugt, das die PDF-Datei goldt.pdf
enthält.
Als zweiter Datensatz wandert ein 100-Fragen-Interview von Moritz von Uslar mit Ralph Lauren aus einem SZ-Magazin des Jahrgangs 2004 ins Archiv:
magsafe -m "SZ Magazin" -t "100 Fragen an Ralph Lauren" -i 2004/37 -p 56 -d lauren.pdf
Wie man sieht, kann der Autor wegfallen, das kommt manchmal bei Artikeln
in Tageszeitungen vor. Falls magsafe
ohne Parameter aufgerufen wird,
fragt es den Benutzer interaktiv nach den Angaben:
$ magsafe [1] New [2] Titanic [3] SZ Magazin Magazine [1]>
Da vorher schon zwei Zeitschriften eingegeben wurden, hat magsafe
sie sich gemerkt und gibt sie in einem numerierten Menü vor. Für eine
neue Zeitschrift wird einfach 1
gewählt und der Name der Publikation
eingegeben:
Magazine [1]> 1 Enter Magazine Name []> Der Spiegel Document []> ...
Das mit der Option -d
(oder oben interaktiv) angegebene PDF-Dokument
kopiert magsafe
in ein Verzeichnis, das intern fest konfiguriert ist. Dabei
numeriert es die einzelnen Dokumente der Reihe nach durch, legt sie
als 000001, 000002, und so weiter im Dokumentenverzeichnis ab und
referenziert die Pfade zu diesen Dateien in der Datenbank.
Eine Datenbank hat natürlich den Vorteil, dass man nach Herzenslust
darin herumsuchen kann. Bei der angelegten Zeitschriftendatenbank, die
in der Datei scanned_docs.dat
liegt, geht
das mit dem Kommandozeilenwerkzeug sqlite3
und dem guten alten SQL:
$ sqlite3 scanned_docs.dat sqlite> SELECT * from doc where title like '%Ralph%' 2|100 Fragen an Ralph Lauren||2|56|2004/37 CTRL-D $
Freilich ist das etwas umständlich, deswegen bietet magsafe
eine
vereinfachte Abfragesprache an: Wird es mit dem Parameter -s
aufgerufen,
erwartet es einen Suchstring im Format
"feld:pattern feld:pattern ..."
Alle Artikel, deren Titel das Wort Ralph
enthalten, fördert
magsafe -s title:Ralph
zutage. magsafe
formt daraus hinter den Kulissen eine SQL-Abfrage
ähnlich der oben gezeigten, nach dem es den Suchbegriff in Prozentzeichen
eingepackt hat. Wer sich hingegen für alle Artikel interessiert, die
im Jahre 2005 erschienen sind, also im Feld issue
den String ``2005''
enthalten, gibt
t -s issue:2005
ein. Wenn das zuviele Treffer anzeigt, beschränkt eine zusätzliche
mag:
-Klausel die Abfrage auf alle Artikel aus der Titanic:
t -s "issue:2005 mag:Titanic"
Die von magsafe
verwendete Datenbankabstraktion Class::DBI
kam schon einige Male im Snapshot vor. Doch heute geht's sogar noch
einfacher: Das Modul Class::DBI::Loader
vereinfacht die Klassendefinition
von Class::DBI
weiter, indem es einfach das Datenbanktabellen-Layout
analysiert und daraus die Abstraktionsklassen und deren Beziehungen
generiert.
Listing magsafe
zeigt die Implementierung.
Das Modul Getopt::Std
verarbeitet die vielen Kommandozeilenoptionen,
die magsafe
versteht. Zeile 24 sucht nach -a
, -m
, -t
, und so
weiter. Die Doppelpunkte im Formatstring der getopts
-Funktion
bestimmen, dass den Optionen jeweils ein Parameter folgt. Deren Werte
legt getopts
im Hash %o
zur späteren Verarbeitung ab.
Zeile 26 prüft, ob das Dokumentenverzeichnis zumindest leer existiert und beschreibbar ist. Falls nicht, sollte es vor dem Programmstart angelegt werden.
Die in Zeile 29 aufgerufene Funktion db_init()
sorgt dafür, dass
der Benutzer sich niemals mit den Details der Datenbank herumschlagen muss.
Existiert sie noch nicht, legt db_init()
ab Zeile 108 mit
einigen SQL-Befehlen eine neue Datenbank
mit zwei Tabellen an. Abbildung 2 zeigt das Schema.
Abbildung 2: Das Layout der SQLite-Datenbank |
Die Tabelle doc
enthält für jedes abgespeicherte Dokument eine
Zeile.
Neben dem Titel des Artikels, dem Autor, der Ausgabe und der Seite steht
dort auch der Name der Publikation, aus der er extrahiert wurde.
Da die meisten Leute nur eine begrenzte
Anzahl von Zeitschriften lesen,
diese aber jeden Monat, wäre es schlechtes Datenbankdesign, den
vollständigen Zeitschriftentitel in jede Zeile hineinzuschreiben.
Statt dessen steht in der Tabelle doc
im Feld mag
eine
numerische ID, die auf eine Zeile der Tabelle mag
verweist, die
neben der ID den ausgeschriebenen Zeitschriftentitel führt.
Die Tabellendefinitionen ab Zeile 112 in magsafe
folgen dem SQL-Standard, lediglich die Zeile
mag INT REFERENCES mag,
in der doc
-Tabelle ist etwas ungewöhnlich: So wird angezeigt,
dass die mag
-Spalte auf die Tabelle mag
verweist, um die oben
erläuterte Beziehung zwischen Zeitschriften-ID und -titel
herzustellen.
Die ersten Spalten beider Tabellen sind numerische IDs, die als
Primary Keys markiert sind. Der in Zeile 31 aufgerufene Konstruktor
der Klasse Class::DBI::Loader
erwartet sie, um Objekten, die
Tabellenzeilen repräsentieren, eindeutige IDs zuweisen zu können.
Neu angelegten Objekten weist er automatisch noch nicht vergebene
IDs zu, indem er die höchste bisher vergebene ID um eins hochzählt.
Die Zeile
namespace => "Scanned::DB"
im Konstruktoraufruf des Datenbank-Laders
bestimmt, dass alle Klassen zur Tabellenabstraktion im Namensraum
Scanned::DB
erscheinen.
Nach dem Aufruf von Class::DBI::Loader->new()
holt die
Methode find_class()
, wie in den Zeilen 39 und 40 vorgeführt,
Objekte hervor, die die Tabellen repräsentieren.
Hierzu nehmen sie den Tabellennamen als Argument entgegen.
$docdb
, ein Objekt vom Typ Scanned::DB::Doc
weist auf
die Dokumententabelle docdb
, $magdb
(Scanned::DB::Mag
)
auf die Zeitschriftentabelle
mag
.
Damit die Objekte nicht nur die Standard-Queries von Class::DBI
beherrschen, sondern auch etwas kompliziertere WHERE-Klauseln,
holt der Parameter additional_classes
in Zeile 34 das Paket
Class::DBI::AbstractSearch
hinzu.
Das Flag relationships
in Zeile 36 weist Class::DBI::Loader
an,
die Beziehungen zwischen den Tabellen doc
und mag
zu analysieren
und zu verdrahten. Wegen der oben erwähnten REFERENCES
-Klausel
im SQL begreift es sofort, dass die Spalte mag
in der Tabelle
doc
nur ein foreign key ist, der in die Tabelle mag
verzweigt.
Die objektorientiert Datenbankabstraktion
wird dann Methoden bereitstellen, um schnell zwischen den beiden
Tabellen zu manövrieren.
Ab Zeile 44 verarbeitet das Skript Kommandozeilenparameter. Ist
-s
nicht gesetzt, sucht der Benutzer nicht nach einem
Datenbankeintrag, sondern möchte einen neuen hinzufügen. Die Zeilen
47 bis 50 nehmen die Werte der verschiedenen Kommandozeilenoptionen wie Titel,
Autor, Dokument, Zeitschrift, Seite und Ausgabe entgegen. Falls eine oder
mehrere nicht gesetzt sind, holt die Funktion ask()
aus dem
Modul Sysadm::Install
die entsprechenden Werte interaktiv beim
Benutzer ab. Nur das Autorenfeld ist optional und wird nicht eingefordert.
Die Auswahl der Zeitschrift ist etwas komplizierter und erfolgt mit
der ab Zeile 133 definierten Funktion mag_pick
. Dort holt die
Methode retrieve_all()
des Datentabellenobjekts $magsdb
alle
bisher eingegebenen Zeilen der mag
-Tabelle ein. Die zurückgelieferten
Zeilenobjekte bieten Methoden an, um zu den Werten für die
einzelnen Felder des Datensatzes zu gelangen: $obj->name()
gibt
so den Namen der Zeitschrift zurück, der im Feld name
der Tabelle
mag
liegt.
Falls $picked
noch nicht gesetzt ist, also keine Kommandozeilenoption für den
Zeitschriftennamen vorliegt, lässt Zeile 141 den Benutzer mit
pick()
(ebenfalls aus Sysadm::Install
) per Menü einen Namen
auswählen. Selektiert der aber den ersten, New
betitelten Eintrag,
invalidiert Zeile 143 die Auswahl wieder und ab Zeile 147 hat der
Benutzer Gelegenheit, den Namen einer neuen Zeitschrift einzugeben,
der dann nächstes Mal in der Auswahlliste erscheinen wird.
Die Methode find_or_create()
erzeugt dann in der Tabelle mag
entweder einen neuen Zeitschrifteneintrag oder findet den zum
angegebenen Namen passenden.
Neu eingetragene oder bereits bestehende Zeitschriften repräsentiert
dann die Variable $mag
in Zeile 45. Weil Class::DBI::Loader
vorher ganze Arbeit geleistet hat und wegen des relationships
-Flags
auch die Beziehungen zwischen den Tabellen doc
und mag
analysiert
hat, kann Zeile 52 einfach $mag->add_to_docs()
aufrufen, um
einen Artikel in die Tabelle doc
einzufügen, dessen mag
-Spalte
auf einen Eintrag der Zeitschrift in der mag
-Tabelle verweist.
Die Methode add_to_docs()
des Zeitschriftenobjekts wurde wirklich
nicht explizit in magsafe
definiert.
Sie entsteht automatisch in der Datenbankabstraktion, sobald die Beziehung
zwischen den Tabellen feststeht.
Um das aktuelle PDF-Dokument in das Dokumentenverzeichnis zu kopieren,
ruft magsafe
in Zeile 59 die cp
-Funktion aus Sysadm::Install auf.
Den vollständigen, zukünftigen Pfad der Datei ermittelt die ab
Zeile 96 definierte Funktion docpath()
, die lediglich die ihr
übergebene ID in einen sechstelligen Integer mit führenden Nullen
umwandelt und den Dokumentenverzeichnispfad davor hängt.
Liegt eine Suchabfrage vor, iteriert Zeile 65 über alle
feld:pattern
-Paare, die mit dem Parameter -s
hereingereicht
wurden und durch Leerzeichen voneinander getrennt sind.
Zeile 67 spaltet dann den Feldnamen vom gesuchten Wert ab.
Ist der Feldname mag
, sucht Zeile 70 erst nach einer passenden
Zeitschrift, indem es den Suchwert in Prozentzeichen einrankt und
eine Suchabfrage mit search_like()
in der Tabelle mag
startet.
Dies kann keine, ein oder mehrere Magazin-Objekte
in @mags
zurückliefern. Deren id()
-Methode fördert die Magazin-IDs
hervor, sodass Zeile 73 im Hash %search
das Wertepaar
"mag" => [$id1, $id2, ...]
ablegt. $id1
, $id2
, und so weiter sind die numerischen IDs für die
auf die Suchanfrage passenden Zeitschrifen, und der Hasheintrag unter
"mag"
weist auf eine Referenz eines Arrays, der sie alle als Elemente
enthält.
Spezifiert der Benutzer
hingegen eine Suchabfrage mit einer Bedingung auf ein anderes Feld als mag
,
tritt der else
-Zweig ab Zeile 75 in Kraft und der Suchbegriff wird,
in Prozentzeichen eingeschlossen, unter dem Feldnamen im Hash %search
abgespeichert.
Der Inhalt des Hashes %search
entspricht nun genau dem Format, das die
in Zeile 80 aufgerufene Methode search_where()
der Dokumententabelle
erwartet. Sie liefert alle Zeilen, auf die alle im Hash angegebenen
Bedingungen passen. Da in einem zusätzlichen optionalen Hash noch der
Compare-Parameter cmp
auf "like"
gesetzt wird, sucht
search_where()
nicht
nach wörtlich passenden Einträgen, sondern nach Patterns,
deren Wildcards gemäß dem SQL-Standard mit %
notieren.
Der print
-Befehl in Zeile 83 wird wegen des nachgestellten
for @objs
für jedes gefundene Objekt aufgerufen. Er fasst alle
Spalten eines gefundenen Tabelleneintrags zusammen und gibt sie
formatiert aus. Den Pfad zum PDF-Dokument setzt wieder die vorher
schon erwähnte Funktion docpath()
zusammen. Findet die Suchabfrage
keine Treffer, gibt Zeile 92 No Entries auf STDERR aus.
001 #!/usr/bin/perl -w 002 ########################################### 003 # magsafe - Archive magazine articles 004 # Mike Schilli, 2005 (m@perlmeister.com) 005 ########################################### 006 use strict; 007 008 use DBI; 009 use Class::DBI::Loader; 010 use Sysadm::Install qw(:all); 011 use Getopt::Std; 012 use Text::Iconv; 013 014 my $DB_NAME = "/home/mschilli/DATA/scanned_docs.dat"; 015 my $DSN = "dbi:SQLite:$DB_NAME"; 016 my $UTF8_TERM = 1; 017 018 my $cv = Text::Iconv->new( 019 "Latin1", "utf8"); 020 $cv->raise_error(1); 021 022 my $DOC_DIR = "/ms1/DOCS"; 023 024 getopts("a:m:t:i:p:d:s:", \my %o); 025 026 die "$DOC_DIR not ready" if 027 !-d $DOC_DIR or !-w $DOC_DIR; 028 029 db_init($DSN) unless -e $DB_NAME; 030 031 my $loader = Class::DBI::Loader->new( 032 dsn => $DSN, 033 namespace => "Scanned::DB", 034 additional_classes => 035 qw(Class::DBI::AbstractSearch), 036 relationships => 1, 037 ); 038 039 my $docdb = $loader->find_class("doc"); 040 my $magdb = $loader->find_class("mag"); 041 042 my @objs = (); 043 044 if(! exists $o{s}) { 045 my $mag = mag_pick($magdb, $o{m}); 046 my $doc = $o{d} || ask "Document", ""; 047 my $author = $o{a} || ""; 048 my $title = $o{t} || ask "Title", ""; 049 my $page = $o{p} || ask "Page", ""; 050 my $issue = $o{i} || ask "Issue", ""; 051 052 my $id = $mag->add_to_docs({ 053 map { $UTF8_TERM ? $_ : $cv->convert($_) } 054 title => $title, 055 page => $page, 056 issue => $issue, 057 author => $author}); 058 059 cp $doc, docpath($id); 060 exit 0; 061 } 062 063 my %search = (); 064 065 for (split ' ', $o{s}) { 066 067 my($field, $expr) = split /:/, $_; 068 069 if($field eq "mag") { 070 my @mags = $magdb->search_like( 071 name => "%$expr%"); 072 073 $search{$field} = [ 074 map { $_->id() } @mags]; 075 } else { 076 $search{$field} = "%$expr%"; 077 } 078 } 079 080 @objs = $docdb->search_where(\%search, {cmp => "like"}); 081 082 if(@objs) { 083 print join(", ", 084 '"' . $_->title() . '"' , 085 $_->author() || "Unknown", 086 $_->mag()->name(), 087 $_->issue(), 088 $_->page(), 089 docpath($_->docid())), 090 "\n" for @objs; 091 } else { 092 print STDERR "No entries\n"; 093 } 094 095 ########################################### 096 sub docpath { 097 ########################################### 098 my($id) = @_; 099 100 return sprintf "%s/%06d", 101 $DOC_DIR, $id; 102 } 103 104 ########################################### 105 sub db_init { 106 ########################################### 107 my($dsn) = @_; 108 109 my $dbh = DBI->connect($dsn, "", ""); 110 111 $dbh->do(q{ 112 CREATE TABLE doc ( 113 docid INTEGER 114 PRIMARY KEY, 115 title VARCHAR(255), 116 author VARCHAR(255), 117 mag INT REFERENCES mag, 118 page INT, 119 issue VARCHAR(32) 120 ); 121 }); 122 123 $dbh->do(q{ 124 CREATE TABLE mag ( 125 magid INTEGER 126 PRIMARY KEY, 127 name VARCHAR(255) 128 ) 129 }); 130 } 131 132 ########################################### 133 sub mag_pick { 134 ########################################### 135 my($magsdb, $picked) = @_; 136 137 my @mags = map { $_->name() } 138 $magsdb->retrieve_all(); 139 140 if(@mags and !$picked) { 141 $picked = pick "Magazine", 142 ["New", @mags], 1; 143 undef $picked if $picked eq "New"; 144 } 145 146 if(!$picked) { 147 $picked = ask 148 "Enter Magazine Name", ""; 149 } 150 151 $picked = $UTF8_TERM ? $picked : 152 $cv->convert($picked); 153 154 my $mag = $magsdb->find_or_create( 155 {name => $picked}); 156 return $mag; 157 }
Zu beachten ist, dass SQLite Strings als UTF-8 erwartet, mit Umlauten
im ISO-8859-1-Format kommt es nicht zurecht.
Ob die Eingabe der Kommandozeilenparameter mit UTF8- oder ISO-8859-1-
Kodierung erfolgt, hängt aber ganz vom lokal betriebenen Terminal ab.
Viele neuere Linux-Distributionen haben UTF-8-Terminals, ältere fahren
typischerweise ISO-8859-1. Einen Hinweis darauf gibt üblicherweise
die Environment-Variable LANG
: Steht dort etwas wie en_US.UTF-8
,
liegt UTF-8 vor.
Das Skript lässt sich an die äußeren Bedingungen anpassen:
Ist die Variable
$UTF8_TERM
in Zeile 16 auf einen wahren Wert gesetzt, interpretiert
das Skript alle Benutzereingeaben als UTF-8 und nimmt keine Umkodierung
vor. Ist $UTF8_TERM
hingegen 0, nimmt magsafe
an, dass Benutzereingaben
als ISO-8859-1 erfolgen und wandelt alles in UTF-8 um, bevor sie in die
Datenbank wandern.
Für etwaige Umwandlungen zieht
magsafe
das Iconv
-Modul vom CPAN heran.
Zeile 18 erzeugt ein Objekt vom Typ Text::Iconv
für die
Transformation von ISO-8859-1 nach UTF-8. Weiter aktiviert es
dessen Methode raise_error()
mit 1, damit etwaige Fehler sofort
eine Exception werfen. Die Methode convert()
wandelt ihr
übergebene Strings von der einen in die andere Kodierung um.
Eine andere Methode wäre das Encode
-Modul, für diejenigen,
die mindestens perl
5.8.x fahren.
Für die Datenbankabstraktion benötigt das Skript die Module
DBI, Class::DBI, Class::DBI::SQLite, Class::DBI::AbstractSearch, und
DBD::SQLite, die es allesamt auf dem CPAN gibt. Weiter kommen
Text::Iconv
und Sysadm::Install
zum Einsatz. Alle Module
lassen sich einfach mit einer CPAN-Shell installieren.
Wer den Commandline-Client sqlite3
will, um manuell mit SQL
in der Datenbank
herumzuschnüffeln, lädt sich am besten den Source-Tarball von [3]
herunter, compiliert und installiert ihn.
Frührere Versionen (sqlite 1 oder 2) funktionieren nicht,
da DBD::SQLite
zur Zeit auf sqlite 3.x basiert, und Datenbanken,
die mit verschiedenen sqlite-Versionen erzeugt wurden, nicht kompatibel
sind. Das Kommandozeilentool der 3er-Version heisst sqlite3
, im Gegensatz
zu den Tools früherer Versionen, die sqlite
heißen.
Achtung: Wer bereits SQLite-Datenbanken für die ein oder andere Anwendung
mit früheren Versionen des Moduls DBD::SQLite
fährt (wie zum Beispiel die aus [5]), und diese weiter nutzen will,
sollte diese vor dem Upgrade ins neue Format überführen:
sqlite OLD.DB .dump | sqlite3 NEW.DB
Liegt das Modul DBD::SQLite nämlich einmal in der neuesten Version vor, kann es Datenbanken, die mit früheren Versionen erzeugt wurden, nicht mehr lesen.
Das Dokumentenverzeichnis legt die Variable $DOC_DIR
in Zeile
22 des magsafe
-Skripts fest. Das Verzeichnis sollte bereits vorliegen und
beschreibbar sein. Der Rest geht automatisch: SQLite wird selbständig
die Datenbank in der Datei scanned_docs.dat
(Name festgelegt in Zeile
14) anlegen und die Tabellen initialisieren.
Interessante Artikel ausgelesener Zeitschriften scannen gewitzte Leser nun flugs ein und werfen das Druckerzeugnis anschließend in den Altpapiercontainer. Und der Lebenspartner freut sich über den freiwerdenden Wohnraum.
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |