Das Perl-Tutorial für Computerlinguisten

169

Transcript of Das Perl-Tutorial für Computerlinguisten

Page 1: Das Perl-Tutorial für Computerlinguisten
Page 2: Das Perl-Tutorial für Computerlinguisten

1 Perl-Installation ........................................................................................ 1

2 Installation des Emacs ............................................................................... 3

3 Benutzung des Emacs ................................................................................ 5

3.1 Struktur des Emacs ................................................................................................ 5

3.2 Grundlegende Emacs-Befehle .................................................................................. 6

3.3 Navigation im Puffer .............................................................................................. 8

3.4 Suche im Puffer ...................................................................................................... 9

3.5 Ersetzen im Puffer.................................................................................................. 9

3.6 Perl-Code im Emacs ausführen ................................................................................10

4 Grundlegende Datenstrukturen................................................................. 12

4.1 Skalare und Operatoren.........................................................................................13

4.2 Variablen .............................................................................................................16

4.3 Konstanten ..........................................................................................................19

4.4 Exkurs: Standardein- und -ausgabe .........................................................................20

4.5 Listen ..................................................................................................................20

4.6 Arrays..................................................................................................................24

4.7 Hashes .................................................................................................................29

4.8 Zusammenfassung.................................................................................................32

5 Kontrollstrukturen................................................................................... 37

5.1 Bedingungen ........................................................................................................40

5.2 Schleifen..............................................................................................................42

5.3 Zusammenfassung.................................................................................................50

5.4 Beispielanwendung...............................................................................................51

6 Operationen auf Dateien .......................................................................... 56

6.1 Dateideskriptoren ................................................................................................56

6.2 Dateizugriff..........................................................................................................56

6.3 Spezielle Dateideskriptoren...................................................................................60

6.4 Exkurs: Formatierung numerischer Zeichenketten ...................................................61

6.5 Zusammenfassung.................................................................................................62

6.6 Beispielanwendung...............................................................................................63

7 Reguläre Ausdrücke................................................................................. 74

7.1 Muster.................................................................................................................75

7.2 Platzhalter ...........................................................................................................77

7.3 Gruppierung und Speicherung.................................................................................85

7.4 Anker ...................................................................................................................87

7.5 Modifikatoren ......................................................................................................88

7.6 Wie funktionieren reguläre Ausdrücke? ...................................................................90

7.7 Gier .....................................................................................................................94

7.8 Ersetzung.............................................................................................................95

7.9 Vor- und zurückschauen..........................................................................................96

7.10 Zusammenfassung...............................................................................................97

Page 3: Das Perl-Tutorial für Computerlinguisten

7.11 Beispielanwendung .............................................................................................98

8 Subroutinen.......................................................................................... 103

8.1 Argumente .........................................................................................................104

8.2 Gültigkeitsbereiche ............................................................................................107

8.3 Rückgabewerte...................................................................................................109

8.4 Zusammenfassung...............................................................................................111

8.5 Beispielanwendung.............................................................................................112

9 Referenzen........................................................................................... 123

9.1 Erzeugen einer Referenz ......................................................................................123

9.2 Verwendung von Referenzen.................................................................................126

9.3 Komplexe Datenstrukturen..................................................................................131

9.4 Referenzen als Subroutinen-Argumente.................................................................137

9.5 Referenzen auf Subroutinen .................................................................................138

9.6 Referenztypen....................................................................................................141

9.7 Zusammenfassung...............................................................................................141

9.8 Beispielanwendung.............................................................................................142

10 Perl-Module ........................................................................................ 145

10.1 Perl-Module installieren ....................................................................................145

10.2 Perl-Module verwenden .....................................................................................147

10.3 Perl-Module selbst erstellen ..............................................................................152

Literatur .................................................................................................... 161

Einführend: .................................................................................................................161

Weiterführend:............................................................................................................161

Computerlinguistik:.....................................................................................................161

Index......................................................................................................... 162

Page 4: Das Perl-Tutorial für Computerlinguisten
Page 5: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

1

1 Perl-Installation Um ein Perl-Skript ausführen zu können, benötigen Sie einen Perl-Interpreter. In Unix-basierten

Betriebssystemen ist dieser meist schon vorinstalliert; Windows-Nutzer müssen ihn selbst installieren.

Eine aktuelle Version des Perl-Interpreters findet sich auf der Download-Seite der Firma

ActiveState

http://www.activestate.com/Products/Download/Download.plex?id= ActivePerl

und auf dem FTP-Server des Sprachwissenschaftlichen Instituts

ftp://ftp.linguistics.rub.de/programming/languages/perl/windows

Wählen Sie bitte Version 5.8.x für Ihr Betriebssystem aus, wobei x die jeweils aktuelle

Unterversion markiert. Darüber hinaus finden sie in der Bezeichnung auch noch eine dreistellige Build-

Nummer.1

Im ersten Schritt doppelklicken Sie bitte auf die von Ihnen herunter geladene Datei, um das

Installationsprogramm zu starten. Nachdem Sie die Lizenzvereinbarung akzeptiert haben, bekommen

Sie die Gelegenheit, die Installation an Ihre Bedürfnisse anzupassen. Beachten Sie, dass die

Installationsroutine nicht automatisch das von Ihrem Betriebssystem für Anwendungen präferierte

Verzeichnis abfragt – für gewöhnlich C:\Programme –, sondern das Installationsverzeichnis direkt unter

dem Wurzelverzeichnis C:\ anlegen will:

Die Aktivierung eines Profils im nächsten Schritt der Installation ist optional. Akzeptieren Sie

zuletzt die voreingestellten Optionen und schließen die Installation damit ab.

1 Auf der ActiveState-Seite finden Sie zwei unterschiedliche Pakete für die Installation unter Windows: Sollten Sie mit

Windows 2000 oder jünger arbeiten, wählen Sie das MSI-Paket, für ältere Versionen das AS-Paket. Beachten Sie im letzten Fall

unbedingt die Hinweise im Kasten auf der rechten Seite.

Page 6: Das Perl-Tutorial für Computerlinguisten

2

Um zu überprüfen, ob die Installation erfolgreich war, starten Sie bitte die Eingabeaufforderung, die

Sie im Startmenü unter Alle Programme > Zubehör finden und geben dort perl –v ein. Sie sollten

nun Informationen über die von Ihnen installierte Perl-Version angezeigt bekommen.

Page 7: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

3

2 Installation des Emacs Da der Emacs unter Unix-basierten Betriebsystemen ebenso wie Perl für gewöhnlich zur

Standardinstallation gehört, beschränken wir uns auch in diesem Abschnitt auf die Installation unter

Windows.

Eine Windows-Version des Emacs findet sich auf dem FTP-Server des Sprachwissenschaftlichen

Instituts unter

ftp://ftp.linguistics.rub.de/programming/editors/emacs/windows/

Laden Sie dort die aktuellste Version herunter. Um die komprimierte Datei entpacken zu können,

benötigen Sie WinZip, das sie unter http://www.winzip.com bekommen, oder ein anderes

Komprimierungswerkzeug, das mit tar.gz-Dateien umgehen kann.

Entpacken Sie im Folgenden die Dateien in ein Verzeichnis Ihrer Wahl. Unter dem Startverzeichnis

des Emacs finden Sie das Verzeichnis bin, in dem sich die Anwendung addpm.exe befindet.

Doppelklicken Sie auf diese, um Emacs im Startverzeichnis zu installieren.

Abschließend sollten Sie eine Konfigurationsdatei für den Emacs anlegen, die Ihnen die Arbeit mit

Perl erleichtert. Sie finden eine solche mit der Bezeichnung _emacs unter folgender Adresse:

http://www.linguistics.rub.de/~halama/lehre/clp/emacs_install/_emacs

Sie besitzt folgenden Inhalt, den Sie ggf. auch im Emacs eintippen können:

(standard-display-european 1) (require 'latin-1) (display-time) (cond ((fboundp 'global-font-lock-mode) (global-font-lock-mode t) (setq font-lock-maximum-decoration t))) (add-hook 'perl-mode-hook 'add-initial-code) (defun add-initial-code () "Use standard modules for Perl code." (interactive) (setq modules (list "strict" "diagnostics")) (if (eq (point-min) (point-max)) (while modules (setq module (car modules)) (setq modules (cdr modules)) (insert "use " module ";\n"))))

Diese Datei wird für gewöhnlich im Home-Verzeichnis des Benutzers abgespeichert; leider ist

dieses unter Windows nicht mit dem Eigene Dateien-Ordner identisch, sondern muss in der

Eingabeaufforderung vermittels set HOMEDRIVE ermittelt werden.2

2 Für gewöhnlich liefert dieser Befehl die Ausgabe C:. Sie können diese Angabe aber Ihren Bedürfnissen anpassen, indem

Sie diese Umgebungsvariable mit set HOMEDRIVE=<Laufwerksbuchstabe>:\<Verzeichnispfad> setzen.

Page 8: Das Perl-Tutorial für Computerlinguisten

4

Page 9: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

5

3 Benutzung des Emacs Der Editor Emacs stammt aus der Unix-Welt und ist eine sehr mächtige Entwicklungsumgebung für

verschiedenste Aufgaben:

• Unterstützung bei der Erstellung von Quellcode in unterschiedlichsten Programmiersprachen

durch:

o Automatische Überprüfung der Klammerung,

o Möglichkeit, eine Anwendung direkt aus dem Editor auszuführen,

o Automatische Einrückung,

o Farbliche Darstellung unterschiedlicher syntaktischer Konzepte.

• Unterstützung bei der Eingabe von Textdokumenten durch:

o Automatische Erzeugung von Dokumentgerüsten (z.B. in generischen Markupsprachen,

wie HTML et al., oder in reinen Formatierungssprachen, wie LaTeX),

o Automatische Einrückung,

o Farbliche Darstellung unterschiedlicher syntaktischer Konzepte.

3.1 Struktur des Emacs Dazu bedient sich der Emacs des Konzepts des Modus. Für die verschiedenen Aufgaben gibt es

unterschiedliche Modi, die der Benutzer auswählen kann, bzw. die durch Verknüpfung mit bestimmten

Dateiendungen automatisch beim Start geladen werden. Für den jeweiligen Modus werden sogenannte

Makros aktiviert (daher der Name Emacs: Extended Macros), die die eigentliche Funktionalität

bereitstellen.

Für Perl stehen gleich zwei Modi zur Verfügung, der cperl-mode und der perl-mode, wobei

letzterer automatisch aktiviert wird, wenn man eine Perl-Datei lädt oder eine neue Datei erstellt.

Bevor wir unsere ersten Schritte mit dem Emacs tun und eine Perl-Datei erstellen, sei kurz der

Aufbau des Editors erklärt. Beim Start sehen Sie folgenden Bildschirm:

Page 10: Das Perl-Tutorial für Computerlinguisten

6

Zuoberst finden Sie wie gewohnt eine Menüleiste, mit der Sie viele Funktionen steuern können. Da

es aber zu den Eigenheiten des Emacs gehört, dass seine Befehle durch (kryptische) Tastenkombina-

tionen eingegeben werden, sollten Sie sich diese Arbeitsweise so schnell wie möglich zu Eigen

machen!

Grundsätzlich macht man alle Eingaben in einem Puffer; während die Inhalte im Hauptpuffer

stehen (in diesem Fall *scratch*, weil noch keine Datei geladen/angelegt wurde), finden sich Befehle

in der Kommandozeile, dem sogenannten Minibuffer.

Da die vom Emacs verwendeten Macros in einer besonderen Variante von Lisp programmiert

wurden, befindet man sich nach Start des Editors im lisp-mode.

3.2 Grundlegende Emacs-Befehle Kommandos lassen sich (abgesehen von den Menüeinträgen) auf zweierlei Arten absetzen:

• Durch Tastenkombinationen, die meist mit Strg-x oder Strg-c beginnen.

• Durch Eingabe des vollständigen Befehls in der Kommandozeile eingeleitet durch Alt-x.

Im ersten Fall gelangen Sie zur Dateiauswahl, indem Sie Strg-x Strg-f drükken.

Die Langversion dieses Befehls leiten Sie durch Alt-x ein und tippen dann im Minibuffer find-

file. Emacs kennt für solcherlei Kommandos die Abkürzung durch die Tabulator-Taste. Geben Sie

z.B. die ersten beiden Buchstaben ein, erscheint folgende Liste der möglichen Ergänzungen:

Geben Sie nun die nächsten Buchstaben ein und drücken danach jeweils die Tabulator-Taste, bis

Page 11: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

7

Sie das vollständige Kommando im Minibuffer sehen.3

Modifizieren Sie jetzt den im Minibuffer vorgeschlagenen Pfad zu einem Verzeichnis, in dem ihre

Perl-Dateien künftig liegen sollen. Anders als unter Windows gewohnt, werden weder im Emacs noch

in Perl Backslashes für Verzeichnispfade angegeben, sondern normale Schrägstriche.

Ihrem letzten Schrägstrich folgt der Name Ihrer Perl-Datei: Standardmäßig endet dieser Dateityp

auf .pl.

Wenn Sie nun die Eingabetaste drücken (und Sie bei der Installation des Emacs alles richtig

gemacht haben), sollten Sie einen (fast) leeren Puffer sehen, dessen Name Ihr Dateiname und dessen

Modus Perl ist:

In die ersten beiden Zeilen fügt die _emacs-Datei automatisch zwei Header-Zeilen ein, die

notwendige Module laden. Am unteren linken Bildrand sehen Sie den Pufferstatus: Erscheinen dort

zwei Sternchen, ist der Puffer modifiziert worden, d.h. er stimmt nicht mehr mit der gespeicherten

Version überein. Geben Sie nun Strg-x Strg-s ein, wird die Datei gespeichert, und anstatt der Stern-

chen erscheinen Striche.4

Wenn Sie nun Perl-Programme erstellen, unterstützt Sie der Emacs bei der Eingabe durch die

3 Wie Sie gemerkt haben, mussten Sie nur find-fi eingeben. An dieser Stelle können Sie schon die Eingabetaste drücken,

ohne vorher Tab betätigt zu haben, da diese Stufe die minimale Ergänzung für den gewünschten Befehl darstellt. 4 Sehen Sie an dieser Stelle zwei Prozentzeichen, kann auf diesen Puffer (d.h. diese Datei) nur lesend zugegriffen werden!

Dieser Zustand lässt sich mit Strg-x Strg-q ändern.

Page 12: Das Perl-Tutorial für Computerlinguisten

8

farbliche Kennzeichnung von Konstrukten und durch die Einrückung. Letzteres geschieht für die

jeweilige Schachtelungsebene automatisch, wenn Sie das Semikolon am Zeilenende eingeben. Sind Sie

allerdings ungeduldig, können Sie Ihre Zeile jederzeit vermittels der Tabulator-Taste richtig einrücken

lassen.

Darüber hinaus informiert der Emacs Sie darüber, ob die von Ihnen gerade eingegebene schließende

Klammer mit der letzten öffnenden Klammer übereinstimmt. Sollte dies nicht der Fall sein, bekommen

Sie im Minibuffer die Fehlermeldung Mismatched parentheses.

3.3 Navigation im Puffer Sie navigieren einen Buffer für gewöhnlich mit den Cursor-Tasten. Allerdings gibt es einige

Tastenkombinationen, die Ihnen die Arbeit erleichtern: Um an den Anfang einer Zeile zu gelangen,

drücken Sie Strg-a, um an das Ende zu kommen, Strg-e. Den Anfang des Puffers erreichen Sie mit

Esc-<, das Ende mit Esc->. Dieses Ende wird durch die Position des Cursors nach dem letzten Zeichen

markiert.

Page 13: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

9

3.4 Suche im Puffer Um einen Puffer zu durchsuchen, gibt es mehrere Möglichkeiten. Grundsätzlich unterscheidet der

Emacs zwischen einfacher Suche, wie man sie aus den meisten Textverarbeitungen kennt, Wortsuche,

bei der nur nach vollständigen Wörtern gesucht wird, und inkrementeller Suche.

In diesem Verfahren beginnt die Suche schon mit dem Tippen des ersten Zeichens: Alle Fundstellen

werden farblich unterlegt, und die Eingabe des nächsten Zeichens schränkt den Suchraum weiter ein.

Darüber hinaus können Sie den Puffer anhand regulärer Ausdrücke durchsuchen. Diese stellen weniger

konkrete Zeichenketten dar, als vielmehr Muster für Zeichenketten-Kombinationen. Allen Suchformen

gemein ist, dass sie sowohl vorwärts als auch rückwärts funktionieren.

Die Kommandos/Tastenkombinationen der verschiedenen Suchverfahren lauten:

• Einfache Suche vorwärts: search-forward Strg-s <Eingabetaste>

• Einfache Suche rückwärts: search-backward Strg-r <Eingabetaste>

• Wortsuche vorwärts: word-search-forward Strg-s <Eingabetaste> Strg-w

• Wortsuche rückwärts: word-search-backward Strg-r <Eingabetaste> Strg-w

• Inkrementelle Suche vorwärts: isearch-forward Strg-s

• Inkrementelle Suche rückwärts: isearch-backward Strg-r

• Inkrementelle Suche nach dem Wort, über dem sich der Cursor gerade befindet: Strg-s

Strg-w • Inkrementelle Suche nach der Zeichenkette von der Cursorposition bis zum Zeilenende: Strg-

s Strg-y

Im Emacs kann – ebenso wie in Perl – nicht nur nach einfachen Zeichenketten suchen, sondern

auch anhand vorgegebener Schablonen bestimmte Muster finden. Diese Schablonen nennt man

reguläre Ausdrücke. Sie spielen für computerlinguistische Analysen eine dermaßen große Rolle, dass

sie in einem eigenen Kapitel betrachtet werden sollen. Dennoch sei hier kurz skizziert, wie man sie im

Emacs verwendet.

Die Suche mit regulären Ausdrücken funktioniert so, dass Sie an die Kommandos der einfachen

Suche bzw. der inkrementellen Suche ein -regexp anhängen; bei den Tastenkombinationen drücken

Sie vor der Strg-Sequenz die Esc-Taste (z.B. isearch-forward-regexp Esc Strg-s).5

3.5 Ersetzen im Puffer Es gibt zwei Möglichkeiten, Text zu ersetzen: Einerseits können Sie ihn markieren, um ihn zu

kopieren und an anderer Stelle wieder einzusetzen oder komplett zu löschen. Andererseits können Sie

global alle Fundstellen einer Zeichenkette durch eine andere ersetzen; dieses Verfahren lässt sich durch

Bestätigung der jeweiligen Ersetzung absichern.

Im ersten Fall greift das Konzept des sogenannten kill rings: Dies ist ein interner Zwischenspeicher,

auf dem alle Zeichenketten abgelegt werden, die gelöscht oder kopiert werden sollen. Um eine

Zeichenkette in diesen Speicher zu schieben, muss zunächst mit Strg-Leertaste oder set-mark-

command eine Markierung gesetzt werden, ab der die zu bearbeitende Region anfängt. Positionieren Sie

den Cursor an das Ende dieser Region, lassen sich mit Strg-w oder kill-region die dazwischen

liegenden Zeichen löschen bzw. mit Esc-w oder kill-ring-save kopieren. Mit Strg-k lassen sich die

5 Hinweis zur inkrementellen Suche: Haben Sie alle für Ihre Suche notwendigen Zeichen eingegeben, navigieren Sie zur

nächsten Fundstelle, indem Sie die Tastenkombination erneut drücken.

Page 14: Das Perl-Tutorial für Computerlinguisten

10

Zeichen von der Position des Cursors bis zum Zeilenende löschen und in den kill ring übernehmen.6

Um nun das Gelöschte oder Kopierte wieder einzufügen, positionieren Sie den Cursor an der

gewünschten Stelle und drücken Strg-y bzw. geben yank in den Minibuffer ein.

Zur globalen Ersetzung von Zeichenketten geben Sie im Minibuffer replace-string oder query-

replace-string ein, wenn Sie die Ersetzungen bestätigen wollen.

3.6 Perl-Code im Emacs ausführen Damit Perl-Code im Emacs interpretiert werden kann, gibt es das Kommando compile. Daraufhin

bietet Ihnen Emacs an, den make-Befehl auf eine Datei anzuwenden, die Sie als Argument übergeben

können. Da dies für unsere Zwecke irrelevant ist, löschen wir diesen Befehl und ersetzen ihn durch

perl, einem Leerzeichen und den Namen des aktuellen Puffers (= der Datei, die wir ausführen wollen).

Wenn Sie nun die Eingabetaste drücken, teilt sich das Anwendungs-Fenster in zwei Bereiche auf:

Im oberen sehen Sie ihren Perl-Quellcode, im unteren (mit dem Namen *compilation*) sehen Sie die

Ausgabe Ihres Programms bzw. Status- und Fehlermeldungen oder Warnungen.7 Um im

*compilation*-Puffer z.B. navigieren oder suchen zu können, müssen Sie mit Strg-x b in diesen

anderen Puffer wechseln; eine Liste aller vorhandenen Puffer bekommen Sie mit Strg-x Strg-b

angezeigt. Wollen Sie allerdings nur einen der beiden Puffer sehen, machen Sie mit Strg-x 1 den

aktuellen Puffer wieder zu einem Fenster:

6 Esc-k kopiert nicht bis zum Zeilenende, sondern löscht den Absatz, in dem der Cursor steht (kill-sentence)! 7 Um einen Interpretationsvorgang abzubrechen, geben Sie im Minibuffer kill-compilation ein. Um einen Befehl zu

wiederholen (z.B. nachdem man einen Fehler in seinem Skript beseitigt hat), drückt man Strg-x Esc Esc.

Page 15: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

11

Um einen Puffer zu beenden, drücken Sie Strg-x k oder geben im Minibuffer kill-buffer ein.

Um den Emacs zu beenden, drücken Sie Strg-x Strg-c oder geben das Kommando save-buffers-

kill-emacs ein.

Dies sind bei weitem nicht alle Kommandos, mit denen man den Emacs steuern kann, Sie stellen

nur eine subjektive Auswahl der im alltäglichen Gebrauch nützlichsten Befehle dar. Eine exzellente

Gedächtnisstütze stellt die Emacs Reference Card dar, die Sie unter dieser Adresse

http://www.linguistics.rub.de/~halama/lehre/clp/emacs_usage/refcard.pdf

im PDF-Format finden.

Page 16: Das Perl-Tutorial für Computerlinguisten

12

4 Grundlegende Datenstrukturen

'What brings you to Paris?' (Small talk, not bad.)

'The Eiffel Tower.'

'Do you like towers?'

'I like structures without cladding.'

'OK, it's a good motto'.

JEANETTE WINTERSON

Computer sind Systeme, die aus Hardware und Software bestehen. Diese beiden Komponenten sind

untrennbar miteinander verbunden; gemeinsam besteht ihre Aufgabe darin, eine Eingabe

entgegenzunehmen, sie zu verarbeiten und daraufhin eine Ausgabe zu erzeugen. Die auf der Ein- und

Ausgabeseite anfallenden Informationen nennt man Daten. Diese werden anhand von Algorithmen

verarbeitet.

Ein Algorithmus ist ein Verfahren zur Lösung eines Problems. Dieses muss in endlich vielen

Schritten lösbar sein, wobei der Verlauf der Problemlösung einem festen Schema folgt, das auf ver-

schiedene Eingaben ausführbar ist.

Daten sind Informationen, die für die maschinelle Verarbeitung angepasst sind. Dabei kann es sich

sowohl um Dinge handeln, die am Computer ähnlich repräsentiert werden, wie wir es aus dem

Alltagsleben gewohnt sind oder um solche, die wir am Rechner nur sehr abstrakt erfahren können. Zur

ersten Gruppe zählen Texte, Zahlen, aber auch Bilder oder Geräusche, deren Repräsentationen durch

Anwendungsprogramme ähnlich dem richtigen Leben erfahrbar sind. In die zweite Kategorie fallen

Dinge wie ein Apfel, ein Atom oder das Radfahren: In all diesen Fällen kann man die Eigenschaften

und das Verhalten dieser Dinge in einem Modell repräsentieren, das aber nicht den Grad der Er-

fahrbarkeit besitzt, wie in der ersten Kategorie. Diese Diskrepanz ergibt sich aus der Tatsache, dass

sich die Dinge der ersten Gruppe oftmals auf einfache Datentypen und Datenstrukturen abbilden

lassen, während die Beschreibung der Dinge der zweiten Gruppe eher komplexe Typen und Strukturen

erfordert.

Einfache Datentypen beschreiben grundlegende Informationen wie z.B. einzelne Buchstaben,

Zeichenketten oder Zahlen in verschiedensten Ausprägungen, z.B. als ganze Zahl, Gleitkommazahl

oder als Binär- oder Hexadezimalzahl. Da Perl eine nicht-typisierte Sprache ist, ist die Unterscheidung

zwischen diesen Informationsarten für die weitere Betrachtung gegenstandslos. Allein zwischen Zahlen

und alphabetischen Zeichenketten besteht eine solche Trennung, wie wir im Folgenden noch sehen

werden.

Wesentlich wichtiger ist der Begriff der Datenstruktur. Betrachten wir dazu zunächst das

unvermeidliche Hallo Welt-Beispiel, das die Zeichenkette Hallo Welt auf dem Bildschirm ausgibt:8

8 Damit Sie dieses Beispiel nachvollziehen können, speichern Sie diese Zeile in eine Datei mit der Endung .pl und führen

sie wie oben beschrieben im Emacs aus oder tippen in der Eingabeaufforderung perl dateiname.pl ein.

Page 17: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

13

print "Hallo Welt"; Eine solche Zeile bezeichnet man als Anweisung. Sie besteht aus der Funktion print und deren

Argument, der Zeichenkette Hallo Welt. Jede Anweisung wird durch ein Semikolon abgeschlossen.

Die Zeichenkette Hallo Welt kann in solch einer Anweisung durch eine Ausprägung eines

beliebigen anderen Datentyps ersetzt werden. Eine solche Ausprägung eines Datentyps bezeichnet man

als Literal. Sie besitzt die Eigenschaft, konstant zu sein, d.h., man kann den Wert der Zeichenkette

Hallo Welt nicht verändern.

print "a"; print "42"; print "3.141592"; print "0b1111111"; Die hier verarbeiteten Daten besitzen einen singulären Charakter, weshalb man diese Datenstruktur

als Skalar bezeichnet. Dieser Begriff stammt aus der Mathematik, in der er einzelne Werte von einer

Reihe von Werten abgrenzt, die dort wiederum als Vektoren bezeichnet werden. Die analoge

Datenstruktur zu solch einer Liste heißt in Perl Array. Eine besondere Form solcher Listen, in denen

ein Eintrag aus einem Schlüssel und einem dazugehörigen Wert besteht, nennt man Hash.

4.1 Skalare und Operatoren In Perl bilden Skalare die kleinste Dateneinheit, aus der die größeren Datenstrukturen, Arrays und

Hashes, zusammengesetzt sind. Skalare lassen sich aber nicht nur ausgeben, sondern man kann auch

Operationen auf ihnen durchführen.

Arithmetische Operatoren

Zahlen lassen sich addieren, subtrahieren, multiplizieren, dividieren und potenzieren. Für derartige

Operationen verwendet man die arithmetischen Operatoren +, -, * und /. Das Ergebnis einer solchen

Operation bezeichnet man als Rückgabewert: 9

print 69 + 118; # Ausgabe: 187 print "25 minus 21 ist ", 25-21; # Ausgabe: 4 In diesen Beispielen wird zunächst die Berechnung durchgeführt, die der print-Funktion dann als

Argument dient.

Des weiteren kennt Perl den Modulo-Operator %, dessen Rückgabewert der ganzzahlige Rest einer

Division ist:

print 9 % 4; # Ausgabe: 1

Exponentialrechnung erfolgt anhand des **-Operators:10

print 5 ** 2; # Ausgabe: 25

9 Die Raute # ist das Kommentarzeichen in Perl; sie kommentiert jeweils eine Zeile aus. Einen besonderen Bezeichner für

mehrzeilige Kommentare gibt es in Perl nicht. 10 In einigen anderen Programmiersprachen ist das Caret ^ der Operator für die Exponentialrechnung; in Perl bewirkt der ^-

Operator eine bitweise XOR-Operation zwischen den Operanden!

Page 18: Das Perl-Tutorial für Computerlinguisten

14

Klammerung von Ausdrücken

Wie in der Mathematik üblich, lässt sich die Reihenfolge der Verarbeitung von Werten durch

Klammerung beeinflussen:

print 3 + 7 * 15; # Ausgabe: 108 print (3 + 7) * 15; # Ausgabe: 10 print (3 + 7) * 10; # Ausgabe: 10

Die Resultate der zweiten und dritten Zeile sind überraschend! Für gewöhnlich würde man

erwarten, dass die zweite Berechnung den Wert 150 und die dritte den Wert 100 als Ausgabe hat. Die

Sichtweise des Perl-Interpreters auf diese Anweisung weicht jedoch von der mathematischen ab: Zuerst

wird derjenige Teil ausgeführt, der die höchste Priorität besitzt. Dies ist die Addition innerhalb der

runden Klammern. Da die print-Funktion, die wie ein Operator wirkt, höhere Priorität besitzt als die

Multiplikation, wird sie als nächstes durchgeführt. Der Rückgabewert der print-Funktion ist 1; dieser

Wert wird zurückgegeben, wenn die Ausführung einer Funktion gelingt. Weil dieser Rückgabewert der

Multiplikation als Eingabe dient, wird dieser Teil der Anweisung jeweils zum Wert des anderen

Operanden ausgewertet. Da auf diesem Wert allerdings keine Ausgabe-Funktion mehr durchgeführt

werden kann, wird er verworfen.

Die richtige Klammerung muss also lauten:

print ((3 + 7) * 15); # Ausgabe: 150

Numerische Vergleichsoperatoren

Eine weitere Art von Operatoren stellen die numerischen Vergleichsoperatoren <, >, ==, <=, >= und

!= dar. Ihr Rückgabewert ist 0, wenn der Vergleich zu falsch ausgewertet wird und 1, wenn der

Vergleich zu wahr ausgewertet wird. Wahrheit definiert sich in Perl derart, dass alles wahr ist, was

nicht 0, eine leere Zeichenkette, ein undefinierter Wert oder eine leere Liste ist:

print "Ist zwei gleich vier? ", 2 == 4; # Ausgabe: print "Ist sechs gleich sechs? ", 6 == 6; # Ausgabe: 1 print "Ist zwei ungleich vier? ", 2 != 4; # Ausgabe: 1 print "Ist sieben kleiner als acht? ", 7 < 8; # Ausgabe: 1 print "Ist zwei größer oder gleich zwei? ", 2 >= 2; # Ausgabe: 1 Argumente lassen sich vermittels Bool'scher Operatoren miteinander verknüpfen. Dazu gibt es in

Perl sowohl eine Zeichenketten- als auch eine Symbol-Repräsentation: Den Zeichenketten and, or und

not entsprechen die Symbole &&, || und !. Der Unterschied zwischen diesen beiden Formen liegt in

der Verarbeitungsreihenfolge: Die Symbole besitzen Vorrang vor den Zeichenkettenrepräsentationen:

print 6 > 3 && 12 > 4; # Ausgabe: 1 print 9 > 7 || 8 < 6; # Ausgabe: 1 print !2 > 3; # Ausgabe: print !(2 > 3); # Ausgabe: 1 print 6 > 3 && 3 > 4; # Ausgabe: print 6 > 3 and 3 > 4; # Ausgabe: 1

Page 19: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

15

In der letzten Zeile ergibt sich der Rückgabewert 1 aus der Auswertung des Ausdrucks 6 > 3

aufgrund der niedrigen Priorität von and. Als nächstes wird die print-Funktion ausgewertet, die eben-

falls 1 als Rückgabewert hat, sodass der negative Rückgabewert des Ausdrucks 3 > 4 für die Ausgabe

irrelevant wird.

Zeichenkettenoperatoren

Da Zeichenketten im Fokus der computerlinguistischen Analyse stehen, sei dieser Datentyp ein

wenig näher betrachtet. Zeichenketten bestehen sowohl aus alphanumerischen Zeichen und Inter-

punktion als auch aus Leerraumzeichen (engl. white space). Da sich einige dieser Leerraumzeichen

nicht literal darstellen lassen, müssen sie maskiert (engl. escaped) werden. So werden Zeilenumbrüche

durch die Maskierungssequenz \n dargestellt, während Tabulatoreinschübe durch \t repräsentiert

werden. Diese Ersetzung wird allerdings nur bei Zeichenketten in doppelten Anführungszeichen

wirksam; in einfachen Anführungszeichen wird die Maskierungssequenz als Literal ausgegeben:

print "\tEine einfache Zeichenkette\n"; print '\tEine einfache Zeichenkette\n'; # Ausgabe: \tEine einfache

Zeichenkette\n

Für Zeichenketten gibt es nur wenige Operatoren: Man kann Zeichenketten vermittels des Punkts .

miteinander verknüpfen (Konkatenation von lat. concatenare, "verketten") und sie anhand des

Multiplikators x n-mal wiederholen:

print "Einige " . "Zeichenketten " . "hintereinander.\n"; print "Los! " x3, "\n"; # Ausgabe: Los! Los! Los! Zeichenketten lassen sich durch die Funktionen uc() und lc() zu vollständig groß bzw. klein

geschriebenen Zeichenketten machen:11

print uc("alles groß!\n"); # Ausgabe: ALLES GROß!12 print lc("ALLES KLEIN!\n"); # Ausgabe: alles klein!

Alternativ dazu existieren auch Schreibweisen mit den Maskierungssequenzen \U bzw. \L:

print "\Ualles groß\n"; # Ausgabe: ALLES GROß! print "\LALLES KLEIN!\n"; # Ausgabe: alles klein!

Darüber hinaus gibt es die Möglichkeit, nur den ersten Buchstaben einer Zeichenkette mit

ucfirst() groß und mit lcfirst() klein schreiben zu lassen:

print ucfirst("aller")," ",ucfirst("anfang ist schwer!"); # Ausgabe: Aller Anfang ist schwer!

11 uc ist die Abkürzung für upper case (Großschreibung) und lc für lower case (Kleinschreibung). 12 Man beachte, dass die sz-Ligatur, die in der Großschreibung keine Entsprechung hat, dennoch in der Kleinschreibung

ausgegeben wird!

Page 20: Das Perl-Tutorial für Computerlinguisten

16

Operatoren für Zeichenkettenvergleiche

Zeichenketten lassen sich anhand der Operatoren für Zeichenkettenvergleiche eq, lt, gt, le, ge und

ne miteinander vergleichen. Zeichen werden vom jeweiligen Betriebssystem intern in sogenannten

Codetabellen verwaltet. In solch einer Tabelle ist jedem Zeichen eine Zahl in einer bestimmten Reihen-

folge zugeordnet, über die auf das Zeichen zugegriffen werden kann. Bei einem Zeichenkettenvergleich

werden diese internen Kodierungsinformationen der einzelnen Zeichen miteinander verglichen. Die

entsprechenden Positionszahlen lassen sich anhand der Funktion ord() ermitteln:

print "Ein A ist als ", ord(A), " kodiert\n"; # Ausgabe: 65 print "Ein a ist als ", ord(a), " kodiert\n"; # Ausgabe: 97 print "A" lt "a"; # Ausgabe: 1

Darüber hinaus existiert ein ternärer Vergleichsoperator mit der Bezeichnung cmp, der ebenso wie

sein numerisches Pendant <=> (auch Raumschiff-Operator genannt) zur Sortierung von Werten

eingesetzt wird. Ihre grundsätzliche Funktionsweise lässt sich so beschreiben: Ist der Operand der

linken Seite kleiner als der der rechten Seite, ist der Rückgabewert –1, ist der Operand der rechten Seite

kleiner, wird 1 zurück gegeben, sind beide Operanden gleich, ist der Rückgabewert 0:

print "A" cmp "a"; # Ausgabe: -1

4.2 Variablen Neben Literalen kennen Programmiersprachen auch Variablen. Diese stellen Container für

veränderliche Werte dar, d.h. einem Bezeichner wird ein Wert zugewiesen, der durch Operationen ge-

gen andere Werte ausgetauscht werden kann.

Skalarvariablen

In skalaren Variablen lassen sich alle einfachen Datentypen speichern, ohne dass man den

jeweiligen Typ angeben müsste. Allein die Datenstruktur einer Variablen muss in Perl explizit gemacht

werden. Dazu stellt man dem Bezeichner der Variablen ein Sonderzeichen voran; im Fall eines Skalars

ist dies ein Dollar-Zeichen $. Man weist einer Variablen vermittels des Zuweisungsoperators = einen

Wert zu. Diese Operation bezeichnet man bei der ersten Zuweisung als Initialisierung, im allgemeinen

Fall als Variablendefinition:

$name = "Groucho";

Will man nur den Namen einer Variablen für die weitere Verwendung vereinbaren, schreibt man

diesen, gefolgt von einem Semikolon, auf eine Zeile. Diese Operation heißt Variablendeklaration.

Page 21: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

17

Variablennamen beginnen entweder mit einem Buchstaben oder einem Unterstrich. Danach dürfen

bis zu 250 Buchstaben, Ziffern oder Unterstriche stehen:

$Ich_habe_einen_langen_Variablennamen; # OK $mail-alias; # Nicht OK! $noam chomsky; # Nicht OK! $einfach; # OK $kiste56; # OK $_apFel; # OK $10cc; # Nicht OK!

Groß- und Kleinschreibung von Variablennamen ist bedeutungsunterscheidend:

$name $Name $NAME $nAMe

Auf einer skalaren Variablen lassen sich die gleichen Operationen ausführen, wie auf einem Literal,

wobei immer über den Variablennamen auf den Wert der Variablen zugegriffen wird:

$name = "Groucho\n"; print $name; # Ausgabe: Groucho $zahl1 = 15; $zahl2 = 35; print $zahl1 + $zahl2; # Ausgabe: 50 print "Ist $zahl1 kleiner als $zahl2? ", $zahl1 < $zahl2; # Ausgabe: 1

An dieser Stelle sei kurz auf den Vorteil der Verwendung von Variablen gegenüber Literalen bei

der Berechnung arithmetischer Ausdrücke eingegangen. Im Gegensatz zum bereits besprochenen

Beispiel, in dem die Präzedenz der Funktion print() bei der Klammerung für die Berechnung

berücksichtigt werden musste, funktioniert die Klammerung im Fall von Variablen wie aus der

Mathematik gewohnt:

my $ergebnis1 = (3 + 7) * 15; my $ergebnis2 = (3 + 7) * 10; print "$ergebnis1\n"; # Ausgabe: 150 print "$ergebnis2\n"; # Ausgabe: 100

Der Wert einer Variablen lässt sich ändern, indem man ihr einen neuen Wert zuweist:

$name = "Groucho Marx"; print $name; # Ausgabe: Groucho Marx

Steht ein Variablenname bei der Ausgabe in doppelten Anführungszeichen, wird der dazugehörige

Wert in der Ausgabe interpoliert; in einfachen Anführungszeichen wird wiederum die literale

Zeichenkette ausgegeben:

print "$zahl1 + $zahl2 = ", $zahl1 + $zahl2; # Ausgabe: 15 + 35 = 50 print '$zahl1 + $zahl2 = ', $zahl1 + $zahl2; # Ausgabe: $zahl1 + $zahl2 = 50

Page 22: Das Perl-Tutorial für Computerlinguisten

18

Werte lassen sich auch ändern, indem man numerische oder Zeichenketten-Operationen auf ihnen

ausführt:

$a = 6 * 9; print "Sechs mal neun ist $a\n"; # Ausgabe: Sechs mal neun ist 54 $b = $a + 3; print "Plus drei ist $b\n"; # Ausgabe: Plus drei ist 57 $c = $b / 3; print "Durch drei ist $c\n"; # Ausgabe: Durch drei ist 19 $d = $c +1; print "Eins dazu ist $d\n"; # Ausgabe: Eins dazu ist 20 print "Die Zwischenergebnisse waren $a, $b, $c\n";

# Ausgabe: Die Zwischenergebnisse waren 54, 57, 19, 20

Benötigt man diese Zwischenschritte nicht, verwendet man nur eine Variable:

$a = 6 * 9; print "Sechs mal neun ist $a\n"; # Ausgabe: Sechs mal neun ist 54 $a = $a + 3; print "Plus drei ist $a\n"; # Ausgabe: Plus drei ist 57 $a = $a / 3; print "Durch drei ist $a\n"; # Ausgabe: Durch drei ist 19 $a = $a +1; print "Eins dazu ist $a\n"; # Ausgabe: Eins dazu ist 20

Die Anwendung von Operatoren und die Variablenzuweisung lassen sich in einem Schritt

vollziehen, indem man dem Zuweisungsoperator den jeweiligen Operator voranstellt und die Variable

auf der rechten Seite der Zuweisung eliminiert:

$a = 6 * 9; print "Sechs mal neun ist $a\n"; # Ausgabe: Sechs mal neun ist 54 $a += 3; print "Plus drei ist $a\n"; # Ausgabe: Plus drei ist 57 $a /= 3; print "Durch drei ist $a\n"; # Ausgabe: Durch drei ist 9 $a += 1; print "Eins dazu ist $a\n"; # Ausgabe: Eins dazu ist 10

Diese Schreibweise lässt sich in Fällen, in denen ein Wert um eins erhöht oder erniedrigt werden

soll, noch kompakter gestalten. Dazu verwendet man den Autoinkrement-Operator ++ bzw. den

Autodekrement-Operator --. Diese lassen sich vor bzw. hinter die Variable schreiben. Der Unterschied

besteht darin, dass in der ersten Variante zuerst die Operation und dann die Zuweisung ausgeführt wird,

während in der Postfix-Schreibweise zuerst die Zuweisung und dann die Operation ausgeführt wird,

was u.U. zu unerwünschten Seiteneffekten führen kann:

$a=4; $b=10; print "Initial haben wir die Werte $a und $b\n"; # 4 und 10 $b = $a++;

print "Jetzt haben wir $a und $b\n"; # 5 und 4 $b = ++$a*2;

print "Nun sind es $a und $b\n"; # 6 und 12 $a = --$b+4;

print "Zum Schluss haben wir $a und $b\n"; # 15 und 11

Page 23: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

19

In der vierten Zeile wird zuerst zugewiesen, dann hochgezählt. In Zeile 6 zunächst inkrementiert,

dann multipliziert und danach findet die Zuweisung statt. In der vorletzten Zeile wird $b erst um eins

vermindert und um 4 erhöht und wird dann $a zugewiesen.

Nützliche Pragmata

Die bis hierhin verwendete Art der Variablendeklaration bzw. –definition birgt die Gefahr, dass der

Perl-Interpreter bei der Operation auf einer vorher bereits deklarierten/definierten Variablen einen

falsch geschriebenen Bezeichner als neue Variable interpretiert und versucht, die Operation darauf

auszuführen. Dies muss nicht immer zu einer Fehlermeldung führen, die ausgegebenen Ergebnisse

können aber sehr irritierend sein. Um dies zu vermeiden, gibt es das Perl-Pragma strict, das u.a.

solche Inkonsistenzen aufzuspüren vermag. Darüber hinaus empfiehlt es sich, vom Perl-Interpreter

möglichst ausführliche Fehler- und Warnungstexte13 ausgeben zu lassen, damit die Quelle eines Pro-

blems möglichst schnell und einfach aufgespürt werden kann. Dazu bedient man sich eines weiteren

Pragmas, das den Namen diagnostics trägt. Allgemein gesagt sind Pragmata

Verarbeitungsanweisungen an den Perl-Interpreter, die ihn zu einem bestimmten Verhalten bei der

Ausführung eines Perl-Programms veranlassen. Sie werden mit der Funktion use in den Quelltext

eingebunden:

use strict; use diagnostics;

Dies hat zur Konsequenz, dass Variablen bei ihrer Deklaration oder Definition das Schlüsselwort my

vorangestellt werden muss:14

my $name = "Groucho"; print $anme; # Ausgabe: Fehlermeldung!

Ohne use strict; bekäme man an dieser Stelle keine Fehlermeldung, sondern lediglich eine leere

Ausgabe, die bei der Fehlersuche nicht sonderlich hilfreich ist, insbesondere, wenn es sich um mehr als

zwei Zeilen Code handelt...

4.3 Konstanten Perl kennt keine eigene Datenstruktur für Werte, die in einem Container stehen, aber nicht

veränderbar sein sollen. Es hat sich allerdings eingebürgert, Konstanten in Variablen abzuspeichern,

deren Bezeichner man komplett groß schreibt:

my $PI = 3.141 my $NAME = "André";

13 Anders als eine Fehlermeldung führt eine Warnung nicht zum Abbruch des Programms, sondern informiert den

Programmierer über Stellen im Quellcode, die problematisch erscheinen. 14 Die eigentliche Funktionalität von my erschließt sich erst richtig, wenn in Kapitel 8 Subroutinen eingeführt werden.

Page 24: Das Perl-Tutorial für Computerlinguisten

20

4.4 Exkurs: Standardein- und -ausgabe Die bis jetzt verwendeten Daten hatten wir immer in das jeweilige Programm hineingeschrieben.

Sie waren dementsprechend für jeden Programmlauf statisch im Code selbst vorhanden. Will man eine

Möglichkeit schaffen, die Dateneingabe flexibler zu gestalten, muss man die Daten vom Programm

abtrennen. Dazu müssen wir zunächst verstehen, wie man die Tastatur während der Ausführung eines

Programms verwendet.

Das Ergebnis einer print-Funktion wird für gewöhnlich auf dem Bildschirm ausgegeben. Diesen

Kanal bezeichnet man auch als Standardausgabe. Analog dazu gibt es auch eine Standardeingabe: In

der Eingabeaufforderung lassen sich über die Tastatur Daten eingeben, auf denen eine Anwendung

operieren soll. Diese Kanäle tragen in Perl die Bezeichnungen <STDOUT> bzw. <STDIN>. Da die print-

Funktion ohne weitere Angaben immer ihre Ausgabe auf <STDOUT> ausgibt, muss man diesen Kanal

nicht explizit angeben. Bei der Standardeingabe werden zunächst Daten aus <STDIN> eingelesen und

einer Variablen übergeben, auf der dann Operationen ausgeführt werden können:

print "Wie heißt Du?\n"; my $name = <STDIN>; print "Hallo, $name";

Da die Tastatureingabe mit dem Druck der Return-Taste abgeschlossen wird, findet sich dieser

Zeilenumbruch auch in der Variablen $name. Um ihn zu entfernen, bedient man sich der Funktion

chomp(), die solch ein Zeilenendezeichen aus einer Zeile beseitigt:15

print "Wie heißt Du?\n"; my $name = <STDIN>; chomp $name; print "Hallo, $name\n";

4.5 Listen Listen kennt man aus dem alltäglichen Leben, z.B. als Handlungsanweisungen in Rezepten:

Sie besitzen die Eigenschaft, aus einzelnen Listenelementen zu bestehen, die geordnet sind. Die

einfachste Form der Liste in Perl ist die leere Liste. Sie wird durch eine öffnende und eine schließende

runde Klammer () dargestellt. Zahlenwerte lassen sich ohne Modifikationen in diese Klammern

schreiben, z.B. (42), während Zeichenketten in Anführungszeichen gesetzt werden müssen: ("Käse").

Einzelne Listenelemente werden durch Kommata voneinander getrennt:

15 Parallel dazu gibt es eine Funktion mit der Bezeichnung chop(), die jedes beliebige Zeichen am Zeilenende entfernt.

Page 25: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

21

("Käse", "Wurst", "Brot")

Eine Funktion, die auf Listen operiert, ist bspw. die print()-Funktion: Sie nimmt eine Liste von

Argumenten, die sie ausgibt:

print ("Hallo ", "Welt", "\n");

Wie bei allen eingebauten Perl-Funktionen können die Klammern um die Argumentliste wegfallen:

print "Hallo ", "Welt", "\n";

Listen dürfen sowohl numerische als auch Zeichenkettenwerte enthalten:

use strict; use diagnostics; my $zahl = 30; print "Hier haben wir eine Liste, die Zeichenketten (diese), ", "Zahlen (", 3.6, "), und Variablen: ", $zahl, " enthält.", "\n";

Da der Perl-Interpreter beliebigen (oder gar keinen) Leerraum zwischen den Listenelementen

erwartet, darf man eine solche Anweisung beliebig formatieren.

Alternativen zu Anführungszeichen

Wie schon bei Skalaren gezeigt, werden Werte in einfachen Anführungszeichen literal dargestellt,

während Variablen und Maskierungssequenzen in doppelten Anführungszeichen interpretiert werden.

Perl kennt alternativ zu den Anführungszeichen die Operatoren q// und qq//:

print q/Eine Zahl: /, q/$zahl/; # Ausgabe: $zahl print qq/Eine Zahl: /, qq/$zahl/; # Ausgabe: 30

Das paarige Trennzeichen // kann durch ein beliebiges anderes paariges Trennzeichen ersetzt

werden. Dabei können durchaus auch Zeichen eingesetzt werden, die ansonsten als Operatoren dienen:

print qq(Eine Zahl: ), qq%$zahl%; # Ausgabe: 30

In Listen gibt es darüber hinaus den qw//-Operator, mit dem die Kommata zum Abtrennen von

Listenelementen durch Leerraum ersetzt werden. Allerdings erfolgt immer eine literale Ausgabe, d.h.,

auch Anführungszeichen werden ausgegeben; Leerraumzeichen werden dabei überlesen:

print qw/"Eine Zahl: " $test/; # Ausgabe: "EineZahl"$test

Page 26: Das Perl-Tutorial für Computerlinguisten

22

Eindimensionalität von Listen

Listen in Perl besitzen die Eigenschaft, flach zu sein, d.h., man kann nicht ohne weiteres Listen in

eine bestehende Liste einbetten. Dies ergibt sich aus dem Umstand, dass die Elemente einer Liste

immer Skalare sein müssen!

(3, 8, 5, 15) ((3, 8), (5, 15)) (3, (8, 5), 15) ("eins", "zwei", "drei", "vier") (('eins', 'zwei', 'drei', 'vier')) (qw(eins zwei drei), 'vier') (qw(eins zwei), q(drei), 'vier') (qw(eins zwei drei vier))

Zugriff auf Listen

Geordnet werden die Listenelemente vermittels Indizes. Ein solcher Index beginnt immer mit 0,

d.h., das erste Element einer Liste besitzt den Index 0, das zweite den Index 1, etc. Um über solch einen

Index auf ein Listenelement zugreifen zu können, schreibt man den Index in eckigen Klammern [ ]

hinter die Liste:

print 'Salz', 'Essig', 'Senf', 'Pfeffer' [2]; # Ausgabe: Senf

Anstatt anhand der literalen Zahl des Index auf ein Element zuzugreifen, kann man auch eine

Variable an diese Stelle setzen:

my $monat = 4; print qw(Januar Februar März April Mai Juni Juli August September Oktober

November Dezember) [$monat]; # Ausgabe: Mai

Verwendet man statt einer ganzen Zahl eine Gleitkommazahl, wird der Nachkommawert ignoriert:

my $monat = 4.9; print qw(Januar Februar März April Mai Juni Juli August September Oktober

November Dezember) [$monat]; # Ausgabe: Mai

Gibt man eine negative Zahl als Index an, greift man vom hinteren Ende der Liste auf die Elemente

zu: [-1] bezeichnet das letzte Element einer Liste, [-2] das vorletzte, etc.:

print qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) [-8]; # Ausgabe: Mai

Will man auf mehrere Elemente einer Liste zugreifen, schreibt man statt eines Skalars eine Liste in

eckige Klammern. Diese Operation bezeichnet man als Slice; ihr Rückgabewert ist eine Liste:

print qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) [3, 4, 5]; # Ausgabe: AprilMaiJuni

Page 27: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

23

Anstatt einzelne Elemente zu spezifizieren, lassen sich auch Bereiche durch zwei Punkte angeben:

print (1 .. 6); # Ausgabe: 123456

Es gilt wiederum, dass die Nachkommastellen von Gleitkommazahlen ignoriert werden:

print (1.4 .. 6.9); # Ausgabe: 123456

Bei der Verwendung negativer Werte gilt, dass der rechts stehende Wert größer sein muss als der

linke:

print (-6 .. 6); # Ausgabe: -6-5-4-3-2-10123456

Will man die umgekehrte Reihenfolge einer Liste erzeugen, verwendet man die Funktion

reverse():

print reverse(-6 .. 6) # Ausgabe: 6543210-1-2-3-4-5-6

Ebenso wie für Zahlenliterale lassen sich auch für Zeichenkettenliterale Bereiche angeben:

print ('a' .. 'k'); # Ausgabe: abcdefghijk

Zwar ist es in Perl möglich, von einem numerischen in einen Zeichenkettenbereich überzuleiten

oder umgekehrt, führt aber zu einer Warnung:16

print (3 .. 'k'); # Ausgabe: Argument "k" isn't numeric in range (or flop)

print ('k' .. 3); # Ausgabe: 0123

Bereiche lassen sich auch in Slices verwenden, um auf eine Reihe von Indizes zugreifen zu können:

print qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) [4 .. 6]; # Ausgabe: MaiJuniJuli

Zuweisung von mehreren Werten an eine Liste

Listen lassen sich sowohl auf der rechten als auch auf der linken Seite einer Zuweisung verwenden:

my ($eins, $zwei) = (1, 2);

Weist man einer Liste eine Liste mit den gleichen Elementen in veränderter Reihenfolge zu, wird

zunächst die Liste auf der rechten Seite aufgebaut, die dann der Liste auf der linken Seite zugewiesen

wird:

my ($eins, $zwei) = ($zwei, $eins); # $eins = 2, $zwei = 1

16 Das Zeichenkettenliteral wird als 0 interpretiert, weshalb es im ersten Beispiel ausschließlich zur Ausgabe der Warnung

kommt, im zweiten aber eine Zahlenreihe ausgegeben wird!

Page 28: Das Perl-Tutorial für Computerlinguisten

24

4.6 Arrays Wie schon bei den Skalaren gesehen, gibt es auch für Listen eine Datenstruktur, in der sie als

Variablen gespeichert werden können. Diese nennt man in Perl Array und sie wird durch den

Klammeraffen @ als Präfixsymbol repräsentiert. Listen lassen sich Arrayvariablen genauso zuweisen

wie skalare Daten an Skalarvariablen:

my @tage = qw(Mo Di Mi Do Fr Sa So);

Bei der Ausgabe von Arrays ist zu beachten, dass sich Arrayvariablen innerhalb von doppelten

Anführungszeichen anders verhalten, als wenn man diese weglässt:

print @tage; # Ausgabe: MoDiMiDoFrSaSo print "@tage"; # Ausgabe: Mo Di Mi Do Fr Sa So

Skalare Variablen, die den gleichen Bezeichner verwenden wie Arrayvariablen, sind von diesen

verschiedenund umgekehrt:

my @tage = qw(Mo Di Mi Do Fr Sa So); my $tage = 31; print "@tage\n"; # Ausgabe: Mo Di Mi Do Fr Sa So print $tage; # Ausgabe: 31

Listenkontext vs. Skalarkontext

Weist man einem Array ein anderes Array zu, erwartet die Variable auf der linken Seite der

Zuweisung eine Liste. Diese Operation findet im sogenannten Listenkontext statt:

my @array1 = qw(Chico Harpo Groucho Gummo Zeppo); my @array2 = @array1; # @array2 enthält die Elemente

Chico, Harpo, Groucho, Gummo und Zeppo

Weist man einer Skalarvariable ein Array zu, erzwingt diese implizit einen skalaren Kontext. In ihm

wird der Rückgabewert der Zuweisung eines Arrays an einen Skalar zur Länge des Arrays:

my @array = qw(Chico Harpo Groucho Gummo Zeppo); my $skalar = @array; # $skalar besitzt den Wert 5

Man kann den Skalarkontext für ein Array auch vermittels der Funktion scalar() auch explizit

erzwingen; das Ergebnis ist zum vorher gezeigten Beispiel identisch:

print scalar(@array); # Ausgabe: 5

Page 29: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

25

Zuweisung von Werten an ein Array

Eine weitere Art, Arrays zu erweitern nutzt die Eigenschaft von Listen aus, grundsätzlich

eindimensional zu sein:

my @array1 = (1, 2, 3); my @array2 = (@array1, 4, 5, 6); print @array2; # Ausgabe: 123456 @array2 = (3, 5, 7, 9); @array2 = (1, @array2, 11); print @array2; # Ausgabe: 1357911

Einer Liste einzelner Elemente kann man umgekehrt auch ein Array zuweisen:

my @array = (10, 20, 30); my ($element1, $element2, $element3) = @array; print "Der erste Skalar ist: $element1\n" # Ausgabe: 10 print "Der zweite Skalar ist: $element2\n" # Ausgabe: 20 print "Der dritte Skalar ist: $element3\n" # Ausgabe: 30

Ebenso lässt sich einem Skalar ein einzelnes Element zuweisen:

my $a = (10, 20, 30)[0]; # $a hat den Wert 10 $a = $array[0]; # $a hat den Wert 10

Zugriff auf Arrays

Eine beliebte Stolperfalle stellt die Vorstellung dar, man müsse mit dem Präfixsymbol @ auf das

Array zugreifen. Entscheidend ist an dieser Stelle nicht die Datenstruktur, von der ausgegangen wird,

sondern die Datenstruktur des Rückgabewerts. Da dieser im letzten Beispiel ein Skalar war, muss auch

über das Präfixsymbol dieser Datenstruktur, das Dollarzeichen $, auf das Array zugegriffen werden!

Folgende Syntax führt zu einer Warnung:

$a = @array[0];

Hier wird versucht, der Skalarvariablen $a einen einelementigen Arrayslice zuzuweisen. Man kann

eine entsprechende Meldung des Perl-Interpreters abfangen, indem man das Array durch Klammerung

explizit zu einer Liste macht:

$a = (@array)[0];

Die korrekte Verwendung eines Arrayslices als Zugriffsmöglichkeit auf mehrere Elemente eines

Arrays hat eine Liste als Rückgabewert, weshalb man an dieser Stelle richtigerweise den Klammeraffen

@ verwendet:

my @array1 = (1, 3, 5, 7, 9); my @array2 = @array1[1, 3 .. 4]; print "@array2"; # Ausgabe: 3 7 9

Page 30: Das Perl-Tutorial für Computerlinguisten

26

Ein Arrayslice funktioniert also wie einfacher Filter für Listen, bei dem die Position auf der Liste

das einzige Kriterium darstellt. Komplexere Filter werden im Laufe des Kurses folgen.

Auf einzelne Elemente eines Arrays lässt sich ebenso wie mit numerischen Skalarliteralen auch mit

Skalarvariablen zugreifen:

my @array1 = (1, 3, 5, 7, 9); my $index = 3; print $array[$index]; # Ausgabe: 7

Operationen auf Arrays

Da ein Array die Eigenschaft besitzt, eine geordnete Liste zu repräsentieren, kommen die Elemente

in einer bestimmten Reihenfolge auf diese Liste:

Man kann sich solch eine Liste also als Stapel vorstellen, auf den man vorne, hinten und mittendrin

Elemente platzieren kann:

Page 31: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

27

Um Elemente sukzessive auf eine Liste zu befördern, wendet man die Funktion push() mit einem

listenwertigen Argument auf ein Array an. Meist ist dies allerdings eine einelementige Liste:

push my @array, 1; push @array, 2; print "@array"; # Ausgabe: 1 2

Um Elemente vom rechten Ende einer Liste zu entfernen, wendet man die Funktion pop() auf ein

Array an. Der Rückgabewert dieser Funktion ist derjenige Skalar, der von der Liste entfernt wurde:

pop @array; print "@array"; # Ausgabe: 1 my $element = pop @array; print "$element\n"; # Ausgabe: 1 print "@array"; # Ausgabe:

Um Elemente von links auf eine Liste zu befördern, wendet man die Funktion unshift() wiederum

mit einem listenwertigen Argument auf ein Array an, wobei auch hier die Liste meist nur aus einem

Element besteht:

unshift @array, 1; unshift @array, 2; print "@array"; # Ausgabe: 2 1

Um Elemente von der linken Seite einer Liste zu entfernen, wendet man die Funktion shift() auf

ein Array an. Der Rückgabewert dieser Funktion ist wiederum derjenige Skalar, der von der Liste

entfernt wurde:

shift @array; print "@array"; # Ausgabe: 1 my $element = shift @array; print "$element\n"; # Ausgabe: 1 print "@array"; # Ausgabe:

Löschoperationen und die Länge von Arrays

Anders als in vielen anderen Programmiersprachen muss die Länge eines Arrays nicht vor seiner

Benutzung festgelegt werden. Hat man ein Array deklariert, kann man ein Element an beliebiger Stelle

einfügen:

my @array; $array[999] = 3; print "@array"; # Ausgabe: 998 mal undef, 317

Mit der Funktion delete() lassen sich Werte anhand ihrer Indizes aus einem Array löschen, bzw.

durch undef ersetzen. Die Länge des Arrays ändert sich nur, wenn man das letzte Element löscht. Der

Rückgabewert der delete()-Funktion ist das gelöschte Element:

17 Die Verwendung undefinierter Werte führt allerdings zu einer Warnung; die Vorbelegung eines Arrays auf diese Weise

macht in Perl nicht sonderlich viel Sinn, sondern kostet nur Speicherplatz!

Page 32: Das Perl-Tutorial für Computerlinguisten

28

my @alphabet = ('a' .. 'z'); my $buchstabe = delete($alphabet[24]); print $buchstabe; print @alphabet ,"\n"; # Ausgabe:

abcdefghijklmnopqrstuvwxz print scalar(@alphabet); # Ausgabe: 26

Löschen und Einfügen von Bereichen

Mit der Funktion splice() lassen sich Bereiche ab einem bestimmten Index im Array löschen.

Dazu übergibt man der Funktion den Namen des Arrays, den Startwert18 des Index, ab dem die

Operation ausgeführt werden soll und die Anzahl der Elemente, die gelöscht werden sollen. Der

Rückgabewert dieser Funktion ist eine Liste der gelöschten Elemente; dieser Seiteneffekt lässt sich sehr

gut für computerlinguistische Analysen ausnutzen, wenn man bspw. sukzessive auf Bereichen eines

Arrays operieren will, wie wir später noch sehen werden. Anders als delete() verändert splice() die

Länge eines Arrays:

my @alphabet = ('a' .. 'z'); my @buchstaben = splice(@alphabet, 5, 3); print "@buchstaben\n"; # Ausgabe: f g h print @alphabet, "\n"; # Ausgabe:

abcdeijklmnopqrstuvwxyz print scalar(@alphabet); # Ausgabe: 23

Übergibt man splice() als viertes Argument eine Liste oder ein Array, wird dieses an die Stelle

der soeben gelöschten Elemente eingefügt:

my @alphabet = ('a' .. 'z'); my @ziffern = (1 .. 3); my @buchstaben = splice(@alphabet, 5, 3, @ziffern); print "@buchstaben\n"; # Ausgabe: f g h print @alphabet, "\n"; # Ausgabe:

abcde123ijklmnopqrstuvwxyz print scalar(@alphabet); # Ausgabe: 26

Sortieren

Mit der Funktion sort() lassen sich Listen und Arrays sortieren. Da diese Funktion

zeichenkettenbasiert ist,19 verwendet sie inhärent den cmp-Operator für den Vergleich:

my @buchstaben = qw(g h e d f c b a); my @zahlen = qw(56 1 234); print sort(@buchstaben), "\n"; # Ausgabe: abcdefgh my @zahlen_sortiert = sort(@zahlen); print "@zahlen_sortiert"; # Ausgabe: 1 234 56

18 Einen solchen Startwert bezeichnet man auch als Offset. 19 In der Literatur wird diese Art der Sortierung manchmal auch als ASCII-betisch bezeichnet...

Page 33: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

29

Will man Zahlen numerisch sortieren, verwendet man den im Abschnitt über Vergleichsoperatoren

eingeführten Raumschiff-Operator <=>. Beide verwenden die speziellen Variablen $a und $b,20 die als

Platzhalter für die zu vergleichenden Werte stehen, wobei $a das erste Argument und $b das zweite re-

präsentiert. Hierbei ist zu beachten, dass die Verwendung dieser Variablen als linker bzw. rechter

Operand die Art der Sortierung beeinflussen: Wie bereits erwähnt ist der Rückgabewert –1 wenn der

Operand der linken Seite kleiner als der der rechten Seite ist, ist der Operand der rechten Seite kleiner,

wird 1 zurück gegeben, sind beide Operanden gleich, ist der Rückgabewert 0. Daher wird aufsteigend

sortiert, wenn $a auf der linken Seite des Operators steht und absteigend, wenn er auf der rechten Seite

steht. Analog gilt dies selbstverständlich für den cmp-Operator.

Darüber hinaus ist zu beachten, dass bei dieser Art der Sortierung der Operator und seine

Operanden in einem sogenannten Block stehen. Da wir das Konzept des Blocks erst richtig definieren,

wenn wir über Kontrollstrukturen sprechen, sei er an dieser Stelle vorläufig eingeführt als Menge von

Anweisungen, die zwischen geschweiften Klammern {} steht:

my @zahlen = qw(56 1 234); my @aufsteigend_sortiert = sort({$a <=> $b} @zahlen); my @absteigend_sortiert = sort({$b <=> $a} @zahlen); print "@aufsteigend_sortiert\n"; # Ausgabe: 1 56 234 print "@absteigend_sortiert\n"; # Ausgabe: 234 56 1

4.7 Hashes Ähnlich wie Arrays sind Hashes Container für Listen. Allerdings besitzen diese Listen die

Eigenschaft, aus Schlüsseln und dazugehörigen Werten zu bestehen.21 Ein solches Schlüssel-

/Wertepaar bildet ein Element im Hash. Anstatt des Dollarzeichens $ oder des Klammeraffens @

verwendet man für diese Datenstruktur das Prozentzeichen % als Präfixsymbol. Wie schon aus Listen

und Arrays bekannt, trennt man die Elemente in einem Hash durch Kommata. Zwischen dem Schlüssel

und seinem Wert steht der Operator =>, der weitestgehend synonym zum Komma ist.

Wie in Arrays üblich, müssen Zeichenketten in Anführungszeichen gesetzt werden. In Hashes

beschränkt sich dies allerdings auf Zeichenketten, die den Wert eines Schlüssels darstellen.22 Anders

als in Arrays kann man aber in Hashes die alternativen Anführungszeichen qw nicht verwenden!

Einen Hash kann man sich z.B. als Adressbuch vorstellen, in dem jeder Bekannte an einem

bestimmten Ort wohnt. Da sich (noch) keiner unserer Bekannten einen Zweitwohnsitz leisten kann,

können wir einen solchen auch (noch) nicht abbilden; dennoch dürfen mehrere Bekannte an einem Ort

wohnen:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt");

20 Ihr Status ist in zweierlei Hinsicht besonders: Einerseits müssen diese Variablen vor der Verwendung nicht deklariert

werden, andererseits enthalten sie automatisch die aktuellen Werte aus der zu sortierenden Struktur für den Vergleich. 21 Deshalb hat man sie früher assoziative Arrays genannt. 22 Tut man dies nicht, wirft das strict-Pragma eine Fehlermeldung auf, laut derer ein Literal an dieser Stelle nicht erlaubt

ist!

Page 34: Das Perl-Tutorial für Computerlinguisten

30

Überträgt man das eben gesagte auf die Situation in Perl-Hashes, ist festzuhalten, dass ein Wert

zwar in mehreren Schlüsseln vorhanden sein darf, ein Schlüssel aber nur genau einmal existieren darf:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt", Frank => "Darmstadt", Peter => "Potsdam"); # Falsch!

Zugriff auf Hashes

Diese Eindeutigkeit muss sichergestellt werden, da man in Hashes nicht wie in Arrays über Indizes

auf die Werte zugreift, sondern über die Schlüssel! Daraus resultiert eine weitere Eigenschaft von

Hashes, nämlich nicht sortiert zu sein. Dies bedeutet, dass man weder von der Reihenfolge, in der man

die Einträge erzeugt hat noch auf eine andere Art auf die interne Ordnung eines Hashes schließen

kann.23

Um über einen Schlüssel auf einen Wert zuzugreifen, verwendet man wie bei Arrays Klammern:

Schrieb man für den Zugriff auf ein Element dieser Datenstruktur eine Zahl in eckige Klammern [ ],

setzt man bei Hashes den Bezeichner des Schlüssels in geschweifte Klammern {}:

my $franks_wohnort = $adressbuch{Frank}; print $franks_wohnort; # Ausgabe: Darmstadt

Ähnlich wie bei Arrays kann man auch in Hashes auf die Werte mehrerer Schlüssel zugreifen.

Diese Operation bezeichnet man folgerichtig als Hashslice. Da der Rückgabewert eine Liste der Werte

ist und kein Skalar oder Hash, verwendet man den Klammeraffen @ als Präfixsymbol für den Zugriff

auf diese Datenstruktur. Allein die geschweiften Klammern {} zeigen in diesem Fall an, dass man auf

einem Hash operiert:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); my @wohnorte = @adressbuch{"Susanne", "Andreas"}; print "@wohnorte"; # Ausgabe: Berlin Darmstadt

Wird ein Hash einem Array zugewiesen, beinhaltet dieses danach an den geradzahligen Indizes

(angefangen bei 0) die Schlüssel und an den ungeradzahligen Indizes die Werte. Wird umgekehrt ein

Array einem Hash zugewiesen, verwendet der Hash die geradzahligen Indizes als Schlüssel und die

ungeradezahligen Indizes als Werte.

23 Eine der wenigen Pluspunkte von Java: In dieser Programmiersprache existieren geordnete Hashes, die dort TreeMap

heißen.

Page 35: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

31

Ebenso lassen sich alle Schlüssel bzw. alle Werte an ein Array übergeben. Im ersten Fall verwendet

man dazu die Funktion keys(), für Werte die Funktion values():

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); my @namen = keys(%adressbuch); print "@namen\n"; # Ausgabe: Susanne Peter Andreas my @wohnorte = values(%adressbuch); print "@wohnorte"; # Ausgabe: Bonn Darmstadt Berlin

Ähnlich wie bei Arrays kann man auch für diese beiden Funktionen den Skalarkontext erzwingen,

um an die Anzahl der Elemente im Hash zu gelangen. Die folgenden vier Zeilen sind synonym

zueinander:

my $elemente = scalar(keys(%adressbuch)); # oder my $elemente = scalar(values(%adressbuch)); # oder my $elemente = keys(%adressbuch); # oder my $elemente = values(%adressbuch); print $elemente; # Ausgabe: 3

Operationen auf Hashes

Wie bei Arrays kann man mit der Funktion delete() ein Schlüssel-/Wert-Paar aus dem Hash

löschen. Der Rückgabewert ist der Wert dieses Elements:

my $geloescht = delete($adressbuch{Peter}); print $geloescht; # Ausgabe: Bonn

Man fügt einem Hash ein Element hinzu, indem man einem neuen Schlüssel einen Wert zuweist.

Da diese beiden Datenstrukturen wie immer Skalare sind, verwendet man wie beim Zugriff auf ein

Element eines Hashes das Dollar-Präfix $; die Hashstruktur wird wiederum durch die geschweiften

Klammern identifiziert. Bei dieser Operation handelt es sich allerdings um eine Zuweisung, sodass man

an dieser Stelle nicht den Komma-Operator =>, sondern – wie gewohnt – den Zuweisungsoperator =

verwendet:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); $adressbuch{Thomas} = "Essen";

Existiert ein Schlüssel bereits, wird gemäß der Eindeutigkeitsbedingung keine neuer Schlüssel

eingefügt, sondern der Wert des vorhandenen Schlüssels modifiziert:

$adressbuch{Susanne} = "Hamburg"; print $adressbuch{Susanne}; # Ausgabe: Hamburg

Genauso wie bei Arrayslices lassen sich auch Hashslices Listen zuweisen. Da es sich bei Hashslices

wie gesagt um Listen handelt, verwendet man wiederum den Klammeraffen @ als Präfixsymbol:

@adressbuch{"Gabi", "Hans"} = ("London", "Madrid");

Page 36: Das Perl-Tutorial für Computerlinguisten

32

4.8 Zusammenfassung In diesem Kapitel wurde gezeigt, wie elementare Datenstrukturen in Perl aussehen:

• Skalar: Als grundlegendste Datenstruktur besteht er aus einem singulären Datum, wie z.B.

einer Zahl oder einer Zeichenkette. Diese treten sowohl als Literale als auch als Variablen auf:

Literale sind konstante Werte, die einer Variablen zugewiesen werden können und auf denen

man verschiedene Operationen durchführen kann. Variablen sind dementsprechend Container

für unterschiedlichste Werte, die durch Zuweisung an den Bezeichner der Variablen verändert

werden können. Eine Skalarvariable wird durch das Dollarzeichen $ als Präfixsymbol gekenn-

zeichnet.

Auf Zahlen lassen sich arithmetische Operationen anhand der Operatoren +, -, *, /, **

(Exponentiation) und % (Modulus) durchführen, während Zeichenketten anhand des

Konkatenationsoperators . mit einander verbunden werden können und durch den

Multiplikationsoperator x wiederholt werden. Auf beiden Datentypen lassen sich Vergleiche

durchführen, wobei die Symbole ==, <, >, <=, => und != auf Zahlen angewandt werden,

während eq, lt, gt, ge, le und ne Zeichenketten miteinander vergleichen.

• Liste: Diese Datenstruktur besteht aus einer Reihe von Skalaren. Auf ein einzelnes Element

daraus kann man vermittels Indizes, die als Zahlen in eckige Klammern [ ] geschrieben

werden, zugreifen. Zu beachten ist, dass die Indexzählung bei 0 beginnt!

• Array: Ein Array ist ein Variablencontainer für eine Liste; das entsprechende Präfixsymbol ist

der Klammeraffe @. Auf dieser Datenstruktur kann man Skalare am Anfang und am Ende der

Liste einfügen und löschen. Dazu dienen die Funktionen unshift()/shift() und

push()/pop(). Anhand eines Indexes kann man Elemente an beliebiger Stelle einfügen und

ihren Wert mit der Funktion delete() löschen. Darüber hinaus lassen sich mehrere Elemente

gleichzeitig vermittels der Funktion splice() aus dieser Datenstruktur entfernen.

• Listen und Arrays lassen sich mit sort() zeichenkettenbasiert sortieren. Dies bedeutet, dass

Zahlen nicht numerisch, sondern nach ihrer Ziffernfolge eingeordnet werden. Will man Zahlen

numerisch sortieren, verwendet man den Raumschiff-Operator <=>, der als Operanden die

Variablen $a und $b benötigt, in denen die Werte des ersten bzw. zweiten zu sortierenden

Operanden automatisch abgelegt sind.

• Hash: Ein Hash ist ein Variablencontainer für eine Liste, die aus Schlüsseln und Werten

besteht. Sein Präfixsymbol ist das Prozentzeichen %. Anders als bei Arrays greift man nicht

über Indizes auf die Elemente eines Hashes zu, sondern über den jeweiligen Schlüssel eines

Elements. Um an alle Schlüssel bzw. Werte eines Hashes zu gelangen, bedient man sich der

Funktionen keys() bzw. values(). Ebenso wie in Arrays kann man mit exists() – dazu im

nächsten Kapitel mehr – überprüfen, ob ein Element in einem Hash vorhanden ist oder nicht

und es mit delete() löschen.

Bei der Verwendung von Datenstrukturen muss unbedingt die Art des Rückgabewerts beachtet

werden. Jede Funktion hat als Ergebnis einen solchen Rückgabewert; für gewöhnlich ist dies ein

Skalar. Führt man also Operationen auf Listen, Arrays und Hashes aus, die das Ergebnis in einer

Variablen speichern, muss darauf geachtet werden, dass bereits beim Zugriff auf die Datenstruktur das

Präfixsymbol des Rückgabewerts verwendet wird. Eine besondere Art von Rückgabewert stellen dabei

Array- und Hashslices dar, deren Datenstruktur in beiden Fällen ein Array ist!

Page 37: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

33

Beispielanwendung

Ein zentrales Konzept computerlinguistischer Analyse stellt das des Bi- oder Trigramms dar. Ein

Bigramm ist eine Menge aus zwei Wörtern, während ein Trigramm eine Menge aus drei Wörtern

darstellt.24 Um aus folgendem Satz

(1) Eine Rose ist eine Rose ist eine Rose .

Bi- bzw. Trigramme zu extrahieren, betrachtet man immer ein Zweier- bzw. Dreier-Fenster dieses

Satzes:

Bigramme Trigramme

Eine Rose Eine Rose ist

Rose ist Rose ist eine

ist eine ist eine Rose

eine Rose eine Rose ist

Rose ist Rose ist eine

ist eine ist eine Rose

eine Rose eine Rose .

Rose .

Will man dies in Perl modellieren, empfiehlt es sich zunächst, die Datenstrukturen und die

notwendigen Algorithmen natürlichsprachlich in Kommentaren zu beschreiben, denen man dann den

eigentlichen Code hinzufügt. Auf diese Weise nähert man sich nicht nur der Implementierung intuitiv,

man sorgt durch kleinschrittige Kommentierung des Quellcodes auch dafür, dass dieser lesbarer wird

und somit besser gewartet und weiter entwickelt werden kann.

Stellen wir uns zunächst den Satz als Liste vor, in dem alle Wörter und der Punkt jeweils ein

Element sind:

# Ein Satz ist eine Liste von Wörtern. my @woerter = qw (Eine Rose ist eine Roste ist eine Rose .);

Um nun alle Bigramme auszugeben, nimmt man jeweils die ersten beiden Elemente von der Liste

und gibt sie aus:

# Ein Bigramm besteht aus zwei Wörtern des Satzes. my ($erstes, $zweites); # Entferne das erste und zweite Element des Arrays und schreibe sie in die # beiden Skalarvariablen für das Bigramm. ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Eine Rose

24 Ein einzelnes Wort nennt man deshalb auch Unigramm.

Page 38: Das Perl-Tutorial für Computerlinguisten

34

Da das zweite Wort des ersten Bigramms zugleich das erste Wort des zweiten Bigramms ist, muss

man es wieder auf das Array zurücklegen:

# Das zweite Wort des ersten Bigramms ist das erste Wort des zweiten # Bigramms. # Deshalb muss es zurück auf die Liste gelegt werden! unshift(@woerter, $zweites);

Bigramme an sich sind wenig interessant. Deshalb soll vom Programm einerseits festgehalten

werden, wie viele einzelne Wörter im Beispielsatz vorhanden sind, und wie viele Bigramme sich

daraus bilden lassen. Dazu halten wir zunächst die Anzahl der Wörter fest:

# Die Anzahl der einzelnen Wörter des Satzes. my $anzahl_woerter = @woerter;

Wann immer ein Bigramm extrahiert wurde, zählen wir dieses:

# Die Anzahl der aus dem Satz extrahierten Bigramme. my $anzahl_bigramme; ... ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Eine Rose unshift(@woerter, $zweites); # Erhöhe die Zahl der Bigramme um eins. $anzahl_bigramme++;

Da wir noch nicht wissen, wann das Array keine Bigramme mehr enthält, wiederholen wir die

letzten drei Zeilen so oft, bis wir durch Experimentieren kein Ergebnis mehr zurück geliefert

bekommen. Das komplette Programm sieht dann so aus:

use strict; use diagnostics; # Initialisierung und Definition von Variablen # Ein Satz ist eine Liste von Wörtern. my @woerter = qw (Eine Rose ist eine Roste ist eine Rose .); # Ein Bigramm besteht aus zwei Wörtern des Satzes. my ($erstes, $zweites); # Die Anzahl der einzelnen Wörter des Satzes. my $anzahl_woerter = @woerter; # Die Anzahl der aus dem Satz extrahierten Bigramme. my $anzahl_bigramme; print "Der Satz besteht aus $anzahl_woerter Wörtern\n"; # Ausgabe: 9

Page 39: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

35

# Entferne das erste und zweite Element des Arrays und schreibe sie in die # beiden Skalarvariablen für das Bigramm. ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Eine Rose # Das zweite Wort des ersten Bigramms ist das erste Wort des zweiten # Bigramms. Deshalb muss es zurück auf die Liste gelegt werden! unshift(@woerter, $zweites); # Erhöhe die Anzahl der Bigramme um eins. $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Rose ist unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: ist eine unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: eine Rose unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Rose ist unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: ist eine unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: eine Rose unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Rose . unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: . unshift(@woerter, $zweites); print "$anzahl_bigramme Bigramme extrahiert!\n"; # Ausgabe: 8

Durch die Operationen der drei letzten Zeilen erhält man ein Bigramm, das aus dem Punkt und

etwas undefiniertem besteht, weshalb der Perl-Interpreter an dieser Stelle eine Warnung ausgibt.

Page 40: Das Perl-Tutorial für Computerlinguisten

36

Zur Extraktion von Trigrammen verfährt man analog, nur dass in diesem Fall die ersten drei

Elemente von der Liste entfernt werden und nach der Ausgabe des Trigramms zunächst das dritte, dann

das zweite Wort auf das Array zurückgelegt werden:

# Ein Trigramm besteht aus drei Wörtern eines Satzes. my ($erstes, $zweites, $drittes); # Die Anzahl der aus dem Satz extrahierten Trigramme. my $anzahl_trigramme; # Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); print "$erstes $zweites $drittes\n"; # Ausgabe: Eine Rose ist # Das zweite und dritte Wort sind die ersten beiden Wörter des nächsten # Trigramms, weshalb zuerst das dritte, dann das zweite an den Anfang des # Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites); # Erhöhe die Anzahl der Trigramme um eins. $anzahl_trigramme++;

Festzuhalten bleibt, dass diese Art der Problemlösung wenig effizient ist, da wir für ein und

dieselbe Operation immer wieder denselben Code hinschreiben müssen. Darüber hinaus besitzen wir

noch keine Möglichkeit, um programmatisch zu ermitteln, wann ein Array keine Elemente mehr

enthält, sodass wir ein Kriterium zur Beendigung der Aufgabe besäßen. Antworten auf diese Probleme

liefert das nächste Kapitel.

Page 41: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

37

5 Kontrollstrukturen

We cannot always control our thoughts, but we can control our words, and

repetition impresses the sub

conscious, and we are then master of the situation.

FLORENCE SCOVEL SHINN

Bisher besaßen die besprochenen Perl-Programme immer einen linearen Programmfluss. Auf

Datenstrukturen wurden sukzessive Operationen ausgeführt, deren Auswertung neue Daten erzeugte:

Anhand von Kontrollstrukturen lässt sich der Programmfluss durch Bedingungen und Schleifen

beeinflussen. Diese Bedingungen können atomar Daten auf eine Eigenschaft prüfen oder in Schleifen

dafür sorgen, dass Operationen wiederholt auf Daten ausgeführt werden. Schleifen geben uns ein

Instrumentarium an die Hand, um Operationen auf allen Elementen eines Arrays oder Hashes

auszuführen.

Anhand von Bedingungen entscheidet man, ob an einem bestimmten Punkt im Programmablauf ein

Kriterium für eine Datenstruktur erfüllt ist. Aufgrund dessen lassen sich unterschiedliche Operationen

auf dieser durchführen, die wiederum unterschiedliche Datenstrukturen zum Resultat haben:

Page 42: Das Perl-Tutorial für Computerlinguisten

38

Page 43: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

39

Schleifen beinhalten ebenfalls Bedingungen; sie werden so oft durchgeführt, bis die Bedingung

nicht mehr erfüllt ist:

Die in den Flussdiagrammen den Bedingungen nachgeordneten Operationen stehen in sogenannten

Blöcken: Ein Block fasst eine oder mehrere Anweisungen durch geschweifte Klammern {} zu einer

operationalen Einheit zusammen.

Page 44: Das Perl-Tutorial für Computerlinguisten

40

5.1 Bedingungen Um zu entscheiden, ob ein Wert ein bestimmtes Kriterium erfüllt, verwendet man die Funktion

if() und die bereits besprochenen Vergleichsoperatoren. Dieser Funktion folgt ein Block, in dem

mindestens eine Anweisung steht:

my $zahl = 15; if($zahl > 12){ print $zahl; # Ausgabe: 15 }

Sollte eine Bedingung zu falsch ausgewertet werden, lässt sich dieser Fall anhand der else-Klausel

behandeln:

my $zahl = 15; if($zahl < 12){ print "$zahl ist kleiner als 12\n"; } else{ print "$zahl ist nicht kleiner als 12\n"; # Ausgabe! }

if()-Bedingungen lassen sich kaskadieren, indem man nach dem ersten if() anhand der elsif()-

Klausel weitere Fälle behandelt und zuletzt auf den allgemeinsten Fall mit else reagiert:25

my $kontostand = -500; if($kontostand < -10000){ print "Gehen Sie zur Schuldnerberatung!\n"; } elsif($kontostand < -2000){ print "Wollen Sie einen Sofortkredit?\n"; } elsif($kontostand < 4000){ print "Sie sind im grünen Bereich!\n"; # Ausgabe! } else{ print "Was wollen Sie mit Ihrem Geld tun?\n"; }

Verwendet man nur die if-Bedingung ohne weitere Klauseln und will auch nur eine Anweisung

ausführen, kann man diese vor die if-Bedingung schreiben und den Block weglassen:

print "Sie sind kreditwürdig!\n" if($kontostand > -2000);

25 Anders als in verschiedenen anderen Programmiersprachen kennt Perl das Konstrukt switch/case nicht, mit dem man

kaskadierte Bedingungen sehr elegant modellieren kann. Wir werden später noch zwei Möglichkeiten aufzeigen, dies dennoch zu

verwenden.

Page 45: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

41

Anhand der Funktion defined() kann man überprüfen, ob eine Datenstruktur definiert ist; es reicht

nicht, sie zu deklarieren. Da der Test auf eine Variable keine andere Funktionalität besitzen kann, darf

man das Schlüsselwort defined weglassen:

my $zahl = 1; print $zahl if(defined($zahl)); # Ausgabe: 1 my @array; print "@array\n" if(@array); # Ausgabe:

Analog dazu lässt sich vermittels der Funktion exists() überprüfen, ob ein Element in einem

Array oder einem Hash vorhanden ist. Im ersten Fall erfolgt der Zugriff wie gewohnt über einen Index,

im zweiten natürlich über den jeweiligen Schlüssel:

my @marxes = qw(Chico Harpo Groucho Gummo Zeppo); my %adressbuch = (Peter => "Bonn", Paul => "London"); print $marxes[1] if(exists($marxes[1])); # Ausgabe: Harpo print $adressbuch{Paul} if(exists($adressbuch{Paul})); # Ausgabe:

London

Eine weitere Möglichkeit, eine Bedingung zu überprüfen, besteht darin, zu schauen, ob sie falsch

ist. Dazu verwendet man die Funktion unless(), deren Syntax analog zu derjenigen von if() ist. Der

entsprechende Block wird – wie gesagt – nur ausgeführt, wenn die Bedingung falsch ist:

my $zahl = 5; unless($zahl < 4){ print "$zahl ist größer als 4.\n"; # Ausgabe: 5 ist größer als 4. }

unless() lässt sich dementsprechend als if(not <Ausdruck>) oder if(! <Ausdruck>)

umformulieren. Auch die Regel, bei Verwendung nur einer Anweisung diese mit der Bedingung auf

eine Zeile schreiben zu dürfen, gilt. Will man allerdings eine unless()-Bedingung mit weiteren

Klauseln koppeln, muss man sich wiederum des elsif()- bzw. else-Konstrukts bedienen. Dabei ist zu

beachten, dass diese die negative Polarität wieder aufheben:

my $zahl = 5; unless($zahl < 4){ print "$zahl ist größer als 4.\n"; # Ausgabe: 5 ist größer als 4. } elsif($zahl < 4){ print "$zahl ist kleiner als 4.\n"; } else{ print "$zahl ist gleich 4."; }

Page 46: Das Perl-Tutorial für Computerlinguisten

42

5.2 Schleifen Perl kennt mehrere Konstrukte, um Operationen aufgrund einer Bedingung zu wiederholen. Diese

Konstrukte unterscheiden sich in mehreren Gesichtspunkten:

• Ein Block wird so oft ausgeführt, bis eine Bedingung falsch wird. Dabei wird zunächst die

Bedingung geprüft.

• Ein Block wird so oft ausgeführt, bis eine Bedingung wahr ist. Auch hierbei wird die

Bedingung zuerst geprüft, bevor der Block ausgeführt wird.

• Ein Block wird auf jeden Fall ausgeführt; erst nach dem ersten Durchlauf wird die Bedingung

geprüft.

Anweisungen wiederholen

while()-Schleifen

Die while()-Schleife entspricht dem ersten der genannten Kriterien: Zunächst wird eine Bedingung

auf Wahrheit überprüft und dann wird ein Block, den man auch als Schleifenrumpf bezeichnet, solange

ausgeführt, wie die Bedingung erfüllt ist. Die syntaktische Struktur der while()-Schleife entspricht

derjenigen der if()-Bedingung. Zusätzlich benötigt man allerdings eine Operation, die den in der

Bedingung verwendeten Wert modifiziert, sodass ein Endzustand erreicht werden kann:

my $countdown = 5; while($countdown > 0){ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } print "Start!\n"; # Ausgabe: Start!

Ließe man die Zeile $countdown--; weg, wäre die Bedingung immer wahr und man befände sich in

einer Endlosschleife.26 Dieser entgeht man allerdings durch Schleifenkontrollmechanismen, wie sie

weiter unten vorgestellt werden sollen.

until()-Schleifen

Dem zweiten Kriterium entsprechen until()-Schleifen: Vor der Verarbeitung des Schleifenrumpfs

wird eine Bedingung so lange abgeprüft, bis sie wahr wird:

my $countdown = 5; until($countdown == 0){ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } print "Start!\n"; # Ausgabe: Start!

In dieser Variante muss der Operand 0 für die Bedingung gewählt werden, da ja die

Abbruchbedingung vor dem Anweisungsblock überprüft wird!

26 Aus einer solchen befreit man sich im Emacs, indem man Alt-x kill-compilation eingibt; in der Eingabeaufforderung

beendet man den aktuellen Perl-Interpreterprozess mit Strg-C.

Page 47: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

43

do{}...while()- und do{}...until()-Schleifen

Den gleichen Effekt bekommt man, wenn man Schleifen verwendet, die dem dritten Kriterium

entsprechen, nämlich zunächst einmal den Schleifenrumpf auszuführen und dann die

Abbruchbedingung zu überprüfen. Dazu bedient man sich des do{}-Konstrukts, das man sowohl mit

while() als auch mit until() verwenden darf:

my $countdown = 5; do{ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } while($countdown > 0); print "Start!\n"; # Ausgabe: Start!

my $countdown = 5; do{ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } until($countdown < 1); print "Start!\n"; # Ausgabe: Start!

for()-Schleifen

Demselben Kriterium wie dem der while()-Schleife entspricht die for()-Schleife: Auch hier wird

vor dem Aufruf des Schleifenrumpfs eine Bedingung abgeprüft und der Block wird so oft wiederholt,

bis die Bedingung nicht mehr wahr ist. In der Tat sind diese beiden Konstrukte austauschbar und unter-

scheiden sich nur in ihrer Syntax.27

Für gewöhnlich benötigt die for()-Schleife drei Argumente:

• die Initialisierung einer Laufvariablen,

• eine Bedingung und

• eine Operation, die den Wert der Laufvariablen ändert:

for(my $i = 5; $i > 0; $i--;){ print "$i\n"; # Ausgabe: 54321 } print "Start!\n"; # Ausgabe: Start!

Eine syntaktisch kompaktere Variante besteht darin, einen Bereich als Argumentliste anzugeben:

for(1 .. 3){ print "Wiederholung\n"; }

27 Und auch ein wenig in den Einsatzgebieten, wie wir noch sehen werden. Grundsätzlich lässt sich aber überall, wo eine

while()-Schleife steht, auch eine for()-Schleife verwenden und umgekehrt.

Page 48: Das Perl-Tutorial für Computerlinguisten

44

Verwendet man nur eine Anweisung, darf man diese wiederum auf dieselbe Zeile wie die for()-

Schleife schreiben:

print "Wiederholung\n" for(1 .. 3);

Iteration über Listenstrukturen

foreach()-Schleifen

Eine syntaktische Alternative zur for()-Schleife stellt die foreach()-Schleife dar, die

ausschließlich dazu verwendet wird, über Listenstrukturen zu iterieren. Ähnlich wie bei der kano-

nischen Verwendungsart von for(), benötigt man hier eine Variable, in der der aktuelle Wert des

Listenelements gespeichert wird:

my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); foreach my $wort (@woerter){ print "$wort\n"; }

foreach() stellt die kanonische Form der Iteration über einen Hash dar. Da wir – wie bereits im

vorherigen Kapitel gesehen – über die Schlüssel auf die Werte zugreifen, sollte der Variablenname, in

dem wir die Schlüssel während jedes Schleifendurchlaufs abspeichern, die Bedeutung der Schlüssel

reflektieren. Um an den Wert eines jeden Elements zu gelangen, schreibt man wie gewohnt das

Präfixsymbol für einen Skalarwert – denn um einen solchen handelt es sich ja auch bei diesem

Rückgabewert – vor den Bezeichner des Hashes. Ihm folgt der Schlüssel in geschweiften Klammern;

da sich dieser aus der jeweiligen Iteration über den Hash ergibt, setzen wir an dieser Stelle unsere

Laufvariable ein:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); foreach my $name (keys %adressbuch){ print "$name wohnt in $adressbuch{$name}.\n"; } # Ausgabe: # Peter wohnt in Bonn. # Andreas wohnt in Darmstadt. # Susanne wohnt in Berlin.

Page 49: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

45

Will man einen Hash sortiert ausgeben, kann man die Sortierung entweder auf den Schlüsseln oder

auf den Werten durchführen. Betrachten wir zunächst den ersten (eher trivialen) Fall:

foreach my $name (sort keys %adressbuch){ print "$name wohnt in $adressbuch{$name}.\n"; } # Ausgabe: # Andreas wohnt in Darmstadt. # Peter wohnt in Bonn. # Susanne wohnt in Berlin.

Soll ein Hash den Werten nach sortiert ausgeben werden, kann man nicht einfach die Funktion

keys() durch values() ersetzen!28 Vielmehr benötigt man den cmp-Operator zur

zeichenkettenbasierten Sortierung oder den Raumschiff-Operator <=> zur numerischen Sortierung in

einem sort-Block. Wie bereits erläutert, verwenden diese Operatoren die speziellen Variablen $a und

$b, um zwei Werte miteinander zu vergleichen und sie dementsprechend einzuordnen. Im Fall der

Hashes sind dies die jeweiligen Werte der dazugehörigen Schlüssel pro Iteration; d.h., man verwendet

$a und $b für die Sortierung anstelle der Laufvariablen, in der normalerweise der Schlüssel steht:

foreach my $name (sort{$adressbuch{$a} cmp $adressbuch{$b}} keys %adressbuch){

print "$name wohnt in $adressbuch{$name}.\n"; } # Ausgabe: # Susanne wohnt in Berlin. # Peter wohnt in Bonn. # Andreas wohnt in Darmstadt.

Dass foreach{} und for{} bei der Iteration über Listen synonym zueinander sind, lässt sich

dadurch zeigen, dass man foreach{} ohne weiteres durch for{} ersetzen kann:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); for my $name (keys %adressbuch){ print "$name wohnt in $adressbuch{$name}.\n"; }

while()-Schleifen

Auch die while()-Schleife eignet sich zur Iteration und wird vorwiegend bei Arrays eingesetzt. Sie

lässt sich zur Iteration über Listenelemente verwenden, die man mit shift() oder pop() für die weitere

Verarbeitung aus der Liste löscht, z.B. um Operationen auf den einzelnen Skalaren durchzuführen oder

sie in eine andere Datenstruktur wie einen Hash zu überführen. In diesem Fall ist die Bedingung der

while()-Schleife solange wahr, wie Elemente auf der Liste vorhanden sind:

28 values() ermöglicht zwar einen Zugriff auf die Werte eines Hashes, die dann der Laufvariablen zugewiesen würden, man

gelangt auf diesem Weg allerdings nicht zurück an die Schlüssel!

Page 50: Das Perl-Tutorial für Computerlinguisten

46

my @marxes = qw(Chico Harpo Groucho Gummo Zeppo); while(@marxes){ my $marx_brother = shift(@marxes); print "$marx_brother ist ein Marx Brother!\n"; } # Ausgabe: # Chico ist ein Marx Brother! # Harpo ist ein Marx Brother! # Groucho ist ein Marx Brother! # Gummo ist ein Marx Brother! # Zeppo ist ein Marx Brother!

$_

Die besondere Variable $_ wird von Perl verwendet, wenn keine andere Variable angegeben ist.

Man kann sie wie das Pronomen es lesen. Sie wird typischerweise als Ersatz für die Laufvariable in

Schleifen verwendet:29

my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); foreach (@woerter){ print "$_\n"; }

Perl geht sogar soweit, dass $_ das Standardargument für print() ist, wenn kein weiteres

angegeben wird:

my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); foreach (@woerter){ print; # Ausgabe: EineRoseisteineRoseisteineRose }

Auch bei der Iteration über einen Hash lässt sich $_ einsetzen:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); foreach (keys %adressbuch){ print "$_ wohnt in $adressbuch{$_}.\n"; }

Bei der Verwendung von $_ sollte man grundsätzlich zwischen der Lesbarkeit/Wartbarkeit des

dadurch entstehenden Codes und den Perl-Tugenden30 abwägen. Es gibt aber auch Anwendungsfälle, in

denen der Einsatz von $_ explizit vorgesehen ist, wie der folgende Exkurs zeigt.

29 Analog gilt dies natürlich auch für for(). 30 Die drei Tugenden eines Perl-Programmierers: Faulheit, Ungeduld und Selbstüberschätzung!

Page 51: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

47

Exkurs: map und grep

Eine weitere Möglichkeit, Listenstrukturen zu filtern bzw. zu transformieren, stellen die Befehle

grep und map dar. Zwar sind sie in ihrer Verwendung sehr effizient, aber gerade für Einsteiger in ihrer

Funktionalität wenig eingängig, weshalb an dieser Stelle nur an einigen einfachen Beispielen gezeigt

werden soll, wie sie sich verwenden lassen.

Listenstrukturen filtern mit grep

Was die Verwendung dieser beiden Befehle erschwert, ist ihr Einsatz der impliziten Variablen $_.

Sie wird im Rumpf der grep- bzw. map-Blöcke als Container für die übergebenen Argumente

gebraucht, weshalb es nicht immer transparent erscheint, was mit den Werten passiert. Betrachten wir

dazu folgendes Beispiel, in dem aus einer Reihe von Zahlen die ungeraden herausgefiltert werden

sollen:

my @zahlen = (1..10); my @ungerade = grep{$_ %2} @zahlen; # 1, 3, 5, 7, 9

Aus dem Array @zahlen wird jedes Element an die Funktion grep übergeben, wo es sich jeweils in

der Standardvariablen $_ befindet. Auf diese wird nun die Operation Modulo 2 ausgeführt, die

erfolgreich ist, wenn die Division durch zwei auf einem ganzzahligen Wert einen Rest produziert.

Dementsprechend besitzt die Funktion grep denjenigen Wert als Rückgabewert, der innerhalb ihres

Rumpfes als wahr ausgewertet wurde; da davon auszugehen ist, dass mehr als ein Wert gefunden

werden kann, verwendet man eine listenwertige Datenstruktur, um die Rückgabewerte zu speichern.31

Genauso wie aus Arrays lassen sich auch Werte aus Hashes filtern:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt", Frank => "Darmstadt"); my @darmstaedter = grep {$adressbuch{$_} eq "Darmstadt"} keys %adressbuch; # Andreas, Frank

Hier wird jeder Schlüssel des Hashes %adressbuch (durch keys) an die Funktion grep übergeben.

Analog zur Iteration mit einer foreach-Schleife und ohne explizite Laufvariable finden sich die Werte

hier in $adressbuch{$_}, auf der jeweils überprüft wird, ob der Wert gleich der Zeichenkette

"Darmstadt" ist. Ist dies der Fall, wird der jeweilige Wert zurückgegeben und in das Array

@darmstaedter geschrieben.

Listenstrukturen mit map transformieren

Ließen sich mit grep bestimmte Werte aus einer Listenstruktur extrahieren, kann man mit map

Operationen auf diesen ausführen:

my @quadratzahlen = map{$_ ** 2} (2..5); # 4, 9, 16, 25 my @wurzeln = map{sqrt($_)} @quadratzahlen; # 2, 3, 4, 5

31 Natürlich hätte man in diesem Fall auch auf die Arrays verzichten können: print grep {$_ %2} (1..10);

Page 52: Das Perl-Tutorial für Computerlinguisten

48

Hier wird wiederum jedes Element der Liste 2 bis 5 der Funktion map als Argument zugeführt,

über die Variable $_ quadriert und im Array @quadratzahlen gespeichert. Umgekehrt wird in der

zweiten Zeile aus den Elementen dieses Arrays die Quadratwurzel gezogen.

Auch diese Funktion lässt sich wieder auf Hashes anwenden:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt", Frank => "Darmstadt"); print map{"$_\t$adressbuch{$_}\n"} keys %adressbuch; # Ausgabe: Andreas Darmstadt Susanne Berlin Frank Darmstadt Peter Bonn

Wie bereits bei grep gesehen, wird auch hier der Hash anhand seiner Schlüssel durchlaufen; für die

Ausgabe fügen wir nun innerhalb des map-Blocks vor jedem Wert einen Tabulatoreinschub ein.

Die hier gezeigten Beispiele muten eher trivial an, wenn man sie schrittweise nachvollzieht;

kompliziert wird es meist, wenn man $_ im grep- oder map-Rumpf verändert, weshalb es sich auch hier

empfiehlt, überlegt mit $_ umzugehen. Im nächsten Kapitel werden wir ein nützliches Idiom

kennenlernen, das map verwendet.

Schleifenkontroll-Konstrukte

Wie bereits erwähnt, besteht die Möglichkeit, dass eine Schleife nicht durch ihre eigentliche

Abbruchbedingung beendet werden kann, sondern als Endlosschleife weiterläuft. Darüber hinaus kann

es nützlich sein, innerhalb des Schleifenrumpfs Bedingungen zu formulieren, die den sofortigen

Abbruch der Schleife, einen neuerlichen Durchlauf der Schleife ohne Abarbeitung der restlichen

Anweisungen oder den Sprung an eine vordefinierte Stelle im Programm bewirkt.

Abbruch mit last()

Ein sehr gutes Beispiel für die Wirkungsweise dieser Funktion stellt das Einlesen von Daten aus der

Standardeingabe dar. Wollten wir im ersten Kapitel wiederholt Werte aus der Standardeingabe

einlesen, mussten wir unser Perl-Programm mehrfach aufrufen. Mit while() und last() bekommen

wir die Möglichkeit, solange Daten einzulesen, bis keine mehr eingegeben werden; dies wird durch

eine Leerzeile, d.h. das Drücken der Return-Taste ohne vorherige Eingabe anderer Zeichen, im

Programm ausgelöst:

Page 53: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

49

while(my $eingabe = <STDIN>){ chomp($eingabe); last unless($eingabe); my $rueckwaerts = reverse($eingabe); print "$rueckwaerts\n"; } # Eingabe: Groucho, Ausgabe: ohcuorG # Eingabe: Marx, Ausgabe: xraM # Eingabe: => Programmende

Nach dem Löschen des Zeilenendes mit chomp() wird die Schleife wiederholt, wenn Daten

eingegeben wurden; wird nur die Return-Taste gedrückt, ist $eingabe nicht definiert und das

Programm durch last() beendet.

Anweisungen überspringen mit next()

Will man den Rumpf einer Schleife verlassen, ohne sie zu beenden, verwendet man next():

my @zahlen = qw(8 3 0 2 12 0); for(@zahlen){ if ($_ == 0){ print "Lasse 0-Element aus!\n"; next; } print "48 durch $_ ist ", 48 / $_, "\n"; } # Ausgabe: # 48 durch 8 ist 6 # 48 durch 3 ist 16 # Lasse 0-Element aus! # 48 durch 2 ist 24 # 48 durch 12 ist 4 # Lasse 0-Element aus!

Benannte Sprungpunkte

Sowohl zu last() als auch zu next() lassen sich Bezeichner angeben, die einen bestimmten Punkt

im Programm markieren. Diese Bezeichner werden komplett groß geschrieben; die Markierung erhält

zusätzlich einen Doppelpunkt angehängt.

Page 54: Das Perl-Tutorial für Computerlinguisten

50

Auf diese Art lässt sich beispielsweise das switch-/case-Konstrukt anderer Programmiersprachen

nachbilden:

my ($nachricht, $kontostand); $kontostand = -500; SWITCH:{ if($kontostand < -10000){ $nachricht = "Gehen Sie zur Schuldnerberatung!\n"; last SWITCH; } if($kontostand < -2000){ $nachricht = "Wollen Sie einen Sofortkredit?\n"; last SWITCH; } if($kontostand < 4000){ $nachricht = "Alles im grünen Bereich!\n"; # Ausgabe! last SWITCH; } $nachricht = "Was wollen Sie mit Ihrem Geld tun?\n"; } print $nachricht;

5.3 Zusammenfassung In diesem Kapitel haben wir den Begriff des Algorithmus ein wenig mit Leben gefüllt: Während

wir im ersten Kapitel nur einzelne Anweisungen auf Datenstrukturen ausführen konnten, wurde uns mit

Bedingungen und Schleifen ein Instrumentarium an die Hand gegeben, mit dem wir in der Lage sind,

einerseits den Programmfluss durch Verzweigungen, die unter bestimmten Kriterien genommen

werden, zu beeinflussen, andererseits können wir nun Operationen auf skalaren Daten wiederholen,

bzw. über die skalaren Elemente einer listenartigen Struktur iterieren.

if()-Bedingungen ermöglichen es, einen Wert anhand eines Kriteriums auf Wahrheit zu

überprüfen. Will man weitere Kriterien hinzuziehen, verwendet man die elsif()-Bedingung. Der

allgemeinste Fall lässt sich mit else behandeln. Das Komplement zu if() bildet unless(); dabei ist zu

beachten, dass es kein Äquivalent zu elsif() gibt, das die negative Konnotation von unless() besitzt,

weshalb weitere Kriterien zu unless() als positive Bedingungen implementiert werden müssen. Das

aus anderen Programmiersprachen bekannte switch-/case-Konstrukt unterstützt Perl nicht.

Allen Bedingungen und Schleifen ist gemein, dass die durch sie ausgewählten Operationen in

einem Block stehen. Dieser wird durch geschweifte Klammern {} gekennzeichnet und dient der

Gruppierung von Anweisungen. Verwendet man nur eine Anweisung kann diese vor die Bedingung

oder die Schleife geschrieben werden und der Block entfällt.

Page 55: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

51

Schleifen enthalten ebenfalls Bedingungen, die ihnen als Abbruchkriterien dienen. Eine

Klassifikation der unterschiedlichen Schleifen und ihrer Funktionsweisen sei hier tabellarisch

wiedergegeben:

Schleife wird ausgeführt, bis Bedin-

gung falsch wird

Schleife wird ausgeführt, bis Bedin-

gung wahr wird

Bedingung wird zuerst überprüft while(<Bedingung>){

<Schleifenrumpf>

}

until(<Bedingung>){

<Schleifenrumpf>

}

Block wird ausgeführt, bevor Bedin-

gung überprüft wird

do{

<Schleifenrumpf>

} while(<Bedingung>);

do{

<Schleifenrumpf>

} until(<Bedingung);

Analog zu den Klassifikationskriterien der while()-Schleife funktioniert die for()-Schleife, die es

in der Variante mit expliziter Laufvariablen for(my $<Laufvariable>; <Bedingung für

Laufvariable>; <Operation auf Laufvariable>){<Schleifenrumpf>}, mit impliziter Laufvariable

for(<Zahl1> .. <Zahl2>){<Schleifenrumpf>} und ohne Argumente for() {<Schleifenrumpf}

gibt. In der Form mit numerischem Bereich lässt sich vermittels der besonderen Variablen $_ auf den

aktuellen Zahlenwert zugreifen. Diese Variable lässt sich auch mit allen anderen Schleifentypen

verwenden.

Zur Iteration über eine Listenstruktur, wie ein Array oder einen Hash, verwendet man entweder eine

while()- oder eine for()-Schleife. Da man im letzten Fall angeben müsste, aus wie vielen Elementen

die Struktur besteht, wenn man eine Form von for() mit Argumenten verwendet, gibt es mit dem

semantisch analogen foreach()-Konstrukt eine syntaktische Variante zur for()-Schleife ohne

Argumente. Will man Werte aus Listenstrukturen herausfiltern oder Listenstrukturen transformieren,

lassen sich alternativ zu den genannten Schleifen auch die Funktionen grep und map gebrauchen.

Eine weitere Möglichkeit, den Programmfluss zu beeinflussen, besteht im Einsatz von

Schleifenkontroll-Konstrukten. Diese ermöglichen es, neben den eigentlichen Schleifenbedingungen

weitere Bedingungen zu formulieren, die es im Fall von last() ermöglichen, die aktuelle Schleife

abzubrechen oder mit next() einen weiteren Schleifendurchlauf ohne die Ausführung der noch aus-

stehenden Anweisungen zu erzwingen. Da sich für Schleifenkontroll-Konstrukte benannte

Sprungpunkte angeben lassen, kann man dadurch an beliebige Punkte im Programm wechseln.

5.4 Beispielanwendung Überführen wir zunächst das im letzten Kapitel entwickelte Programm zur Zählung von Bigrammen

bzw. Trigrammen am Beispiel der Trigramme in eine Form, die mit Schleifen arbeitet, bevor wir es um

zwei wichtige computerlinguistische Konzepte erweitern.

Mit den uns nun bekannten Sprachmitteln können wir eine Bedingung angeben, die es uns

programmatisch ermöglicht, zu entscheiden, ob sich noch weitere Trigramme aus dem Beispielsatz

extrahieren lassen: Solange mehr als zwei Elemente im Array aus den Wörtern des Satzes vorhanden

sind, können wir Trigramme bilden. Dadurch wird unser Programm einerseits kompakter, andererseits

stellt es keine Ad-hoc-Lösung mehr für den einen von uns gewählten Beispielsatz dar:

Page 56: Das Perl-Tutorial für Computerlinguisten

52

# Solange mehr als zwei Wörter im Array vorhanden sind, lassen sich # Trigramme extrahieren. while(@woerter > 2){ ... }

Den Schleifenrumpf bildet die bereits aus der letzten Version bekannte Einheit aus der Extraktion

der ersten drei Elemente des Wörter-Arrays, der Ausgabe des jeweiligen Trigramms und dem

Zurücklegen des dritten und zweiten Elements auf das Array:

# Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); print "$erstes $zweites $drittes\n"; # Das zweite und dritte Wort sind die ersten beiden Wörter des nächsten # Trigramms, weshalb zuerst das dritte, dann das zweite an den Anfang des # Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites);

Das komplette Programm sieht dementsprechend so aus:

use strict; use diagnostics; my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); # Solange mehr als zwei Wörter im Array vorhanden sind, lassen sich # Trigramme extrahieren. while(@woerter > 2){ # Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. my ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); print "$erstes $zweites $drittes\n"; # Das zweite und dritte Wort sind die ersten beiden Wörter des # nächsten Trigramms, weshalb zuerst das dritte, dann das zweite an # den Anfang des Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites); }

Anders als in der Version aus dem letzten Kapitel haben wir hier nicht die Anzahl der Wörter und

der Trigramme bestimmt. Dazu erweitern wir unser Instrumentarium um das Konzept des Tokens und

des Types. Ein Token entspricht genau einem zählbaren Vorkommen eines Worts in unserem Beispiel-

satz. Ein Type hingegen ist jedes voneinander unterscheidbare Wort:

Page 57: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

53

Token Type

Eine Eine

Rose Rose

ist ist

eine eine

Rose

ist

eine

Rose

. .

Die Types entsprechen also dem Vokabular unseres Beispielsatzes: Während es insgesamt neun

Token gibt, finden wir in ihm nur fünf Types. Eine elegante Form der Bestimmung der Anzahl an

Token und Types in Perl besteht darin, jedes Wort als Schlüssel in einen Hash zu schreiben; der

dazugehörige Wert ist die Häufigkeit, mit der dieses Token im Satz vorhanden ist. Um diese Häufigkeit

zu bestimmen, nutzen wir einerseits die Tatsache aus, dass die Schlüssel in einem Hash eindeutig sein

müssen, andererseits bedienen wir uns des Autoinkrement-Operators: Wann immer wir ein bestimmtes

Wort sehen, erhöht sich seine Häufigkeit automatisch um eins.

In unserer Perl-Implementierung iterieren wir dazu über das Wörter-Array, schreiben das jeweilige

Wort in den Hash und erhöhen seinen Wert um eins:

# In diesem Hash stehen die Unigramm-Häufigkeiten. my %unigramm_haeufigkeiten; # Iteriere über den Satz und zähle die Wort-Häufigkeiten. foreach my $wort(@woerter){ $unigramm{$wort}++; }

Die Anzahl der Types ergibt sich aus der Anzahl der Schlüssel dieses Hashes:

my $unigramm_type_haeufigkeiten = keys(%unigramm_haeufigkeiten);

Auf die gleiche Weise lassen sich nun die Bi- und Trigramm-Häufigkeiten ermitteln; dies sei

wiederum an der Ermittlung der Trigramm-Häufigkeiten demonstriert. Um ein Trigramm in einen Hash

schreiben zu können, konkatenieren wir es jeweils zu einer Zeichenkette:

# In diesem Hash stehen die Trigramm-Häufigkeiten. my %trigramm_haeufigkeiten; while(@woerter > 2){ ... # Konkateniere die ersten drei Elemente zu einem Trigramm. my $trigramm = $erstes . " " . $zweites . " " . $drittes; # Ermittle die Trigramm-Häufigkeiten. $trigramm_haeufigkeiten{$trigramm}++; ... }

Page 58: Das Perl-Tutorial für Computerlinguisten

54

Die Gesamtzahl der Token im Satz lässt sich wie schon im vorherigen Kapitel gesehen dadurch

ermitteln, dass man das Wörter-Array in einen Skalarkontext bringt:

# Die Anzahl aller Token im Satz. my $anzahl_token = @woerter;

Geben wir nun die gewonnenen Häufigkeiten in absteigender Sortierung aus:

# Unigramm-Häufigkeiten absteigend sortiert. foreach my $unigramm(sort{$unigramm_haeufigkeiten{$b} <=>

$unigramm_haeufigkeiten{$a}} keys %unigramm_haeufigkeiten){ print "$unigramm: $unigramm_haeufigkeiten{$unigramm}\n"; } # Trigramm-Häufigkeiten absteigend sortiert. foreach my $trigramm(sort{$trigramm_haeufigkeiten{$b} <=>

$trigramm_haeufigkeiten{$a}} keys %trigramm_haeufigkeiten){ print "$trigramm: $trigramm_haeufigkeiten{$trigramm}\n"; }

Das komplette Programm sieht dann so aus:

use strict; use diagnostics; my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); # In diesem Hash stehen die Unigramm-Häufigkeiten. my %unigramm_haeufigkeiten; # In diesem Hash stehen die Trigramm-Häufigkeiten. my %trigramm_haeufigkeiten; # Iteriere über den Satz und zähle die Wort-Häufigkeiten. foreach my $wort(@woerter){ $unigramm{$wort}++; } # Die Anzahl aller Token im Satz. my $anzahl_token = @woerter; print "Der Satz besteht aus $anzahl_token Wörtern\n"; # Solange mehr als zwei Wörter im Array vorhanden sind, lassen sich # Trigramme extrahieren. while(@woerter > 2){ # Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. my ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); # Konkateniere die ersten drei Elemente zu einem Trigramm. my $trigramm = $erstes . " " . $zweites . " " . $drittes;

Page 59: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

55

# Ermittle die Trigramm-Häufigkeiten. $trigramm_haeufigkeiten{$trigramm}++; # Das zweite und dritte Wort sind die ersten beiden Wörter des # nächsten Trigramms, weshalb zuerst das dritte, dann das zweite an # den Anfang des Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites); } print "Die nach ihrer Häufgkeit sortierten Unigramme sind:\n"; # Unigramm-Häufigkeiten absteigend sortiert. foreach my $unigramm(sort{$unigramm_haeufigkeiten{$b} <=>

$unigramm_haeufigkeiten{$a}} keys %unigramm_haeufigkeiten){ print "$unigramm: $unigramm_haeufigkeiten{$unigramm}\n"; } print "Die nach ihrer Häufigkeit sortierten Trigramme sind:\n"; # Trigramm-Häufigkeiten absteigend sortiert. foreach my $trigramm(sort{$trigramm_haeufigkeiten{$b} <=>

$trigramm_haeufigkeiten{$a}} keys %trigramm_haeufigkeiten){ print "$trigramm: $trigramm_haeufigkeiten{$trigramm}\n"; } # Ausgabe: # Der Satz besteht aus 9 Wörtern. # Die nach ihrer Häufigkeit sortierten Unigramme sind: # Rose: 3 # ist: 2 # eine: 2 # Eine: 1 # .: 1 Die nach ihrer Häufigkeit sortierten Trigramme sind: # ist eine Rose: 2 # Rose ist eine: 2 # eine Rose .: 1 # eine Rose ist: 1 # Eine Rose ist: 1

Die von uns durch dieses Programm gewonnenen Häufigkeiten sind an sich noch nicht interessant,

da sie absolute Zahlen sind. Damit sie statistisch verwertbar werden, muss man sie zu den

Gesamthäufigkeiten in Verhältnis setzen. Da diese in unserem Beispiel bisher allerdings noch zu gering

sind, um linguistisch signifikante Aussagen zu treffen, werden wir im nächsten Kapitel zeigen, wie sich

wesentlich größere Datenmengen verarbeiten lassen.

Page 60: Das Perl-Tutorial für Computerlinguisten

56

6 Operationen auf Dateien

It is possible to store the mind with a million facts and still be entirely

uneducated.

ALEC BOURNE

Die bisher verarbeiteten Daten stammten entweder aus der Standardeingabe oder sie waren statisch

als Datenstruktur im Programm verankert. Da der Großteil computerlinguistischer Analysen auf großen

Datenmengen operiert, gilt es einige Aspekte zu bedenken, die für unser bisheriges Vorgehen noch un-

erheblich waren:

• Die Daten sollten möglichst unabhängig von einer Implementation existieren. Dadurch werden

sie sowohl für unterschiedliche Lösungsansätze eines Problems als auch für verschiedene

Fragestellungen nutzbar.

• Es ist wenig praktikabel, größere Datenmengen für jeden Programmlauf per Hand einzugeben!

Ebenso sollte es möglich sein, Ergebnisse nicht nur auf der Standardausgabe anzuzeigen, sondern

diese permanent zu sichern. Solche Operationen finden sowohl für die Eingabe- wie auch für die

Ausgabeseite in Dateien statt. Um diese nutzen zu können, müssen wir unser vorhandenes Perl-

Instrumentarium nur unwesentlich erweitern, da die grundlegenden Konzepte mit STDIN/STDOUT und

der print()-Funktion bereits bekannt sind.

6.1 Dateideskriptoren STDIN und STDOUT bezeichnet man als Dateideskriptoren (engl. file handles). Wie Variablen stellen

sie eine Abstraktion über ihre Inhalte dar. Im Fall der Variablen waren dies Werte, die in

unterschiedlichen Datenstrukturen abgelegt wurden und über den Variablennamen zugreifbar waren.

Dateideskriptoren sind analog dazu Container für die Inhalte von Dateien und machen diese über ihren

Namen zugreifbar. Dementsprechend sind die Standardein- und –ausgabe aus der Sicht der meisten

Programmiersprachen Dateien, aus denen Daten gelesen werden bzw. in die Daten geschrieben werden.

Syntaktisch bestehen Dateideskriptoren aus einem komplett groß geschriebenen Bezeichner ohne

Präfixsymbol. Dieser Bezeichner ist bis auf einige Ausnahmen frei wählbar: Wie bereits gesehen,

stehen STDIN und STDOUT für die Standardein- und –ausgabe. Darüber hinaus existiert mit STDERR ein

Kanal, über den es möglich ist, Fehler und Warnungen unabhängig von der Standardausgabe

anzuzeigen. Da aber sowohl STDOUT als auch STDERR den Bildschirm als Ausgabemedium verwenden,

ist diese Unterscheidung für unsere Zwecke meist unerheblich und soll hier nicht weiter vertieft

werden. Des Weiteren sind die Dateideskriptoren DATA und ARGV von Perl reserviert; auf ihre

Funktionalität soll weiter unten eingegangen werden.

6.2 Dateizugriff Um auf eine Datei zugreifen zu können, muss sie für die auf ihr auszuführende Operation geöffnet

werden. Dies ist für gewöhnlich das Lesen aus einer Datei bzw. das Schreiben in eine Datei. In beiden

Fällen verwendet man die Funktion open(), auf deren Argumentliste man zunächst einen Bezeichner

für den Dateideskriptor vereinbart, den man dann mit dem Namen der gewünschten Datei verbindet:

Page 61: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

57

open(IN, "dokument.txt");

Diese Zeile öffnet die Datei dokument.txt im selben Verzeichnis, in dem sich das aktuelle Perl-

Programm befindet, für den lesenden Zugriff. Zusätzlich zum Dateinamen lässt sich an dieser Stelle der

komplette Verzeichnispfad einschließlich Laufwerksbuchstaben angeben. Dabei ist zu beachten, dass

das Trennzeichen für die einzelnen Schritte des Verzeichnispfads unabhängig vom Betriebssystem

normale Schrägstriche / sind, und nicht wie unter Windows gewohnt Backslashes, die von Perl – wie

bereits gesehen – zur Zeichenmaskierung verwendet werden:

open(EINGABE, "C:/texte/dokument.txt");

Will man eine Datei für den schreibenden Dateizugriff öffnen, lenkt man die Ausgabe in die

entsprechende Datei um. Dies geschieht analog zur Arbeitsweise der Ausgabe-Umlenkung in den

Kommandozeilen der verschiedenen Betriebssysteme: Um eine Datei neu anzulegen, verwendet man

das größer-als-Zeichen >; in Perl besitzt es darüber hinaus die Funktionalität, eine bereits vorhandene

Datei zu überschreiben.32 Schreibt man an dieser Stelle zwei größer-als-Zeichen, werden die Daten

beim Schreiben an eine Datei angehängt bzw. eine Datei neu angelegt:

open(AUSGABE, ">C:/texte/neues_dokument.txt"); open(ANHANG, ">>C:/texte/kummulatives_dokument.txt");

Hat man die Operationen auf einer Datei beendet, schließt man sie wieder. Dazu verwendet man die

Funktion close(), die den Bezeichner des Dateideskriptors als Argument nimmt:

open(EINGABE, "dokument.txt"); ... close(EINGABE);

Da der Perl-Interpreter für gewöhnlich selbst dafür sorgt, dass ein geöffneter Dateideskriptor

spätestens bei Beendigung des Programmlaufs geschlossen wird, ist die close()-Anweisung nicht

zwingend notwendig...

Fehlerbehandlung beim Dateizugriff

Bei der Arbeit mit Dateien muss man immer damit rechnen, dass man aus irgendeinem Grund nicht

aus einer Datei lesen oder in eine Datei schreiben kann. Um solche Fälle adäquat behandeln zu können,

bietet Perl die Möglichkeit, eigene Fehlermeldungen auszugeben.33

Obwohl Perl eine interpretierte Sprache ist, unterscheidet man auch hier zwischen sogenannten

Fehlern zur Kompilierzeit und Laufzeitfehlern.34 Fehler zur Kompilierzeit treten auf, wenn der Perl-

Interpreter einen Syntaxfehler wie z.B. pritn statt print findet. Laufzeitfehler werden vom Perl-

Interpreter erst aufgeworfen, nachdem er den Quellcode auf Richtigkeit überprüft hat. So liefert z.B.

die Division durch Null einen Laufzeitfehler; die Operation ist syntaktisch richtig, aber aus

semantischen Gründen kann sie nicht durchgeführt werden. Da also der Fehler, nicht aus einer Datei

32 In den meisten Betriebssystemen ist es notwendig, dem größer-als-Zeichen ein Ausrufezeichen voranzustellen, wenn man

eine Datei überschreiben will. Dies ist unter Perl nicht so und kann u.U. zu unerwünschten Komplikationen führen! 33 Weitere Methoden zur Fehlersuche und –behandlung werden in einem späteren Kapitel vorgestellt. 34 Dies ergibt sich aus der Tatsache, dass der Perl-Interpreter den Quellcode vor der Ausführung in einen internen Bytecode

übersetzt, wobei allerdings keine kompilierte Objektdatei erzeugt wird.

Page 62: Das Perl-Tutorial für Computerlinguisten

58

lesen oder nicht in eine Datei schreiben zu können, einen schwerwiegenden Fehler zur Laufzeit

darstellt, muss man dafür sorgen, dass das Programm an dieser Stelle abbricht.

Um solch eine Fehlermeldung mit gleichzeitiger Beendigung des Programms zu initiieren,

verwendet man die Funktion die(). Sie wird anhand des or- oder des ||-Operators mit derjenigen

Funktion verknüpft, die zur Laufzeit Probleme bereiten könnte. Dies ist in unserem Fall die open()-

Funktion, da z.B. möglicherweise die gewünschte Datei zum Lesen nicht vorhanden ist, oder weil in

eine vorhandene Datei nicht geschrieben werden kann, da sie vom Betriebssystem oder einer anderen

Anwendung z.Zt. blockiert wird. Der die()-Funktion übergibt man für gewöhnlich ein Argument,

nämlich eine eigene Fehlermeldung, die die Situation adäquat beschreibt und/oder eine

Systemmeldung. Diese steht in der besonderen Variablen $! und wird wie mit print() gewohnt ausge-

geben:

open(EINGABE, "dokument.txt") or die "Fehler beim Öffnen der Datei: $!"; # Ausgabe bei nicht vorhandener Datei: # Uncaught exception from user code: # Fehler beim Öffnen der Datei: No such file or directory at test.pl

line 1.

Verwendung des lesenden Dateizugriffs

Genauso wie wir STDIN zum Einlesen von Daten aus der Eingabeaufforderung verwendet haben,

können wir mit unseren eigenen Dateideskriptoren Zeilen aus Dateien einlesen. Dazu umschließt man

den jeweiligen Dateideskriptor wie schon gesehen mit spitzen Klammern <>, dem sogenannten

Diamant- oder auch readline-Operator. Um nun eine Datei zeilenweise einzulesen und Operationen auf

diesen Zeilen auszuführen, verwendet man eine while()-Schleife, in der die aktuelle Zeile aus dem

Dateideskriptor einer entsprechenden Skalarvariablen explizit zugewiesen wird, oder implizit in $_

steht:

open(EINGABE, "dokument.txt") or die $!; while(my $zeile = <EINGABE>){ print $zeile; }

open(EINGABE, "dokument.txt") or die $!; while(<EINGABE>){ print; }

Eine der wichtigsten Operationen, die man bei der Behandlung von Zeichenketten vornehmen

muss, besteht darin, die eingelesene Zeile in die gewünschten Komponenten zu zerlegen; für

gewöhnlich sind dies Wörter und Buchstaben.35 Die entscheidende Grenze verläuft dabei zwischen den

Sätzen und den Wörtern: Auf den ersten Blick erscheint ein – Tokenisierung genannter – Arbeitsschritt,

der zwischen diesen beiden Komponenten unterscheidet, trivial, doch werden wir im nächsten Kapitel

noch sehen, dass dem nicht so ist! Gehen wir an dieser Stelle davon aus, dass das vorliegende

Dokument bereits tokenisiert ist, können wir vermittels der Funktion split() die einzelnen

Komponenten einer Zeile in entsprechende Arrays schreiben. Dazu verwendet split() als erstes

35 Sätze sind aus statistischer Sicht wenig interessant, da es sehr unwahrscheinlich ist, einen und denselben Satz mehrmals in

einem Dokument zu finden.

Page 63: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

59

Argument ein Trennzeichen, das ähnlich wie bei den alternativen Anführungszeichen zwischen zwei

Schrägstrichen steht, und als zweites eine Skalarvariable, in der die zu verwendende Zeile steht. Der

Rückgabewert dieser Funktion ist eine Liste. Will man die Elemente dieser Liste in einem Array spei-

chern, reicht es nicht, sie einfach diesem Array zuzuweisen, da dieses bei jeder Schleifeniteration

überschrieben würde. Vielmehr muss man dafür sorgen, dass die Wörter bzw. Buchstaben anhand der

Funktion push() hintereinander in das Array geschrieben werden. Als Trennzeichen für die Wörter

verwenden wir ein Leerzeichen, für die Buchstaben geben wir kein Trennzeichen an, da wir in diesem

Fall ja auf jedem Zeichen trennen wollen:

my (@woerter, @buchstaben); open(EINGABE, "dokument.txt") or die $!; while(my $zeile = <EINGABE>){ push(@woerter, split(/ /, $zeile)); } foreach my $wort(@woerter){ push(@buchstaben, split(//, $wort)); } print "@woerter\n"; print "@buchstaben\n"; # Gehen wir davon aus, dass in der Datei dokument.txt der Satz # Eine Rose ist eine Rose ist eine Rose . steht, erhält man folgende # Ausgabe: # Eine Rose ist eine Rose ist eine Rose . # E i n e R o s e i s t e i n e R o s e i s t e i n e R o s e .

Im umgekehrten Fall, dass man Elemente eines Arrays oder einer Liste zu einem Skalar

zusammenfassen will, verwendet man die Funktion join(), die wiederum ein Trennzeichen und ein

Array als Argumente benötigt. Anders als bei split() kommen hier allerdings keine Schrägstriche als

Begrenzungszeichen zum Einsatz, sondern doppelte Anführungszeichen:

# Ausgehend vom obigen Array @buchstaben: print join("|", @buchstaben); # Ausgabe: # E|i|n|e|R|o|s|e|i|s|t|e|i|n|e|R|o|s|e|i|s|t|e|i|n|e|R|o|s|e|.|

Schlürfmodus

Eine weitere Möglichkeit, Dateien einzulesen besteht darin, sie nicht zeilenweise, sondern komplett

auf ein Mal in einen Skalar oder ein Array zu lesen. Diese Vorgehensweise bezeichnet man auch als

Schlürfen (engl. slurping); zwar ist diese Operation im Vergleich zum zeilenweisen Einlesen schneller,

doch ist sie auch speicherintensiver und empfiehlt sich eher bei wenig umfangreichen Dateien.

Page 64: Das Perl-Tutorial für Computerlinguisten

60

Liest man eine Datei in ein Array ein, so bilden ihre Zeilen die Elemente des Arrays:

open(EINGABE, "dokument.txt") or die $!; my @datei = <EINGABE>; print $datei[0]; # Ausgabe: Erste Zeile des Dokuments. print $datei[1]; # Ausgabe: Zweite Zeile des Dokuments.

Will man jedoch eine Datei in einen Skalar einlesen, muss man aufgrund der singulären Natur

dieser Datenstruktur dafür sorgen, dass Zeilenendezeichen ignoriert werden. Dazu setzt man den

sogenannten input record separator $/, in dem festgelegt ist, welches Zeichen das Zeilenende abbildet

(meist \n), auf undef.36 Sind die Operationen auf der Datei ausgeführt, muss der input record

separator wieder auf seinen Ursprungswert zurückgesetzt werden, um eventuelle Komplikationen beim

Einlesen weiterer Dateien zu vermeiden:

open(EINGABE, "dokument.txt") or die $!; undef($/); my $datei = <EINGABE>; print $file; # Ausgabe: Das gesamte Dokument einschließlich

Zeilenumbrüchen. $/ = "\n";

Verwendung des schreibenden Dateizugriffs

Will man Daten in eine Datei schreiben, übergibt man der print()-Funktion als erstes Argument

den Bezeichner des Dateideskriptors und dann die Daten:

open(AUSGABE, ">neues_dokument.txt") or die $!; print AUSGABE "In dieser Datei steht nur ein Satz!\n";

Verwendet man im Programm für die Ausgabe von Daten hauptsächlich einen anderen

Dateideskriptor als STDOUT, kann man diesen vermittels der Funktion select() als Standardausgabe

festlegen:

open(AUSGABE, ">neues_dokument.txt") or die $!; select AUSGABE; print "In dieser Datei steht nur ein Satz\n";

6.3 Spezielle Dateideskriptoren Wie bereits erwähnt, sind neben den Dateideskriptoren für die Standardein- und –ausgabe in Perl

noch zwei weitere Dateideskriptoren, nämlich DATA und ARGV, als Schlüsselwörter reserviert.

DATA ist ein im Sinne des rapid prototypings sehr nützlicher Dateideskriptor, da er Daten aus einem

mit __DATA__ oder __END__ gekennzeichneten Abschnitt nach dem eigentlichen Programm einliest,

wobei er anders als bei normalen Dateideskriptoren nicht explizit geöffnet werden muss. Ähnlich wie

in den vorherigen Kapiteln werden dabei zwar die Daten an das Programm gebunden, doch geschieht

dies nicht in Form von vorbelegten Variablen, sondern in derselben Form wie beim Lesen aus einer

Datei, sodass man zwischen dem Testen und einem "echten" Programmlauf nur den Bezeichner des

Dateideskriptors austauschen muss:

36 Dies bedeutet allerdings nicht, dass die Zeilenendezeichen wie bei chomp() entfernt werden!

Page 65: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

61

# open(EINGABE, "nzz_schweiz.txt") or die $!; # while(<EINGABE>){ while(<DATA>){ print; } ... # Weitere Anweisungen ... __END__ Der Bürger vor den neuen Selbstenwaffnungsinitiativen Sicherheit zum Nulltarif? Genau sechs Monate nach der EWR-Abstimmung, in der sich das Schweizervolk und eine Mehrzahl der Stände für Selbstbehauptung im Alleingang und gegen die Teilnahme an der ...

Der Dateideskriptor ARGV repräsentiert den sogenannten Argumentvektor, eine Liste, in der die beim

Programmaufruf übergebenen Argumente stehen. Diese stehen wiederum für Dateinamen, die bei der

Iteration über diesen Dateideskriptor automatisch geöffnet werden:37

# Programmaufruf: perl programm.pl dokument.txt # Programm: while(<ARGV>){ print; # Ausgabe: Zeilen aus dokument.txt }

Der Diamant-Operator <> stellt eine Kurzschreibweise für die Verwendung von ARGV dar:

# Programmaufruf: perl programm.pl dokument.txt # Programm: while(<>){ print; # Ausgabe: Zeilen aus dokument.txt }

Die Argumente selbst stehen im besonderen Array @ARGV, das auf diese Art auch abgefragt werden

kann. Will man z.B. sicherstellen, dass dem Programm genau ein Dateiname übergeben wurde, testet

man dies wie bei jedem anderen Array ab:

die "Sie müssen genau einen Dateinamen angeben!\n" unless(@ARGV == 1);

6.4 Exkurs: Formatierung numerischer Zeichenketten Bei der computerlinguistischen Analyse mit statistischen Methoden bekommen wir durch

verschiedene Rechenverfahren Werte, die viele Nachkommastellen besitzen. Während diese für weitere

37 In der Literatur ist im Zusammenhang mit ARGV häufig auch die Rede vom magic open.

Page 66: Das Perl-Tutorial für Computerlinguisten

62

Berechnungen unverzichtbar sind, werden sie in der Repräsentation von Ergebnissen sehr schnell

unübersichtlich, weshalb man bestrebt sein sollte, die Nachkommastellen für die Ausgabe auf ein

adäquates Maß zu begrenzen und das Ergebnis dementsprechend zu runden.

Für diese Aufgabe steht uns die Funktion sprintf() zur Verfügung.38 Das erste Argument dieser

Funktion besteht aus einem Formatierungsteil, der durch ein Prozentzeichen eingeleitet wird, dem die

Formatgröße des ganzzahligen Teil der Zahl und durch einen Dezimalpunkt getrennt die Formatgröße

des Nachkommateils der Zahl folgt, und der Angabe des Typs der Zeichenkette. Dieser

Formatierungsteil steht in doppelten Anführungszeichen.

Das zweite Argument für sprintf() ist die zu formatierende Zeichenkette. Will man den

ganzzahligen Anteil oder den Nachkomma-Anteil einer Zahl nicht formatieren, kann man ihn bei der

Formatierungsangabe weglassen. Die Funktion sprintf() selbst liefert entgegen ihrer Namensgebung

keine Ausgabe, sondern hat einen Skalar als Rückgabewert:

my $radius = 1000; # Liefert die ersten sechs Nachkommastellen. my $kreistreffer = 0; my $quadrattreffer = (2 * $radius) ** 2; for(my $y = -$radius; $y <= $radius; $y++){ for(my $x = -$radius; $x <= $radius; $x++){ $kreistreffer++ if(sqrt($x ** 2 + $y ** 2) <= $radius); } } print 4 * $kreistreffer / $quadrattreffer."\n"; # Ausgabe: 3.141549 print sprintf("%.2f", (4 * $kreistreffer / $quadrattreffer))."\n";

# Ausgabe: 3.14

6.5 Zusammenfassung Die Gewinnung von Daten aus Dateien und das Speichern von Ergebnisdaten in Dateien erfolgt in

Perl über Dateideskriptoren, die ähnlich einer Variablen eine Zugriffsmöglichkeit auf die physikalisch

vorhandenen Dateien herstellen. Um den Zugriff zu ermöglichen, muss eine Datei vermittels open()

für den lesenden oder schreibenden Dateizugriff geöffnet werden. Da eine solche Operation auch

fehlschlagen kann, was vom Perl-Interpreter unbeachtet bleibt, muss man diese Möglichkeit anhand der

die()-Funktion behandeln.

Ist das Öffnen geglückt, lässt sich ein Dokument durch Iteration über seine Zeilen anhand des in

spitzen Klammern geschriebenen Dateideskriptors in eine entsprechende Datenstruktur (meist ein

Array oder einen Hash) einlesen. Darüber hinaus sieht Perl die Möglichkeit vor, eine Datei komplett in

ein Array oder einen Skalar einzulesen, wozu man den Dateideskriptor direkt einer Array- oder

Skalarvariablen zuweist. Im letzten Fall muss dazu der sogenannte record input separator außer Kraft

gesetzt werden.

Will man Ergebnisdaten in eine Datei schreiben, genügt es, beim Öffnen der Datei die Ausgabe in

diese durch eine oder zwei schließende spitze Klammern vor dem Dateinamen umzulenken und

anschließend den Bezeichner des Dateideskriptors ohne spitze Klammern zwischen print und die

auszugebenden Daten zu schreiben.

38 Diese Funktion stammt aus der Programmiersprache C und wird dort für die formatierte Ausgabe von Zeichenketten

verwendet. Wir beschränken uns an dieser Stelle auf die skizzierte Aufgabenstellung, obwohl der Funktionsumfang von

sprintf() weit darüber hinaus geht.

Page 67: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

63

Neben den bereits bekannten Dateideskriptoren für die Standardein- und –ausgabe finden sich in

Perl noch die reservierten Dateideskriptoren DATA und ARGV. Ersterer ermöglicht die Angabe von

Testdaten in einem __END__- oder __DATA__-Abschnitt nach dem eigentlichen Programm, während

ARGV oder seine Abkürzung, der Diamant-Operator <>, den Zugriff auf Argumente des Pro-

grammaufrufs erlauben.

6.6 Beispielanwendung Datengrundlage statistischer Sprachverarbeitung sind umfangreiche Texte. Hatten sich unsere

Betrachtungen bis jetzt auf einzelne Sätze oder wenige Absätze beschränkt, haben wir mit der

Möglichkeit, Operationen auf Dateien ausführen zu können, ein Werkzeug bekommen, mit dem man

eine brauchbare Datenbasis verarbeiten kann. In der Linguistik bezeichnet man eine solche Datenbasis

als Korpus39. Diese besteht – je nach Anwendungszweck – aus vielen Texten einer Textsorte oder aus

umfangreichen Zusammenstellungen unterschiedlicher Textsorten.

Das im folgenden Beispiel verwandte Korpus besteht aus den englischsprachigen Versionen von

Fjodor Dostojevskis Der Idiot und Die Brüder Karamasow. Mit knapp einer Million Wörtern ist es für

unsere Zwecke ausreichend groß proportioniert; ernsthafte Anwendungen bewegen sich allerdings eher

in Bereichen von hundert Millionen Wörtern. Darüber hinaus ist es bereits tokenisiert, d.h. es ist so

präpariert worden, dass sich Wörter auf einfache Weise daraus extrahieren lassen.

Wie schon im letzten Kapitel werden wir auch für dieses Korpus die absoluten Häufigkeiten für

Uni-, Bi- und Trigramm-Token und -Types bestimmen.40 Darüber hinaus wollen wir in diesem Kapitel

auch die relativen Häufigkeiten für diese sprachlichen Einheiten bestimmen. Einerseits lässt sich daraus

ermitteln, ob die von uns gewählten Einheiten aus Zwei- und Drei-Wort-Kombinationen statistisch

sinnvoll sind, weshalb wir in diesem Beispiel auch Tetragramme betrachten. Andererseits wollen wir

herausfinden, welche Wörter häufig in Korpora zu finden sind und wie ihr Anteil im Verhältnis zur

Gesamtmenge der Wörter ist. In diesem Zusammenhang ist es darüber hinaus interessant, die

Häufigkeiten von n-Grammen zu bestimmen, die nur sehr selten, d.h. in diesem Fall nur genau einmal,

im Korpus zu finden sind. Über ihren Anteil an der Gesamtzahl der sprachlichen Einheiten lässt sich

ein Verhältnis absehen, wie viele Wörter selten oder gar nicht in unserem Korpus zu finden sind.

Ziel solcher Verfahren ist die Ermittlung eines Modells, das Voraussagen über Regularitäten einer

Sprache ermöglicht, so z.B. die Frage, mit welcher Wahrscheinlichkeit man das Auftreten eines

bestimmten Worts erwarten kann, wenn ihm n andere bestimmte Wörter vorangingen.

Für die Implementierung benötigen wir zunächst einige Variablen, in denen wir die Gesamt-

Häufigkeiten der n-Gramm Token41 sowie die n-Gramm Types und ihre jeweiligen Häufigkeiten

speichern wollen:

# Diese Variablen speichern die Häufigkeiten der jeweiligen n-Gramm-Token. my ( $anzahl_bigramme, $anzahl_trigramme, $anzahl_tetragramme ); # Diese Variablen speichern die Häufigkeiten von n-Gramm-Token, # die nur einmal im Korpus vorkommen. my ( $unigramm_hl, $bigramm_hl, $trigramm_hl, $tetragramm_hl ); # In diesen Hashes stehen die n-Gramme und ihre jeweiligen Häufigkeiten.

39 Das Genus dieses Worts ist das Neutrum, nicht das Maskulinum! 40 An dieser Stelle sei die terminologische Abstraktion des n-Gramms über solche sprachlichen Einheiten eingeführt. 41 Eigentlich sollte die Zahl der Bi-, Tri- und Tetragramm-Token natürlich äquivalent zur Anzahl der Token sein. Dazu

müsste man allerdings jeweils n-1 Dummy-Token anfügen; da diese aber für unsere Betrachtung irrelevant würden, verzichten

wir hier darauf und machen den Unterschied durch die verschiedenen Häufigkeiten transparent.

Page 68: Das Perl-Tutorial für Computerlinguisten

64

my ( %unigramm_haeufigkeit, %bigramm_haeufigkeit, %trigramm_haeufigkeit, %tetragramm_haeufigkeit );

Zuerst lesen wir die Token aus unserem vortokenisierten Korpus in ein Array ein:

# In diesem Array stehen die Token des Korpus. my @token; # Lesenden Zugriff auf das Korpus herstellen. open( IN, "dostojevski.tok" ) or die $!; # Korpus einlesen und Token in einer Liste speichern. while (<IN>) { chomp; push( @token, split(/ /) ); }

Die Gesamtzahl der Token im Korpus entspricht der Anzahl der Elemente in diesem Array:

# In dieser Variablen steht die Gesamtzahl der Token. my $anzahl_token = @token;

Um die Anzahl der Types, d.h. die Größe des Vokabulars, bestimmen zu können und gleichzeitig zu

zählen, wie oft das jeweilige Token im Korpus vorhanden ist, iterieren wir über das Array der Wörter,

schreiben jedes einzelne als Schlüssel in einen Hash, wobei sich der jeweilige Wert aus der

kumulativen Häufigkeit, die sich aus der Verwendung des Autoinkrement-Operators ergibt, speist:

# Unigramm-Types und ihre Häufigkeiten ermitteln. foreach (@token) { $unigramm_haeufigkeit{$_}++; }

Da die Operationen zur Ermittlung der Bigramme und ihrer Häufigkeiten das ursprüngliche Array

zerstören werden, legen wir Kopien davon an, um daraus auch Tri- und Tetragramme und ihre

Häufigkeiten extrahieren zu können:

# Kopie für Trigramme. my @token2 = @token; # Kopie für Tetragramme. my @token3 = @token;

Die Extraktion von Bi-, Tri- und Tetragrammen und die Ermittlung ihrer jeweiligen Häufigkeiten

erfolgt wie bereits im letzten Kapitel diskutiert und sei hier nur kurz am Beispiel der Tetragramme

vorgeführt:

# Token-/Type-Häufigkeiten der Tetragramme ermitteln. while ( @token3 > 3 ) { my ( $erstes, $zweites, $drittes, $viertes ) = splice( @token3, 0, 4

);

Page 69: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

65

my $tetragramm = $erstes . " " . $zweites . " " . $drittes . " " . $viertes;

$tetragramm_haeufigkeit{$tetragramm}++; unshift( @token3, $viertes ); unshift( @token3, $drittes ); unshift( @token3, $zweites ); $anzahl_tetragramme++; }

Die Anzahl der jeweiligen n-Gramm-Types ergibt sich aus der Anzahl der Elemente in den

jeweiligen Hashes:

# Die Anzahl der Types bildet das Vokabular des Korpus. my $vokabular = keys %unigramm_haeufigkeit; # Diese Variablen speichern die Anzahl der jeweiligen n-Gramm-Types. my $bigramm_types = keys %bigramm_haeufigkeit; my $trigramm_types = keys %trigramm_haeufigkeit; my $tetragramm_types = keys %tetragramm_haeufigkeit;

Im nächsten Schritt bestimmen wir die Gesamtzahl der n-Gramm-Types, die genau einmal im

Korpus zu finden sind. Dies sei hier nur knapp am Beispiel der Unigramme gezeigt:42

# Ermittle die Anzahl der Types mit der Häufigkeit 1. foreach ( keys %unigramm_haeufigkeit ) { $unigramm_hl++ if ( $unigramm_haeufigkeit{$_} == 1 ); }

Für die Ausgabe berechnen wir mit dem sogenannten n-Gramm-Raum die Anzahl der möglichen zu

findenden Types für das jeweilige n-Gramm. Im Falle der Bigramme berechnet sich der Bigramm-

Raum, indem man die Zahl der Unigramm-Types quadriert. Für die Berechnung des Trigramm-Raums

potenzieren wir die Unigramm-Types mit drei, bei den Tetragrammen mit vier. Zusätzlich berechnen

wir den prozentualen Anteil der Types am jeweiligen n-Gramm-Raum, hier anhand der Tetragramme

exemplifiziert:

# Ausgabe der Anzahl der Tetragramm-Token und –Types, des Tetragramm-Raums # und des Anteils der Tetragramm-Types am Tetragramm-Raum. print "\nEs gibt $anzahl_tetragramme Tetragramm-Token und $tetragramm_types

Tetragramm-Typen.\nDas ergibt " . ( $vokabular**4 ) . " mögliche Tetragramme, von denen " . ( ( $tetragramm_types / $vokabular**4 ) * 100 ) . "%\nim Dokument vorkommen.\n";

Darüber hinaus berechnen wir den prozentualen Anteil derjenigen n-Gramm-Types, die nur einmal

im Korpus vorkamen:

42 Die Bezeichnung hl an den Variablennamen ergibt sich aus dem Begriff hapax legomenon, der ein Wort meint, das nur

einmal in einem Korpus vorkommt.

Page 70: Das Perl-Tutorial für Computerlinguisten

66

# Ausgabe des Anteils der Tetragramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $tetragramm_hl / $tetragramm_types ) * 100 ) ) . "% aller Tetragramm-Types kommt nur einmal im Korpus vor!\n";

Zuletzt bestimmen wir die absoluten und relativen Häufigkeiten der jeweils zehn häufigsten

Vertreter eines n-Gramms. Damit wir nach zehn Einheiten aufhören können, benötigen wir eine

Zählvariable außerhalb der Schleife, die über den jeweiligen Hash iteriert; ist der Wert zehn erreicht,

verlassen wir die Schleife mit last(). Auch diese Operationen seien wiederum am Beispiel der

Tetragramme vorgeführt:

# Ermittlung der absoluten und relativen Häufigkeiten der Tetragramme # und Ausgabe der zehn häufigsten. $i = 0; foreach ( sort { $tetragramm_haeufigkeit{$b} <=>

$tetragramm_haeufigkeit{$a} } keys %tetragramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $tetragramm_haeufigkeit{$_} / $anzahl_tetragramme ) * 100 ); print "$_: $tetragramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Tetragramm-Token.\n"; last if ( $i > 10 ); $i++; }

Hier das komplette Programm:

use strict; use diagnostics; # Diese Variablen speichern die Häufigkeiten der jeweiligen n-Gramm-Token. my ( $anzahl_bigramme, $anzahl_trigramme, $anzahl_tetragramme ); # Diese Variablen speichern die Häufigkeiten von n-Gramm-Token, # die nur einmal im Korpus vorkommen. my ( $unigramm_hl, $bigramm_hl, $trigramm_hl, $tetragramm_hl );

Page 71: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

67

# In diesen Hashes stehen die n-Gramme und ihre jeweligen Häufigkeiten. my ( %unigramm_haeufigkeit, %bigramm_haeufigkeit, %trigramm_haeufigkeit, %tetragramm_haeufigkeit ); # In diesem Array stehen die Token des Korpus. my @token; # Lesenden Zugriff auf das Korpus herstellen. open( IN, "dostojevski.tok" ) or die $!; # Korpus einlesen und Token in einer Liste speichern. while (<IN>) { chomp; push( @token, split(/ /) ); } # In dieser Variablen steht die Gesamtzahl der Token. my $anzahl_token = @token; print "Korpus im Umfang von $anzahl_token Token eingelesen!\n\n"; # Unigramm-Types und ihre Häufigkeiten ermitteln. foreach (@token) { $unigramm_haeufigkeit{$_}++; } # Die Operationen zur Ermittlung der Häufigkeiten von Bi-, Tri- und # Tetragrammen zerstört die ursprüngliche Wortliste, weshalb wir für # diese Operationen Kopien anlegen müssen. # Kopie für Trigramme. my @token2 = @token; # Kopie für Tetragramme. my @token3 = @token; # Token-/Type-Häufigkeiten der Bigramme ermitteln. while ( @token > 1 ) { my ( $erstes, $zweites ) = splice( @token, 0, 2 ); my $bigramm = $erstes . " " . $zweites; $bigramm_haeufigkeit{$bigramm}++; unshift( @token, $zweites ); $anzahl_bigramme++; } # Token-/Type-Häufigkeiten der Trigramme ermitteln. while ( @token2 > 2 ) { my ( $erstes, $zweites, $drittes ) = splice( @token2, 0, 3 ); my $trigramm = $erstes . " " . $zweites . " " . $drittes; $trigramm_haeufigkeit{$trigramm}++; unshift( @token2, $drittes ); unshift( @token2, $zweites ); $anzahl_trigramme++; }

Page 72: Das Perl-Tutorial für Computerlinguisten

68

# Token-/Type-Häufigkeiten der Tetragramme ermitteln. while ( @token3 > 3 ) { my ( $erstes, $zweites, $drittes, $viertes ) = splice( @token3, 0, 4

); my $tetragramm = $erstes . " " . $zweites . " " . $drittes . " " .

$viertes; $tetragramm_haeufigkeit{$tetragramm}++; unshift( @token3, $viertes ); unshift( @token3, $drittes ); unshift( @token3, $zweites ); $anzahl_tetragramme++; } # Die Anzahl der Types bildet das Vokabular des Korpus. my $vokabular = keys %unigramm_haeufigkeit; print "Die Größe des Vokabulars beträgt $vokabular.\n"; # Diese Variablen speichern die Anzahl der jeweiligen n-Gramm-Types. my $bigramm_types = keys %bigramm_haeufigkeit; my $trigramm_types = keys %trigramm_haeufigkeit; my $tetragramm_types = keys %tetragramm_haeufigkeit; # Ermittle die Anzahl der Types mit der Häufigkeit 1. foreach ( keys %unigramm_haeufigkeit ) { $unigramm_hl++ if ( $unigramm_haeufigkeit{$_} == 1 ); } # Ermittle die Anzahl der Bigramm-Types mit der Häufigkeit 1. foreach ( keys %bigramm_haeufigkeit ) { $bigramm_hl++ if ( $bigramm_haeufigkeit{$_} == 1 ); } # Ermittle die Anzahl der Trigramm-Types mit der Häufigkeit 1. foreach ( keys %trigramm_haeufigkeit ) { $trigramm_hl++ if ( $trigramm_haeufigkeit{$_} == 1 ); } # Ermittle die Anzahl der Tetragramm-Types mit der Häufigkeit 1. foreach ( keys %tetragramm_haeufigkeit ) { $tetragramm_hl++ if ( $tetragramm_haeufigkeit{$_} == 1 ); } # Ausgabe des Anteils der Types mit der Häufigkeit 1 am Vokabular. print sprintf( "%.2f", ( ( $unigramm_hl / $vokabular ) * 100 ) ) . "% aller Types kommt nur einmal im Korpus vor!\n"; # Ausgabe der Anzahl der Bigramm-Token und –Types, des Bigramm-Raums # und des Anteils der Bigramm-Types am Bigramm-Raum. print "\nEs gibt $anzahl_bigramme Bigramm-Token und $bigramm_types Bigramm-

Types.\nDas ergibt " . ( $vokabular**2 ) . " mögliche Bigramme, von denen " . ( ( $bigramm_types / $vokabular**2 ) * 100 ) . "%\nim Dokument vorkommen.\n";

Page 73: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

69

# Ausgabe des Anteils der Bigramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $bigramm_hl / $bigramm_types ) * 100 ) ) . "% aller Bigramm-Types kommt nur einmal im Korpus vor!\n"; # Ausgabe der Anzahl der Trigramm-Token und –Types, des Trigramm-Raums # und des Anteils der Trigramm-Types am Trigramm-Raum. print "\nEs gibt $anzahl_trigramme Trigramm-Token und $trigramm_types Trigramm-

Types.\nDas ergibt " . ( $vokabular**3 ) . " mögliche Trigramme, von denen " . ( ( $trigramm_types / $vokabular**3 ) * 100 ) . "%\nim Dokument vorkommen.\n"; # Ausgabe des Anteils der Trigramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $trigramm_hl / $trigramm_types ) * 100 ) ) . "% aller Trigramm-Types kommt nur einmal im Korpus vor!\n"; # Ausgabe der Anzahl der Tetragramm-Token und –Types, des Tetragramm-Raums # und des Anteils der Tetragramm-Types am Tetragramm-Raum. print "\nEs gibt $anzahl_tetragramme Tetragramm-Token und $tetragramm_types

Tetragramm-Types.\nDas ergibt " . ( $vokabular**4 ) . " mögliche Tetragramme, von denen " . ( ( $tetragramm_types / $vokabular**4 ) * 100 ) . "%\nim Dokument vorkommen.\n"; # Ausgabe des Anteils der Tetragramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $tetragramm_hl / $tetragramm_types ) * 100 ) ) . "% aller Tetragramm-Types kommt nur einmal im Korpus vor!\n"; # Ermittlung der absoluten und relativen Häufigkeiten der Unigramme # und Ausgabe der zehn häufigsten. my $i = 0; print "\nDie zehn häufigsten Unigramme sind:\n"; foreach ( sort { $unigramm_haeufigkeit{$b} <=> $unigramm_haeufigkeit{$a} } keys %unigramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $unigramm_haeufigkeit{$_} / $anzahl_token ) * 100

); print "$_: $unigramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Token.\n"; last if ( $i > 10 ); $i++; }

Page 74: Das Perl-Tutorial für Computerlinguisten

70

# Ermittlung der absoluten und relativen Häufigkeiten der Bigramme # und Ausgabe der zehn häufigsten. $i = 0; print "\nDie zehn häufigsten Bigramme sind:\n"; foreach ( sort { $bigramm_haeufigkeit{$b} <=> $bigramm_haeufigkeit{$a} } keys %bigramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $bigramm_haeufigkeit{$_} / $anzahl_bigramme ) *

100 ); print "$_: $bigramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Bigramm-Token.\n"; last if ( $i > 10 ); $i++; } # Ermittlung der absoluten und relativen Häufigkeiten der Trigramme # und Ausgabe der zehn häufigsten. $i = 0; print "\nDie zehn häufigsten Trigramme sind:\n"; foreach ( sort { $trigramm_haeufigkeit{$b} <=> $trigramm_haeufigkeit{$a} } keys %trigramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $trigramm_haeufigkeit{$_} / $anzahl_trigramme ) * 100 ); print "$_: $trigramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Trigramm-Token.\n"; last if ( $i > 10 ); $i++; } # Ermittlung der absoluten und relativen Häufigkeiten der Tetragramme # und Ausgabe der zehn häufigsten. $i = 0; print "\nDie zehn häufigsten Tetragramme sind:\n"; foreach ( sort { $tetragramm_haeufigkeit{$b} <=>

$tetragramm_haeufigkeit{$a} } keys %tetragramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $tetragramm_haeufigkeit{$_} / $anzahl_tetragramme ) * 100 ); print "$_: $tetragramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Tetragramm-Token.\n"; last if ( $i > 10 ); $i++; }

Page 75: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

71

# Ausgabe: # Korpus im Umfang von 995095 Token eingelesen! # Die Größe des Vokabulars beträgt 21085. # 38.43% aller Types kommt nur einmal im Korpus vor! # Es gibt 995094 Bigramm-Token und 219324 Bigramm-Types. # Das ergibt 444577225 mögliche Bigramme, von denen 0.0493331614096966% # im Dokument vorkommen. # 65.58% aller Bigramm-Types kommt nur einmal im Korpus vor! # Es gibt 995093 Trigramm-Token und 572129 Trigramm-Types. # Das ergibt 9373910789125 mögliche Trigramme, von denen

6.10341844370598e-06% # im Dokument vorkommen. # 82.12% aller Trigramm-Types kommt nur einmal im Korpus vor! # Es gibt 995092 Tetragramm-Token und 826581 Tetragramm-Types. # Das ergibt 1.97648908988701e+17 mögliche Tetragramme, von denen # 4.18206710185916e-10% im Dokument vorkommen. # 92.05% aller Tetragramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Unigramme sind: # ,: 66476 Das sind 6.68037% aller Token. # .: 39722 Das sind 3.99178% aller Token. # the: 30937 Das sind 3.10895% aller Token. # ": 30012 Das sind 3.01599% aller Token. # and: 23277 Das sind 2.33917% aller Token. # to: 21011 Das sind 2.11146% aller Token. # I: 17985 Das sind 1.80737% aller Token. # of: 16409 Das sind 1.64899% aller Token. # : 16151 Das sind 1.62306% aller Token. # a: 15821 Das sind 1.58990% aller Token. # he: 13039 Das sind 1.31033% aller Token. # Die zehn häufigsten Bigramme sind: # . ": 11202 Das sind 1.12572% aller Bigramm-Token. # , and: 8864 Das sind 0.89077% aller Bigramm-Token. # !: 7729 Das sind 0.77671% aller Bigramm-Token. # ?: 7673 Das sind 0.77108% aller Bigramm-Token. # " ": 5222 Das sind 0.52477% aller Bigramm-Token. # , ": 4880 Das sind 0.49041% aller Bigramm-Token. # ? ": 3446 Das sind 0.34630% aller Bigramm-Token. # , but: 3125 Das sind 0.31404% aller Bigramm-Token. # . He: 3048 Das sind 0.30630% aller Bigramm-Token. # . I: 2938 Das sind 0.29525% aller Bigramm-Token. # of the: 2917 Das sind 0.29314% aller Bigramm-Token.

Page 76: Das Perl-Tutorial für Computerlinguisten

72

# Die zehn häufigsten Trigramme sind: # ? ": 3446 Das sind 0.34630% aller Trigramm-Token. # ! ": 2677 Das sind 0.26902% aller Trigramm-Token. # . " ": 2465 Das sind 0.24772% aller Trigramm-Token. # ? " ": 1638 Das sind 0.16461% aller Trigramm-Token. # . " I: 992 Das sind 0.09969% aller Trigramm-Token. # , " he: 828 Das sind 0.08321% aller Trigramm-Token. # , " said: 807 Das sind 0.08110% aller Trigramm-Token. # ! " ": 694 Das sind 0.06974% aller Trigramm-Token. # I don 't: 681 Das sind 0.06844% aller Trigramm-Token. # ! I: 654 Das sind 0.06572% aller Trigramm-Token. # ... .: 629 Das sind 0.06321% aller Trigramm-Token. # Die zehn häufigsten Tetragramme sind: # ? " ": 1638 Das sind 0.16461% aller Tetragramm-Token. # ! " ": 694 Das sind 0.06974% aller Tetragramm-Token. # ! " cried: 303 Das sind 0.03045% aller Tetragramm-Token. # , of course ,: 289 Das sind 0.02904% aller Tetragramm-Token. # ? " he: 269 Das sind 0.02703% aller Tetragramm-Token. # . " " I: 254 Das sind 0.02553% aller Tetragramm-Token. # ! " he: 249 Das sind 0.02502% aller Tetragramm-Token. # ? " " I: 242 Das sind 0.02432% aller Tetragramm-Token. # , " he said: 228 Das sind 0.02291% aller Tetragramm-Token. # , " said the: 224 Das sind 0.02251% aller Tetragramm-Token. # I don 't know: 222 Das sind 0.02231% aller Tetragramm-Token.

Die Beobachtungen, die man anhand dieser Ausgabe machen kann, gliedern sich in linguistische

und programmiertechnische Erkenntnisse. Als sprachwissenschaftliches Ergebnis lässt sich festhalten,

dass der Anteil der tatsächlich im Korpus vorhandenen Bi-, Tri- und Tetragramme im Vergleich zu den

jeweils möglichen n-Grammen extrem gering ist, während der Anteil der seltenen sprachlichen

Einheiten mit zunehmendem n der n-Gramme ansteigt. Wir haben also ein Problem der knappen Daten

(engl. sparse data problem). Diese Eigenschaft natürlicher Sprache ist auch als Zipf'sches Gesetz

bekannt: Es gibt sehr wenige häufig auftretende Worttypen, während es eine große Menge an seltenen

Worttypen gibt.

Page 77: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

73

Dieser Zustand ändert sich auch nicht, wenn man die Größe des Korpus erhöht. Es muss also eine

Möglichkeit geben, seltene oder unbekannte sprachliche Einheiten trotzdem mit den Mitteln der

Statistik erfassen zu können, um Voraussagen verschiedenster Art treffen zu können. Für das

Verständnis dieser Methoden sind tiefergehende Kenntnisse der Statistik vonnöten, die den Umfang

dieser Diskussion sprengen würden.

Darüber hinaus sehen wir, dass in den Trefferlisten der n-Gramme Interpunktionszeichen sehr

prominent vertreten sind. Dies rührt daher, dass durch die Tokenisierung die Interpunktion isoliert

wurde, wenn sie als Satzzeichen identifiziert werden konnte, sie aber nicht aus dem Korpus entfernt

wurde oder durch uns beim Einlesen ignoriert wurde. Zusätzlich finden sich in den Listen der

häufigsten n-Gramme viele Wörter geschlossener Wortklassen, wie z.B. Artikel, Konjunktionen,

Präpositionen etc. Je nach Anwendung können auch diese für die computerlinguistische Analyse

interessant sein, für gewöhnlich wird man sie aber herausfiltern, um Wörter mit höherem

Informationsgehalt zu fokussieren. Diesen Problemen werden wir uns im nächsten Kapitel widmen, in

dem wir mit regulären Ausdrücken das wohl mächtigste Werkzeug, das Perl zu bieten hat, kennen

lernen.

Auf der programmiertechnischen Seite ist festzuhalten, dass wir wiederum eine Menge redundanten

Code produziert haben, um die verschiedenen n-Gramme behandeln zu können. Dies wird im

übernächsten Kapitel adressiert, wenn wir über Subroutinen reden.

Page 78: Das Perl-Tutorial für Computerlinguisten

74

7 Reguläre Ausdrücke

Homo sapiens are about pattern recognition [...].

Both a gift and a trap.

WILLIAM GIBSON

Bis jetzt beschränken sich die Operationen, die wir zum Vergleich von Zeichenketten oder Zahlen

kennen, auf die vollständige Äquivalenz oder auf einen größer- oder kleiner-als-Vergleich. Dazu

benötigten wir immer zwei konkrete Instanzen für den Vergleich, seien dies Literale oder Variablen.

Anhand regulärer Ausdrücke können wir nun einerseits über konkrete zu vergleichende Werte

abstrahieren, andererseits können wir Bestandteile eines Werts auf Äquivalenz untersuchen.

Reguläre Ausdrücke sind eine Weiterentwicklung der in den 1950er Jahren vom Mathematiker

Stephen Kleene erdachten Notation zur Manipulation regulärer Mengen. Als solche stellen reguläre

Ausdrücke Muster dar, die man wie eine Schablone auf einen Wert legt, um dann zu entscheiden, ob

das Muster darauf passt, weshalb man diese Operation auch als Mustervergleich (engl. pattern

matching) bezeichnet:

Die bisher besprochenen Perl-Konstrukte finden nur die drei Vorkommen von "ein":

my @woerter; my $suchwort = "ein"; while(<DATA>){ # Daten s.o.! push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Suchwort gefunden: $wort\n" if($wort eq $suchwort); } # Ausgabe: # Suchwort gefunden: ein # Suchwort gefunden: ein # Suchwort gefunden: ein

Page 79: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

75

Reguläre Ausdrücke finden die Zeichenkette "ein" auch, wenn sie Teil eines Worts ist oder groß

geschrieben wurde:

my @woerter; my $muster = "ein"; while(<DATA>){ # Daten s.o.! push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ m/$muster/ig); } # Ausgabe: # Muster gefunden: ein # Muster gefunden: ein # Muster gefunden: Vereinigten # Muster gefunden: Eindeutig # Muster gefunden: ein # Muster gefunden: einzuschalten

Der einzige Unterschied zwischen diesen beiden Programmen besteht in der if()-Bedingung, in

der wir im zweiten Fall drei neue Sprachkonstrukte finden:

• den Musterbindungsoperator =~,

• den Vergleichsoperator m// und

• die Modifikatoren i und g.

7.1 Muster Der Musterbindungsoperator bindet einen skalaren Ausdruck an ein Muster, d.h. das linke

Argument dieses Operators ist die zu durchsuchende Zeichenkette und das rechte Argument ein Literal

oder eine Skalarvariable, nach der gesucht werden soll. Den Vergleichsoperator kennen wir bereits in

seiner Kurzschreibweise: Wenn wir im letzten Kapitel die split()-Funktion verwendet haben, war ihr

erstes Argument ein regulärer Ausdruck, mit dem wir die eingelesene Zeile auf Leerzeichen durchsucht

haben, an denen wir die Zeichenkette nach Wörtern aufgetrennt haben. Man kann also den

Vergleichsoperator m// auch einfach als // schreiben; im Laufe dieses Kapitels werden wir mit s///

und tr/// zwei weitere Operatoren kennen lernen, die ebenfalls mit regulären Ausdrücken operieren,

die allerdings keine Kurzschreibweisen zulassen. Wie bereits bei den alternativen Anführungszeichen

gesehen, dürfen die Begrenzungszeichen // auch hier durch andere paarige Begrenzungszeichen ersetzt

werden.43 Dies ist insbesondere dann sinnvoll, wenn das Muster selbst Schrägstriche, wie z.B. in

Verzeichnispfaden enthält, da man diese dann – wie gewohnt – durch Backslashes maskieren müsste,

was sehr unübersichtlich werden kann – man spricht deshalb auch vom leaning toothpick syndrome.

Will man also eine komplette Zeichenkette oder einen Teil einer Zeichenkette finden, schreibt man

als erstes Argument einer if()-Bedingung eine gegebene Zeichenkette, d.h. die Quelle, den

Musterbindungsoperator und dann das Muster als Literal oder Skalar zwischen den Vergleichsoperator:

43 Dies bedeutet allerdings im Umkehrschluss nicht, dass die Trennzeichen bei den alternativen Anführungszeichen q//, qq//

und qw// ebenfalls Begrenzungszeichen für reguläre Ausdrücke sind!

Page 80: Das Perl-Tutorial für Computerlinguisten

76

my $zeichenkette = "Ein einfacher Satz zur Illustration ."; print "Ist enthalten!\n" if($zeichenkette =~ /einfacher Satz/); my $muster = "einfacher Satz"; print "Ist enthalten!\n" if($zeichenkette =~ /$muster/); # Ausgabe: # Ist enthalten! # Ist enthalten!

Eine Ausnahme bildet die Verwendung von $_ als Quelle: In diesem Fall kann sowohl auf die

Angabe des Bezeichners der Quelle, also $_, wie auf den Musterbindungsoperator verzichtet werden:

my $muster = "einfacher Satz"; while(<DATA>){ print "Ist enthalten!\n" if(/$muster/); # Ausgabe: Ist

enthalten! } __END__ Ein einfacher Satz zur Illustration .

In der symbolischen Computerlinguistik bearbeitet man allerdings häufig Aufgabenstellungen, in

denen man davon ausgehen muss, nur Teile des Musters angeben zu können, während andere Teile

veränderlich sind. Ein Beispiel hierfür wäre z.B. die Erfassung von Flexionsendungen deutscher

Verben. Gegeben eine Liste von Wortstämmen, ließe sich zwar entscheiden, ob eine gegebene

Zeichenkette das jeweilige Verb enthält, wir hätten allerdings noch keine Möglichkeit, möglichst

abstrakte Kriterien anzugeben, die es uns erlauben, die Endungen aus der Zeichenkette zu isolieren:

my @woerter; my $muster = "spiel"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt

Zwar werden die einschlägigen Wörter richtig herausgefiltert, doch können wir programmatisch

noch nicht festhalten, dass "en" und "t" Verbendungen sind. Um sie erfassen zu können, benötigt man

Platzhalter, welche die Merkmale dieser Zeichenketten auf einer abstrakten Ebene repräsentieren.

Page 81: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

77

7.2 Platzhalter Platzhalter (engl. wildcards) repräsentieren Zeichenketten, die man nicht explizit angeben will oder

kann. Um z.B. die gefundenen Verbendungen für den Stamm "spielen" zu klassifizieren, könnte man

einen Platzhalter dergestalt formulieren, dass dem Verbstamm mindestens ein Buchstabe folgen muss,

aber auch mehrere Buchstaben folgen können. Wir machen also sowohl Angaben über das Paradigma

möglicher Zeichen als auch über das Syntagma, nämlich dass die Endung dem Stamm folgt, und dass

die zu verwendenden Zeichen eine bestimmte Quantität besitzen. In Bezug auf das Paradigma spricht

man von Zeichenklassen, die eine Abstraktion über Zeichen darstellen, während das einschlägige

Sprachmittel für das Syntagma Quantoren sind.

Zeichenklassen

Wie bereits gesehen, lassen sich in regulären Ausdrücken Zeichenketten angeben, die als

Suchmuster innerhalb gegebener Zeichenketten dienen. Will man über die Suchmuster abstrahieren,

verwendet man Zeichenklassen. Von diesen gibt es in Perl einige vordefinierte, man kann aber auch

eigene Klassen angeben.

Eine Zeichenklasse besteht aus einer Menge von Zeichen, die immer nur disjunktiv voneinander

ausgewählt werden können, d.h. aus der Menge {a, b, c, d} kann nur a oder b oder c oder d ausgewählt

werden. Im Umkehrschluss bedeutet dies aber auch, dass eine komplette Zeichenklasse genau ein

Zeichen repräsentiert!

In Perl wird eine solche Menge nicht wie aus der Mathematik gewohnt durch geschweifte

Klammern, sondern durch eckige Klammern [ ] angezeigt. Will man also beispielsweise aus

folgenden Sätzen nur die Instanzen der zweiten und dritten Person Singular des Verbs "spielen", nicht

aber die der ersten Person Singular oder Plural ermitteln, könnte man das Literal spiel durch die Zei-

chenklasse [st] erweitern:

my @woerter; my $muster="spiel[st]"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielt # Muster gefunden: spielst

Page 82: Das Perl-Tutorial für Computerlinguisten

78

Beim Durchlaufen des Wörter-Arrays findet der Perl-Interpreter zunächst "spielt", da das

angegebene Muster "spiel+t" enthält, während "spielst" gefunden wird, weil das Muster "spiel+s"

enthält. Das "t" in der Zeichenklasse ist für diesen letzten Mustervergleich irrelevant, da nur eine der in

der Zeichenklasse enthaltenen Zeichen auch in der gegebenen Zeichenkette vorhanden sein muss. Die

genaue Arbeitsweise der in Perl für reguläre Ausdrücke zuständigen Instanz soll im Laufe des Kapitels

noch besprochen werden.

Alternativen in regulären Ausdrücken lassen sich nicht nur durch Zeichenklassen repräsentieren.

Das Pipe-Symbol | trennt ebenfalls Zeichen disjunktiv voneinander:

my @woerter; my $muster="spiels|t"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielt # Muster gefunden: spielst

Neben einzelnen Zeichen lassen sich in Zeichenklassen auch Bereiche repräsentieren. So werden

z.B. alle Kleinbuchstaben mit Ausnahme der Umlaute und der sz-Ligatur als [a-z] darstellen,

Großbuchstaben durch [A-Z]. Analog dazu werden Ziffern durch die Zeichenklasse [0-9] abgebildet.

Um beispielsweise einen (naiven) Parser für l33t sp34k – einer bei jungen Hackern beliebten

Schreibvariante des Englischen, in der einige alphabetische Zeichen durch Ziffern ersetzt werden, die

ähnlich aussehen – zu bauen, könnten wir die Zeichenklasse [a-z][0-9][0-9][a-z] angeben:

my @woerter; my $muster="[a-z][0-9][0-9][a-z]"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ?

Page 83: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

79

# Ausgabe: # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d

Zeichenklassen lassen sich auch negieren, doch im Gegensatz zu anderen Ausdrücken verwendet

man nicht das Ausrufezeichen !, sondern das Caret ^, das am Anfang der Zeichenklasse stehen muss.

Suchen wir also beispielsweise alle Instanzen von "spielen", die nicht die Morphologie der zweiten

oder dritten Person Singular beinhalten, können wir /spiel[^st]/ als regulären Ausdruck verwenden:

my @woerter; my $muster = "spiel[^st]"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: mitspiele

Vordefinierte Zeichenklassen

Da Buchstaben und Ziffern einen Großteil der von uns verwendeten Zeichen ausmachen, gibt es in

Perl dafür vordefinierte Zeichenklassen. So entspricht die Zeichenklasse [A-Za-z0-9_] den

sogenannten Wortzeichen, weshalb sie mit \w abgekürzt wird. Ziffern sind in der abgekürzten

Zeichenklasse \d und Leerraumzeichen, also das Leerzeichen, der Tabulatorvorschub und der

Zeilenumbruch, werden durch \s abgekürzt. Der reguläre Ausdruck für l33t sp34k lässt sich

dementsprechend zu \w\d\d\w verkürzen:44

my @woerter; my $muster = "\\w\\d\\d\\w"; while(<DATA>){ push(@woerter, split(/ /)); }

44 Aufgrund der Interpolation der vordefinierten Zeichenklassen aus einer Skalarvariablen muss der Backslash aus den

abgekürzten Zeichenklassen durch einen weiteren Backslash maskiert werden!

Page 84: Das Perl-Tutorial für Computerlinguisten

80

while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ? # Ausgabe: # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d

Allerdings ist zu beachten, dass dieser reguläre Ausdruck übergeneriert, d.h. er würde auch auf

andere Zeichenketten passen, die nicht l33t sp34k sind, wie z.B. Jahreszahlen, denn durch die

Inklusion der Ziffern in der Klasse der Wortzeichen passt \w\d\d\w auch auf vierstellige Zahlen!

Zu allen genannten vordefinierten Zeichenklassen existieren analog zur Negation in eigenen

Zeichenklassen auch Komplemente. Diese werden durch einen Großbuchstaben angezeigt, sodass \W

alle Zeichen außer Wortzeichen erfasst, \D für alle Zeichen außer Ziffern und \S für alle Zeichen außer

Leerraumzeichen steht.

Darüber hinaus darf man nicht vergessen, dass \w die deutschen Umlaute und die sz-Ligatur nicht

erfasst! Will man einen deutschsprachigen Text bearbeiten, muss man diese Zeichen entweder durch

eine Erweiterung der Zeichenklasse explizit mit angeben [\wÄÖÜäöüß] oder man verwendet das Pragma

locale. Dieses liest vom Betriebssystem vorgehaltene Informationen über die zu verwendenden

Ländereinstellungen aus und erweitert \w automatisch um die oben genannten Zeichen. Man muss sich

allerdings auch darüber im Klaren sein, dass die Portierbarkeit bzw. Wiederverwendbarkeit des

jeweiligen Perl-Skripts dadurch eingeschränkt wird, da man nicht notwendigerweise davon ausgehen

kann, dass jeder Rechner, auf dem das Skript laufen soll, diese Voreinstellungen besitzt!

Universaler Mustervergleich

Einen Sonderfall im Zeichenparadigma stellt der Punkt . dar. Er steht in einem regulären Ausdruck

für jedes beliebige Zeichen inklusive Leerraumzeichen außer einem Zeilenendezeichen. Setzt man ihn

im Beispiel des Verbs "spielen" für ein beliebiges Zeichen nach dem Wortstamm ein, sorgt er dafür,

dass nur Instanzen mit einer echten Verbendung erfasst werden, der Imperativ aber nicht:

my @woerter; my $muster = "spiel."; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); }

Page 85: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

81

__END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt # Muster gefunden: mitspiele # Muster gefunden: spielst

Dementsprechend muss ein literaler Punkt in einem regulären Ausdruck durch einen Backslash

maskiert werden:

my @woerter; my $muster = "\\."; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: . # Muster gefunden: . # Muster gefunden: . # Muster gefunden: . # Muster gefunden: .

Quantoren

Im letzten l33t sp34k-Beispiel ist die doppelte Angabe der Zeichenklasse für Ziffern eher

suboptimal. Perl erlaubt es an dieser Stelle, vermittels Quantoren anzugeben, wie oft ein Zeichen oder

eine Zeichenklasse in einer gegebenen Zeichenkette enthalten sein muss, damit das Muster darauf

passt.

Anhand von Quantoren lassen sich sowohl konkrete Zahlen für die Häufigkeit des Vorkommens

angeben als auch über Häufigkeiten abstrahieren. Zu letzterer Kategorie von Quantoren zählen die nach

Kleene benannten Stern * und Plus + sowie das Fragezeichen ?.

Der Stern * besagt, dass von dem zu quantifizierenden Ausdruck null oder mehrere Instanzen in

einer gegebenen Zeichenkette vorhanden sein müssen. Das Plus-Zeichen + steht für mindestens eine

Page 86: Das Perl-Tutorial für Computerlinguisten

82

oder mehrere Instanzen des Ausdrucks, während das Fragezeichen ? Optionalität anzeigt, also keine

oder genau eine Instanz zu finden sein muss. Wenden wir dies auf unser Problem der Verbmorphologie

an, können wir die Flexionsendung als \w+ zu mindestens einem oder mehreren Wortzeichen

abstrahieren. Zwar übergeneriert auch dieser Ausdruck – aber nur, wenn man wie bei l33t sp34ak

damit rechnen müsste, Ziffern oder Unterstriche in einem Wort vorzufinden:

my @woerter; my $muster = "spiel\\w+"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt # Muster gefunden: mitspiele # Muster gefunden: spielst

Um auch den Imperativ erfassen zu können, muss man die Verbendung durch einen Stern oder das

Fragezeichen optional machen:

my @woerter; my $muster = "spiel\\w*"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst .

Page 87: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

83

# Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt # Muster gefunden: spiel # Muster gefunden: mitspiele # Muster gefunden: spielst

Die Häufigkeit, eines Zeichens in einer gegebenen Zeichenkette, die durch das Muster beschrieben

werden soll, lässt sich auch konkret angeben. Dazu verwendet man die geschweiften Klammern {}

nach dem entsprechenden Zeichen im Muster. In die geschweiften Klammern kann man nun sowohl

eine Zahl schreiben, die die Häufigkeit repräsentiert, als auch einen Bereich, mit dem sich Minimal-

und/oder Maximal-Häufigkeiten abbilden lassen. Anders als bei Listen werden Bereiche hier allerdings

nicht durch zwei Punkte angezeigt, sondern durch ein Komma, das den Minimalwert vom

Maximalwert trennt. Will man also alle l33t sp34k-Wörter erfassen, die aus einem Wortzeichen, zwei

Ziffern und wiederum einem Wortzeichen bestehen, kann man beispielsweise den regulären Ausdruck

/\w\d{2}\w/ verwenden:

my @woerter; my $muster = "\\w\\d{2}\\w"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ? # Ausgabe: # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d

Alle Wörter, die mindestens aus drei Zeichen bestehen, bildet das Muster /\w{3,}/ ab:

my @woerter; my $muster = "\\w{3,}"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ?

Page 88: Das Perl-Tutorial für Computerlinguisten

84

# Ausgabe: # Muster gefunden: C4n # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d

Alle Wörter, die maximal aus zwei Zeichen bestehen, werden durch das Muster /\w{0,2}/ erfasst.

Man beachte, dass anders als bei der Angabe des Minimums beide Werte explizit angegeben werden

müssen, auch wenn man ein Minimum von 0 ansetzt; es reicht nicht, /\w{,2}/ zu notieren, auch wenn

dies mehr nach Perl aussieht:

my $muster = "\\w{0,2}"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ? # Ausgabe: # Muster gefunden: C4n # Muster gefunden: u # Muster gefunden: r34d # Muster gefunden: my # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: , # Muster gefunden: d00d # Muster gefunden: ?

Das Komma und das Fragezeichen werden gefunden, weil wir die Klasse der Wortzeichen durch

die 0 optional gemacht haben. Ansonsten entsprechen die Ergebnisse unseren Erwartungen.

Variableninterpolation

Bis jetzt sind wir relativ sorglos mit der Frage umgegangen, wie man skalare Variablen in regulären

Ausdrücken verwendet: Wie in doppelten Anführungszeichen werden sie interpoliert und ihre Werte

als Muster in den jeweiligen regulären Ausdruck übernommen. Was geschieht aber, wenn die soeben

diskutierten Quantoren oder der Punkt durch Interpolation Bestandteil des Musters werden? Betrachten

wir dazu folgendes Beispiel, in dem wir als gegebene Zeichenkette etwas wie einen regulären

Ausdruck "(.+)" (z.B. aus einem Einführungsbuch zu Perl) und als Muster den regulären Ausdruck

/.+)/ haben:

Page 89: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

85

my $muster = ".+)"; my $zeichenkette = "(.+)"; print "$zeichenkette" if($zeichenkette =~ /$muster/); # Ausgabe: # Uncaught exception from user code: Unmatched ) in regex; marked by <-- HERE in m/.+) <-- HERE / at

programm.pl line 4.

Durch die Variableninterpolation interpretiert Perl den Punkt als Abstraktion über alle Zeichen und

das Plus als Quantor. Dann allerdings stößt es auf eine schließende runde Klammer: Aus der

Fehlermeldung lässt sich schließen, dass der Interpreter ein öffnendes Pendant dazu erwartet. Runde

Klammern besitzen also eine eigene Funktionalität innerhalb regulärer Ausdrücke, wie wir gleich sehen

werden. Dennoch sollte es doch möglich sein, die literalen Zeichen zu finden! Dazu gibt es in Perl den

sogenannten Quotemeta-Operator \Q, der innerhalb regulärer Ausdrücke die Zeichen . * ? + [ ] ( ) { }

^ $ | und \ maskiert, bis er auf ein \E oder das Ende des regulären Ausdrucks trifft:

my $muster = ".+)"; my $zeichenkette = "(.+)"; print "$zeichenkette" if($zeichenkette =~ /\Q$muster\E/); # Ausgabe:

(.+)

7.3 Gruppierung und Speicherung Bis jetzt konnten wir innerhalb der Suchmuster mit einem Quantor immer nur ein Zeichen erfassen.

Analog zur Gruppierung von Termen bei arithmetischen Operationen lassen sich auch in regulären

Ausdrücken Zeichen durch runde Klammern zu einer Einheit zusammenfassen. So passt z.B. der

reguläre Ausdruck /(tam){2}/ auf die Zeichenkette "tamtam":

my $zeichenkette = "tamtam"; my $muster = "(tam){2}"; print $zeichenkette if($zeichenkette =~/$muster/); # Ausgabe: tamtam

Doch die runden Klammern besitzen nicht nur die Funktionalität der Gruppierung von Zeichen,

sondern speichern auch die Fundstelle ab, sodass diese durch Indizes sowohl von außerhalb des

regulären Ausdrucks als auch innerhalb zugreifbar werden. Die Nummer des Index entspricht dabei der

n-ten runden Klammer im regulären Ausdruck. Dazu verwendet Perl eine Indexzählung, die anders als

bei Arrayindizes nicht bei 0 anfängt, sondern mit eins. Um auf eine Fundstelle außerhalb des regulären

Ausdrucks zuzugreifen, verwendet man den Index wie eine Skalarvariable, d.h. man schreibt ein

Dollarzeichen und die Nummer des Index:

my $zeichenkette = "tamtam"; my $muster = "(tam){2}"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: tam

Page 90: Das Perl-Tutorial für Computerlinguisten

86

Auf diese Weise lassen sich jetzt beispielsweise auch die Verbendungen aus den Beispielsätzen

auflisten:45

my (@woerter); my $muster = "spiel(\\w*)"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Endung gefunden: $1\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Endung gefunden: en # Endung gefunden: t # Endung gefunden: # Endung gefunden: e # Endung gefunden: st

Ebenso kann man bereits im Muster gespeicherte Zeichen innerhalb des regulären Ausdrucks

wieder aufnehmen. Dazu schreibt man einen Backslash vor den jeweiligen Index:

my $zeichenkette = "tamtam"; my $muster = "(tam)\\1"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: tam

Benötigt man nur die Funktionalität der Gruppierung, schreibt man ?: an den Anfang der

Klammern; die Verwendung einer Indexvariablen führt dementsprechend zu einer Fehlermeldung:

my $zeichenkette = "tamtam"; my $muster = "(?:tam){2}"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: Uncaught exception from user code: Reference to nonexistent group in regex; marked by <-- HERE in

m/(?:tam){2} <-- HERE / at programm.pl line 4.

Weitere durch das Fragezeichen eingeleitete Konstrukte werden uns weiter unten noch begegnen.

45 Man beachte, dass der Imperativ richtigerweise als mit dem Wortstamm identische Form ausgegeben wird.

Page 91: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

87

7.4 Anker Eine weitere Möglichkeit, die syntagmatischen Verhältnisse innerhalb eines Musters zu

beeinflussen, liegt in der Verwendung sogenannter Anker. Diese legen bestimmte Positionen fest, ab

denen oder bis zu denen ein Muster auf die gegebene Zeichenkette, d.h. die Zeile, passen soll. Will man

sicherstellen, dass das Muster am Anfang einer Zeile gefunden wird, schreibt man ein Caret ^ nach das

erste Trennzeichen des regulären Ausdrucks. Soll das Muster umgekehrt auf das Ende einer Zeile

passen, verwendet man das Dollarzeichen $ vor dem zweiten Trennzeichen des regulären Ausdrucks.

Will man beispielsweise alle Zeilen finden, die mit einem "P" anfangen, verwendet man den regulären

Ausdruck /^P/:

while(<DATA>){ print if(/^P/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " " Schade , dass Paul keine Zeit hat ! " # Ausgabe: # Paul und Maria spielen Schach . # Peter spielt Ball . # Peter sagt , " Dann spiel halt mit . "

Umgekehrt passt /"$/ auf alle Zeilen, die mit einem doppelten Anführungszeichen enden:

while(<DATA>){ print if(/"$/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " " Schade , dass Paul keine Zeit hat ! " # Ausgabe: # Peter sagt , " Dann spiel halt mit . " # " Schade , dass Paul keine Zeit hat ! "

Einen besonderen Anker stellt \b (für eng. boundary) dar: Er markiert die Position zwischen einem

Wortzeichen \w und einem Nicht-Wortzeichen \W. Durch seine Verwendung lässt sich sicherstellen,

dass die durch das Muster gesuchte Zeichenkette nicht in einer anderen Zeichenkette enthalten ist:

Page 92: Das Perl-Tutorial für Computerlinguisten

88

So können wir beispielsweise durch den regulären Ausdruck /\bspiel(\w*)/ sicherstellen, dass

Endungen nur zusammen mit dem Wortstamm "spiel" gefunden werden und nicht solche, die in

Komposita, wie "mitspielen" enthalten sind:

my (@woerter); my $muster = "\\bspiel(\\w*)"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Endung gefunden: $1 in $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Endung gefunden: en in spielen # Endung gefunden: t in spielt # Endung gefunden: in spiel # Endung gefunden: st in spielst

7.5 Modifikatoren Um das Verhalten regulärer Ausdrücke an unterschiedliche Gegebenheiten anzupassen, verwendet

man sogenannte Modifikatoren. Diese schreibt man hinter das zweite Trennzeichen des regulären

Ausdrucks:

• Der Modifikator i ermöglicht es, die Unterscheidung zwischen Groß- und Kleinschreibung

beim Mustervergleich auszuschalten.

• Mithilfe des Modifikators g kann man global innerhalb einer Zeichenkette nach mehreren

Instanzen eines Musters zu suchen. Sollen alle Fundstellen verarbeitet werden, verwendet man

statt der if()-Bedingung eine while()-Schleife, hier am Ausgangsbeispiel dieses Kapitels

illustriert:

my $muster = "\\b(ein)\\b"; while(<DATA>){ print "$1\n" while(/$muster/g); } __END__ Ob Frau oder Mann - Parlamentarier wie Öffentlichkeit sollten sich vor der Wahl ein

Page 93: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

89

verlässliches Bild, insbesondere ein klares Bild von Verlässlichkeit, Denkweise und Handlungsabsichten der neuen Bundesrätin oder des neuen Bundesrats machen können. Zuverlässige und präzise Recherchen der Medien oder gegebenenfalls auch Anhörung der Kandidaten durch die Fraktionen anderer Parteien dienen solcher Klärung vor der Wahlausmarchung in der Vereinigten Bundesversammlung. Auch heikle Fragen sind dabei nicht auszusparen. Eindeutig unlauter und in jeder Hinsicht fragwürdig sind die Mittel und Methoden, mit denen sich dieser Tage ein anonymes Komitee in den Prozess der Kandidatenauslese einzuschalten versucht hat. # Ausgabe: # ein # ein # ein

• Anders als die Bezeichnungen für die Modifikatoren s (single-line) und m (multi-line) vermuten

lassen, dass man Zeichenketten als eine bzw. mehrere Zeilen verarbeiten kann, beeinflussen sie

lediglich das Verhalten der Anker ^ und $ sowie des Punkts .. Zeilen, die beispielsweise durch

das Einlesen einer Datei in einen Skalar auch Zeilenendezeichen enthalten, können immer,

unabhängig von der Verwendung dieser beiden Modifikatoren, verarbeitet werden. Verwendet

man das s, passt der Punkt auch auf ein Zeilenendezeichen. Im multi-line Modus passt das

Caret sowohl auf den Anfang der Zeichenkette als auch nach jedem Zeilenendezeichen,

während das Dollar vor jedem Zeilenendezeichen und am Ende der Zeichenkette passt; deshalb

spricht man in diesem Zusammenhang auch von logischen Zeilen. Das Verhalten des Punkts

wird durch den Modifikator m nicht beeinflusst. Kombiniert man allerdings die Funktionalität

von s und m, erhält man einen regulären Ausdruck, dessen Anker mehrere durch Zeilenende

getrennte logische Zeilen verarbeiten können, wobei der Punkt auch auf ein solches Zeilenende

passt. Da diese Funktionalität eher selten gebraucht wird, verzichten wir an dieser Stelle auf ein

Beispiel und belassen es bei der Klarstellung der Terminologie.

• Der Modifikator o macht die Verarbeitung regulärer Ausrücke effizienter, ist aber mit Bedacht

einzusetzen: Sind im Muster keine Variablen zu interpolieren, speichert Perl standardmäßig bei

der ersten Verwendung des regulären Ausdrucks eine interne Repräsentation davon ab, da sich

diese nicht verändern kann, wenn dieser Ausdruck erneut im Programm verwendet wird. Gibt

es nun andererseits eine zu interpolierende Variable, wird der reguläre Ausdruck jeweils neu in

eine interne Repräsentation übersetzt. Dies ist aber nicht immer notwendig, z.B. dann, wenn

der Wert der Variablen während des Mustervergleichs konstant bleibt und sich lediglich die zu

vergleichenden Zeichenketten ändern. In diesem Fall sorgt o dafür, dass der reguläre Ausdruck

nur einmal übersetzt wird. Dies kann aber problematisch sein, wenn man dies auch bei

regulären Ausdrücken anwendet, deren Werte sich ändern: Trotzdem der Wert in der zu

interpolierenden Variablen ein neuer ist, wird Perl den einmal übersetzten Wert für alle

weiteren Mustervergleiche verwenden und den neuen Wert komplett ignorieren!

• Durch den Modifikator x lassen sich Kommentare und Zeilenumbrüche in das Muster des

regulären Ausdrucks einfügen, um komplexe Ausdrücke besser strukturieren und

zusammengehörige Komponenten als solche kennzeichnen zu können.

Page 94: Das Perl-Tutorial für Computerlinguisten

90

7.6 Wie funktionieren reguläre Ausdrücke? Im Gegensatz zu den Wörtern und Sätzen der natürlichen Sprache gehören reguläre Ausdrücke zu

den formalen Sprachen. Der grundlegende Unterschied zwischen natürlichen und formalen Sprachen

liegt darin, dass man anhand eines Algorithmus nicht entscheiden kann, ob ein gegebener Satz einer

natürlichen Sprache grammatisch ist oder nicht, da eine Grammatik, die versuchen würde, alle

möglichen Sätze einer natürlichen Sprache zu erfassen, zu komplex wäre. Umgekehrt ist es – wie wir

gesehen haben – sehr wohl möglich, zu entscheiden, ob eine gegebene Zeichenkette der durch ein

Muster vorgegebenen "Grammatik" entspricht.

Damit Perl entscheiden kann, ob ein Muster auf eine Zeichenkette passt, benötigt es einen

mathematischen Formalismus, der entscheidet, ob ein Zeichen aus dem Muster mit einem Zeichen der

gegebenen Zeichenkette übereinstimmt, und ob darüber hinaus die gegebene Sequenz durch das Muster

abgedeckt wird. Dazu bedient sich Perl eines sogenannten endlichen Automaten.

Ein endlicher Automat ist ein Algorithmus, der in endlichen vielen Schritten (Zustände genannt)

entscheidet, ob eine gegebene Zeichenkette durch das jeweilige Muster verarbeitet werden kann. Dazu

befindet sich der Automat zunächst in einem Startzustand. Aus diesem liest er zeichenweise die

gegebene Zeichenkette: Jeder Zustand akzeptiert entweder das gelesene Zeichen oder verwirft es; eine

Übergangsfunktion zwischen den einzelnen Zuständen sorgt dafür, dass bei der Konsumption der

gegebenen Symbole ein Folgezustand erreicht wird. Letztendlich erreicht der Automat einen

Endzustand, an dem entschieden ist, ob die gegebene Zeichenkette auf das Muster passt oder nicht.

Betrachten wir zur Illustration einen endlichen Automaten, der die Symbole {h,a,!} kennt und

dessen Grammatik mindestens eine, aber beliebig viele Abfolgen der Zeichenkette "ha" erkennt, die

von einem Ausrufezeichen abgeschlossen wird; der reguläre Ausdruck dazu lautet /(ha)+!/:

Aus dem Startzustand gelangt der Automat durch Konsumption des Symbols "h" in den

Folgezustand , von dem er in den Zustand gelangt, indem er ein "a" konsumiert. An dieser Stelle

wäre also die Minimalbedingung unseres regulären Ausdrucks, mindestens einmal die Zeichenkette

"ha" zu lesen, erfüllt. Danach kann der Automat in den Zustand zurückkehren, wenn er erneut auf

eine Zeichenkette trifft, die mit einem "h" anfängt. Ansonsten geht er in den Zustand über, wenn er

dabei ein Ausrufezeichen lesen kann.

Dies ist aber nicht die einzige Möglichkeit, einen endlichen Automaten zu entwerfen, der derartige

Zeichenketten verarbeitet:

In diesem Automat kommt man vom Startzustand in den Folgezustand , indem das Symbol "h"

gelesen wird. Von dort gelangt man aber durch Konsumption des Zeichens "a" entweder in den

Page 95: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

91

Zustand , sodass genau einmal die Zeichenkette "ha" gelesen wurde, oder zurück in den Startzustand

, aus dem weitere Abfolgen von "ha" konsumiert werden können. Dennoch muss die Abfolge

einmal durchlaufen werden, um nach Konsumption des Symbols "!" in den Endzustand zu gelangen.

Der Unterschied zwischen diesen beiden Automaten besteht darin, dass man im ersten Fall immer

entscheiden kann, in welchem Zustand sich der Automat nach Konsumption eines Zeichens befindet,

während dies im zweiten Automaten nicht möglich ist. Man bezeichnet daher den ersten Automaten

auch als deterministischen endlichen Automaten, während der zweite nichtdeterministischer endlicher

Automat genannt wird.

Die in den verschiedenen Programmiersprachen implementierten Maschinen zur Verarbeitung

regulärer Ausdrücke gehören meist zu den nichtdeterministischen endlichen Automaten. Betrachten wir

zunächst am einfachen Beispiel der gegebenen Zeichenkette "Ananas" und dem simplen Muster

/anas/i, wie reguläre Ausdrücke generell abgearbeitet werden:

Um vom Startzustand in den ersten Folgezustand zu gelangen, muss ein "a" konsumiert werden; der

Modifikator i sorgt an dieser Stelle dafür, dass der Großbuchstabe als ein solches erkannt wird. In den

nächsten beiden Schritten passen die in der Zeichenkette vorhandenen Symbole "n" und "a" auf das

Muster, doch müsste darauf ein "s" folgen, um dem Muster zu entsprechen. Also wird das Muster in

Schritt 5 um ein Symbol in der gegebenen Zeichenkette weiter nach rechts bewegt. Wiederum wird der

Anfang des Musters mit dem aktuellen Zeichen verglichen; da das "n" des Musters nicht auf das "a" der

Zeichenkette passt, wird das Muster erneut um ein Zeichen weiter bewegt. In Schritt 7 kann nun das

Symbol "a" konsumiert werden, ebenso wie die nachfolgenden Zeichen in den Schritten 8-10, sodass

der endliche Automat entscheiden kann, dass das Muster /anas/i auf das Ende der Zeichenkette

"Ananas" passt.

Page 96: Das Perl-Tutorial für Computerlinguisten

92

Wie wir oben gesehen haben, stellen Zeichenklassen, bzw. Alternation ein wichtiges Mittel der

Abstraktion über Zeichenketten dar. Betrachten wir nun exemplarisch, wie Perl die Zeichenkette

"Delphin" abarbeitet, wenn das Muster /Del(?:f|ph)in/ gegeben ist:

Aus dem Startzustand heraus werden die ersten drei Zustände durch die Identität der Symbole "D",

"e" und "l" im Muster und der Zeichenkette durchlaufen. Das nächste Zeichen, "p", passt allerdings

nicht auf das im Muster angegebene "f". Anstatt nun das Muster um ein Symbol weiter zu bewegen,

wird das "f" durch die alternativen Zeichen "p" und "h" ersetzt, wie in Schritt 5 zu sehen ist. Daraufhin

passt das "p" in der Zeichenkette auf das im Muster angegebene Symbol, und auch die weiteren

Vergleiche glücken, sodass entschieden werden kann, dass das Muster /Del(?:f|ph)in/ auf die

Zeichenkette passt.

Wieso bezeichnet man die Verarbeitung regulärer Ausdrücke in Perl als nichtdeterministischen

endlichen Automaten? Um diese Frage beantworten zu können, betrachten wir die Funktionalität der

Quantoren am Beispiel der Zeichenkette "Bootsmann" und dem Muster /B.*mann/:

Nachdem aus dem Startzustand heraus das Symbol "B" konsumiert wurde, passt der Punkt auf das

nächste Zeichen, das "o". Aufgrund des Sterns erweitert sich der Gültigkeitsbereich des Punkts auf den

Rest der gegebenen Zeichenkette (Schritte 4-10). Doch obschon durch diese Symbole des Musters die

gesamte Zeichenkette konsumiert werden könnte, müssen auch noch die übrig gebliebenen Zeichen

"m", "a", "n" und "n" überprüft werden. Um dies zu bewerkstelligen, wird das restliche Muster solange

nach links zurück geschoben, bis ihr erstes Symbol wieder auf ein Zeichen der Zeichenkette passt

(Schritte 11-14). Diesen Vorgang bezeichnet man als Backtracking: Der endliche Automat kann so

viele Zeichen zurücknehmen, wie nötig, um sich eine Option auf einen erfolgreichen Mustervergleich

offen zu halten. Dies ist bei einem deterministischen endlichen Automaten nicht nötig, da er ja zu jeder

Zeit weiß, in welchem Zustand er sich gerade befindet.

Page 97: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

93

Zuletzt sei noch gezeigt, wie Perl Anker verarbeitet. Dazu schauen wir uns die unterschiedliche

Verarbeitung der Zeichenketten "da mal" und "damals" durch den regulären Ausdruck /\bda\b/ an:

Page 98: Das Perl-Tutorial für Computerlinguisten

94

In beiden Fällen passt der Anker \b auf die Anfänge der Zeichenketten. Danach werden die

Symbole "d" und "a" konsumiert. Während aber in der Zeichenkette "da mal" nun ein Leerzeichen

folgt, das durch die Zeichenklasse \W abgedeckt wird und somit eine Wortgrenze im Sinne von \b

darstellt, trifft das Muster in der Zeichenkette "damals" auf ein weiteres Wortzeichen und somit keine

Wortgrenze. Deshalb passt der reguläre Ausdruck /\bda\b/ nur auf "da mal", nicht aber auf "damals".

7.7 Gier Wie im Beispiel der Zeichenkette "Bootsmann" und des Musters /B.*mann/ gesehen, besteht eine

der Eigenschaften der multiplikativen Quantoren * und + darin, so viele Symbole wie möglich zu

konsumieren. Dies bezeichnet man auch als Gier. Dieses Verhalten ist aber oftmals gar nicht

gewünscht, wenn man z.B. sicherstellen möchte, dass man die erste Instanz einer Zeichenkette findet:

my $muster = "(.*a4)"; my $zeichenkette = "eins zwei drei a4 vier fuenf sechs a4 sieben"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: eins zwei drei a4 vier fuenf sechs a4

Will man einen multiplikativen Quantor minimieren, sodass die Verarbeitung der gegebenen

Zeichenkette durch den regulären Ausdruck nach der ersten Fundstelle erfolgreich beendet wird,

schreibt man ein Fragezeichen hinter den Quantor:

my $muster = "(.*?a4)"; my $zeichenkette = "eins zwei drei a4 vier fuenf sechs a4 sieben"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: eins zwei drei a4

Page 99: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

95

7.8 Ersetzung Neben der Suche kann man Zeichenketten anhand regulärer Ausdrücke auch modifizieren. Dazu

verwendet man die in der Einleitung bereits erwähnten Operatoren s/// und tr///, die beide jeweils

ein Suchmuster und ein Ersetzungsmuster als Argumente benötigen; allerdings ist die Funktionalität

der beiden Operatoren unterschiedlich, sodass wir zunächst s/// diskutieren:

my $suchmuster = "toben"; my $ersetzungsmuster = "spazieren"; my $zeichenkette = "Paul und Maria toben duch den Wald ."; print "$zeichenkette\n"; # Ausgabe: Paul und Maria toben durch den Wald . $zeichenkette =~ s/$suchmuster/$ersetzungsmuster/; print "$zeichenkette\n"; # Ausgabe: Paul und Maria spazieren durch den Wald

.

Wie wir sehen, steht auch hier die gegebene Zeichenkette auf der linken Seite des

Musterbindungsoperators: Kann das Suchmuster in der Zeichenkette gefunden werden, wird es durch

das Ersetzungsmuster ausgetauscht und der Variablen zugewiesen.

Genauso wie beim Vergleichsoperator kann man auch mit dem Ersetzungsoperator Fundstellen im

regulären Ausdruck referenzieren. Allerdings kommt hier nicht der Backslash und ein numerischer

Index zum Einsatz, sondern das Dollarzeichen. So lässt sich auf einfache Weise aus einem deutschen

Aussagesatz einen Fragesatz machen:

my $zeichenkette = "du liest gerade ein buch"; $zeichenkette =~ s/(\w+)\s(\w+)/$2 $1/; print $zeichenkette."?"; # Ausgabe: liest du gerade ein buch?

Die Operation bei einem zusammengesetzten Subjekt sieht dann so aus:

my $zeichenkette = "peter und paul spielen ball"; $zeichenkette =~ s/((?:\w+\s){2}\w+)\s(\w+)/$2 $1/; print $zeichenkette."?"; # Ausgabe: spielen peter und paul

ball?

Der andere Ersetzungsoperator, tr///, operiert zwar genauso wie s/// auf einem Such- und einem

Ersetzungsmuster, korreliert er anders als s/// jedes Zeichen des Suchmusters mit jedem Zeichen des

Ersetzungsmusters. Seine Funktionsweise kann man sich also so vorstellen, dass sowohl im Such- als

auch im Ersetzungsmuster eine Zeichenklasse steht. Dementsprechend kann man in ihm weder

Zeichenklassen noch deren Abkürzungen verwenden und auch keine Zeichen gruppieren:

my $zeichenkette = "Peter und Paul spielen Ball ."; $zeichenkette =~ tr/ /_/; print $zeichenkette; # Ausgabe:

Peter_und_Paul_spielen_Ball_.

Page 100: Das Perl-Tutorial für Computerlinguisten

96

tr/// funktioniert also analog zu s///g. Darüber hinaus lassen sich Bereiche angeben:

my $zeichenkette = "2011064"; $zeichenkette =~ tr/0-9/a-j/; print $zeichenkette; # Ausgabe: cabbage

Der Rückgabewert von tr/// ist die Häufigkeit der vorgenommenen Ersetzungen. Da man aber

nicht notwendigerweise etwas ersetzen muss, d.h. tr/// auch mit einem leeren Ersetzungsoperator

verwendet werden kann, stellt sich der günstige Seiteneffekt ein, dass man tr/// zum Zählen einzelner

Zeichen einsetzen kann:

my $zeichenkette = "Dies ist ein schöner Satz , ein sehr schöner ."; my $haeufigkeit = $zeichenkette =~ tr/aeiou//; print $haeufigkeit; # Ausgabe: 11

In diesem Fall werden keine Zeichen ersetzt, man kann aber die gefundenen Zeichen löschen,

indem man den Modifikator d angibt:

my $zeichenkette = "Dies ist ein schöner Satz , ein sehr schöner ."; my $haeufigkeit = $zeichenkette =~ tr/aeiou//d; print "$haeufigkeit\n$zeichenkette\n"; # Ausgabe: # 11 # ds st n schönr stz n shr schönr

Darüber hinaus kann man mit tr/// die Modifikatoren c, der das Komplement eines Suchmusters

bildet, und s, der mehrere mögliche Ersetzungen auf eine reduziert, verwenden.

7.9 Vor- und zurückschauen Manchmal möchte man eine Fundstelle davon abhängig machen, ob ihr eine bestimmte

Zeichenkette folgt oder vorangeht. Dazu gibt es in Perl den sogenannten lookahead-Operator ?= und

den lookbehind-Operator ?<=. Wichtigstes Merkmal dieser Operatoren ist, dass sie selbst keine

Symbole konsumieren. Sie schauen ab der Stelle, in der sie im Muster verwendet werden, zurück oder

voraus, sodass die Entscheidung über das Akzeptieren oder das Verwerfen des Mustervergleichs von

ihnen abhängig gemacht wird. Man kann sie sich also wie eine erweiterte Variante von Bedingungen

innerhalb regulärer Ausdrücke vorstellen. Man benutzt diese beiden Operatoren, indem man sie mit

dem zu überprüfenden Teilmuster in runde Klammern schreibt. Diese besitzen allerdings keine

speichernde Funktion, da die Zeichen, wie gesagt, nicht konsumiert werden:

my $zeichenkette = "Schwarzwälder Uhren und Schwarzwälder Schinken"; $zeichenkette =~ s/Schwarzwälder(?= Uhren)/Schweizer/; print $zeichenkette; # Ausgabe: Schweizer Uhren und Schwarzwälder

Schinken

Page 101: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

97

my $zeichenkette = "Deutsche Wertarbeit und Schweizer Wertarbeit"; $zeichenkette =~ s/(?<=Schweizer )Wertarbeit/Kreditinstitute/; print $zeichenkette; # Ausgabe: Deutsche Wertarbeit und Schweizer Kreditinstitute

Ersetzt man die Gleichheitszeichen in den Operatoren durch Ausrufezeichen, bekommt man die

jeweilige Negation der Bedingung. Daher nennt man die erste Variante auch positive

lookahead/lookbehind und die der Negation negative lookahead/lookbehind.

7.10 Zusammenfassung Die Möglichkeit, in Perl reguläre Ausdrücke zum Suchen und Ersetzen in Texten zu verwenden,

erweitert unseren bisher vorhandenen Werkzeugkasten erheblich. Wir haben gesehen, dass wir

Zeichenkettenliterale und -variablen nicht nur auf Identität untersuchen können, sondern dass wir auch

Teilstrukturen erfassen können. Dazu können wir vermittels Zeichenklassen und deren

Kurzschreibweisen über Zeichen abstrahieren. Die in Zeichenklassen stehenden Symbole werden

immer disjunktiv verarbeitet; will man mehrere Zeichen alternativ verwenden, benutzt man die

Alternation.

Innerhalb regulärer Ausdrücke besitzen wir aber nicht nur Kontrolle über die auszuwählenden

Zeichen, also das Paradigma der für das jeweilige Muster gültigen Symbole, sondern auch über ihre

sequentielle Anordnung, das Syntagma. In ihm sind es vor allem die Quantoren, die uns Aussagen über

die von uns erwarteten Häufigkeiten der Symbole zu treffen. Runde Klammern ermöglichen uns die

Gruppierung und Speicherung von Fundstellen, sodass wir sie innerhalb und außerhalb des regulären

Ausdrucks verwenden können. Anhand von Ankern können wir die Position spezifizieren, an der wir

eine Fundstelle erwarten. Darüber hinaus besitzen wir mit den lookahead- und lookbehind-Operatoren,

innerhalb des regulären Ausdrucks Bedingungen darüber zu formulieren, ob eine Zeichenkette auf das

formulierte Muster passt, abhängig davon, ob ein anderes Muster vor oder hinter der entsprechenden

Zeichenkette zu finden ist.

Modifikatoren ermöglichen die Einflussnahme auf die Verarbeitung regulärer Ausdrücke sowohl

bezüglich des Paradigmas als auch des Syntagmas. So verändert der Modifikator i beispielsweise die

Behandlung von Groß- und Kleinschreibung in regulären Ausdrücken, während g den

Gültigkeitsbereich des Musters auf die gesamte Zeichenkette ausweitet.

Fassen wir diese Sprachmittel in einem Diagramm zusammen:

Page 102: Das Perl-Tutorial für Computerlinguisten

98

7.11 Beispielanwendung Als wir im letzten Kapitel n-Gramme gezählt haben, liefen wir in das Problem, dass

Interpunktionszeichen durch die Vortokenisierung des Korpus ebenso durch Leerzeichen isoliert

worden sind, wie Wörter, sodass diese Interpunktionszeichen in den Listen der n-Gramm-Häufigkeiten

derart prominent vertreten waren, dass sich aus diesen Frequenzen keine linguistisch verwertbaren

Ergebnisse ableiten ließen. Mit den regulären Ausdrücken haben wir nun ein Werkzeug bei der Hand,

mit dem sich diese Zeichen schon beim Einlesen des Korpus eliminieren lassen.

Zunächst wollen wir alle Zeilen ignorieren, die ausschließlich aus Leerraumzeichen bestehen. Da

wir dies beim Einlesen der Daten tun, verlassen wir die while()-Schleife mit next, wenn wir auf eine

solche Zeile treffen:

# Zeilen ignorieren, die ausschließlich aus Leerraumzeichen bestehen. next if(/^\s+$/); Außerdem wollen wir sicherstellen, dass sich im Text keine überflüssigen Leerzeichen befinden,

die möglicherweise Eingang in die Zählung finden könnten. Dazu ersetzen wir zwei oder mehr

nacheinander vorkommende Leerzeichen durch ein einzelnes:

# Überflüssige Leerzeichen durch ein einzelnes ersetzen. s/\s{2,}/ /g;

Des Weiteren muss eine Entscheidung darüber getroffen werden, ob Klitika wie "I'll", "don't" oder

"he's" als ein oder zwei Token betrachtet werden sollen. Durch den Tokenizer wurden sie getrennt, in

unserer Analyse möchten wir sie jedoch als ein Wort zählen, weshalb wir sie jetzt wieder

zusammenfassen:

# Klitika sollen als ein Token gezählt werden! s/\s'([lst])/'$1/g;

Page 103: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

99

Im nächsten Schritt entfernen wir die eigentlichen Interpunktionszeichen. Durch die Tokenisierung

ist einem jeden solchen Zeichen ein Leerzeichen vorangestellt worden, sodass wir auch dieses

eliminieren müssen. Dies gilt allerdings nicht pauschal für Anführungszeichen: Da diese auch am

Anfang einer Zeile vorkommen, müssen wir in diesem Fall die Anführungszeichen und das folgende

Leerzeichen entfernen:

# Interpunktion nach Tokenisierung entfernen. s/^"\s//; s/\s[.?!":;,$%&-\/()\[\]]+//g;

Der veränderte Code sieht an dieser Stelle also so aus:

# Korpus einlesen und Token in einer Liste speichern. while (<IN>) { chomp; # Zeilen ignorieren, die ausschließlich aus Leerraumzeichen bestehen. next if(/^\s+$/); # Überflüssige Leerzeichen durch ein einzelnes ersetzen. s/\s{2,}/ /g; # Klitika sollen als ein Token gezählt werden! s/\s'([lst])/'$1/g; # Interpunktion nach Tokenisierung entfernen. s/^"\s//; s/\s[.?!":;,$%&-\/()\[\]]+//g; push( @token, split(/ /) ); }

Dadurch ergeben sich folgende neue Resultate:

# Korpus im Umfang von 795881 Token eingelesen! # Die Größe des Vokabulars beträgt 22456. # 41.59% aller Types kommt nur einmal im Korpus vor! # Es gibt 795880 Bigramm-Token und 263269 Bigramm-Types. # Das ergibt 504271936 mögliche Bigramme, von denen 0.0522077437202454% # im Dokument vorkommen. # 71.47% aller Bigramm-Types kommt nur einmal im Korpus vor! # Es gibt 795879 Trigramm-Token und 592210 Trigramm-Types. # Das ergibt 11323930594816 mögliche Trigramme, von denen

5.22972120891582e-06% # im Dokument vorkommen. # 87.87% aller Trigramm-Types kommt nur einmal im Korpus vor! # Es gibt 795878 Tetragramm-Token und 746411 Tetragramm-Types. # Das ergibt 2.54290185437188e+17 mögliche Tetragramme, von denen # 2.93527254587799e-10% im Dokument vorkommen. # 96.24% aller Tetragramm-Types kommt nur einmal im Korpus vor!

Page 104: Das Perl-Tutorial für Computerlinguisten

100

# Die zehn häufigsten Unigramme sind: # the: 30892 Das sind 3.88148% aller Token. # and: 23258 Das sind 2.92230% aller Token. # to: 20993 Das sind 2.63771% aller Token. # of: 16387 Das sind 2.05898% aller Token. # I: 16176 Das sind 2.03246% aller Token. # a: 15792 Das sind 1.98422% aller Token. # he: 12691 Das sind 1.59459% aller Token. # you: 12522 Das sind 1.57335% aller Token. # that: 11847 Das sind 1.48854% aller Token. # in: 11707 Das sind 1.47095% aller Token. # was: 10648 Das sind 1.33789% aller Token. # Die zehn häufigsten Bigramme sind: # of the: 2918 Das sind 0.36664% aller Bigramm-Token. # in the: 2835 Das sind 0.35621% aller Bigramm-Token. # to the: 1897 Das sind 0.23835% aller Bigramm-Token. # he had: 1576 Das sind 0.19802% aller Bigramm-Token. # I am: 1563 Das sind 0.19639% aller Bigramm-Token. # on the: 1471 Das sind 0.18483% aller Bigramm-Token. # that he: 1442 Das sind 0.18118% aller Bigramm-Token. # at the: 1440 Das sind 0.18093% aller Bigramm-Token. # he was: 1349 Das sind 0.16950% aller Bigramm-Token. # to be: 1160 Das sind 0.14575% aller Bigramm-Token. # in a: 1076 Das sind 0.13520% aller Bigramm-Token. # Die zehn häufigsten Trigramme sind: # that he had: 292 Das sind 0.03669% aller Trigramm-Token. # that he was: 288 Das sind 0.03619% aller Trigramm-Token. # out of the: 262 Das sind 0.03292% aller Trigramm-Token. # I am not: 233 Das sind 0.02928% aller Trigramm-Token. # I don't know: 222 Das sind 0.02789% aller Trigramm-Token. # he did not: 187 Das sind 0.02350% aller Trigramm-Token. # that it was: 185 Das sind 0.02324% aller Trigramm-Token. # said the prince: 162 Das sind 0.02035% aller Trigramm-Token. # he had been: 162 Das sind 0.02035% aller Trigramm-Token. # he could not: 160 Das sind 0.02010% aller Trigramm-Token. # as though he: 159 Das sind 0.01998% aller Trigramm-Token. # Die zehn häufigsten Tetragramme sind: # for the sake of: 70 Das sind 0.00880% aller Tetragramm-Token. # out of the room: 60 Das sind 0.00754% aller Tetragramm-Token. # as though he were: 60 Das sind 0.00754% aller Tetragramm-Token. # What do you mean: 57 Das sind 0.00716% aller Tetragramm-Token. # at the same time: 55 Das sind 0.00691% aller Tetragramm-Token. # for the first time: 54 Das sind 0.00678% aller Tetragramm-Token. # as though he had: 53 Das sind 0.00666% aller Tetragramm-Token. # I don't want to: 49 Das sind 0.00616% aller Tetragramm-Token. # the middle of the: 47 Das sind 0.00591% aller Tetragramm-Token. # in spite of the: 44 Das sind 0.00553% aller Tetragramm-Token. # I should like to: 44 Das sind 0.00553% aller Tetragramm-Token.

Zunächst lässt sich festhalten, dass das Entfernen der Interpunktion den Umfang des Korpus um fast

ein Viertel des ursprünglichen Umfangs reduziert hat. Gleichzeitig hat die Behandlung der Klitika als

ein Token das Vokabular vergrößert. Wir sehen auch, dass sich unser Eindruck aus dem vorherigen Ka-

pitel verstärkt hat: Die Funktionswörter machen in allen vier Kategorien den größten Anteil am

sprachlichen Material aus. Während sich dies für die Uni- und Bigramme ausschließlich negativ

Page 105: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

101

niederschlägt, da sich aus diesen Daten kaum relevante Analysen ableiten lassen, finden sich unter den

Tri- und Tetragrammen Kandidaten, die linguistisch interessant sind, da sie in just dieser Konfiguration

eine bestimmte Bedeutung tragen, die sich durch Austauschen des lexikalischen Materials oder durch

syntaktische Veränderungen nicht ergäbe. Solche n-Gramme nennt man Kollokationen. Beispiele aus

diesen Listen wären "in spite of", "for the sake of" oder "at the same time".

Wie ermittelt man nun relevante Uni- und Bigramme? Dazu bedient man sich so genannter

Stoppwort-Listen (engl. stop word list), in denen die häufigsten Vertreter der geschlossenen

Wortklassen vertreten sind. Eine solche Liste setzt man als Filter nach dem Aufbau der n-Gramm-

Hashes ein:

# Liste mit hochfrequenten Wörtern, die herausgefiltert werden sollen. my @stoppwort_liste = qw(a an and the this that as at with by to for from

of on in out up I he she it we you they me my his her him our your us them their be am is are was were has have had do does did can could will would all no not but so what or if yes);

Dementsprechend muss man sie auch auf jeden n-Gramm-Hash anwenden. Da das Verfahren an

sich immer analog ist, illustrieren wir es hier anhand der Bigramme:

# Stoppworte herausfiltern foreach(keys %bigramm_haeufigkeit){ foreach my $stoppwort(@stoppwort_liste){ delete($bigramm_haeufigkeit{$_}) if(/\b$stoppwort\b/i); next; } }

Zunächst iterieren wir über den Bigramm-Hash; für jeden seiner Schlüssel überprüfen wir, ob dieser

in der Stoppwort-Liste vorhanden ist. Ist dies der Fall, löschen wir ihn wieder und fahren mit dem

nächsten Bigramm fort.

Das Resultat dieser Operation sieht so aus:

# Korpus im Umfang von 795861 Token eingelesen! # Die Größe des Vokabulars beträgt 21832. # 41.36% aller Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Unigramme sind: # said: 2606 Das sind 0.32744% aller Unigramm-Token. # one: 2270 Das sind 0.28523% aller Unigramm-Token. # been: 2243 Das sind 0.28183% aller Unigramm-Token. # know: 2113 Das sind 0.26550% aller Unigramm-Token. # about: 2088 Das sind 0.26236% aller Unigramm-Token. # there: 2047 Das sind 0.25721% aller Unigramm-Token. # now: 1968 Das sind 0.24728% aller Unigramm-Token. # only: 1810 Das sind 0.22743% aller Unigramm-Token. # very: 1756 Das sind 0.22064% aller Unigramm-Token. # man: 1742 Das sind 0.21888% aller Unigramm-Token. # who: 1673 Das sind 0.21021% aller Unigramm-Token.

Page 106: Das Perl-Tutorial für Computerlinguisten

102

# Es gibt 795860 Bigramm-Token und 115409 Bigramm-Types. # Das ergibt 476636224 mögliche Bigramme, von denen 0.0242132247170538% # im Dokument vorkommen. # 82.75% aller Bigramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Bigramme sind: # Katerina Ivanovna: 353 Das sind 0.04435% aller Bigramm-Token. # don't know: 304 Das sind 0.03820% aller Bigramm-Token. # Fyodor Pavlovitch: 255 Das sind 0.03204% aller Bigramm-Token. # just now: 253 Das sind 0.03179% aller Bigramm-Token. # old man: 237 Das sind 0.02978% aller Bigramm-Token. # Nastasia Philipovna: 216 Das sind 0.02714% aller Bigramm-Token. # more than: 206 Das sind 0.02588% aller Bigramm-Token. # three thousand: 180 Das sind 0.02262% aller Bigramm-Token. # Lizabetha Prokofievna: 168 Das sind 0.02111% aller Bigramm-Token. # Dmitri Fyodorovitch: 166 Das sind 0.02086% aller Bigramm-Token. # young man: 165 Das sind 0.02073% aller Bigramm-Token. # Es gibt 795859 Trigramm-Token und 70777 Trigramm-Types. # Das ergibt 10405922042368 mögliche Trigramme, von denen

6.80160774910954e-007% # im Dokument vorkommen. # 96.50% aller Trigramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Trigramme sind: # three thousand roubles: 50 Das sind 0.00628% aller Trigramm-Token. # don't know how: 39 Das sind 0.00490% aller Trigramm-Token. # day before yesterday: 38 Das sind 0.00477% aller Trigramm-Token. # Ha ha ha: 29 Das sind 0.00364% aller Trigramm-Token. # said just now: 24 Das sind 0.00302% aller Trigramm-Token. # sat down again: 23 Das sind 0.00289% aller Trigramm-Token. # don't know whether: 22 Das sind 0.00276% aller Trigramm-Token. # said Mrs. Epanchin: 22 Das sind 0.00276% aller Trigramm-Token. # burst into tears: 21 Das sind 0.00264% aller Trigramm-Token. # know nothing about: 21 Das sind 0.00264% aller Trigramm-Token. # more than once: 19 Das sind 0.00239% aller Trigramm-Token. # Es gibt 795858 Tetragramm-Token und 32221 Tetragramm-Types. # Das ergibt 2.27182090028978e+017 mögliche Tetragramme, # von denen 1.41828961939253e-011% im Dokument vorkommen. # 99.36% aller Tetragramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Tetragramme sind: # art just O Lord: 5 Das sind 0.00063% aller Tetragramm-Token. # fresh air fresh air: 5 Das sind 0.00063% aller Tetragramm-Token. # shall see each other: 4 Das sind 0.00050% aller Tetragramm-Token. # two thousand three hundred: 4 Das sind 0.00050% aller Tetragramm-Token. # always worth while speaking: 4 Das sind 0.00050% aller Tetragramm-Token. # expected something quite different: 4 Das sind 0.00050% aller

Tetragramm-Token. # within these peeling walls: 3 Das sind 0.00038% aller Tetragramm-Token. # these two hundred roubles: 3 Das sind 0.00038% aller Tetragramm-Token. # more than three thousand: 3 Das sind 0.00038% aller Tetragramm-Token. # thousand five hundred roubles: 3 Das sind 0.00038% aller Tetragramm-

Token. # these last few days: 3 Das sind 0.00038% aller Tetragramm-Token.

Page 107: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

103

8 Subroutinen

The only routine with me is no routine at all.

JAQUELINE KENNEDY

Bis jetzt besaßen unsere Perl-Programme die Eigenschaft, aus einem monolithischen Code-Block

zu bestehen, der linear abgearbeitet wird. Daraus ergeben sich die Nachteile, dass unser Code einerseits

konzeptuell wenig strukturiert ist, andererseits müssen Aufgaben, die wiederholt bearbeitet werden,

auch mehrmals geschrieben werden. Konsequenz dessen ist, dass unser Quellcode schwer zu lesen und

zu pflegen ist, und dass er fehleranfällig ist. Subroutinen erlauben es uns nun, den Code sinnvoll zu

organisieren und Bestandteile wiederzuverwenden.

Subroutinen kann man sich konzeptuell als kleine Programme mit begrenzter Funktionalität

vorstellen, die aus einem großen "Hauptprogramm", aus anderen Subroutinen oder aus sich selbst

rekursiv aufgerufen werden. Wir werden noch sehen, dass sie den in Perl eingebauten Funktionen sehr

ähnlich sind. Die Definition einer Subroutine besteht aus dem Schlüsselwort sub, einem Bezeichner

und einem Codeblock:

sub marine{ ... }

Für die Benennung einer Subroutine sollte man sich an folgende Regeln halten:

• Wird durch die Subroutine eine Aktion ausgeführt, verwendet man ein Verb als

Subroutinennamen.

• Steht Information als Ergebnis der Subroutine im Vordergrund, benennt man sie danach.

• Prüft man eine Aussage auf Gültigkeit, leitet man den Namen mit "ist" oder "kann" ein.

• Überführt man Daten in ein anderes Format, nennt man beide Strukturen und verbindet sie mit

"zu", "nach" oder dem englischen "2".

In einer Subroutine dürfen alle Konstrukte verwendet werden, die auch in einem normalen

Programm vorkommen:

sub beatles_singen{ my $text = "yellow submarine"; print "We all live in a "; for (1 .. 3){ print $text; } }

Für gewöhnlich schreibt man Subroutinen – dem Top-down-Ansatz folgend – an das Ende des

Quellcodes. Davor formuliert man abstrakt die Funktionalität eines Programms in seine

Einzelprobleme zerlegt. Dies sind die Aufrufe der einzelnen Subroutinen und bilden das

Hauptprogramm. Will man eine Subroutine aufrufen, gibt es zwei Notationsmöglichkeiten:

Standardmäßig schreibt man ein "kaufmännisches und" (engl. ampersand) & vor den Bezeichner. Alter-

nativ kann man dieses weglassen, wenn man dem Aufruf eine (leere) Argumentliste übergibt:

Page 108: Das Perl-Tutorial für Computerlinguisten

104

&beatles_singen; # Ausgabe: We all live in a yellow submarine yellow submarine yellow submarine

beatles_singen(); # Ausgabe: We all live in a yellow submarine yellow submarine yellow submarine

Diese letzte Notation zeigt die Nähe zu den eingebauten Funktionen; wie wir gesehen haben,

können wir diese auch ohne die runden Klammern verwenden. Dies funktioniert im Fall der

Subroutinen allerdings nur, wenn man sie vor dem Aufruf definiert hat, ansonsten ist Perl dieser

Bezeichner nicht bekannt und der Interpreter gibt eine Fehlermeldung aus:

beatles_singen; # Ausgabe: Bareword "beatles_singen" not allowed...

8.1 Argumente Die meisten eingebauten Funktionen haben wir allerdings mit einer Argumentliste aufgerufen. Dies

können wir auch mit Subroutinen tun:

beatles_singen("Yesterday", "LSD", $ringo); # Ausgabe: We all live in a yellow submarine yellow submarine yellow submarine

In diesem Fall zeigt ein solcher Aufruf aber noch keine Wirkung auf die Ausführung unseres

Programms, da wir die an die Subroutine übergebenen Argumente noch nicht bearbeiten.

Im Gegensatz zu vielen anderen Programmiersprachen müssen wir bei der Definition einer

Subroutine nicht angeben, wie viele und welche Argumente sie verarbeiten soll.46 Stattdessen stehen

die Argumente im Array @_. Wie schon vorher gesagt, besitzen die verschiedenen Datenstrukturen ihre

eigenen Namensräume, sodass @_ nichts mit $_ zu tun hat und auch nicht mit diesem Skalar

verwechselt werden darf! Auf diesem Array kann man innerhalb einer Subroutine alle Operationen

ausführen, die man auch auf normalen Arrays ausführen kann. Da man der Argumentliste aber in der

Subroutine keine Elemente hinzufügen will, verwendet man nur den lesenden Zugriff über shift(),

pop() oder die einzelnen Array-Elemente $_[0], $_[1] etc.47 Es empfiehlt sich allerdings, die

Elemente von @_ nicht direkt zu manipulieren, sondern auf Kopien zu operieren, um unerwünschte

Seiteneffekte zu vermeiden:

my $zahl = 12; print erhoehe($zahl); # Ausgabe: 13 print $zahl; # Ausgabe: 13 sub erhoehe{ ++$_[0]; }

In den verschiedenen Programmiersprachen können Argumente entweder per Referenz (call by

reference) oder als Wert (call by value) an eine Subroutine übergeben werden. In Perl wird – wie wir

46 Zwar lassen sich an dieser Stelle sogenannte Prototypen angeben, mit denen sich kontrollieren lässt, welche Datenstruktur

übergeben wird. Dies ist aber aus verschiedenen Gründen nicht ganz unproblematisch, sodass hier dem in der Perl-Gemeinde

allgemein vorherrschenden Ratschlag, sie nicht zu verwenden, gefolgt wird. 47 Die wie gesagt nichts mit $_ zu tun haben!

Page 109: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

105

sehen – nicht der Wert, sondern eine Referenz auf den Wert des Hauptprogramms an die Subroutine

übergeben. Deshalb wird der Wert der Variablen aus dem Aufruf innerhalb der Subroutine verändert.

Dies wird allgemein als schlechter Programmierstil angesehen, da wir die Funktionalität der Subroutine

nicht vom Rest des Programms trennen. Wollten wir explizit den call by value versuchen, bekommen

wir eine Fehlermeldung:

print erhoehe(12); # Ausgabe: Modification of a read-only value attempted

sub erhoehe{ ++$_[0]; }

Die richtige Verwendung eines Arguments besteht also darin, ihn aus dem Array @_48 in eine lokale

Variable zu kopieren, auf der man dann die entsprechende Operation ausführt. Zwar hat @_ nichts mit

$_ zu tun, doch besitzt es die gleiche "Pragmatik" wie $_, nämlich wie eine implizite Liste verwendet

werden zu können, d.h., man muss bei einer shift()-Operation den Bezeichner @_ nicht explizit

angeben, um an die Elemente des Arrays zu kommen:

print erhoehe($zahl); # Ausgabe: 13 print $zahl; # Ausgabe: 12 sub erhoehe{ my $zahl = shift; ++$zahl; }

Man kann an Subroutinen aber nicht nur Skalare, sondern auch Arrays und Hashes übergeben.

Hashes werden dabei zu Listen heruntergebrochen, sodass man an den geraden Indizes, angefangen bei

0, die Schlüssel und an den ungeraden die Werte findet:

my $wort = "Wort"; my @satz = qw(Hier steht ein Satz); my %haeufigkeit = (Hier => 1, steht => 1, ein => 1, Satz => 1); zeige_elemente($wort); # Ausgabe: Wort zeige_elemente(@satz); # Ausgabe: Hier steht ein Satz zeige_elemente(%haeufigkeit); # Ausgabe: Hier 1 Satz 1 ein 1 steht 1 sub zeige_elemente{ print "@_\n"; }

Im Gegensatz zu vielen anderen Programmiersprachen verhält sich Perl also nicht nur bezüglich der

Datentypen, sondern auch in Hinsicht auf Datenstrukturen agnostisch: Solange die Argumentliste ein

Element enthält, kann es von dieser Subroutine ausgegeben werden. Was passiert aber, wenn wir all

diese Datenstrukturen auf einmal übergeben wollen?

48 Hatte ich schon erwähnt, dass @_ völlig verschieden von $_ ist?!

Page 110: Das Perl-Tutorial für Computerlinguisten

106

my $wort = "Wort"; my @satz = qw(Hier steht ein Satz); my %haeufigkeit = (Hier => 1, steht => 1, ein => 1, Satz => 1); zeige_elemente(@satz, $wort, %haeufigkeit); sub zeige_elemente{ print "@_\n"; } # Ausgabe: Hier steht ein Satz Wort ein 1 steht 1 Satz 1 Hier 1

Die Elemente der Datenstrukturen werden wiederum zu einer Liste vereinheitlicht; wir haben also

ohne die genaue Kenntnis über den Aufbau der Datenstruktur keine Möglichkeit, zwischen den

Elementen zu unterscheiden. So können wir beispielsweise innerhalb der Subroutine nicht ohne

weiteres sagen, ob "Wort" Bestandteil des Arrays @satz ist oder nicht! Wie wir diese Funktionalität in

Perl dennoch einsetzen können, lernen wir im nächsten Kapitel.

Benannte Parameter

Übergibt man einer Subroutine eine Argumentiste, die viele Literale enthält, wird der Aufruf

schnell unübersichtlich, da man den einzelnen Argumenten meist nur mühsam eine Bedeutung

zuordnen kann, ohne in die Subroutine selbst zu schauen. Um diesem Problem zu entgehen, sollte man

die Argumente als Werte eines Hashes übergeben, deren Schlüssel das Argument näher bezeichnen.

Ein weiterer Vorteil dieser Art des Aufrufs ergibt sich aus der Tatsache, dass die Argumente nun nicht

mehr in einer bestimmten Reihenfolge angegeben und verarbeitet werden müssen, sondern dass sie

eben über Bezeichnungen innerhalb der Subroutine zugreifbar werden.

Als Beispiel schauen wir uns eine Subroutine an, die eine Verbindung zu einem anderen Rechner

aufbaut und deren Argumente aus einem Benutzernamen, dem Passwort und dem Namen des Rechners

bestehen. In manchen Konfigurationen ist es durchaus denkbar, dass Joe User für alle diese Parameter

denselben Wert vergibt, sodass sich im Programm durch Ansicht des Aufrufs kaum entscheiden lässt,

welches Argument nun welche Bedeutung trägt. Anders bei einem Aufruf mit benannten Parametern,

bei dem die Benennungen für Klarheit sorgen:

logon(Benutzer => "groucho", Passwort => "groucho", Rechner => "groucho"); sub logon{ my %args = @_; print "Benutzer $args{Benutzer} meldet sich mit dem Passwort

$args{Passwort} am Rechner $args{Rechner} an.\n"; # Weiterer Code, der die eigentliche Anmeldung implementiert... } # Ausgabe: # Benutzer groucho meldet sich mit dem Passwort groucho am Rechner groucho

an.

Page 111: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

107

8.2 Gültigkeitsbereiche Als wir gerade über das Kopieren der Argumente in die Subroutine sprachen, hieß es, dass wir dazu

eine lokale Variable verwenden. In der Subroutine steht allerdings nur eine mit my deklarierte Variable,

die sich keineswegs von denjenigen unterscheidet, die wir bis hierhin benutzt haben. Deswegen soll an

dieser Stelle die Funktionalität von my näher beleuchtet und das Konzept des Gültigkeitsbereichs

eingeführt werden.

In der Einführung dieses Kapitels war davon die Rede, dass Subroutinen kleinen Programmen

ähneln, die u.a. aus einem "Hauptprogramm" aufgerufen werden. Auch haben wir gesehen, dass der

Wert einer mit my im Hauptprogramm deklarierten Variablen in einer Subroutine verändert werden

kann. Sie ist also auch dann zugreifbar, wenn auf ihr in einem "Unterprogramm" operiert wird.

Andererseits haben wir gesehen, dass Operationen auf einer in der Subroutine mit my deklarierten

Kopie mit dem gleichen Bezeichner wie im Hauptprogramm keinerlei Auswirkungen auf den Wert

dieser Variablen hatten. Welchen Gültigkeitsbereich besitzen also mit my deklarierte Variablen? Um

diese Frage klären zu können, müssen wir den Begriff des Programms genauer definieren. Jedes

Programm, das wir schreiben, gehört einem bestimmten Paket an. In unserem Fall ist dies immer das

Paket Main, das wir allerdings – anders als in anderen Programmiersprachen – nicht immer explizit

angeben müssen.49 Ferner ist es so, dass man sich ein Perl-Programm wie einen Block vorstellen muss,

der implizit in geschweiften Klammern steht. Daraus ergibt sich, dass eine mit my deklarierte Variable

immer lokal ist. Im ersten Fall ist sie lokal relativ zum Paket Main, im zweiten Fall besitzt sie einen

lokalen Gültigkeitsbereich innerhalb des Blocks, der die Subroutine bildet. Dies gilt aber auch für alle

anderen Blöcke, die wir bisher kennen gelernt haben: Eine beispielsweise in einem if()- oder

while()-Block mit my deklarierte Variable ist außerhalb dieses if()- oder while()-Blocks genauso

wenig zugreifbar, wie außerhalb einer Subroutine!

Wenn es einen lokalen Gültigkeitsbereich gibt, sollte Perl auch einen globalen Gültigkeitsbereich

kennen. Diesen hatten wir im Prinzip schon mit der für das Paket Main lokal deklarierten Variablen

gesehen, da sie ja von überall her verändert werden konnte. Dennoch gibt es in Perl noch das

Schlüsselwort our, das eine Variable explizit als global kennzeichnet. Dieser feine Unterschied hat

durchaus praktische Auswirkungen, wie wir gleich sehen werden!

Neben dem mit my lokal deklarierten Gültigkeitsbereich gibt es allerdings noch einen mit local

lokal deklarierten Gültigkeitsbereich. Deshalb bezeichnet man my zur Unterscheidung häufig auch als

lexikalischen Gültigkeitsbereich. Das Schlüsselwort local ermöglicht es, in einem Block auf dem Wert

einer vorher deklarierten Variablen zu operieren, ohne diesen in der ursprünglichen Variablen zu

verändern. Dies bezeichnet man auch als dynamischen Gültigkeitsbereich (engl. dynamic scoping):

our $variable = "Paket"; print "Ursprungswert: $variable\n"; local_demo1(); local_demo2(); print "Nach den Subroutinen: $variable\n"; sub local_demo1{ local $variable = "Lokal"; print "In der ersten Subroutine: $variable\n"; local_demo2(); }

49 Wenn wir in einem der nächsten Kapitel Module verwenden, bzw. selbst schreiben, erstellen wir unsere eigenen Pakete,

die wir dann sehr wohl explizit benennen müssen.

Page 112: Das Perl-Tutorial für Computerlinguisten

108

sub local_demo2{ print "In der zweiten Subroutine: $variable\n"; } # Ausgabe: # Ursprungswert: Paket # In der ersten Subroutine: Lokal # In der zweiten Subroutine: Lokal # In der zweiten Subroutine: Paket # Nach den Subroutinen: Paket

Hätten wir die Variable $variable ursprünglich mit my deklariert, wäre der Programmlauf mit einer

Fehlermeldung darüber, dass die Variable in der ersten Zeile der ersten Subroutine nicht in einen

lokalen Kontext gebracht werden kann, abgebrochen, da sie ja bereits lokal ist (nämlich relativ zum

Paket). Durch die Verwendung von our wird $variable zu einer echten globalen Variablen, die

dementsprechend auch in einen lokalen Kontext überführt werden darf. Ursprünglich besitzt $variable

den Wert "Paket", der in der ersten Subroutine durch die local-Deklaration mit "Lokal" überschrieben

wird. Durch den ersten Aufruf der zweiten Subroutine innerhalb der ersten wird derjenige Wert für die

weitere Verarbeitung verwendet, der in diesem Gültigkeitsbereich aktuell ist. Da dies "Lokal" ist, wird

zunächst dieser Wert ausgegeben. Danach ist die erste Subroutine abgearbeitet und die zweite wird aus

dem "Hauptprogramm" aufgerufen. Der aktuelle Wert von $variable ist immer noch "Paket", da er ja

durch die lokale Verwendung nicht überschrieben wurde, sodass dieser jetzt von der zweiten

Subroutine ausgegeben wird. Dies geschieht auch nachdem beide Subroutinen abgearbeitet sind. Hätten

wir $variable in der ersten Subroutine mit my deklariert, wäre sie nur dort lokal gewesen:

my $variable = "Paket"; print "Ursprungswert: $variable\n"; local_demo1(); local_demo2(); print "Nach den Subroutinen: $variable\n"; sub local_demo1{ my $variable = "Lokal"; print "In der ersten Subroutine: $variable\n"; local_demo2(); } sub local_demo2{ print "In der zweiten Subroutine: $variable\n"; } # Ausgabe: # Ursprungswert: Paket # In der ersten Subroutine: Lokal # In der zweiten Subroutine: Paket # In der zweiten Subroutine: Paket # Nach den Subroutinen: Paket

Hier noch einmal der Unterschied visualisiert:

Page 113: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

109

Vorherrschende Meinung für die Verwendung von local ist aber: Immer my benutzen!

8.3 Rückgabewerte Genauso wie wir Subroutinen mit Argumenten aufrufen können, können wir auch Werte dahin

zurückgeben, wo wir die Subroutine aufgerufen haben. Dies bedeutet, dass wir in Subroutinen nicht nur

Operationen auf Datenstrukturen durchführen können, sondern analog zu den in Perl eingebauten

Funktionen eigene Rückgabewerte definieren dürfen. Ohne es bis jetzt explizit gesagt zu haben, haben

wir solche Rückgabewerte schon im ersten Beispiel verwendet:

my $zahl = 12; print erhoehe($zahl); # Ausgabe: 13 sub erhoehe{ my $zahl = shift; ++$zahl; }

In Perl ist es so, dass der Wert des zuletzt ausgewerteten Ausdrucks automatisch dorthin

zurückgegeben wird, wo die Subroutine aufgerufen wurde. Hier wurde also der aktuelle Wert von

$zahl aus der Subroutine an die Stelle des Aufrufs zurückgegeben und dient der print()-Funktion als

Argument. Für gewöhnlich geben wir aber den oder die Werte in eine Datenstruktur zurück, die mit

jener aus der Subroutine kompatibel ist. Darüber hinaus erhöht es die Lesbarkeit des Codes, wenn man

nicht diese implizite Form der Rückgabe, sondern eine durch das Schlüsselwort return() eingeleitete

Schreibweise einsetzt:

Page 114: Das Perl-Tutorial für Computerlinguisten

110

my $zahl = 12; $zahl = erhoehe($zahl); print $zahl; # Ausgabe: 13 sub erhoehe{ my $zahl = shift; ++$zahl; return $zahl; }

Genauso wie Skalare kann man natürlich auch Arrays und Hashes zurückgeben; zu beachten ist

allerdings wiederum, dass bei der Rückgabe mehrerer Datenstrukturen diese abermals zu einer Liste

vereinheitlicht werden. Wie wir dennoch mehrere Datenstrukturen zurückgeben können, sehen wir im

nächsten Kapitel.

Die Datenstruktur eines Rückgabewerts ist immer davon abhängig, ob die Datenstruktur im Aufruf

einen Skalar- oder einen Listenkontext erwartet:

my $anzahl_elemente = array_demo(); my @elemente = array_demo(); print "$anzahl_elemente\n"; # Ausgabe: 4 print "@elemente\n"; # Ausgabe: Dies ist ein Satz sub array_demo{ my @words = qw(Dies ist ein Satz); }

Im ersten Aufruf wird der Skalarkontext durch die Variable $anzahl_elemente erzwungen; wie

gewohnt gibt ein Array in einem Skalarkontext die Anzahl seiner Elemente zurück, d.h. in diesem Fall

die Anzahl der Elemente des Arrays in der Subroutine. Im zweiten Aufruf ist die Ziel-Datenstruktur für

den Rückgabewert ein Array, sodass hier die Elemente des Arrays in der Subroutine zurückgegeben

werden. Will man in einer Subroutine sicherstellen, dass ein Wert in einem bestimmten Kontext

zurückgegeben wird, prüft man mit der Funktion wantarray() ab, ob es sich bei der Datenstruktur im

Aufruf um eine Listenstruktur oder einen Skalar handelt – nur im ersten Fall liefert die Funktion true

zurück:

my $anzahl_elemente = array_demo(); my @elemente = array_demo(); print "$anzahl_elemente\n"; # Ausgabe: Skalar print "@elemente\n"; # Ausgabe: Liste sub array_demo{ my @words = qw(Dies ist ein Satz); unless(wantarray){ return "Skalar"; } else{ return "Liste"; } }

Page 115: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

111

Der Funktion return() kann man auch eine leere Argumentliste übergeben. Im Skalarkontext

liefert sie dann undef, im Listenkontext eine leere Liste zurück:

my $satz1 = "Dies ist ein Satz"; my $satz2 = "Das ist ein Satz"; my @woerter1 = tokenize($satz1); my @woerter2 = tokenize($satz2); print "woerter1: @woerter1\n"; # Ausgabe: woerter1: print "woerter2: @woerter2\n"; # Ausgabe: woerter2: Das ist ein

Satz sub tokenize{ my $satz = shift; return unless($satz =~/^Das/); my @woerter = split(/ /, $satz); }

In diesem Beispiel wird eine leere Liste an @woerter1 zurückgegeben, da die in der Subroutine

formulierte Bedingung durch den ersten Satz nicht erfüllt wird. Man muss sich klar machen, dass damit

auch die weitere Verarbeitung der Anweisungen innerhalb der Subroutine abbricht, denn return()

bedeutet für den Programmfluss "nimm die Argumentliste und kehre dahin zurück, wo diese

Subroutine aufgerufen wurde":

my ($zahl1, $zahl2) = multipliziere(); print "$zahl1 $zahl2\n"; # Ausgabe: Use of uninitialized

value, 9 sub multipliziere{ my $produkt1 = 3*3; my $produkt2 = 4*4; return $produkt1; return $produkt2; }

Die zweite return()-Anweisung wird nie erreicht, weshalb $zahl2 nicht initialisiert werden kann!

8.4 Zusammenfassung Subroutinen bieten die Möglichkeit, den Quellcode besser zu strukturieren und gleiche Operationen

unabhängig von Datenstrukturen auszuführen. Einer Subroutine kann man genauso wie einer Perl-

internen Funktion Argumente übergeben und ebenso Rückgabewerte in entsprechenden

Datenstrukturen weiterverarbeiten.

Die im Aufruf einer Subroutine als Liste übergebenen Argumente sind dann innerhalb der

Subroutine aus dem Array @_ zugreifbar, das nichts mit der Skalarvariablen $_ zu tun hat. Auf @_

sollten keine anderen Operationen als das Lesen der Elemente ausgeführt werden, da die Argumentliste

per Referenz an die Subroutine übergeben wird, weshalb man auf unerwünschte Seiteneffekte stoßen

kann, wenn man die Werte aus @_ direkt manipuliert. Zwar kann man einer Subroutine mehrere, auch

Page 116: Das Perl-Tutorial für Computerlinguisten

112

unterschiedliche, Datenstrukturen übergeben, doch muss man sich bewusst sein, dass dabei die Werte

zu einer Liste vereinheitlicht werden.

Das gleiche passiert auch, wenn man mehrere Datenstrukturen aus einer Subroutine zurückgeben

will. Generell kann man mit der return()-Funktion Werte an den Ort des Aufrufs der Subroutine in

eine entsprechende Datenstruktur zurückgeben. Implizit geschieht dies auch ohne die Angabe von re-

turn(), indem der zuletzt ausgewertete Ausdruck automatisch zurückgegeben wird. Gibt man

return() keine Argumente mit, so liefert diese Funktion undef zurück, wenn beim Aufruf ein skalarer

Rückgabewert erwartet wurde, im Falle eines listenwertigen Rückgabewerts eine leere Liste. Innerhalb

einer Subroutine lässt sich anhand der Funktion wantarray() überprüfen, ob ein Skalar oder eine Liste

als Rückgabewert erwartet wird.

8.5 Beispielanwendung Zerlegt man die Anwendung zur Berechnung von Uni-, Bi-, Tri- und Tetragrammen in ihre

Bestandteile, können wir auf der Ebene der höchsten Abstraktion gut eine Handvoll Funktionen

ausmachen, die das Grundgerüst unserer Anwendung bilden: Zunächst lesen wir das Korpus ein und

legen es in einer von uns verarbeitbaren Datenstruktur ab; da wir diese in den weiteren Schritten

zerstören, benötigen wir noch einige Kopien davon. Dann folgt die Berechnung und Ausgabe der

Token- und Typehäufigkeiten sowie deren relative Häufigkeiten.

Das Einlesen des Korpus gliedert sich in das eigentliche Einlesen der Datei und

Vorverarbeitungsschritte. Ebenso lassen sich die n-Gramm-Berechnungen in eigene Funktionseinheiten

unterteilen: Auf die Speicherung eines n-Gramms mit seiner jeweiligen Häufigkeit in einem Hash folgt

die Eliminierung der Stoppworte und die Zählung der n-Gramm Types, die Suche nach den n-

Grammen, die jeweils nur einmal vorkommen, die Berechnungen zum Anteil aller jeweiligen n-

Gramme am Gesamtkorpus und die Berechnung der relativen Häufigkeiten der zehn prominentesten n-

Gramme. Daraus ergibt sich folgende Graphik:

Page 117: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

113

Bevor wir uns die Struktur des Programms näher anschauen, erzeugen wir zunächst eine globale

Hash-Konstante, in der die Zahlen der n-Gramme auf griechischen Zahlen-Vorsilben abgebildet

werden:

my %GRIECHISCHE_ZAHLEN = ( 1 => "Uni", 2 => "Bi", 3 => "Tri", 4 => "Tetra", 5 => "Penta", 6 => "Hexa", 7 => "Hepta", 8 => "Okta", 9 => "Ennea", 10 => "Deka" );

Diese verwenden wir später, um die Bezeichnungen aus den als Argumenten übergebenen Zahlen

interpolieren zu können.

Das "Hauptprogramm" besteht des Weiteren aus den globalen Variablen, in denen wir die Liste der

Wörter speichern und die Größe des Vokabulars. Allerdings verwenden wir nur letztere als globale

Variable; das Array mit der Wörterliste und seine Kopien dienen jeweils den Subroutinen zur

Berechnung der n-Gramme und werden in diesen lokal verarbeitet:

my @token = korpus_einlesen("dostojevski.tok"); my @token2 = @token; my @token3 = @token; my $vokabular = unigramme_berechnen(@token); bigramme_berechnen(@token); trigramme_berechnen(@token2); tetragramme_berechnen(@token3);

Das Einlesen des Korpus ist recht einfach: Beim Aufruf übergeben wir den Dateinamen als

Argument, das in seiner lexikalischen Form wie gewohnt der open()-Funktion übergeben wird. Jede

eingelesene Zeile wird der Subroutine korpus_vorverarbeiten() übergeben, deren Rückgabewert ein

Array der Wörter ist. Diese Liste wird wiederum auf ein Array geschrieben, das schlussendlich an die

Stelle des Subroutinenaufrufs zurückgegeben wird:

sub korpus_einlesen { my @token; my $dateiname = shift; open( IN, $dateiname ) or die $!; while (<IN>) { push( @token, korpus_vorverarbeiten($_) ); } return @token; }

Page 118: Das Perl-Tutorial für Computerlinguisten

114

Die Subroutine korpus_vorverarbeiten() leistet die Ersetzungen von Sonderzeichen und

überflüssigem Leerraum und sorgt dafür, dass Klitika als ein Token verarbeitet werden:

sub korpus_vorverarbeiten { my $zeile = shift; chomp($zeile); next if ( $zeile =~ /^\s+$/ ); $zeile =~ s/\s{2,}/ /g; $zeile =~ s/\s'([cdlrstv])/'$1/g; $zeile =~ s/^\"\s//; $zeile =~ s/\s[.?!\":;,$%&-\/()]+//g; return @token; }

Im nächsten Schritt berechnen wir zunächst die Häufigkeiten der Token und Types. Da sich die

Unigramme bezüglich der Berechnung des n-Gramm-Raums anders verhalten als die übrigen n-

Gramme, bzw. wir eine andere Ausgabe für sie vorsehen, unterscheidet sich die Implementation dieser

Subroutine von den übrigen:

sub unigramme_berechnen { my @token = @_; my $unigramm_hl; my %unigramm_haeufigkeit; my $anzahl_token = @token; print "Korpus im Umfang von $anzahl_token Token eingelesen!\n\n"; foreach (@token) { $unigramm_haeufigkeit{$_}++; } %unigramm_haeufigkeit =

stoppworte_herausfiltern(%unigramm_haeufigkeit); my $anzahl_types = keys %unigramm_haeufigkeit; print "Die Größe des Vokabulars beträgt $anzahl_types.\n"; $unigramm_hl = hapax_legomena_berechnen(%unigramm_haeufigkeit); print sprintf( "%.2f", ( ( $unigramm_hl / $anzahl_types ) * 100 ) ), "% aller Types kommt nur einmal im Korpus vor!\n"; relative_haeufigkeit_berechnen( $anzahl_token, 1,

%unigramm_haeufigkeit ); return $anzahl_types; }

Neu sind die Filterung der Stoppwörter sowie die Berechnung der Hapax Legomena und der

relativen Häufigkeiten in eigenen Subroutinen. Da wir über diese noch sprechen werden, seien hier nur

ihre Aufrufe erklärt: Zunächst übergeben wir der Stoppwort-Filterung den Hash mit den n-Grammen

und ihren Häufigkeiten, den wir dann in verringertem Umfang aus der Subroutine zurückgegeben

bekommen. Die Hapax Legomena sind ja jeweils Bestandteile der Hashes, in denen die n-Gramm-

Häufigkeiten gespeichert sind; diese übergeben wir an die entsprechende Subroutine. Die

Page 119: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

115

Parameterliste der Subroutine relative_haeufigkeit_berechnen() ist ein wenig komplexer, da sie

aus der jeweiligen Anzahl der Token, der Angabe der Wertigkeits des "n"s des n-Gramms und dem

oben erwähnten Hash mit Häufigkeiten besteht. Obschon wir für diesen Aufruf mehrere

Datenstrukturen verwenden, benötigen wir noch keine Referenzen, da es sich bei den Argumenten

zunächst um Skalare handelt, denen ein Hash folgt, sodass wir diese Strukturen gut auseinander halten

können.

Bevor wir uns der Funktionalität dieser Subroutinen widmen, schauen wir uns aber die Subroutinen

zur Berechnung der weiteren n-Gramme an. In ihnen gibt es zusätzlich einen Aufruf zur Berechnung

der Token-/Typehäufigkeiten. Da dieser eine Parameterliste besitzt, die ausschließlich aus Skalaren

besteht, ist er eher unproblematisch. Auch können wir die Generierung und Zählung der n-Gramme

abstrahieren, da wir dazu lediglich das "n" und das jeweilige Array mit den Token übergeben und den

Skalar mit der Anzahl der n-Gramme sowie den Hash mit den n-Grammen und ihren Häufigkeiten

zurückgeben müssen. Die Struktur dieser Subroutinen sei wiederum am Beispiel der Berechnung der

Tetragramme illustriert:

sub tetragramme_berechnen { my @token = @_; my ( $tetragramm_hl, $anzahl_tetragramme ); my %tetragramm_haeufigkeit; ( $anzahl_tetragramme, %tetragramm_haeufigkeit ) = n_gramme_zaehlen(

4, @token ); %tetragramm_haeufigkeit =

stoppworte_herausfiltern(%tetragramm_haeufigkeit); my $tetragramm_types = keys %tetragramm_haeufigkeit; $tetragramm_hl = hapax_legomena_berechnen(%tetragramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_tetragramme, $tetragramm_types, $vokabular, $tetragramm_hl, 4 ); relative_haeufigkeit_berechnen( $anzahl_tetragramme, 4, %tetragramm_haeufigkeit ); }

In der Subroutine zur Filterung der Stoppwörter iterieren wir wie bereits gesehen über den als

Argument übergebenen Hash aus n-Grammen und ihren Häufigkeiten, wobei wir bei jeder Iteration

über die Stoppwort-Liste schauen, ob eines davon im jeweiligen n-Gramm enthalten ist. Der

Rückgabewert ist der gefilterte Hash:

sub stoppworte_herausfiltern { my @stoppwort_liste = qw(a an and the this that as at with by to for from of on in out

up I he she it we you they me my his her him our your us them their be am is are was were has have had do does did can could will would all no not but so what or if yes);

my %n_gramm_haeufigkeit = @_;

Page 120: Das Perl-Tutorial für Computerlinguisten

116

foreach(keys %n_gramm_haeufigkeit){ foreach my $stoppwort (@stoppwort_liste) { delete($n_gramm_haeufigkeit{$_}) if ( /\b$stoppwort\b/i ); next; } } return %n_gramm_haeufigkeit; }

Die Subroutine zur Berechnung der Hapax Legomena nimmt diesen Hash als Argument, berechnet

daraus die Anzahl der Elemente mit der Häufigkeit 1 und gibt diese zurück:

sub hapax_legomena_berechnen { my $n_gramm_hl; my %n_gramm_haeufigkeit = @_; foreach ( keys %n_gramm_haeufigkeit ) { $n_gramm_hl++ if ( $n_gramm_haeufigkeit{$_} == 1 ); } return $n_gramm_hl; }

In der Subroutine relative_haeufigkeit_berechnen() machen wir uns zum ersten Mal den Hash

mit den Konstanten für griechische Zahlenpräfixe zunutze: Wann immer wir eine solche Bezeichnung

ausgeben wollen, interpolieren wir sie aus dem Wert des Hashes und konkatenieren sie entsprechend:

sub relative_haeufigkeit_berechnen { my $anzahl_n_gramme = shift; my $bezeichner = shift; my %n_gramm_haeufigkeit = @_; my $i = 0; print "\nDie zehn häufigsten " . $GRIECHISCHE_ZAHLEN{$bezeichner} . "gramme sind:\n"; foreach ( sort { $n_gramm_haeufigkeit{$b} <=> $n_gramm_haeufigkeit{$a} } keys %n_gramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $n_gramm_haeufigkeit{$_} / $anzahl_n_gramme ) * 100 ); print "$_: $n_gramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller $GRIECHISCHE_ZAHLEN{$bezeichner}gramm-Token.\n"; last if ( $i > 10 ); $i++; } }

Page 121: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

117

Diese Möglichkeit schöpfen wir in der Subroutine token_type_haeufigkeit() noch weiter aus: Da

der Exponent zur Berechnung des möglichen n-Gramm-Raums immer dem "n" entspricht, können wir

hier dieses numerische Argument sowohl zur Berechnung als auch für die Interpolation in der Ausgabe

einsetzen:

sub token_type_haeufigkeiten { my $anzahl_token = shift; my $anzahl_types = shift; my $groesse_vokabular = shift; my $n_gramm_hl = shift; my $exponent = shift; # "n" des n-Gramms zur Berechnung des n-

Gramm-Raums print "\nEs gibt $anzahl_token $GRIECHISCHE_ZAHLEN{$exponent}gramm-Token und

$anzahl_types $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types.\nDas ergibt "

. ( $vokabular**$exponent ) . " mögliche $GRIECHISCHE_ZAHLEN{$exponent}gramme, von denen " . ( ( $anzahl_types / $vokabular**$exponent ) * 100 ) . "%\nim Dokument vorkommen.\n"; print sprintf( "%.2f", ( ( $n_gramm_hl / $anzahl_types ) * 100 ) ) . "% aller $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types kommt nur

einmal im Korpus vor!\n"; }

Eine substantielle Verbesserung stellt die Zählung der n-Gramme dar: In dieser Subroutine

verallgemeinern wir den Prozess, der strukturell für alle n-Gramme gleich ist und einen nicht

unbeträchtlichen Raum im Programm einnahm. Algorithmisch betrachtet, verläuft die Berechnung der

n-Gramme so, dass wir solange über das Array der Token iterieren, wie n-1 Token vorhanden sind.

Innerhalb der Schleife extrahieren wir n Token in ein Array, aus dem wir wie gewohnt unser n-Gramm

durch Zusammenfügung erzeugen, das wiederum in einem Hash gezählt wird. Daraufhin entfernen wir

das erste Element dieses Arrays und fügen den Rest vorne an unser Token-Array an. Zurückgegeben

wird wie gesagt die Anzahl der n-Gramme und der Hash mit den jeweiligen Häufigkeiten:

sub n_gramme_zaehlen{ my $n = shift; my @token = @_; my %n_gramm_haeufigkeit; my $anzahl_n_gramme; while ( @token > $n-1 ) { my @n_gramm = splice( @token, 0, $n ); my $n_gramm = join(" ", @n_gramm); $n_gramm_haeufigkeit{$n_gramm}++; shift(@n_gramm); unshift( @token, @n_gramm ); $anzahl_n_gramme++; } return( $anzahl_n_gramme, %n_gramm_haeufigkeit ); }

Page 122: Das Perl-Tutorial für Computerlinguisten

118

Hier das gesamte Programm:50

use strict; use diagnostics; my %GRIECHISCHE_ZAHLEN = ( 1 => "Uni", 2 => "Bi", 3 => "Tri", 4 => "Tetra", 5 => "Penta", 6 => "Hexa", 7 => "Hepta", 8 => "Okta", 9 => "Ennea", 10 => "Deka" ); my @token = korpus_einlesen("dostojevski.tok"); my @token2 = @token; my @token3 = @token; my $vokabular = unigramme_berechnen(@token); bigramme_berechnen(@token); trigramme_berechnen(@token2); tetragramme_berechnen(@token3); sub korpus_einlesen { my @token; my $dateiname = shift; open( IN, $dateiname ) or die $!; while (<IN>) { push( @token, korpus_vorverarbeiten($_) ); } return @token; } sub korpus_vorverarbeiten { my $zeile = shift; chomp($zeile); next if ( $zeile =~ /^\s+$/ ); $zeile =~ s/\s{2,}/ /g; $zeile =~ s/\s'([cdlrstv])/'$1/g; $zeile =~ s/^\"\s//; $zeile =~ s/\s[.?!\":;\,$%&-\/()\[\]]+//g; my @token=split(/ /,$zeile); return @token; }

50 Da sich die Ausgabe nicht verändert hat, verzichten wir darauf, sie an dieser Stelle abermals wiederzugeben.

Page 123: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

119

sub stoppworte_herausfiltern { my @stoppwort_liste = qw(a an and the this that as at with by to for from of on in out up I

he she it we you they me my his her him our your us them their be am is are was were has have had do does did can could will would all no not but so what or if yes);

my %n_gramm_haeufigkeit = @_; foreach(keys %n_gramm_haeufigkeit){ foreach my $stoppwort (@stoppwort_liste) { delete($n_gramm_haeufigkeit{$_}) if ( /\b$stoppwort\b/i ); next; } } return %n_gramm_haeufigkeit; } sub unigramme_berechnen { my @token = @_; my $unigramm_hl; my %unigramm_haeufigkeit; my $anzahl_token = @token; print "Korpus im Umfang von $anzahl_token Token eingelesen!\n\n"; foreach (@token) { $unigramm_haeufigkeit{$_}++; } %unigramm_haeufigkeit =

stoppworte_herausfiltern(%unigramm_haeufigkeit); my $anzahl_types = keys %unigramm_haeufigkeit; print "Die Größe des Vokabulars beträgt $anzahl_types.\n"; $unigramm_hl = hapax_legomena_berechnen(%unigramm_haeufigkeit); print sprintf( "%.2f", ( ( $unigramm_hl / $anzahl_types ) * 100 ) ), "% aller Types kommt nur einmal im Korpus vor!\n"; relative_haeufigkeit_berechnen( $anzahl_token, 1,

%unigramm_haeufigkeit ); return $anzahl_types; }

Page 124: Das Perl-Tutorial für Computerlinguisten

120

sub bigramme_berechnen { my @token = @_; my ( $bigramm_hl, $anzahl_bigramme ); my ( %bigramm_haeufigkeit, %anzahl_bigramme ); ( $anzahl_bigramme, %bigramm_haeufigkeit ) = n_gramme_zaehlen( 2,

@token ); %bigramm_haeufigkeit = stoppworte_herausfiltern(%bigramm_haeufigkeit); my $bigramm_types = keys %bigramm_haeufigkeit; $bigramm_hl = hapax_legomena_berechnen(%bigramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_bigramme, $bigramm_types,

$vokabular, $bigramm_hl, 2 ); relative_haeufigkeit_berechnen( $anzahl_bigramme, 2,

%bigramm_haeufigkeit ); } sub trigramme_berechnen { my @token = @_; my ( $trigramm_hl, $anzahl_trigramme ); my %trigramm_haeufigkeit; ( $anzahl_trigramme, %trigramm_haeufigkeit ) = n_gramme_zaehlen( 3,

@token ); %trigramm_haeufigkeit =

stoppworte_herausfiltern(%trigramm_haeufigkeit); my $trigramm_types = keys %trigramm_haeufigkeit; $trigramm_hl = hapax_legomena_berechnen(%trigramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_trigramme, $trigramm_types,

$vokabular, $trigramm_hl, 3 ); relative_haeufigkeit_berechnen( $anzahl_trigramme, 3, %trigramm_haeufigkeit ); }

Page 125: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

121

sub tetragramme_berechnen { my @token = @_; my ( $tetragramm_hl, $anzahl_tetragramme ); my %tetragramm_haeufigkeit; ( $anzahl_tetragramme, %tetragramm_haeufigkeit ) = n_gramme_zaehlen(

4, @token ); %tetragramm_haeufigkeit =

stoppworte_herausfiltern(%tetragramm_haeufigkeit); my $tetragramm_types = keys %tetragramm_haeufigkeit; $tetragramm_hl = hapax_legomena_berechnen(%tetragramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_tetragramme, $tetragramm_types, $vokabular, $tetragramm_hl, 4 ); relative_haeufigkeit_berechnen( $anzahl_tetragramme, 4, %tetragramm_haeufigkeit ); } sub hapax_legomena_berechnen { my $n_gramm_hl; my %n_gramm_haeufigkeit = @_; foreach ( keys %n_gramm_haeufigkeit ) { $n_gramm_hl++ if ( $n_gramm_haeufigkeit{$_} == 1 ); } return $n_gramm_hl; } sub relative_haeufigkeit_berechnen { my $anzahl_n_gramme = shift; my $bezeichner = shift; my %n_gramm_haeufigkeit = @_; my $i = 0; print "\nDie zehn häufigsten " . $GRIECHISCHE_ZAHLEN{$bezeichner} . "gramme sind:\n"; foreach ( sort { $n_gramm_haeufigkeit{$b} <=> $n_gramm_haeufigkeit{$a} } keys %n_gramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $n_gramm_haeufigkeit{$_} / $anzahl_n_gramme ) * 100 ); print "$_: $n_gramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller $GRIECHISCHE_ZAHLEN{$bezeichner}gramm-Token.\n"; last if ( $i > 10 ); $i++; }

Page 126: Das Perl-Tutorial für Computerlinguisten

122

} sub token_type_haeufigkeiten { my $anzahl_token = shift; my $anzahl_types = shift; my $groesse_vokabular = shift; my $n_gramm_hl = shift; my $exponent = shift; # "n" des n-Gramms zur Berechnung des n-

Gramm-Raums print "\nEs gibt $anzahl_token $GRIECHISCHE_ZAHLEN{$exponent}gramm-Token und

$anzahl_types $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types.\nDas ergibt "

. ( $vokabular**$exponent ) . " mögliche $GRIECHISCHE_ZAHLEN{$exponent}gramme, von denen " . ( ( $anzahl_types / $vokabular**$exponent ) * 100 ) . "%\nim Dokument vorkommen.\n"; print sprintf( "%.2f", ( ( $n_gramm_hl / $anzahl_types ) * 100 ) ) . "% aller $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types kommt nur

einmal im Korpus vor!\n"; } sub n_gramme_zaehlen{ my $n = shift; my @token = @_; my %n_gramm_haeufigkeit; my $anzahl_n_gramme; while ( @token > $n-1 ) { my @n_gramm = splice( @token, 0, $n ); my $n_gramm = join(" ", @n_gramm); $n_gramm_haeufigkeit{$n_gramm}++; shift(@n_gramm); unshift( @token, @n_gramm ); $anzahl_n_gramme++; } return( $anzahl_n_gramme, %n_gramm_haeufigkeit ); }

Page 127: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

123

9 Referenzen

Sanity is only that which is within the frame of reference of conventional

thought.

ERICH FROMM

Aus den letzten Kapiteln ergaben sich die Desiderate, einerseits zusammengehörige komplexe

Daten in eine einzelne Datenstruktur schreiben zu können, andererseits wollen wir mehrere Variablen,

die über Skalare hinaus gehen, als Argumente an Subroutinen übergeben bzw. aus Subroutinen

zurückgeben können. In beiden Fällen verhindert die Prämisse, dass die Bestandteile von

Datenstrukturen nur Skalare sein dürfen, eine einfache Lösung: Wir können weder Arrays oder Hashes

ineinander einbetten, um eine komplexe Datenstruktur aufzubauen, noch können wir mehrere Arrays

oder Hashes an Subroutinen übergeben oder aus diesen zurückgeben, ohne dass sie zu einer einzigen

Listenstruktur vereinheitlicht würden. Wir benötigen also eine spezielle Art von Skalaren, die es uns

ermöglicht, diese Operationen durchzuführen.

Solche Skalare nennt man Referenzen. Ihre Funktionalität besteht darin, auf andere

Datenstrukturen51 zu zeigen. Diese Datenstrukturen können wiederum Skalare, Arrays oder Hashes

sein. Konzeptuell kann man sich eine Referenz wie einen Eintrag im Index eines Buches vorstellen:

Das Stichwort verweist auf eine Seite im Buch, auf der man die gewünschte Information finden kann.

Aber ebenso wenig, wie das Stichwort die Information selbst ist, stellt solch ein Skalar die referenzierte

Datenstruktur dar, sondern einen Verweis auf den Speicherort des Werts dieser Struktur:52

9.1 Erzeugen einer Referenz Die einfachste Möglichkeit, eine Referenz zu erzeugen, besteht darin, der Datenstruktur, auf die

man verweisen will, einen Backslash voranzustellen. Will man also eine Referenz auf einen Skalar

erzeugen, weist man einem Skalar eine originäre Skalarvariable zu, der man einen Backslash

voranstellt:

my $skalar = 42; my $skalar_referenz = \$skalar;

51 Wir werden im Laufe dieses Kapitels noch sehen, dass dies auch für Subroutinen gilt! 52 Anders als in anderen Programmiersprachen, sind Dinge wie Speicherarithmetik o.Ä. weder nötig noch möglich!

Page 128: Das Perl-Tutorial für Computerlinguisten

124

Analog dazu sieht die Erzeugung einer Arrayreferenz so aus:

my @marxes = qw(Chico Harpo Groucho Gummo Zeppo); my $marxes_referenz = \@marxes;

Und einen Hash referenziert man so:

my %woerterbuch = (apple => "pomme", pear => "poire"); my $woerterbuch_referenz = \%woerterbuch;

Da Referenzen einfache Skalare sind, können wir sie z.B. zu Listen in Arrays zusammenfassen:

my $ziffer_eins = 3; my $ziffer_zwei = 4; my $ziffer_drei = 5; my @ziffern_referenzen = (\$ziffer_eins, \$ziffer_zwei, \$ziffer_drei); #

oder... my @ziffern_referenzen = \($ziffer_eins, $ziffer_zwei, $ziffer_drei); Genauso können wir Referenzen – in diesem Fall Arrayreferenzen – in einen Hash einfügen:

my @english = qw(January February March April); my @french = qw(Janvier Fevrier Mars Avril); my %dictionary = (english => \@english, french => \@french);

Referenzen dürfen zwar rein technisch auch Schlüssel in einem Hash sein, man sollte sie allerdings

nicht so verwenden, da sie mit dem use strict-Pragma kollidieren.

Aus obigem Beispiel ergibt sich, dass wir analog zur Einbettung von Arrays in Hashes auch Arrays

in Arrays speichern können:

my @array1 = (10, 20, 30, 40); my @array2 = (1, 2, \@array1, 3, 4);

Da sich diese Operation beliebig oft wiederholen lässt, können wir auf diese Weise ineinander

geschachtelte Arrays aufbauen:

my @array3 = (2, 4, \@array2, 6, 8); my @array4 = (100, 200, \@array3, 300, 400);

Wir erzeugen also folgende komplexe Datenstruktur:

Page 129: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

125

Erzeugen anonymer Datenstrukturen

Ein weiteres Merkmal von Referenzen besteht darin, dass sie den Aufbau anonymer

Datenstrukturen ermöglichen. Dies bedeutet, dass man mit Datenstrukturen operieren kann, denen kein

Variablenname zugeordnet ist. Da dies nur auf Arrays und Hashes anwendbar ist, betrachten wir hier

nur die Alternativen zu obigen Beispielen.

Will man statt eines benannten Arrays eine Referenz auf ein anonymes Array erzeugen, zeigt man

die Arrayreferenz durch eckige Klammern [ ] an:

my $ziffern_referenz = [1, 2, 3, 4, 5];

Analog dazu erzeugt man einen anonymen Hash durch die Verwendung geschweifter Klammern

{}:

my $woerterbuch_referenz = {apple => "pomme", pear => "poire" };

Anonyme Datenstrukturen lassen sich genauso in Arrays und Hashes einfügen, wie Referenzen, die

auf benannte Datenstrukturen verweisen:

my %dictionary = (english => [January, February, March, April], french => [Janvier, Fevrier, Mars, Avril] );

Und ebenso, wie wir die benannten Arrays ineinander geschachtelt haben, können wir dies auch mit

anonymen Arrays tun. Wollen wir die Struktur aus obigem Beispiel nachbilden, müssen wir allerdings

darauf achten, dass zunächst die Elemente aus @array4, dann an entsprechender Stelle jeweils die Ele-

mente aus @array3, @array2 und @array1 stehen:

my @array = (100, 200, [2, 4, [1, 2, [10, 20, 30, 40], 3, 4], 6, 8], 300, 400);

Page 130: Das Perl-Tutorial für Computerlinguisten

126

Eine solche Datenstruktur ist sehr unübersichtlich! Um dennoch überblicken zu können, wie diese

komplexe Struktur aufgebaut ist, bedient man sich des Moduls Data::Dumper, das sich mit use

Data::Dumper; genauso in Programme einbinden lässt, wie die Pragmata strict und diagnostics.

Um dieses Modul zu verwenden, schreibt man das Wort Dumper ähnlich einem Dateideskriptor

zwischen die print()-Funktion und die auszugebende Datenstruktur:

use strict; use diagnostics; use Data::Dumper; my @array = (100, 200, [2, 4, [1, 2, [10, 20, 30, 40], 3, 4], 6, 8], 300,

400); print Dumper @array; # Ausgabe: # $VAR1 = 100; # $VAR2 = 200; # $VAR3 = [ # 2, # 4, # [ # 1, # 2, # [ # 10, # 20, # 30, # 40 # ], # 3, # 4 # ], # 6, # 8 # ]; # $VAR4 = 300; # $VAR5 = 400;

Da es sich bei Data::Dumper um ein sehr nützliches Werkzeug handelt, das bei der Arbeit mit

Referenzen fast unerlässlich ist, sollte man es ab sofort immer in die Programme einbinden!53

9.2 Verwendung von Referenzen Um Datenstrukturen dereferenzieren zu können, kennt Perl drei syntaktische Varianten, die aber

alle dieselbe Funktionalität besitzen, nämlich die einzelnen Datenstrukturen zugreifbar zu machen. Wir

konzentrieren uns zunächst auf eine dieser Schreibweisen, um das Konzept der Verwendung von

Referenzen klarzumachen, und werden dann die Alternativen vorstellen.

Will man Datenstrukturen dereferenzieren, schließt man den Variablennamen der Referenz in

geschweifte Klammern {} ein und schreibt davor das Präfixsymbol der referenzierten Datenstruktur.

53 Im Emacs lässt sich dazu der letzte Block der _emacs-Datei derart modifizieren, dass diejenige Zeile, in der die

Modulnamen festgelegt werden, so aussieht: (setq modules (list "strict" "diagnostics" "Data::Dumper"))

Page 131: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

127

Will man also einen Skalar dereferenzieren, schreibt man ein Dollar-Zeichen $ vor die Variable, bei

einem Array einen Klammeraffen @ und bei einem Hash ein Prozentzeichen %:

Dereferenzieren eines Skalars:

my $skalar = 42; my $skalar_referenz = \$skalar; my $skalar2 = ${$skalar};

Dereferenzieren eines Arrays:

my @ziffern = (1, 2, 3, 4, 5); my $ziffern_referenz = \@ziffern; my @ziffern2 = @{$ziffern_referenz};

Dereferenzieren eines Hashes:

my %woerterbuch = (apple => "pomme", pear => "poire"); my $woerterbuch_referenz = \%woerterbuch; my %woerterbuch2 = %{$woerterbuch_referenz};

Auf Referenzen lassen sich alle Operationen ausführen, die man auch auf normale Datenstrukturen

anwenden kann:

my @ziffern = (1, 2, 3, 4, 5); my $ziffern_referenz = \@ziffern; print "Das dereferenzierte Array: @{$ziffern_referenz}\n"; foreach(@{$ziffern_referenz}){ print "Element $_\n"; } # Ausgabe: # Das dereferenzierte Array: 1 2 3 4 5 # Element 1 # Element 2 # Element 3 # Element 4 # Element 5

Um auf ein bestimmtes Element dieser Arrayreferenz zugreifen zu können, gelten die gleichen

Spielregeln, wie wir sie von normalen Arrays gewohnt sind: Der Zugriff erfolgt über einen Index, der

in eckigen Klammern steht; da der Rückgabewert aber ein Skalar ist, verwenden wir für die

Dereferenzierung keinen Klammeraffen, sondern ein Dollar-Zeichen:

print "Das dritte Element ist ${$ziffern_referenz}[2]\n"; # Ausgabe: Das dritte Element ist 3

Welche Ausgabe erhalten wir, wenn wir die Referenz direkt ausgeben?

Page 132: Das Perl-Tutorial für Computerlinguisten

128

print "So sieht die Referenz aus: @ziffern_referenz\n"; # Ausgabe: So sieht die Referenz aus: ARRAY(0x80a2b8)

Wie bereits gesagt, verweist die Referenz auf den Speicherplatz des Werts der entsprechenden

Datenstruktur. Man muss also immer daran denken, die Datenstruktur zu dereferenzieren, bevor man

sie verwendet!

Modifiziert man den Wert einer Referenz, verändert sich auch der Wert der Datenstruktur, auf die

verwiesen wird:

my @band = qw (Gahan Gore Fletcher Wilder); my $band_referenz = \@band; print "Gruppenmitglieder vorher: @band\n"; # Ausgabe: Gahan Gore

Fletcher Wilder pop(@{$band_referenz}); print "Gruppenmitglieder nachher: @band\n"; # Ausgabe: Gahan Gore Fletcher

Das gleiche gilt im Fall mehrerer anonymer Referenzen:

my $band_referenz = [qw(Gahan Gore Fletcher Wilder)]; my $band_referenz2 = $band_referenz; print "Gruppenmitglieder vorher: @band_referenz\n"; # Ausgabe: Gahan

Gore Fletcher Wilder pop(@{$band_referenz2}); print "Gruppenmitglieder nachher: @band_referenz\n"; # Ausgabe: Gahan

Gore Fletcher

Durch die Kopie ist auch $band_referenz2 eine Referenz auf das anonyme Array, in dem die

Namen der Musiker stehen:

Page 133: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

129

Greift man über diese Referenz auf die Datenstruktur zu, passiert das gleiche, als hätte man die

Werte über $band_referenz manipuliert! Dies lässt sich auch an einzelnen Daten eines anonymen

Arrays nachvollziehen:

my $zahlen_referenz = [68, 101, 114, 111, 117]; print "Vorher: @{$zahlen_referenz}\n"; ${$zahlen_referenz}[0] = 100; print "Nachher: @{$zahlen_referenz}\n"; # Ausgabe: # Vorher: 68 101 114 111 117 # Nachher: 100 101 114 111 117

Um die Verarbeitung ineinander geschachtelter Datenstrukturen zu demonstrieren, erzeugen wir ein

simples Konkordanzwörterbuch der Sprachen Deutsch, Englisch und Französisch, in dem die Wörter

"Haus", "Auto" und "Kind" und ihre fremdsprachigen Entsprechungen abgelegt sind:

my %dictionary; my @german = qw(Haus Auto Kind); my @english = qw(house car child); my @french = qw(maison voiture enfant); foreach(@german){ $dictionary{$_} = {english => shift(@english), french => shift(@french) }; }

Page 134: Das Perl-Tutorial für Computerlinguisten

130

Dazu legen wir einen Hash an, dessen Schlüssel die deutschsprachigen Wörter sind. Die Werte zu

diesen Schlüsseln sind wiederum Hashes, deren Schlüssel entweder das Wort "english" oder "french"

ist. Deren Werte sind dann die entsprechenden Übersetzungen, sodass wir folgende Datenstruktur

erhalten:

$VAR1 = 'Kind'; $VAR2 = { 'french' => 'enfant', 'english' => 'child' }; $VAR3 = 'Haus'; $VAR4 = { 'french' => 'maison', 'english' => 'house' }; $VAR5 = 'Auto'; $VAR6 = { 'french' => 'voiture', 'english' => 'car' }; Um auf diese Informationen zugreifen zu können, iterieren wir zunächst wie gewohnt über die

Schlüssel des zugrunde liegenden Hashes, d.h. der deutschen Wörter:

foreach(keys %dictionary){ print "$_ => ${$dictionary{$_}}{english}

${$dictionary{$_}}{french}\n"; }

An die englischen bzw. französischen Pendants gelangen wir, indem wir zunächst den Wert des

deutschen Schlüssels durch ${$dictionary{$_}} dereferenzieren und dann den entsprechenden

Schlüssel des eingebetteten Hashes wie gewohnt auswählen. Dementsprechend erhalten wir folgende

Ausgabe:

# Kind => child enfant # Haus => house maison # Auto => car voiture

Nicht ganz unproblematisch (aber logisch) ist die Klammerung der verschiedenen Elemente, die

schnell unübersichtlich werden kann. Als zweite syntaktische Variante der Dereferenzierung können

die geschweiften Klammern um den Bezeichner der Referenz wegfallen:

@{$array_referenz} => @$array_referenz %{$hash_referenz} => %$hash_referenz

Da auch diese Variante nur bedingt lesbarer erscheint, lassen sich Werte auch über die Pfeilnotation

-> dereferenzieren:

${$array_referenz}[0] => $array_referenz->[0] ${$hash_referenz}{$_} => $hash_referenz->{$_}

Dadurch lässt sich die Ausgabe im vorherigen Beispiel folgendermaßen entwirren:

Page 135: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

131

foreach(keys %dictionary){ print "$_ => $dictionary{$_}->{english} $dictionary{$_}->{french}\n"; }

9.3 Komplexe Datenstrukturen Bevor wir den Einsatz von Referenzen bei Subroutinenargumenten diskutieren, betrachten wir

zunächst die Möglichkeit, Referenzen zur Erzeugung komplexer Datenstrukturen einzusetzen. Wie

bereits im letzten Abschnitt skizziert, lassen sich durch Referenzen Datenstrukturen, die über Skalare

hinausgehen, in Arrays und Hashes einbetten. Dadurch können wir folgende komplexe Datenstrukturen

erzeugen:

• Arrays of Arrays (oftmals auch fälschlicherweise als Lists of lists (LoL) genannt).

• Hashes of Hashes (HoH).

• Arrays of Hashes (AoH).

• Hashes of Arrays (HoL).

• Gemischte Datenstrukturen, die in anderen Programmiersprachen als struct oder record

bezeichnet werden.

Arrays of Arrays (LoL)

Arrays, die in Arrays eingebettet sind, kann man sich wie eine (mehrdimensionale) Tabelle

vorstellen, der die Überschriften fehlen. Stattdessen lassen sich die Daten (wie bei allen Arrays) über

ihre Indizes zugreifen. Diese lesen sich wie Koordinaten: Hat man ein zweidimensionales Array

erzeugt, greift man beispielsweise mit den Indizes ->[0]->[0] auf das erste Element des ersten Arrays

zu; bei jeder weiteren Dimension kommt ein Indexfeld hinzu. Dies bedeutet gleichzeitig, dass mit jeder

neuen Dimension auch eine weitere Ebene der Indirektion hinzukommt. Will man also über die

Elemente eines zweidimensionalen Arrays iterieren, benötigt man zwei ineinander geschachtelte

Schleifen. Betrachten wir zunächst die Möglichkeit, vermittels for()-Schleifen über die numerischen

Indizes auf die Daten zuzugreifen. Gegeben folgende Struktur

muss man sich zunächst fragen, wie sich die Größe der beiden Arrays berechnet. Auf der y-Achse

besteht das Array aus zwei Elementen, den Listen (40, 50, 60) und (10, 20, 30), die ja wiederum jeweils

auf der x-Achse drei Elemente besitzen. Es bietet sich also folgende Modellierung als anonymes Array

an:

my $lol = [[40,50,60],[10,20,30]];

Page 136: Das Perl-Tutorial für Computerlinguisten

132

Um nun die Anzahl der Elemente des äußeren Arrays (der y-Achse) zu ermitteln, dereferenzieren

wir $lol und bringen den Wert in einen Skalarkontext scalar(@{$lol}). Die Länge einer der beiden

Listen erhalten wir, indem wir das erste Element des anonymen Arrays dereferenzieren und den Wert

wiederum in den skalaren Kontext bringen scalar(@{$lol}->[0]):

for(my $y = 0; $y < scalar(@{$lol}); $y++){ for(my $x = 0; $x < @{scalar($lol->[0])}; $x++){ print "y: $y, x: $x, $lol->[$y]->[$x]\n"; } }

Durch die Angabe der "Koordinaten" in der Dereferenzierung erhalten wir die erwartete Ausgabe:

# Ausgabe: # y: 0, x: 0, 40 # y: 0, x: 1, 50 # y: 0, x: 2, 60 # y: 1, x: 0, 10 # y: 1, x: 1, 20 # y: 1, x: 2, 30 Die gleichen Werte bekommen wir auch, wenn wir eingebettete foreach()-Schleifen verwenden:

foreach(@{$lol}){ foreach my $data(@{$_}){ print "$data\n"; } }

In beiden Fällen ist zu beachten, dass die beiden Listen gleich groß sein müssen; ansonsten werden

entweder zu wenige Elemente oder Warnungen über nicht initialisierte Werte ausgegeben.

Hashes of Hashes (HoH)

Wie bereits gesehen, sind Hashes of Hashes Listen von Attributen, die wiederum Attribut-

/Wertepaare als Werte besitzen. Genauso wie bei einem normalen Hash gelangt man über die Schlüssel

an die Werte. Da diese allerdings wiederum Schlüssel sind, benötigt man eine entsprechende Anzahl

von Schleifen, um an den letzten (atomaren) Wert zu gelangen.

Auch die Sortierung erfolgt analog zu normalen Hashes: Mit jedem Schleifendurchlauf erhält man

die Möglichkeit, mit sort die Schlüssel und mit sort $a <=> $b die Werte zu sortieren. In beiden

Fällen ist natürlich darauf zu achten, dass die entsprechende Dereferenzierung auf der jeweiligen Ebene

eingehalten wird:

Page 137: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

133

my %hoh = ( AB3484 => { Airline => "Air Berlin", Flugzeug => "Boeing 737-400", Schalter => "80-81", Gate => "14", Abflug => "17:40", Ziel => "Malaga" }, LH2041 => { Airline => "Lufthansa", Flugzeug => "Aerospatiale ATR 72", Schalter => "51-87", Gate => "16", Abflug => "17:20", Ziel => "München" }, BUS2 => { Fahrzeug => "Bus", Gate => "13", Abflug => "14:00", Ziel => "Flughafenführung" } ); foreach(keys %hoh){ print "Flugnummer: $_\n"; foreach my $schluessel(keys %{$hoh{$_}}){ print "$schluessel: $hoh{$_}->{$schluessel}\n"; } print "\n"; } # Ausgabe: # Flugnummer: BUS2 # Gate: 13 # Ziel: Flughafenführung # Fahrzeug: Bus # Abflug: 14:00 # # Flugnummer: AB3484 # Gate: 14 # Ziel: Malaga # Schalter: 80-81 # Abflug: 17:40 # Flugzeug: Boeing 737-400 # Airline: Air Berlin # # Flugnummer: LH2041 # Gate: 16 # Ziel: München # Schalter: 51-87 # Abflug: 17:20 # Flugzeug: Aerospatiale ATR 72 # Airline: Lufthansa

Page 138: Das Perl-Tutorial für Computerlinguisten

134

Sortierung über die Schlüssel, sodass die "Flüge" nach ihren Flugnummern und den Schlüsseln im

inneren Hash sortiert sind:

foreach(sort keys %hoh){ print "Flugnummer: $_\n"; foreach my $schluessel(sort keys %{$hoh{$_}}){ print "$schluessel: $hoh{$_}->{$schluessel}\n"; } print "\n"; } # Ausgabe: # Flugnummer: AB3484 # Abflug: 17:40 # Airline: Air Berlin # Flugzeug: Boeing 737-400 # Gate: 14 # Schalter: 80-81 # Ziel: Malaga # # Flugnummer: BUS2 # Abflug: 14:00 # Fahrzeug: Bus # Gate: 13 # Ziel: Flughafenführung # # Flugnummer: LH2041 # Abflug: 17:20 # Airline: Lufthansa # Flugzeug: Aerospatiale ATR 72 # Gate: 16 # Schalter: 51-87 # Ziel: München

Sortierung nach den Werten, sodass die "Flüge" nach ihren Abflugzeiten sortiert sind:

foreach(sort {$hoh{$a}{Abflug} cmp $hoh{$b}{Abflug}} keys %hoh){ print "Flugnummer: $_\n"; foreach my $schluessel(sort keys %{$hoh{$_}}){ print "$schluessel: $hoh{$_}->{$schluessel}\n"; } print "\n"; } # Ausgabe: # Flugnummer: BUS2 # Abflug: 14:00 # Fahrzeug: Bus # Gate: 13 # Ziel: Flughafenführung #

Page 139: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

135

# Flugnummer: LH2041 # Abflug: 17:20 # Airline: Lufthansa # Flugzeug: Aerospatiale ATR 72 # Gate: 16 # Schalter: 51-87 # Ziel: München # Flugnummer: AB3484 # Abflug: 17:40 # Airline: Air Berlin # Flugzeug: Boeing 737-400 # Gate: 14 # Schalter: 80-81 # Ziel: Malaga

Eine weitere Möglichkeit, einen HoH zu erzeugen, besteht in der sogenannten Autovivikation:

Obwohl noch auf keiner Ebene unserer Datenstruktur ein Element bekannt ist, können wir es

automatisch durch Einsetzung an die gewünschte Stelle definieren. Wir hätten also auch Teile unseres

Beispiel-HoHs so aufbauen können:

my $hoh{"AB3484"}{"Gate"}=14; $hoh{"LH2041"}{"Flugzeug"}="Aerospatiale ATR 72"; $hoh{"LH2041"}{"Airline"}="Lufthansa";

Arrays of Hashes (LoH)

Wie bei einem "normalen" Array besteht ein LoH aus einer Liste, nur sind seine Werte Referenzen

auf Hashes:

my @loh = ( {"name" => "Peter", "groesse" => "1, 72", "e-mail" => "peter\@web.de"}, {"name" => "George Bush", "e-mail" => "schorsch\@whitehous.gov", "beruf" => "Präsident"}, {"name" => "Mika Häkkinen", "beruf" => "Ex-Rennfahrer", "nationalität" => "Finnisch"});

Zum Aufbau eines LoHs hätten wir ebenso gut bereits bestehende Hashes mit push(@loh,

{%hash}) in die Datenstruktur einbringen können.

Wollen wir diese Daten ausgeben, müssen wir zunächst über die Elemente des Arrays und dann

jeweils über die dereferenzierten Hashelemente iterieren. In der eigentlichen Ausgabe gelangen wir

wiederum durch Dereferenzierung des jeweiligen Hashelements via Index des Arrays und dem

entsprechenden Schlüssel an den Wert:

Page 140: Das Perl-Tutorial für Computerlinguisten

136

for(my $i=0;$i<@loh;$i++){ foreach(keys %{$loh[$i]}){ print "$_: $loh[$i]->{$_}\n"; } print "\n"; } # Ausgabe: # name: Peter # e-mail: [email protected] # groesse: 1, 72 # # name: George Bush # beruf: Präsident # e-mail: [email protected] # # name: Mika Häkkinen # nationalität: Finnisch # beruf: Ex-Rennfahrer

Hashes of Arrays (HoL)

HoLs sind Hashes, in denen die Schlüssel einfache Zeichenketten sind und die Werte Referenzen

auf Arrays:

my %hol = ("Paul" => ["rot", "blau"], "Rainer" => ["weiß", "grün"], "Ute" => ["orange", "blau", "schwarz"]);

Zur Ausgabe iteriert man im Gegensatz zu LoHs nun zuerst über den Hash, dann über die

jeweiligen anonymen Arrays:

foreach(sort keys %hol){ print "Lieblingsfarben von $_: "; foreach my $farbe(sort @{$hol{$_}}){ print "$farbe "; } print "\n"; } # Ausgabe: # Lieblingsfarben von Paul: blau rot # Lieblingsfarben von Rainer: grün weiß # Lieblingsfarben von Ute: blau orange schwarz

Um sukzessive Elemente auf ein anonymes Array zu packen, setzt man am Schlüssel desjenigen

Hashelements an, dessen Array man erweitern will, und wendet dann push() oder unshift() auf das

dereferenzierte Hashelement an:

foreach(keys %hol){ push(@{$hol{Paul}},"gelb"); }

Page 141: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

137

9.4 Referenzen als Subroutinen-Argumente Um mehrere listenwertige Datenstrukturen – Arrays und/oder Hashes – an eine Subroutine

übergeben zu können oder sie aus Subroutinen zurückzugeben, macht man aus ihnen Referenzen.

Betrachten wir dazu folgendes Beispiel, in dem wir die Inhalte zweier Arrays in einer Subroutine

miteinander vergleichen:

my @a = (1, 2, 3, 4, 5); my @b = (1, 2, 4, 5, 6); my @c = (1, 2, 3, 4, 5); print "@a ist gleich @b\n" if(arrays_vergleichen(\@a, \@b)); print "@a ist gleich @c\n" if(arrays_vergleichen(\@a, \@c)); sub arrays_vergleichen{ my ($array_1, $array_2) = @_; for(my $i = 0; $i < @{$array_1}; $i++){ return unless($array_1->[$i] eq $array_2->[$i]); } return 1; } # Ausgabe: # 1 2 3 4 5 ist gleich 1 2 3 4 5

Im Aufruf der Subroutine erzeugen wir jeweils durch Voranstellen eines Backslashes wie gewohnt

Referenzen auf die Arrays. Da sie dadurch zu Skalaren geworden sind, stehen sie als einzelne Elemente

in @_. In der Folge iterieren wir über die Indizes des ersten Arrays und vergleichen an jeder Position die

dereferenzierten Elemente miteinander, wodurch wir das erwartete Ergebnis erhalten.

Umgekehrt können wir genauso mehrere listenwertige Datenstrukturen zurückgeben. Dazu setzen

wir sie einfach auf die Argumentliste der return()-Funktion. Dies sei an einer einfachen Subroutine

illustriert, die einen Hash in zwei Arrays transformiert:

my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); my ($namen, $wohnorte) = hash2arrays(%adressbuch); print "@{$namen}\n"; print "@{$wohnorte}\n"; sub hash2arrays{ my %hash = @_; my (@array1, @array2); foreach(sort keys %hash){ push(@array1, $_); push(@array2, $hash{$_}); } return (\@array1, \@array2); }

Page 142: Das Perl-Tutorial für Computerlinguisten

138

# Ausgabe: # Andreas Peter Susanne # Darmstadt Bonn Berlin

Zu beachten ist, dass die Rückgabewerte Skalare sind und auch als solche auf der linken Seite des

Aufrufs vorhanden sein müssen.

9.5 Referenzen auf Subroutinen Als wir im vorigen Kapitel als Präfixsymbol ein Ampersand & vor Subroutinenaufrufe geschrieben

haben, deutete sich bereits die Nähe zu Datenstrukturen an: Denn genauso, wie wir Referenzen auf

Skalare, Arrays und Hashes erzeugen können, dürfen wir dies auch für Subroutinen tun. Dazu

schreiben wir auch hier einen Backslash vor das diesmal obligatorische Ampersand:

my $addition_referenz = \&addiere; sub addiere{ $_[0] + $_[1]; }

Ebenso lassen sich anonyme Subroutinenreferenzen durch Zuweisung eines Subroutinenblocks an

einen Skalar erzeugen. Dabei ist zu beachten, dass nach der schließenden geschweiften Klammer des

Subroutinenblocks ein Semikolon stehen muss:

my $addition_referenz = sub{$_[0] + $_[1]};

Um solch eine Subroutinenreferenz verwenden zu können, schreibt man entweder zu

Dereferenzierung ein Ampersand vor die Referenz in geschweiften Klammern oder wendet die

Pfeilnotation an. Die Argumentliste steht in beiden Fällen wie gewohnt in runden Klammern:

print &{$addition_referenz}(4, 5); # Ausgabe: 9 print $addition_referenz->(4, 5); # Ausgabe: 9

Zwar könnte man nun einwenden, dass ein solches Konstrukt nach l'art pour l'art aussieht, doch

lassen sich durchaus nützliche Dinge mit Subroutinenreferenzen realisieren. Beispielsweise können wir

nun Subroutinen abhängig von Aufrufparametern verwenden, indem wir sie zu Werten in einem Hash

machen:

my %berechne = (plus => sub{$_[0] + $_[1]}, minus => sub{$_[0] - $_[1]}, mal => sub{$_[0] * $_[1]}, durch => sub{$_[0] / $_[1]}); print $berechne{plus}->(3, 4)."\n"; # Ausgabe: 7 print $berechne{minus}->(4, 3)."\n"; # Ausgabe: 1 print $berechne{mal}->(3, 4)."\n"; # Ausgabe: 12 print $berechne{durch}->(3, 4)."\n"; # Ausgabe: 0.75

Page 143: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

139

Callbacks

Eine wichtigere Anwendung von Subroutinenreferenzen sind allerdings sogenannte Callbacks.

Dabei dienen Subroutinenreferenzen anderen Subroutinen als Argumente, sodass unser Code unter

bestimmten Bedingungen von diesen Subroutinen wieder aufgerufen werden kann (daher der Name).

Dies erlaubt uns, eine recht allgemein gehaltene Subroutine schreiben, der wir weitere Funktionalität in

Form von Argumenten mitgeben, um unterschiedliche, detailliertere Aufgaben damit lösen zu können.

Illustrieren wir dies an einer Subroutine, die einen Filter implementiert: Unsere Subroutine soll im

allgemeinen Fall zwei Zahlen addieren; wir wollen aber auch die Möglichkeit haben, ausschließlich

Gleitkommazahlen bzw. nur ganze Zahlen zu addieren. Formulieren wir zunächst die Filter:

sub gleitkommazahlen{ return 1 if($_[0] =~ /\./); } sub ganze_zahlen{ return if($_[0] =~ /\./); return 1; } sub alle_zahlen{ return 1 if($_[0] =~ /\d+(?:\.\d+)*/); }

In den ersten beiden Fällen ist das Vorhandensein des Dezimalpunkts das Filterkriterium, die letzte

Subroutine bildet den allgemeinen Fall einer Zahl ab. In der eigentlichen Subroutine übernehmen wir

zunächst eine Referenz auf ein Array mit Zahlen und die Referenz auf einen der Filter als Argumente.

Um sicherzustellen, dass wir die Funktion mehrmals hintereinander aufrufen können, legen wir eine

lokale Kopie der Testdaten an, indem wir sie dereferenzieren. Dann iterieren wir über diese Liste der

Zahlen, aus der wir jeweils die ersten beiden für die Berechnung entfernen. Zuerst überprüfen wir, ob

überhaupt ein Filter definiert wurde; ist dies nicht der Fall, addieren wir ohne weiteres die beiden

Operanden miteinander und initiieren mit next einen neuen Schleifendurchlauf, damit der Rest des

Blocks nicht abgearbeitet wird.

Die eigentlich interessante Funktionalität findet sich in der nächsten Zeile: Hier stellen wir sicher,

dass beide Operanden dem jeweils formulierten Kriterium gehorchen, indem wir über die

Subroutinenreferenz $filter jeweils einen der Operanden übergeben und die Ergebnisse der

Überprüfung durch ein logisches "und" miteinander verknüpfen. Nur wenn beide Operanden vom glei-

chen Typ sind, gibt es ein Ergebnis:

Page 144: Das Perl-Tutorial für Computerlinguisten

140

sub addiere{ my ($zahlen, $filter) = @_; my @zahlen = @{zahlen}; while(@zahlen){ my($operand_1, $operand_2) = splice(@zahlen, 0, 2); unless($filter){ my $ergebnis = $operand_1 + $operand_2; print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); next; } my $ergebnis = $operand_1 + $operand_2 if(&{$filter}($operand_1)

&& &{$filter}($operand_2)); print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); } }

Rufen wir nun addiere() mit einigen Testdaten und den verschiedenen Filtern auf, bekommen wir

die zu erwartenden Ergebnisse:

my @zahlen = qw(1 14 38 57.95 62.8237 78.1245); addiere(\@zahlen, \&gleitkommazahlen); # Ausgabe: 62.8237 + 78.1245 =

140.9482 addiere(\@zahlen, \&ganze_zahlen); # Ausgabe: 1 + 14 = 15 addiere(\@zahlen, \&alle_zahlen); addiere(\@zahlen); # Ausgabe für die beiden letzten Aufrufe: # 1 + 14 = 15 # 38 + 57.95 = 95.95 # 62.8237 + 78.1245 = 140.9482

Closures

Ein weiteres sehr nützliches Werkzeug stellen die sogenannten Closures dar. Sie erhalten ihren

Namen aus der Funktionalität, eine mit my deklarierte lexikalische Variable innerhalb eines anonymen

Subroutinenblocks vor dem Zugriff von außen zu verbergen; eine solche Variable kann nur durch den

Aufruf der Subroutine manipuliert werden. Die Variable bleibt also so lange zugreifbar, wie eine

Referenz auf die Subroutine besteht.

Illustrieren wir dies am Beispiel eines einfachen Zählers. Innerhalb der Subroutine

mache_zaehler() definieren wir eine lokale Variable $wert, die in einer anonymen Subroutine

hochgezählt wird. Der Rückgabewert der anonymen Subroutine ist gleichzeitig der Rückgabewert von

mache_zaehler():

sub mache_zaehler{ my $wert = 0; return sub{return $wert++;}; }

Um von außen auf diesem Wert operieren zu können, benötigen wir zunächst eine Referenz auf

mache_zaehler():

Page 145: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

141

my $zaehler = mache_zaehler();

Wann immer wir diese nun dereferenzieren, wird $wert um eins inkrementiert. Ansonsten gibt es

keine Möglichkeit, diese Variable zu manipulieren:

for(1 .. 5){ print &{$zaehler}."\n"; # Ausgabe: 0 1 2 3 4 }

Auch dieses Sprachkonstrukt könnte auf den ersten Blick als überflüssige Spielerei gesehen werden,

stellt aber eine wichtige Möglichkeit dar, die Integrität von Daten sicherzustellen!

9.6 Referenztypen Da Referenzen ja immer Skalare sind, sieht man nur an der Stelle, an der sie die referenzierten

Daten zugewiesen bekommen, welche Datenstruktur sie repräsentieren. Um programmatisch ermitteln

zu können, um welche Datenstruktur es sich bei einer Referenz handelt, gibt es in Perl die Funktion

ref(), deren Rückgabewert SCALAR ist, wenn es sich um einen Skalar handelt, ARRAY bei einem Array,

HASH bei einem Hash und CODE, wenn es sich um eine Subroutinenreferenz handelt:

my $skalar = \"Ich bin ein Skalar!"; my $array = [qw(Elemente eines Arrays)]; my $hash = {Schluessel => "Wert"}; my $sub = sub{return 1;}; print ref($skalar)."\n"; # Ausgabe: SCALAR print ref($array)."\n"; # Ausgabe: ARRAY print ref($hash)."\n"; # Ausgabe: HASH print ref($sub)."\n"; # Ausgabe: CODE

9.7 Zusammenfassung Anhand von Referenzen ist es uns möglich, einerseits komplexe Datenstrukturen zu erzeugen,

andererseits mehrere listenwertige Datenstrukturen an eine Subroutine zu übergeben bzw. aus dieser

zurückzugeben. Darüber hinaus können wir mit Referenzen auch auf Subroutinen verweisen, sodass

sich die Funktionalität von Subroutinen durch Callbacks erweitern lässt, während Closures die

Kapselung von Daten ermöglichen.

Da Referenzen Skalare sind, die auf den Speicherplatz der Werte einer Datenstruktur zeigen, lassen

sich mehrdimensionale Datenstrukturen erzeugen, die vorher durch die Eigenschaft der

Eindimensionalität von Listen nicht möglich gewesen wären. Solche komplexen Datenstrukturen

kommen in Perl in Form von Arrays of Arrays, Arrays of Hashes, Hashes of Arrays, Hashes of Hashes

und gemischten Strukturen vor. Grundsätzlich erzeugt man eine Referenz, indem man entweder einen

Backslash vor die zu referenzierende Datenstruktur schreibt oder indem man durch die entsprechende

Klammerung anonyme Datenstrukturen erzeugt. Wichtig ist, dass sich diese immer einem Skalar

zuweisen lassen. Um eine Referenz verwenden zu können, schreibt man das Präfixsymbol der

referenzierten Datenstruktur vor optionale geschweifte Klammern, die wiederum der Referenz

vorangehen oder man verwendet die Pfeilnotation, wenn man lediglich auf die Werte zugreift. Will

man über eine listenwertige Referenz iterieren, muss man für jede vorhandene Dimension der

Page 146: Das Perl-Tutorial für Computerlinguisten

142

Datenstruktur eine Schleife verwenden. Während man mit dem Modul Data::Dumper ein Werkzeug an

der Hand hat, um komplexe Datenstrukturen ohne Dereferenzierung auszugeben, kann man den Typ

einer Referenz vermittels der Funktion ref() ausgeben.

Aus der Eigenschaft, dass Referenzen Skalare sind, leitet sich auch ab, dass wir nun mehrere

listenwertige Argumente an Subroutinen übergeben können, die als einzelne Elemente in @_ stehen.

Umgekehrt lassen sich dementsprechend auch mehrere listenwertige Datenstrukturen zurückgeben; zu

beachten ist wiederum, dass es sich dabei um Skalare handelt.

Ebenso wie auf Datenstrukturen können wir auch Referenzen auf Subroutinen erzeugen, sodass sich

hinter der Verwendung einer Skalarvariablen kein Datenwert, sondern Funktionalität verbirgt. Eine

interessante Anwendung dessen ergibt sich in Callbacks, in denen Subroutinen selbst Argumente für

den Aufruf anderer Subroutinen sind. Dadurch lässt sich einer recht allgemein gehaltenen Subroutine

durch den Aufruf erweiterte Funktionalität mitgeben, sodass sich beispielsweise auf elegante Art Filter

implementieren lassen. Darüber hinaus eignen sich Referenzen auf Subroutinen dazu, als Closures in

ihnen als lexikalisch deklarierte Variablen vor Operationen zu verbergen; sie werden allein durch den

Aufruf der Referenz auf die Subroutine zugreifbar.

9.8 Beispielanwendung Eine Möglichkeit, unser Programm zur Zählung von n-Grammen zu erweitern, bestünde darin, die

n-Gramme in komplexen Datenstrukturen zu zählen. Allerdings verlören wir dadurch die Funktionalität

der Subroutine n_gramme_zaehlen(), da wir nicht in der Lage sind, dynamisch einen Hash of Hashes

zu erzeugen, dessen Dimensionalität unbekannt ist. Daher beschränken wir uns an dieser Stelle darauf,

an Bigrammen beispielhaft zu demonstrieren, wie man komplexe Datenstrukturen für eine solche

Teilaufgabe einsetzen könnte.

Wie gewohnt, bekommt eine Subroutine zur Extraktion von Bigrammen ein Array mit Token

übergeben, aus dem wiederum jeweils zwei Token entfernt werden. Anders als sonst werden diese

beiden Wörter allerdings nicht zusammengefügt und bilden somit den Schlüssel eines Hashes, sondern

das erste Wort ist Schlüssel des äußeren Hashes, während das zweite Wort einerseits der Wert zu

diesem Schlüssel ist, gleichzeitig aber der Schlüssel im eingebetteten Hash. Dessen Wert ist dann die

Häufigkeit des Bigramms:

sub bigramme_extrahieren{ my @token = @_; my %bigramme; while(@token > 2){ my ($eins, $zwei) = splice(@token, 0, 2); $bigramme{$eins}{$zwei}++; unshift(@token, $zwei); } return \%bigramme; }

Gibt man letztendlich den Hash of Hashes z.B. mit Data::Dumper aus, erhält man für das

Dostojevski-Korpus beispielsweise folgende Bigramme:

# Ausgabe: # $VAR1 = { # 'human # ' => { # 'blood' => 2,

Page 147: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

143

# 'motives' => 1, # 'race' => 1, # 'destiny' => 1, # 'mind' => 1, # 'being' => 2, # 'contradictions' => 1, # 'endurance' => 1, # 'heart' => 3 # }, # 'prisoner # ' => { # 'chose' => 1, # 'tells' => 1, # 'himself' => 1, # 'must' => 1 # }, # ... # };

Durch Referenzen können wir nun auch für unsere Subroutinen token_type_haeufigkeiten() und

relative_haeufigkeit_berechnen() benannte Parameter angeben. Im ersten Fall wäre dies auch ohne

dieses Sprachmittel möglich gewesen, da wir ja nur Skalare übergeben. Hier zur Illustration der Aufruf

für Tetragramme und der veränderte Ausschnitt aus der Subroutine:

token_type_haeufigkeiten( anzahl => $anzahl_tetragramme, n_gramm_types => $tetragramm_types, vokabular => $vokabular, hl => $tetragramm_hl, n_gramme => 4 ); ... sub token_type_haeufigkeiten { my %args = @_; my $anzahl_token = $args{anzahl}; my $anzahl_types = $args{n_gramm_types}; my $groesse_vokabular = $args{vokabular}; my $n_gramm_hl = $args{hl}; my $exponent = $args{n_gramme}; ... }

Um bei der Berechnung der relativen Häufigkeiten die Hashes mit den n-Grammen und ihren

Häufigkeiten als Werte des Hashes der Parameterliste zu übergeben, muss man sie zu Hashreferenzen

umwandeln:

relative_haeufigkeit_berechnen( anzahl => $anzahl_tetragramme, n_gramme => 4, n_gramm_haeufigkeit => \%tetragramm_haeufigkeit ); In der Subroutine selbst muss dieser Hash dann wiederum dereferenziert werden:

Page 148: Das Perl-Tutorial für Computerlinguisten

144

sub relative_haeufigkeit_berechnen { my %args = @_; my $i = 0; my $anzahl_n_gramme = $args{anzahl}; my $bezeichner = $args{n_gramme}; my %n_gramm_haeufigkeit = %{ $args{n_gramm_haeufigkeit} }; ... }

Page 149: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

145

10Perl-Module

Everything you need for better future and success has already been written.

And guess what? All you have to do is go to the library.

HENRI FREDERIC AMIEL

Module stellen eine wichtige Möglichkeit dar, sowohl quantitativ als auch qualitativ optimierten

Code zu schreiben. Oftmals sieht man sich mit (Teil-) Aufgaben konfrontiert, die sehr grundlegend sind

und von denen man annehmen kann, dass andere Leute bereits Lösungen dafür implementiert haben.

Dabei geht es weniger darum, eine komplett fertige Implementation an die Hand zu bekommen, die auf

das aktuelle Problem passt, als vielmehr Perl um Funktionen zu erweitern, die über (Teil-) Probleme

abstrahieren und deren Lösung somit vereinfachen. Mit der Verwendung von Modulen gehen also fol-

gende Vorteile einher:

• Wiederverwendbarkeit: Module lassen sich sowohl an andere Programmierer weitergeben als

auch zu einem eigenen "Werkzeugkasten" zusammenstellen.

• Zentralisierung: Die Pflege und Weiterentwicklung des Codes wird durch Abgrenzung von der

eigentlichen Verwendung wesentlich erleichtert.

• Abstraktion: Der Anwender bekommt nicht die Details der Implementierung zu sehen, sondern

kann den Code anhand einer Schnittstelle (API = Application Programming Interface)

benutzen.

Module bestehen aus (Perl-)54 Quellcode, dessen Datenstrukturen und Subroutinen für andere Perl-

Programme nutzbar sind. Wie Pragmata werden sie über die use-Funktion in ein Perl-Programm

eingebunden.55 Module unterscheiden sich von Pragmata allerdings darin, dass letztere keine

Funktionalität nutzbar machen, sondern den internen Übersetzungsprozess eines Programms

beeinflussen. Anders als "normale" Perl-Programme werden Module nicht mit der Dateiendung .pl,

sondern als .pm gespeichert.

10.1 Perl-Module installieren Module sind für die verschiedensten Einsatzgebiete in einer großen Zahl verfügbar. Neben der

Bereitstellung von Modulen auf den Homepages oder auf FTP-Servern ihrer Autoren finden sich die

meisten im Comprehensive Perl Archive Network (CPAN); größere Projekte legen ihren Code oft auch

auf http://www.sourceforge.net ab. Eine weitere Quelle – gerade für Perl unter Windows – stellen

die von ActiveState vorgehaltenen Module dar.

54 Manche Module sind in einer Sprache namens XS geschrieben, mit der man performanzkritische Funktionen in C

implementieren kann, diese gleichzeitg aber mit Perl verbinden kann. Die Konsequenzen eines solchen Vorgehens sollen weiter

unten diskutiert werden. 55 Daneben gibt es auch die Möglichkeit, Module mit require einzubinden. Der Unterschied zu use besteht darin, dass

require Module erst zur Laufzeit lädt, während use dies schon zur Übersetzungszeit tut. Da der Unterschied für unsere Zwecke

in den meisten Fällen irrelevant ist, empfiehlt sich die Verwendung von use.

Page 150: Das Perl-Tutorial für Computerlinguisten

146

CPAN

Das Comprehensive Perl Archive Network stellt auf einem zentralen Server, von dem es weltweit

mehrere hundert Spiegel gibt – so z.B. auf dem FTP-Server der Ruhr-Universität, die Möglichkeit

bereit, Module für den öffentlichen Gebrauch zu speichern. Dazu sind die Module nach den

Autorennamen, Modulnamen und Kategorien geordnet und als sogenannte tarballs (Archive, die mit

den Unix-Werkzeugen tar und gz komprimiert wurden, aber auch mit WinZip zu öffnen sind) zum

Download verfügbar. Allerdings müssen sie dann manuell installiert werden, was im übernächsten

Abschnitt vorgestellt werden soll.

Eine weitere Möglichkeit, auf das CPAN zuzugreifen, besteht in der Verwendung des Perl-Moduls

CPAN.pm.56 Dies funktioniert allerdings nur in Unix-Umgebungen, weshalb wir an dieser Stelle nur auf

den einschlägigen Einzeiler perl –MCPAN –e shell aufmerksam machen wollen, aus dem man dann

mit install <Modulname> das gewünschte Modul installieren kann. Dabei ist zu beachten, dass die

Modulnamen zumeist nach den Kategorien, in die sie einsortiert sind, gegliedert werden – jedes

Element eines solchen Bezeichners wird durch zwei Doppelpunkte voneinander getrennt.

PPM

Einer der großen Vorteile eines zentralisierten Archivs mit automatisierter Softwareverteilung

besteht darin, dass Abhängigkeiten zwischen Modulen automatisch aufgelöst werden können und somit

alle relevanten Module bei der Installation berücksichtigt werden. Dies ist nicht nur bei der Installation

von Perl-Modulen mit CPAN.pm der Fall, sondern auch bei der Verwendung des Programms ppm.exe,

das Bestandteil jeder ActiveState Perl-Distribution ist. Ruft man es in der Eingabeaufforderung auf,

befindet man sich in einer eigens für die Installation dieser Modul-Distribution programmierten

Kommandozeile. In ihr lassen sich Module genauso wie bei CPAN.pm mit install <Modulname>

installieren. Weitere Informationen zur Verwendung bietet die mit help aufrufbare Hilfe.

Manuelle Installation

Wenn man zum ersten Mal mit Modulen und ihren unterschiedlichen Installationsmöglichkeiten

konfrontiert wird, fragt man sich, warum es nicht eine einheitliche Lösung für diese Operation gibt. Die

Antwort liegt in der Tatsache begründet, dass nicht alle Module nur aus Perl bestehen. Oftmals werden

Funktionen in ihnen dadurch optimiert, dass sie in C geschrieben sind; eine sogenannte Glue Language

mit dem Namen XS vermittelt dann zwischen den Perl und C Funktionen. Der eigentliche Punkt ist

aber, dass solche Module durch einen C-Compiler übersetzt werden müssen, damit diese Funktionen

nutzbar werden. Unter Unix-Umgebungen ist dies meist unkritisch, da dort mit CC bzw. GCC solche

Compiler bereits vorhanden sind.

Unter Windows stellt sich die Angelegenheit allerdings ein wenig komplizierter dar: Dadurch, dass

die ActiveState-Distribution von Perl eine mit besonderen Optionen und dem Microsoft Visual Studio

6.0 bzw. Visual Studio .Net Compiler57 übersetzte Version ist, benötigt man eben diesen Compiler und

muss einiges an Vorarbeit leisten, um ein in XS vorhandenes Modul installieren zu können. Aus

diesem Grunde gibt es das bei ActiveState angesiedelte durch ppm aufrufbare Archiv von Perl-

Modulen.58

56 Bei der ersten Inbetriebnahme dieses Moduls muss es noch in einem interaktiven Prozess konfiguriert werden. 57 Die Compiler, Linker und Bibliotheken selbst sind bei Microsoft unter bestimmten Voraussetzungen kostenlos erhältlich. 58 Einige interessante Module, die aus dem Gebiet Web-Programmierung/XML-Verarbeitung stammen und zu übersetzende

Komponenten enthalten, werden nicht von ActiveState gepflegt, sondern von Randy Kobes, dessen Archiv unter

http://theoryx5.uwinnipeg.ca zugreifbar ist.

Page 151: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

147

In ihm findet sich zwar eine große Teilmenge der auf CPAN erhältlichen Module, aber nicht alle.

Sollte es also nicht möglich sein, ein Modul mit ppm zu installieren, muss man es manuell installieren.

Zu diesem Zweck benötigt man das von Microsoft kostenlos erhältliche Werkzeug nmake.59 Nachdem

man die einschlägigen Dateien, die durch Ausführen der heruntergeladenen .exe-Datei entpackt

wurden, in ein Verzeichnis des Systempfads verschoben hat, kann man sich das gewünschte Modul von

CPAN besorgen, es entpacken und dann mit folgender Prozedur installieren:

• Im Hauptverzeichnis des entpackten Moduls befindet sich eine Datei namens Makefile.PL, aus

der man mit perl Makefile.PL ein sogenanntes Makefile generiert, in dem verschiedene

Installationsparamter für das Modul festgelegt sind. Auch überprüft diese Operation, ob ggf.

weitere Module vorher installiert werden müssen, bevor die Abhängigkeiten des aktuell

vorliegenden Moduls erfüllt sind.

• Ist ein Makefile erzeugt worden, stößt man den Installationsprozess mit dem Befehl nmake60 an.

• Im nächsten Schritt überprüft man mit nmake test, ob die Funktionsweise dieses Moduls

gewährleistet ist. Dazu hat der Autor des Moduls einige Testprogramme geschrieben, die mit

vorgegebenen Daten immer ein konsistentes Ergebnis liefern.

• Sind die Tests erfolgreich gewesen, kann man das Modul mit nmake install installieren. Dazu

wird die .pm-Datei in ein ihrer Kategorie entsprechendes Verzeichnis kopiert und die

Dokumentation des Moduls im System verfügbar gemacht.

10.2 Perl-Module verwenden Perl-Module lassen sich in zwei unterschiedlichen Programmierparadigmen implementieren: So,

wie wir bis jetzt programmiert haben, nämlich prozedural, und objektorientiert. Unter objektorientierter

Programmierung versteht man eine Herangehensweise an die Problemlösung, die Objekte (der

richtigen Welt) als komplexe Datenstrukturen begreift, die bestimmte Eigenschaften besitzen und auf

denen bestimmte Methoden ausgeführt werden können. Darüber hinaus werden Objekte als konkrete

Ausprägungen von Klassen gedacht; in ihnen werden Eigenschaften und Methoden je nach Granularität

ihrer Implementierung hierarchisch eingeordnet, wobei spezifischere Klassen Eigenschaften und

Methoden aus allgemeineren Klassen erben, diese aber auch durch eigene Implementationen

überschreiben können. Für die Benutzung eines objektorientierten Perl-Moduls reicht allerdings das

Wissen aus, dass Objekte konkrete Ausprägungen von Klassen sind, die Eigenschaften besitzen und auf

denen man Methoden ausführen kann.

Aber woher weiss man, ob es sich um ein prozedurales oder objektorientiertes Modul handelt, bzw.

welche Datenstrukturen und Subroutinen verfügbar sind? Dazu lässt man sich mit perldoc

<Modulname> die Dokumentation des Moduls ausgeben, in der die komplette Programmierschnittstelle

beschrieben ist. Oftmals lässt sich schon an der kurzen Zusammenfassung am Anfang der Doku-

mentation erkennen, um welche Art von Modul es sich handelt und wie seine typische

Verwendungsweise aussieht.

Prozedurale Module

Wie bereits gesagt, wird die durch Module bereitgestellte Funktionalität durch use im Programm

verfügbar gemacht. Dadurch importieren wir bei einem im prozeduralen Stil geschriebenen Modul

diejenigen Datenstrukturen und Subroutinen, die es exportiert. Diese sind direkt im eigenen Programm

59 ftp://ftp.microsoft.com/Softlib/MSLFILES/nmake15.exe 60 Unter Unix-Umgebungen heißt der Befehl einfach make.

Page 152: Das Perl-Tutorial für Computerlinguisten

148

verwendbar. Will man nicht alle, sondern nur einige bestimmte Subroutinen importieren, kann man der

use-Funktion eine Argumentliste übergeben, in der die Bezeichner dieser Subroutinen stehen.

Zur Illustration schauen wir uns an, wie man in Perl Optionen für die Verwendung des eigenen

Programms einsetzt. Dazu gibt es zwei Module: Getopt::Std erlaubt die Eingabe sogenannter

Switches, Optionen die aus einem Minuszeichen, einem Buchstaben und einem optionalen Wert, der

durch ein Leerzeichen vom Switch getrennt wird, bestehen und anzeigen, ob ein Parameter gesetzt sein

soll oder nicht. Mit Getopt::Long lassen sich darüber hinaus Optionen angeben, die aus zwei

Minuszeichen und einem ganzen Wort sowie einem optionalen Wert, der durch ein Leerzeichen oder

ein Gleichheitszeichen von der Option getrennt wird, bestehen. Da Getopt::Long das mächtigere der

beiden Module darstellt und extrem nützlich ist, soll es an dieser Stelle detaillierter betrachtet werden.

Getopt::Long stellt die Funktion GetOptions() zur Verfügung, deren Argumentliste die Optionen

beschreibt. Eine solche Beschreibung besteht aus dem eigentlichen Bezeichner der Option und einer

Referenz auf eine mit dieser Option verbundenen Variable. Modellieren wir die Funktion addiere()

aus dem letzten Kapitel mit Getopt::Long so, dass wir sowohl die Typen der Operanden als auch deren

Werte beim Aufruf des Programms angeben können. Zunächst deklarieren wir eine Reihe von

Skalarvariablen, die definiert sind, wenn sie beim Aufruf angegeben werden oder die beim Aufruf der

Option übergebenen Werte enthalten. In der Funktion GetOptions() stehen die Optionen als benannte

Argumente: Während die Typbezeichnungen wie Switches agieren, sollen die Optionen operand1 und

operand2 auch Werte entgegennehmen. Dazu erweitert man den Optionsnamen um ein

Gleichheitszeichen, wenn es sich um ein obligatorisches Argument handelt, und um einen

Doppelpunkt, wenn das Argument fakultativ sein soll. Danach gibt man den Typ des Arguments an: Da

wir ja im Programm erst entscheiden, welchen Typ unsere Zahlen besitzen, geben wir hier ein s für

String, also eine Zeichenkette an. Alternativ kann man bei einem verbindlichen ganzzahligen Argument

iac oder i bei einem optionalen ganzzahligen Argument, f-n für ein obligatorisches

Gleitkommazahlen-Argument oder f für ein fakultatives Gleitkommazahlen-Argument angeben:61

use Getopt::Long; my @zahlen; my ($gleitkommazahlen, $ganze_zahlen, $alle_zahlen, $op1, $op2); GetOptions("gleitkomma" => \$gleitkommazahlen, "ganze" => \$ganze_zahlen, "alle" => \$alle_zahlen, "operand1=s" => \$op1, "operand2=s" => \$op2, );

In der Folge überprüfen wir zunächst, ob Operanden eingegeben wurden; ist dies nicht der Fall,

verwenden wir unsere ursprüngliche Liste als Testdaten. Danach kontrollieren wir, ob einer der

Switches gesetzt wurde, ansonsten tritt der Default-Fall ein, dass wir einfach die gegebenen Operanden

miteinander addieren:

unless($op1 && $op2){ @zahlen = qw(1 14 38 57.95 62.8237 78.1245); } else{ @zahlen = ($op1, $op2); }

61 Das i steht für das englische Wort für ganze Zahlen, nämlich integer, und das f für floating point, also Gleitkommazahlen.

Page 153: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

149

if($gleitkommazahlen){ addiere(\@zahlen, \&gleitkommazahlen); } elsif($ganze_zahlen){ addiere(\@zahlen, \&ganze_zahlen); } elsif($alle_zahlen){ addiere(\@zahlen, \&alle_zahlen); } else{ addiere(\@zahlen); }

Der Rest des Codes bleibt unverändert,

sub gleitkommazahlen{ return 1 if($_[0] =~ /\./); } sub ganze_zahlen{ return if($_[0] =~ /\./); return 1; } sub alle_zahlen{ return 1 if($_[0] =~ /\d+(?:\.\d+)*/); } sub addiere{ my ($zahlen, $filter) = @_; my @zahlen = @{zahlen}; while(@zahlen){ my($operand_1, $operand_2) = splice(@zahlen, 0, 2); unless($filter){ my $ergebnis = $operand_1 + $operand_2; print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); next; } my $ergebnis = $operand_1 + $operand_2 if(&{$filter}($operand_1)

&& &{$filter}($operand_2)); print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); } }

sodass wir beim Aufruf mit perl berechnen.pl --ganze --operand1 39 --operand2 11 die zu

erwartende Ausgabe 39 + 11 = 50 erhalten.

Objektorientierte Module

Objektorientierte Module exportieren keine Datenstrukturen oder Subroutinen, sondern machen

diese dadurch zugreifbar, dass man ein Objekt der entsprechenden Klasse erzeugt. Dazu bedient man

sich eines sogenannten Konstruktors, der – anders als in vielen anderen Programmiersprachen – keinen

festen Bezeichner hat. Da Konstruktoren in vielen Programmiersprachen allerdings new() heißen,

werden sie in Perl meist auch so benannt. Die Eigenschaften eines Objekts können in Perl auf

Page 154: Das Perl-Tutorial für Computerlinguisten

150

unterschiedliche Arten angegeben werden – häufig stehen sie als benannte Parameter im Aufruf des

Konstruktors. Das Verhalten eines Objekts beeinflusst man über Methoden; dies sind Referenzen des

Objekts auf Subroutinen im entsprechenden Modul.

Als Beispiel für ein einfaches objektorientiertes Modul betrachten wir Text::Ngrams, das die

Funktionalität bereitstellt, aus Texten wort- oder zeichenweise n-Gramme zu erzeugen und zu zählen.

Da dieses Modul als Voreinstellung zeichenweise n-Gramme extrahiert, muss man beim Aufruf den

type-Parameter mit dem Wert word belegen, um wortweise n-Gramme zu erhalten:

use Text::Ngrams; my $ng = Text::Ngrams->new(type => "word");

Voreingestellt ist auch die Größe der n-Gramme, nämlich als Trigramme; will man größere

Einheiten betrachten, spezifiziert man dies als Wert des Parameters windowsize im Konstruktor. Als

nächstes übergibt man der Methode process_text() eine Liste mit zu analysierenden Wörtern.

Alternativ dazu kennt das Modul auch eine Methode process_files(), die Dateinamen und

Referenzen auf Dateikennungen als Argumente akzeptiert. Darüber hinaus kann man mit

feed_tokens() auch einzelne sprachliche Einheiten analysieren lassen. Wie bereits gesagt, ist eine

Methode – hier process_text() – eine Referenz des Objekts – $ng – auf eine Subroutine –

process_text() aus dem Modul Text::Ngrams –, der man eine Argumentliste übergeben kann – in

diesem Fall eine Liste mit Wörtern:

my @woerter = <DATA>; $ng->process_text(@woerter);

Das Resultat der n-Gramm-Analyse steht nun in der komplexen Datenstruktur $ng. Um sie

ausgeben zu können, reicht es nicht, sie der print()-Funktion als Argument zu übergeben – wir

erhielten nur den Hinweis, dass es sich um einen Hash aus dem Modul Text::Ngrams an einer

bestimmten Speicheradresse handelt. Damit wir diese komplexe Datenstruktur nicht selbst analysieren

müssen, gibt es im Modul eine Methode namens to_string(), die eine spezielle Listenausgabe der

Daten erzeugt. Da wir daran interessiert sind, die Ergebnisse nach Häufigkeiten sortiert präsentiert zu

bekommen, geben wir dem Parameter orderby den Wert frequency:

print $ng->to_string(orderby => "frequency"); __END__ Eine Rose ist eine Rose ist eine Rose . # Ausgabe: # BEGIN OUTPUT BY Text::Ngrams version 1.1 # # 1-GRAMS (total count: 8) # ------------------------ # Rose 3 # eine 2 # ist 2 # Eine 1 #

Page 155: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

151

# 2-GRAMS (total count: 7) # ------------------------ # Rose_ist 2 # eine_Rose 2 # ist_eine 2 # Eine_Rose 1 # # 3-GRAMS (total count: 6) # ------------------------ # ist_eine_Rose 2 # Rose_ist_eine 2 # Eine_Rose_ist 1 # eine_Rose_ist 1 # # END OUTPUT BY Text::Ngrams

Setzt man den Parameter normalize auf wahr,

print $ng->to_string(orderby => "frequency", normalize => 1 );

bekommt man die relativen Häufigkeiten ausgegeben:

# Ausgabe: # BEGIN OUTPUT BY Text::Ngrams version 1.1 # # 1-GRAMS (total count: 8) # ------------------------ # Rose 0.375 # ist 0.25 # eine 0.25 # Eine 0.125 # # 2-GRAMS (total count: 7) # ------------------------ # Rose_ist 0.285714285714286 # eine_Rose 0.285714285714286 # ist_eine 0.285714285714286 # Eine_Rose 0.142857142857143 # # 3-GRAMS (total count: 6) # ------------------------ # Rose_ist_eine 0.333333333333333 # ist_eine_Rose 0.333333333333333 # eine_Rose_ist 0.166666666666667 # Eine_Rose_ist 0.166666666666667 # # END OUTPUT BY Text::Ngrams

Schon dieses einfache Beispiel zeigt, wie sich die Verwendung von Modulen positiv auf den

eigenen Quellcode auswirkt: Er ist nicht nur kompakter geworden, sondern durch die Abstraktion über

die Einzelschritte konnte die Problemlösung auch wesentlich einfacher erreicht werden.

Page 156: Das Perl-Tutorial für Computerlinguisten

152

10.3 Perl-Module selbst erstellen

Prozedurale Module

Wie wir bereits in Kapitel 5 gesehen haben, existiert Perlcode immer in einem bestimmten Paket:

Während unsere eigenen Programme bis jetzt immer dem Paket Main angehörten, finden sich Module

in einem durch die Anweisung package <Modulname>; gekennzeichneten Namensraum. Darüber

hinaus müssen Module in bestimmten Verzeichnissen stehen, die der jeweiligen Perl-Installation

bekannt sind – man findet diese im Array @INC.

Um ein eigenes prozedurales Modul zu erstellen, schreibt man dessen Namen zunächst in eine

Zeile, die mit package beginnt. Danach bindet man das Modul Exporter zur Laufzeit vermittels

require ein und teilt dem System mit, dass man Symbole exportieren will. Diese stehen dann im Array

@EXPORT:

package berechnen; use strict; require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(plus minus mal durch hoch);

Danach folgt wie gewohnt die Definition der Subroutinen:

sub plus{ $_[0] + $_[1]; } sub minus{ $_[0] - $_[1]; } sub mal{ $_[0] * $_[1]; } sub durch{ $_[0] / $_[1]; } sub hoch{ $_[0] ** $_[1]; }

In einem eigenen Programm können wir nun diese Subroutinen durch use berechnen; verwenden:

use berechnen; print plus(3, 4); # Ausgabe: 7 print hoch(3, 2); # Ausgabe: 9

Page 157: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

153

Objektorientierte Module

In objektorientierter Schreibung müssen wir wie gesagt nichts exportieren, sondern lediglich eine

Methode vorsehen, die unser Objekt erzeugt, den Konstruktor:

package Zahl; use strict; sub new{ my $class = shift; my $self = {@_}; bless($self, $class); return $self; }

Das erste Argument, das der Konstruktor entgegen nimmt, ist der Name des Moduls. Da dieser mit

dem Namen der Klasse identisch ist, benennt man den Bezeichner meist auch so. Die Parameter aus

dem Aufruf bilden den Rest der Argumentliste. Da wir diese gerne als benannte Parameter behandeln

wollen, machen wir daraus eine Hash-Referenz. Mit der neuen Funktion bless() binden wir das

Objekt an unsere Klasse und geben es im nächsten Schritt an den Ort des Aufrufs zurück. Dies ist die

eigentliche objektorientierte Funktionalität. Im folgenden müssen wir nur darauf achten, dass diese

Parameterliste das erste Argument einer jeden Subroutinen-Definition ist, sodass wir jetzt die Index-

zählung um eins erhöhen müssen, da wir ja in dieser Implementierung keine Parameter übergeben:

sub plus{ $_[1] + $_[2]; } sub minus{ $_[1] - $_[2]; } sub mal{ $_[1] * $_[2]; } sub durch{ $_[1] / $_[2]; } sub hoch{ $_[1] ** $_[2]; } 1;

In der letzten Zeile müssen wir einen wahren Wert zurückgeben, damit die Einbindung dieses

Codes nicht fehlschlägt. Wie bereits gesehen, verwenden wir unser Modul so, dass wir zunächst ein

Objekt erzeugen, auf dem wir dann die zur Berechnung definierten Methoden ausführen können:

Page 158: Das Perl-Tutorial für Computerlinguisten

154

use Zahl; my $rechner = Zahl->new(); print $rechner->mal(3, 4); # Ausgabe: 12 print $rechner->durch(3, 4); # Ausgabe: 0.75

Vererbung

Bis jetzt haben wir ein Modul, das man wohl weniger als objektorientiert, denn objektbasiert

bezeichnen würde, da wir das wohl markanteste Merkmal der Objektorientierung – nämlich Vererbung

von Eigenschaften und/oder Methoden – noch nicht implementiert haben. Betrachten wir unser Modul,

so führen wir derzeit die Grundrechenarten auf ganzen Zahlen und Dezimalzahlen aus. Eine besondere

Art solcher Zahlen stellen Brüche dar: Sie repräsentieren eine Division aus einem Zähler durch einen

Nenner, man kann auf ihnen aber auch die oben gezeigten Grundrechenarten ausführen.

Dies bedeutet für unsere Klasse entweder, dass wir die Methoden zur Berechnung auf diese

Gegebenheiten anpassen, oder dass wir diejenigen Eigenschaften und Methoden aus unserer Klasse

weiter verwenden, die auch für Brüche geeignet sind und in unserer Unterklasse die nicht geeigneten

Strukturen überschreiben.

Vergegenwärtigen wir uns zunächst noch einmal, wie man mit Brüchen rechnet. Der einfachste Fall

besteht darin, zwei Brüche miteinander zu multiplizieren, indem man die jeweiligen Werte im Zähler

und im Nenner miteinander multipliziert. Bei der Division bildet man den Kehrwert eines Bruches und

multipliziert ihn mit dem anderen. Komplizierter stellen sich die Addition und die Subtraktion dar: In

diesen beiden Fällen muss man zunächst die Nenner der beiden zu behandelnden Brüche auf einen

gemeinsamen Nenner bringen. Dazu muss aus den beiden Nennern das kleinste gemeinsame Vielfache

berechnet werden. Zwei weitere Operationen, die man auf Brüchen ausführen kann, sind das Erweitern,

wobei der Zähler und der Nenner jeweils mit demselben Faktor multipliziert werden, und das Kürzen,

bei dem Zähler und Nenner durch den eventuell vorhandenen gleichen Faktor dividiert werden. Als

kosmetische Anpassung lassen sich Brüche, deren Zähler größer als ihre Nenner sind, auf eine ganze

Zahl und einen Bruch aus Divisionsrest und Nenner normalisieren.

Um dem Perl-Interpreter mitzuteilen, dass die Klasse Bruch Eigenschaften und Methoden von der

Klasse Zahl erbt, müssen wir zuerst auf Verzeichnisebene sicherstellen, dass Bruch als Kind von Zahl

erscheint. Dazu erzeugen wir im Verzeichnis, in dem die Datei Zahl.pm abgelegt ist, ein

Unterverzeichnis namens Zahl, in das wir die Datei Bruch.pm abspeichern. Des Weiteren machen wir

in der package-Anweisung deutlich, dass Bruch von Zahl abstammt, indem wir zunächst den Namen

der Oberklasse, zwei Doppelpunkte und dann den Namen der Unterklasse schreiben:

package Zahl::Bruch;

Da Brüche wie gesagt eine Art von Zahlen sind, benötigen wir keinen eigenen Konstruktor für diese

Klasse; wir erben ihn – und alle weiteren Eigenschaften und Methoden - aus der Oberklasse, indem wir

sie einerseits mit use wie gewohnt verwenden, sie andererseits aber auch im systemeigenen Array @ISA

verfügbar machen:

use Zahl; our @ISA = qw( Zahl );

Page 159: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

155

Diese Schreibweise schränkt die Vererbungshierarchie allerdings genau auf diese eine Klasse ein;

benötigen wir weitere Oberklassen, müssen wir sie in dieser Liste angeben oder den Klassennamen auf

das Array @ISA pushen. Ein Modul, das sowohl die Einbindung der Klasse als auch das Hinzufügen zur

Vererbungshierarchie automatisiert, ist base:

use base qw( Zahl );

Betrachten wir anhand der Multiplikation von Brüchen, wie unsere Implementierung aussehen soll.

Grundsätzlich legen wir fest, dass wir die Operanden (unsere Brüche) als Zeichenketten der Form "(-

)a/b" modellieren, sodass wir sie durch einen regulären Ausdruck in ihre Komponenten zerlegen

können:

my $obj = Zahl::Bruch->new(); print $obj->multipliziere( "3/4", "3/4" );

Beim Aufruf des Konstruktors new() sucht der Perl-Interpreter zuerst in der Klasse Zahl::Bruch

nach einer entsprechenden Klassenmethode. Da diese aber nicht dort vorhanden ist, wird das Array

@ISA in einer Tiefensuche von links nach rechts durchsucht, bis die einschlägige Methode gefunden

wurde. Dies ist in diesem Fall der Konstruktor der direkten Oberklasse Zahl.

Wie bereits an den Objektmethoden der Klasse Zahl gezeigt, ist unser erstes Argument für die

Methode multipliziere() die Referenz auf das Objekt, dem die beiden Brüche folgen:

sub multipliziere { my $self = shift; my $op1 = shift; my $op2 = shift;

Da das Ergebnis der Multiplikation wiederum ein Bruch ist, vereinbaren wir an dieser Stelle zwei

Variablen, die diesen repräsentieren:

my ( $zaehler, $nenner ) = 0;

Um nun einerseits unsere Operanden in ihre Zähler und Nenner zerlegen zu können, andererseits

sicherzustellen, dass es sich bei den eingegebenen Werten um ganze Zahlen handelt und keiner der

Nenner 0 ist, parsen wir im nächsten Schritt die Operanden und überprüfen die Nenner. Dies fassen wir

als als Initialisierung auf:

my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 );

Wie oben beschrieben, gliedern wir diesen Initialisierungsschritt in die Zerlegung der Elemente und

die Validierung der Daten:

sub _init { my $op1 = shift; my $op2 = shift; my ( $zaehler1, $nenner1 ) = _parse_ausdruck($op1); my ( $zaehler2, $nenner2 ) = _parse_ausdruck($op2); croak "Nenner darf nicht 0 sein!" if ( $nenner1 == 0 || $nenner2 == 0

); return ( $zaehler1, $nenner1, $zaehler2, $nenner2 ); }

Page 160: Das Perl-Tutorial für Computerlinguisten

156

Die Parsing-Routine sieht dann so aus:

sub _parse_ausdruck { return ( $1, $2 ) if ( shift() =~ /(-?\d+)\/(\d+)/ ); croak "Kann nur mit Ziffern rechnen!\n"; }

Kehren wir nun zu unserer eigentlichen Aufgabe, der Multiplikation von Brüchen, zurück. Wie

oben bereits diskutiert, besteht diese Operation aus der einfachen Multiplikation des ersten Zählers mit

dem zweiten und des ersten Nenners mit dem zweiten, also jeweils zwei ganzen Zahlen, wie wir es aus

der Multiplikation in der Basisklasse Zahl schon kennen. Da wir als Bezeichner für unsere Methode

denselben Namen wie in der Oberklasse gewählt haben, können wir nicht mehr wie im Falle des

Konstruktors einfach auf diese Methode zugreifen, sondern müssen dem Perl-Interpreter explizit sagen,

dass wir multiplizieren() aus der Oberklasse meinen. Dazu lassen sich zwei Vorgehensweisen

denken: Einerseits könnten wir über unsere Objektreferenz explizit den Namen der Klasse angeben,

deren Methode multiplizieren() wir meinen, es gibt aber in Perl die sogenannte Pseudo-Routine

SUPER, die auf die in @ISA stehende Oberklasse verweist.62 Dieses Vorgehen ist im Sinne der

Wiederverwendbarkeit wesentlich robuster als das erstgenannte, da wir nicht von einem bestimmten

Klassennamen abhängig sind:

$zaehler = $self->SUPER::multipliziere( $zaehler1, $zaehler2 ); $nenner = $self->SUPER::multipliziere( $nenner1, $nenner2 );

Im nächsten Schritt versuchen wir, den neuen Bruch zu kürzen:

( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner );

In der Klassenmethode _kuerzen() überprüfen wir zunächst, ob die Komponenten des Bruchs

gleich sind; ist dies der Fall, haben wir eine ganze Zahl, die wir in Form des Zählers zurückgeben. Sind

Zähler und Nenner unterschiedlich, müssen wir den größten gemeinsamen Teiler (ggT) dieser beiden

Zahlen ermitteln; die Rückgabewerte sind in diesem Fall die Quotienten aus dem Zähler bzw. Nenner

und dem ggT:

sub _kuerzen { my $zaehler = shift; my $nenner = shift; return $zaehler if ( $zaehler == $nenner ); my $ggt = _ggt( $zaehler, $nenner ); return ( $zaehler / $ggt, $nenner / $ggt ); }

Um den ggT zu berechnen, verwenden wir den Algorithmus von Euklid: Solange unser

ursprünglicher Nenner ungleich 0 ist, berechnen wir den Modulo-Wert aus der Division des Zählers

durch den Nenner, wobei wir den Ursprungsvariablen die Rückgabewerte jeweils über Kreuz zuweisen.

Die Klassenmethode selbst gibt dann den vorzeichenlosen größten gemeinsamen Teiler zurück:

62 Stehen dort mehrere Oberklassen, initiiert der Perl-Interpreter eine linksseitige Tiefensuche nach der angegebenen

Methode.

Page 161: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

157

sub _ggt{ my ($x,$y)=@_; while($y!=0){ ($x,$y)=($y,$x%$y); } return abs($x); }

Im letzten Schritt überprüfen wir, ob der Zähler größer als der Nenner ist, sodass wir den Bruch zu

einer ganzen Zahl plus Bruch normalisieren können, andernfalls geben wir den neuen Bruch zurück:

if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }

In der Klassenmethode _normalisiere() gewinnen wir die ganze Zahl, indem wir zunächst den

Zaehler durch den Nenner teilen und mit der Funktion int() daraus eine Zahl ohne Nachkommastellen

machen. Den Zähler unseres neuen Bruchs berechnen wir, indem wir den Modulo-Wert aus dem Zähler

und Nenner berechnen:

sub _normalisiere { my $zaehler = shift; my $nenner = shift; my $ganze = int( $zaehler / $nenner ); my $rest = $zaehler % $nenner; return ( $ganze, $rest, $nenner ); }

Die gesamte Methode multipliziere() sieht dementsprechend so aus:

sub multipliziere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1 ) = _parse_ausdruck($op1); my ( $zaehler2, $nenner2 ) = _parse_ausdruck($op2); croak "Nenner darf nicht 0 sein!" if ( $nenner1 == 0 || $nenner2 == 0

); $zaehler = $self->SUPER::multipliziere( $zaehler1, $zaehler2 ); $nenner = $self->SUPER::multipliziere( $nenner1, $nenner2 ); ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner ); if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; }

Page 162: Das Perl-Tutorial für Computerlinguisten

158

}

Für die Methode dividiere() nutzen wir den Umstand aus, dass die Division von Brüchen durch

die Multiplikation des ersten Bruches mit dem Kehrwert des zweiten modelliert wird.

Dementsprechend verwenden wir hier die oben beschriebene Methode multipliziere():

sub dividiere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); $op2 = "$nenner2/$zaehler2"; # Kehrwert bilden multipliziere( $self, $op1, $op2 ); }

Die Addition und Subtraktion von Brüchen stellt sich wie gesagt ein wenig komplizierter dar, da

wir dafür sorgen müssen, dass die jeweiligen Nenner gleich sind. Zunächst überprüfen wir, ob die

Nenner ungleich sind; ist dies der Fall, berechnen wir das kleinste gemeinsame Vielfache (kgV), das

wir anschließend zu unserem neuen Nenner machen:63

if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv;

Zur Berechnung des kgVs nutzen wir aus, dass sich dieser Wert aus dem Produkt der beiden Nenner

durch ihren ggT berechnen lässt:

sub _kgv { my ( $x, $y ) = @_; my $kgv = ( $x * $y ) / _ggt( $x, $y ); return $kgv; }

Um nun beide Nenner auf diesen Wert erweitern zu können, berechnen wir den jeweiligen Faktor,

der sich aus der Division des kgVs durch den Nenner ergibt:

my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,

$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,

$faktor2 );

In der Klassenmethode _erweitern() multiplizieren wir lediglich den Zähler und den Nenner mit

dem vorher berechneten Faktor:

sub _erweitern { my ( $zaehler, $nenner, $faktor ) = @_; $zaehler *= $faktor; $nenner *= $faktor; return ( $zaehler, $nenner );

63 Wir verzichten an dieser Stelle der Übersichtlichkeit halber auf die Darstellung des Initialisierungsschritts, da dieser

analog zu jenen in den Methoden multipliziere() bzw. dividiere() ist.

Page 163: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

159

}

Zuletzt rufen wir zur Addition der Zähler wiederum die Methode addiere() aus der Oberklasse mit

SUPER:: auf. Hier die vollständige Methode addiere() für Brüche:

sub addiere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv; my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,

$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,

$faktor2 ); $zaehler = $self->SUPER::addiere( $zaehler1, $zaehler2 ); } else { $nenner = $nenner1; $zaehler = $self->SUPER::addiere( $zaehler1, $zaehler2 ); } ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner ); if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }

Völlig analog dazu verhält sich die Methode subtrahiere(), bei der die beiden Zähler vermittels

der subtrahiere()-Methode der Oberklasse voneinander abgezogen werden:

sub subtrahiere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv; my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,

$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,

$faktor2 ); $zaehler = $self->SUPER::subtrahiere( $zaehler1, $zaehler2 ); }

Page 164: Das Perl-Tutorial für Computerlinguisten

160

else { $nenner = $nenner1; $zaehler = $self->SUPER::subtrahiere( $zaehler1, $zaehler2 ); } ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner ); if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }

Da diese beiden Methoden eine große Menge gleichen Codes enthalten, können wir sie zu einer

Klassenmethode _strichrechnung() zusammenfassen, der wir den Aufruf für die jeweilige Rechenart

als Zeichenkette übergeben und durch eval() auswerten lassen; dadurch werden diese beiden

Methoden auf jeweils eine Zeile reduziert:

sub addiere { _strichrechnung( @_, '$zaehler=$self->SUPER::addiere($zaehler1,$zaehler2);' ); } sub subtrahiere { _strichrechnung( @_, '$zaehler=$self->SUPER::subtrahiere($zaehler1,$zaehler2);' ); }

Hier die entsprechende Klassenmethode _strichrechnung():

sub _strichrechnung { my $self = shift; my $op1 = shift; my $op2 = shift; my $methode = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv; my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,

$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,

$faktor2 ); eval($methode); } else { $nenner = $nenner1; eval($methode); } ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner );

Page 165: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

161

if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }

Literatur

Einführend: • Cozens, Simon (2000), Beginning Perl, Birmingham: Wrox (Online als PDF-Dateien unter

http://learn.perl.org/library/beginning_perl)

• Johnson, Andrew L. (2002), Einstieg in Perl, Bonn: Galileo Press

• Klier, Rainer (2002), Nitty Gritty Perl, München: Addison Wesley

• Schwartz, Randal L. und Tom Phoenix (20054), Learning Perl, Sebastopol: O'Reilly

• Ziegler, Joachim (2002), Programmieren lernen mit Perl, Berlin: Springer

Weiterführend: • Christiansen, Tom und Nathan Torkington (20032), Perl Cookbook, Sebastopol: O'Reilly

• Conway, Damian (2005), Perl Best Practices, Sebastopol: O’Reilly

• Conway, Damian (2001), Objektorientierte Programmierung mit Perl: Konzepte und

Techniken, München: Addison Wesley

• Cross, David (2001), Data Munging with Perl, Greenwich: Manning

• Friedl, Jeffrey (20032), Mastering Regular Expressions, Sebastopol: O'Reilly

• Hall, Joseph (1998), Effective Perl Programming. Writing Better Programs with Perl, Reading:

Addison Wesley

• Orwant, Jon (Hrsg.) (2002), Computer Science and Perl Programming. Best of The Perl

Journal, Sebastopol: O'Reilly

• Schwartz, Randal L. (2005), Randal Schwartz’s Perls of Wisdom, Berkeley: Apress

• Schwartz, Randal L. und Tom Phoenix (2003), Learning Perl Objects, References and

Modules. Beyond the Basics of Perl, Sebastopol: O'Reilly

• Scott, Peter (2004), Perl Medic: Transforming Legacy Code, Reading: Addison Wesley

Computerlinguistik: • Jurafsky, Daniel S. und James H. Martin (2000), Speech and Language Processing: An

Introduction to Natural Language Processing, Computational Linguistics, and Speech

Recognition. Upper Saddle River: Prentice Hall

• Klabunde, Ralf et al. (Hrsg.) (20042), Computerlinguistik und Sprachtechnologie. Eine

Einführung, München: Elsevier

• Manning, Christopher und Hinrich Schütze (2000), Foundations of Statistical Natural

Language Processing, Boston: MIT Press

Page 166: Das Perl-Tutorial für Computerlinguisten

Index

162

Index

$_, 46, 58, 76

@_, 104

@ARGV, 61

Algorithmus, 12

Anweisung, 13

Array

Datei einlesen in, 60

Elemente platzieren, 26

exists(), 41

Iteration mit while(), 45

pop(), 27

push(), 27

shift(), 27

sort(), 28

splice(), 28

unshift(), 27

Zugriff, 25

Arrays of Arrays (LoL), 131

Arrays of Hashes (LoH), 131, 135

Autovivikation, 135

Bedingungen, 37

else, 40

elsif(), 40

if(), 40

in Schleifen, 42, 43

unless(), 41

Begrenzungszeichen, 59, 75

leaning toothpick syndrome, 75

Bigramm, 33, 51, 63, 100, 112, 142

Block

Sortierung, 29

Callbacks, 139

Closures, 140

cmp, 45

Comprehensive Perl Archive Network

(CPAN), 145

Data::Dumper, 126

Datei

Ausgabe umlenken, 57

Lesen, 56

open(), 56

Schlürfmodus, 59

Schreiben, 56

Dateideskriptor, 56, 58, 60

ARGV, 60

DATA, 60

Daten, 12

Datenstrukturen, 12

Anonyme, 125

Array, 24

Arrays of Arrays (LoL), 131

Arrays of Hashes (LoH), 131, 135

Autovivikation, 135

Data::Dumper, 126

defined(), 41

exists(), 41

Gemischte, 131

Hash, 29

Hashes of Arrays (HoL), 131, 136

Hashes of Hashes (HoH), 131, 132

Komplexe, 131

Namensräume, 24

record, 131

Skalar, 13, 123

struct, 131

Datentypen, 12

Defaultvariable

$_, 46, 58

@_, 104

defined(), 41

delete(), 27, 31

die(), 58

do{}, 43

do{}...until(), 43

do{}...while(), 43

else, 40

elsif(), 40

Emacs

_emacs, 3

Ersetzen, 9

Installation, 3

kill ring, 9

Kommandos, 6

Konfiguration, 3

Modus, 5

Navigation, 8

Page 167: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

163

Perl-Code ausführen, 10

Puffer, 6, 7, 10

Referenz, 11

Suche, 9

Tastenkombinationen, 6

exists(), 41

Fehler, 58

die(), 58

Variable $!, 58

for(), 43

foreach(), 44

Funktion, 13

grep, 47

Gültigkeitsbereich, 107

Dynamischer, 107

Globaler, 107

Lexikalischer, 107

Lokaler, 107

Paket, 107

Hash, 29

delete(), 31

exists(), 41

Hashslice, 30, 31

iterieren mit foreach(), 44

keys(), 31

Schlüssel, 29

Schlüssel sind Unikate, 30

Skalarkontext, 31

sortieren, 45

Tokenhäufigkeit bestimmen, 53

Typehäufigkeit bestimmen, 53

values(), 31

Wert, 29

Zugriff, 30

Hashes of Arrays (HoL), 131, 136

Hashes of Hashes (HoH), 131, 132

if(), 40

Index, 22

input record separator

Variable $/, 60

join(), 59

keys(), 31

Kleene, 74

Kollokation, 101

Konstante, 19

Kontrollstrukturen, 37

Bedingungen, 37, 40

Bedingungen in Schleifen, 42

Block, 39, 42

do{}...until(), 43

do{}...while(), 43

else, 40

elsif(), 40

for(), 43

foreach(), 44

if(), 40

last(), 48

Laufvariable, 43, 45

next(), 49

Postfix-Schreibweise ohne Block, 40

Schleifen, 37, 42

unless(), 41

until(), 42

while(), 42, 45, 58

Korpus, 63

last(), 48

Laufvariable, 43, 45

Liste, 20

Bereich, 23

Eindimensionalität, 22, 25, 106

Index, 22

Listenelement, 20

Listenkontext, 24

Slice, 22

sort(), 28

Zugriff, 22

Literal, 13

map, 47

Module, 145

Comprehensive Perl Archive Network

(CPAN), 145

Exporter, 152

Konstruktor, 149

Manuelle Installation, 146

nmake, 147

Objektorientierte, 147, 149, 153

package, 152

perldoc, 147

PPM, 146

Prozedurale, 147, 152

XS, 146

Mustervergleich, 74

Natürliche vs. formale Sprachen, 90

next(), 49

Page 168: Das Perl-Tutorial für Computerlinguisten

Index

164

n-Gramm, 63, 72

Bigramm, 33

extrahieren mit splice(), 33

Trigramm, 33

open(), 56

Operatoren

Anführungszeichen, 21

arithmetische Operatoren, 13

Autodekrement-Operator, 18

Autoinkrement-Operator, 18

cmp, 16, 28, 45

Diamant-Operator, 58

lookahead-Operator ?=, 96

lookbehind-Operator ?<=, 96

Musterbindungsoperator, 75

numerische Vergleichsoperatoren, 14

Operatoren für Zeichenkettenvergleiche, 16

Quotemeta-Operator, 85

Raumschiff-Operator, 16, 29, 45

readline-Operator, 58

s///, 95

ternäre Vergleichsoperatoren, 16

tr///, 95

Vergleichsoperator, 75

Zeichenkettenoperatoren, 15

Zuweisungsoperator, 16

Perl

Installation, 1

pop(), 27, 45

Pragma

diagnostics, 19

locale, 80

strict, 19

Präzedenz, 14

push(), 27, 59

Raumschiff-Operator, 45

ref(), 141

Referenzen, 123

Arrayreferenz, 124, 127

Arrays of Arrays (LoL), 131

Arrays of Hashes (LoH), 131, 135

Art der Referenz, 141

Autovivikation, 135

Callbacks, 139

Closures, 140

Data::Dumper, 126

Dereferenzierung, 126

Hash, 127

Hashes of Arrays (HoL), 131, 136

Hashes of Hashes (HoH), 131, 132

Hashreferenz, 124

Indirektion, 131

Pfeilnotation, 130

record, 131

ref(), 141

Skalarreferenz, 123, 127

struct, 131

Subroutinenargumente, 137

Subroutinenreferenz, 138

Reguläre Ausdrücke, 74

Anker, 87

Backtracking, 92

Deterministischer endlicher Automat, 91

Endlicher Automat, 90

Ersetzung, 95

Funktionsweise, 90

Gier, 94

Gruppierung, 85, 86

Indexzählung bei Speicherung, 85

Komplemente von Zeichenklassen, 80

lookahead-Operator ?=, 96

lookbehind-Operator ?<=, 96

Modifikatoren, 88

Musterbindungsoperator, 75

Nichtdeterministischer endlicher Automat,

91

Platzhalter, 77

Punkt ., 80

Quantoren, 81

Speicherung, 85

Übergenerierung, 80

Variableninterpolation, 84

Vergleichsoperator, 75

Wortgrenze, 87

Zeichenklasse, 77, 79

Zeichenklasse der Leerraumzeichen, 79

Zeichenklasse der Wortzeichen, 79

Zeichenklasse der Ziffern, 79

return(), 109

Leere Argumentliste, 111

Rückgabewert, 13, 109

Hashslice, 30

Listenkontext vs. Skalarkontext, 24, 110

Listenslice, 22

Listenwertige Datenstrukturen, 137

Page 169: Das Perl-Tutorial für Computerlinguisten

Sprachwissenschaftliches Institut

165

return() (leere Argumentliste), 111

split(), 59

wantarray(), 110

Schleifen, 39, 42

do{}...until(), 43

do{}...while(), 43

Endlosschleife, 42

Endlosschleifen, 48

for(), 43

foreach(), 44

last(), 48

Laufvariable, 43, 45

next(), 49

Schleifenrumpf, 42

until(), 42

while(), 42, 45, 58

select(), 60

shift(), 27, 45

Skalar, 13

Datei einlesen in, 60

Skalarkontext, 24

Slice

Arrayslice, 25

Hashslice, 30, 31

sort(), 28, 45

Sortieren, 45

sparse data problem, 72

splice(), 28

split(), 58, 75

sprintf(), 62

Standardausgabe, 20

Standardeingabe, 20

Stoppwörter, 101

Subroutinen, 103

@_, 104

Argumente, 137

Argumentliste, 104, 106

Aufruf, 103

Benannte Parameter, 106, 143

call by reference vs. call by value, 104

Callbacks, 139

Closures, 140

Listenkontext vs. Skalarkontext, 110

return(), 109

return() (leere Argumentliste), 111

Rückgabewerte, 109, 137

wantarray(), 110

Tetragramm, 63, 101, 112

Token, 52

Häufigkeit, 34

Trennzeichen, 59

Trigramm, 33, 51, 63, 101, 112

Type, 52

unless(), 41

unshift(), 27

until(), 42

values(), 31

Variable, 16

Bezeichner, 17

Initialisierung, 16

Interpolation, 17

my, 19

Skalarvariablen, 16

Variablendefinition, 16

Variablendeklaration, 16

Verzeichnispfade, 57

wantarray(), 110

while(), 42, 45, 58

Zipf'sches Gesetz, 72