Auswärtsspiel im Hexenkessel (Linux-Magazin, März 2002)

Mit dem Modul Spreadsheet::WriteExcel lassen sich aus Perl heraus plattformunabhängig Dateien für Microsofts Tabellenkalkulation Excel erzeugen.

Meine CD-Sammlung lagere ich in den praktischen Sammelboxen von Discgear ([3] und Abbildung 1) -- die sind 35cm breit und nehmen 80 CDs auf. Allerdings stinkt die mitgelieferte Software zum Himmel und ich drucke mir die Boxenbeschriftung, die die Interpreten und Titel von 80 CDs auf kleinstem Raum auflistet, selber aus.

Dies tat ich bislang dadurch, dass ich die CD-Titeldaten aus einer ASCII-Datei extrahierte und mit Cut-und-Paste nach Excel kopierte, in vier Spalten a 20 Zeilen umschichtete und anschließend von dort an den Drucker sandte.

Abbildung 1: Die Disc-Gear-Box mit dem Excel-Inhaltsverzeichnis oben im Deckel

Dann fiel mir [2] aus dem Perl Journal ein und dass man Dateien für Microsoft Excel auch mit Perl erzeugen kann. Das Modul Spreadsheet::WriteExcel von John McNamara vom CPAN installiert sich wie immer mit

    perl -MCPAN -e'install Spreadsheet::WriteExcel'

und beschränkt sich darauf, neue Excel-Dateien zu erzeugen. Es kann keine bestehenden Dateien lesen (dafür ist Spreadsheet::ParseExcel zuständig) oder beschreiben, und auch nicht alles, was Win32::OLE mit Excel anstellen kann. Allerdings setzt Win32::OLE im Gegensatz zu Spreadsheet::WriteExcel das Windows-Betriebssystem und ein installiertes Excel voraus. Um aber nur einige Excel-Zellen zu füllen, kommt Spreadsheet::WriteExcel gerade recht.

Meine CD-Datenbank mit Indexsuche (die ich ein andermal vorstellen werde) kann die CD-Dateien im in Abbildung 2 gezeigten Format exportieren: ASCII, ein Eintrag pro Zeile, wobei Interpret und CD-Titel durch ein Komma getrennt stehen.

Abbildung 2: Die Rohdaten in der Datei.

Listing toexcel greift sich die Rohdaten, formatiert sie so, dass in eine 20 mal 4 große Excel-Matrix passen und nutzt Spreadsheet::WriteExcel, um die Excel-Datei cd.xls zu schreiben. Kopiert man die Ausgabedatei cd.xls danach von Linux nach Windows und startet Excel damit, nimmt dieses, wie Abbildung 3 zeigt, das ins Nest gelegte Ei glücklich auf, zeigt die gesetzten Spalten und Zeilen ohne Murren an und druckt sie auf Kommando aus.

Abbildung 3: In Excel.

Zeile 5 in toexcel aktiviert, wie in jedem professionellen Perlskript, mit use strict strenge Regeln für Variablen, Referenzen und Funktionen. Variablen müssen entweder mit my oder our deklariert, oder aber über den vollen Package-Namen spezifiziert werden. use warnings in Zeile 6 stellt ab Perl 5.6.0 den aktiven Warnungsmodus an, der vor offensichtlich begangenen Leichtsinnsfehlern warnt und verhindert zum Beispiel wirksam, dass man wegen einer nicht ordentlich initialisierten Variable stundenlang nach dem Fehler sucht.

In $IN und $OUT in den Zeilen 8 und 9 liegen die Konfigurationsparameter des Skripts, die die Namen von Ein- bzw. Ausgabedatei festlegen.

Die while-Schleife ab Zeile 17 liest die in Abbildung 3 gezeigte Datei zeilenweise ein. Der anschließende chomp-Befehl schneidet den jeder Zeile anhängenden Zeilenumbruch ab. split() in Zeile 19 spaltet die Zeile am Komma in Interpret und Titel. Der letzte Parameter 2 gibt an, dass die Zeile maximal in zwei Felder gespalten wird, auch wenn mehr als ein Komma darin vorkommt. Falls also der CD-Titel ein Komma enthält, spaltet split() die Datenzeile trotzdem in Interpret und Titel, da es nach dem ersten gefundenen Komma den split-Vorgang abbricht.

Sollte eine Zeile allerdings kein Komma enthalten, steht also nur der Interpret da, bleibt $title auf dem Wert undef. Wegen des strengen Warnungsmodus hätte dies allerdings hässliche Meldungen zur Folge, weswegen Zeile 20 den eventuell undefinierten Skalar $title auf den Leerstring setzt. Das Konstrukt $title ||= "" ist dabei eine Perl-Abkürzung für

    $title || $title = "";

Falls also $title falsch oder undefiniert ist, wird der rechte Teil der logischen Oder-Verknüpfung ausgeführt und $title mit dem Leerstring besetzt. Sollte irgendwann mal eine CD mit dem Title "0" herauskommen, ginge das natürlich voll in die Hose, da "0" logisch falsch ist -- aber das kann ich riskieren.

Die gefundenen Interpreten/Titel-Paare speichert toexcel bis zur späteren Abarbeitung in dem Array @entries. Da jedes Element von @entries aber aus zwei Unterelementen (Interpret und Titel) besteht, formt Zeile 21 aus dem Wertepaar mit den eckigen Klammern [...] einen anonymen Array und puscht eine Referenz darauf auf den Array @entries.

Im Zeichen des Excel

