Entrümpler (Linux-Magazin, Mai 2005)

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.

Scannen und Packen

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.

Ab ins Archiv

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.

Suche im Heuhaufen

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"

Abstrakte Datenbank

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.

Steuern per Kommando

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.

Einfacher als SQL

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.

Listing 1: magsafe

    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 }

Es ü-ht und ö-ht

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.

Installation

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.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2005/05/Perl

[2]
``PDF Hacks'', Sid Steward, O'Reilly 2004

[3]
SQLite Home Page, http://www.sqlite.org

[4]
http://www.sane-project.org

[5]
``Trainierter DJ'', Michael Schilli, Linux-Magazin 07/2004, http://www.linux-magazin.de/Artikel/ausgabe/2004/07/perl/perl.html

Michael Schilli

arbeitet 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.