22.04.2015

OpenOffice-Dokumente mit perl-Skript bearbeiten

Offene Dateiformate sind eine tolle Sache!

Das merke ich immer wieder bei lustigen Problemen, wenn mir z.B. jemand ein Office-Dokument schickt und ich damit irgendetwas total langweiliges erledigen soll.

Es macht viel mehr Spaß, dafür ein Programm oder ein Skript zu schreiben und den Computer die Arbeit machen zu lassen, statt selbst dröge Arbeiten zu erledigen, die nur aus Wiederholungen von Tastendrücken und Mausbewegungen bestehen!

Letzte Woche hatte ich so einen Auftrag: ein Kollege schickte mir eine Excel-Datei mit mehreren Tabellenblättern und bat mich, daraus mehrere Tables einer Datenbank zu befüllen.

Mein erster Ansatz war, jedes Blatt als Textdatei (CSV) abzuspeichern und mit ein wenig Skripting die SQL-Befehle pro CSV-Datei zu generieren.

Der nächste Gedanke war einiges cooler und hat mir deshalb noch viel besser gefallen: ich speichere die Excel-Datei im OpenOffice-Format (ODF=OpenDocumentFormat) ab und lasse ein Skript die ganze Arbeit machen! Die Datei enthält um die 15 Tabellenblätter - allein vor dem Abspeichern von 15 CSV-Dateien in Handarbeit grauste es mir schon!

Hinzu kommt, dass mein Arbeitgeber OpenOffice als Standardwerkzeug und Standardformat festgelegt hat und es somit vollkommen natürlich ist, mit dem OpenDocument-Format zu arbeiten. Erwähnte ich schon, dass mein Arbeitsplatz mit Linux ausgestattet ist?

Ein ODF-Dokument ist eigentlich ganz einfach aufgebaut: es ist im Wesentlichen eine zip-Datei und der Inhalt ist immer in der Datei content.xml zu finden. Alles andere ist Beiwerk (Stylesheets, Bilder, ...), das ich für meinen Zweck ignorieren kann.

Natürlich verwende ich perl zum Programmieren meiner Umwandlungsskripte, und natürlich gibt es ein Modul für jeden Zweck. Für die Verbindung zu OpenOffice gibt es sogar mehrere, und ich habe mir OpenOffice::OODoc ausgesucht. Mir ist nämlich in der Beschreibung aufgefallen, dass es eine ganz bestimmte Funktion gibt, die haargenau das erledigt, was ich mir als Lösungsweg überlegt hatte: den Tabelleninhalt als CSV-Text auszuliefern.

Damit war mein perl-Skript plötzlich fertig, kaum dass ich 20 Zeilen geschrieben hatte, wobei das meiste davon sogar ein fast fertiges Beispiel aus der man-page war! Ich musste nur ein wenig die Daten massieren, als ich mit einem Testskript anfing und die ersten Daten probeweise auf dem Bildschirm ausgeben ließ.

Das folgende Skript erledigt nun den ersten Teil meiner Aufgabe: es zerlegt ein OpenOffice-Spreadsheet in mehrere CSV-Dateien, eine pro Tabellenblatt.
Als Ziel schwebt mir vor, dass die temporären CSV-Dateien gar nicht mehr gebraucht werden und ich direkt den SQL-Code erzeuge, aber ich denke, die Umwandlung von .ods in .csv kann man immer mal brauchen.

Einzige Bedingung: der Name des Tabellenblatts sollte sinnvoll gesetzt sein, weil daraus nämlich der Name der CSV-Datei gebildet wird.  Wenn man das nicht macht, heißen die Dateien einfach "Sheet1.csv", "Sheet2.csv" usw.
#!/usr/bin/perl -w
use OpenOffice::OODoc;
sub out {
    my ($table,$data)=@_;
    my $f=$table->{'att'}->{'table:name'}.".csv";
    return unless (open(OUT,">",$f));
    print STDERR "# $f\n";
    print OUT $data;
    close(OUT);
}
my $file=shift||"file.ods";
my $doc = odfText(file => $file);
foreach my $table ($doc->getTableList()) {

    $doc->normalizeSheet($table);
    my $data = $doc->getTableText($table);
    $data=~s!(<<|>>)!!msgo;
    $data=~s!^[\s;]*$!!msgo;
    $data=~s!;$!!msgo;
    chomp($data);
    out($table,$data);
}
Die Ausgabe-Funktion bildet den Dateinamen aus dem Attribut table:name des Blatts. Die eigentliche Umwandlung findet in der Methode getTableText statt: hier wird ein gesamtes Tabellenblatt mit einstellbaren Trennzeichen für Spalten und Zeilen in CSV-Text zurückgeliefert. Manche Blattnamen werden von "<<" und ">>" eingerahmt, was ich natürlich in Dateinamen nicht brauchen kann, deshalb werden die Daten noch etwas gesäubert. Welche Blattnamen es gibt, kann man mit getTableList erfragen.

Natürlich kann man auch andere Methoden einsetzen, um einzelne Zeilen, Spalten oder Zellen zu adressieren, und man kann mit diesem perl-Modul auch Daten ändern und die Änderungen abspeichern. Ein geniales Modul! Wie gesagt, die bequemste Funktion war für mich die Umwandlung in CSV mit einem einzigen Aufruf.

[Update: OpenOffice optimiert die Tabellenstruktur in der internen Verwaltung (content.xml), wenn mehrere Zellen denselben Inhalt haben; deshalb muss man vor der Benutzung der Methode getTableText das XML wieder "entzerren". Dazu setze ich vor das Auslesen der Tabelle die Methode normalizeSheet ein. ]

Und zack!, schon ist der erste Teil der Aufgabe erledigt - das Auslesen einer ODF-Datei.

Den zweiten Teil - die Umwandlung von Text in SQL - gibt es bei der nächsten Märchenstunde.

Und jetzt am Schluss verrate ich noch ein Geheimnis: ich erzähle, wie man dieses Modul in sein eigenes perl integriert (Punkte zwischen [...] optional).
  1. Modul herunterladen
  2. Auspacken
  3. In das Directory wechseln
  4. perl Makefile.PL
  5. make
  6. [make test]
  7. make install

oder noch viel bequemer mit dem CPAN-Modul:
  1. perl -MCPAN -e 'install OpenOffice::OODoc'
[Update 20150521: Umbruch im perl-Skript repariert]