Ab Zeile 26 wütet dann Spreadsheet::WriteExcel und erzeugt zunächst ein Workbook-Objekt in der Excel-Welt, dessen addworksheet()-Methode in Zeile 27 ein Excel-Worksheet anlegt. Auf diesem ``Arbeitsblatt'' stehen später die Zeilen und Spalten des Spreadsheets.

Für spezielle Formatangaben, die Fonts, deren Größe oder andere Gestaltungsparameter festlegen, dienen in Excel Formate, die Spreadsheet::WriteExcel über Formatobjekte definiert. Die add_format()-Methode eines Workbook-Objekts erzeugt ein neues Format. Sie nimmt Argumente in der Form Parametername, Wert entgegen. Zeile 28 stellt den Font Arial in der Größe 8 ein.

Um die 80 Zeilen der Ausgangsdatei in eine Matrix von 20 Zeilen mal 4 Spalten zu übersetzen, rattern die for-Konstrukte in den Zeilen 33 und 35 von Zeile 0 bis 19 und Spalte 0 bis 3. Jedesmal holt Zeile 37 eine Referenz auf einen Unterarray aus @entries, der wiederum Interpret und Titel der aktuellen CD als Elemente enthält. Gibt's keine CDs mehr, bricht Zeile 39 die for-Schleife ab.

Zeile 41 macht aus der Unter-Arrayreferenz $e mittels @$e wieder ein Array und weist dessen Elemente den Skalaren $artist und $titel zu. Die write()-Methode des Worksheet-Objekts in Zeile 44 nimmt den Zeilen- und Spaltenindex der Excel-Zelle entgegen, in die es den Text des dritten Parameters schreiben wird. Der vierte Parameter bestimmt das verwendete Format.

Eigentlich besteht unser Worksheet entsprechend Abbildung 3 aber nicht aus 4 Spalten, sondern aus 8 -- jeder Doppelspalteneintrag führt nämlich eine laufenden Nummer (von 1 bis 80) und rechts daneben den eigentlichen Text. Der Spaltenindex der Nummer ist dementsprechend 2 * $col und der des Texts ist 2 * $col + 1, wenn $col der Index der ursprünglich angenommenen 'virtuellen' Kolumne ist.

So schreibt Zeile 44 die laufende Nummer des Eintrags und Zeile 47 den Eintrag selbst, der aus Interpret und CD-Titel besteht, die beide durch einen Gedankenstricht getrennt werden.

Um anschließend die Breite der Nummernspalte auf 2 und die der Eintragsspalte auf 20 zu setzen, iteriert das for-Konstrukt ab Zeile 52 noch einmal über alle Spalten und legt mittels der set_column()-Methode ihre Eigenschaften fest. set_column() nimmt Anfangs- und Endindex eines Spaltenbereichs, sowie die gewünschte Spaltenbreite entgegen. Um zum Beispiel nur die Spalte 0 des Spreadsheets auf die Breite 2 zu stellen, ist der Aufruf

    $sheet->set_column(0, 0, 2);

erforderlich. Zeile 59 schreibt mit der close()-Methode des Excel-Workbook-Objekts das Spreadsheet in die Ausgabedatei und schließt den Vorgang ab. Ganz einfach -- dank schlauer Modultechnik vom CPAN! Konvertiert fleißig zwischen den Welten!

Listing 1: toexcel

    01 #!/usr/bin/perl
    02 ##################################################
    03 # toexcel --Mike Schilli, 2001 (m@perlmeister.com)
    04 ##################################################
    05 use strict;
    06 use warnings;
    07 
    08 my $IN  = "cd2.dat";   # Eingabedatei
    09 my $OUT = "cd.xls";   # Ausgabe-(Excel)-Datei
    10 
    11 use Spreadsheet::WriteExcel;
    12 
    13 open IN, "<$IN" or die "Cann open $IN";
    14 
    15 my @entries = ();
    16 
    17 while(<IN>) {
    18     chomp;
    19     my($artist, $title) = split /-/, $_, 2;
    20     $title ||= "";
    21     push @entries, [$artist, $title];
    22 }
    23 
    24 close IN;
    25 
    26 my $book   = Spreadsheet::WriteExcel->new($OUT);
    27 my $sheet  = $book->addworksheet();
    28 my $format = $book->addformat(font => "Arial", 
    29                               size => 8);
    30 
    31 my $count  = 0;
    32 
    33 for my $col (0..3) {
    34 
    35     for my $row (0..19) {
    36 
    37         my $e = shift @entries;
    38 
    39         last unless $e;   # Keine Einträge mehr?
    40 
    41         my($artist, $title) = @$e;
    42 
    43             # Zähler schreiben
    44         $sheet->write($row, 2*$col, ++$count, 
    45                                     $format);
    46             # Eintrag schreiben
    47         $sheet->write($row, 2*$col+1, 
    48                      "$artist - $title", $format);
    49     }
    50 }
    51 
    52 for my $col (0..3) {
    53         # Zählerfeld: 2 Zeichen breit
    54     $sheet->set_column(2*$col, 2*$col, 2);
    55         # CD-Feld: 20 Zeichen breit
    56     $sheet->set_column(2*$col+1, 2*$col+1, 20);
    57 }
    58 
    59 $book->close();

Infos

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

[2]
John McNamara, ``Spreadsheet::WriteExcel'', The Perl Journal, Herbst 2000, http://homepage.eircom.net/~jmcnamara/perl/tpj.html

[3]
Die praktischen CD-Boxen von Discgear: http://www.discgear.com

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.