Python-Mengen: Umfangreiches Beispiel

Einfache Computerlinguistik mit Python

James Joyce analysieren

Dieses Kapitel beschäftigt sich mit natürlicher Sprache und Literatur. Es ist auch ein Beispiel von einfacher statistischer Computerlinguistik. Aber in erster Linie geht es darum ein umfangreiches Beispiel und einen interessanten Anwendungsfall für Python-Mengen bieten. Neulinge in Python denken oft, dass Mengen nur ein Spielzeug für Mathematiker sind und dass es keinen wirklichen Anwendungsfall in der Programmierung gibt. Das Gegenteil ist der Fall. Es gibt mehrere Anwendungsfälle für die Mengen also den Datentyp 'set' in der Python-Programmierung. Sie werden z. B. verwendet, um Doubletten - mehrfach vorkommende Elemente - in einer Liste zu beseitigen, d. h. um eine Liste eindeutig zu machen.

Im folgenden Beispiel werden wir den Datentyp 'set' verwenden, um die verschiedenen Wörter zu bestimmen, die in einem Roman vorkommen. Unser Anwendungsfall dreht sich um einen Roman, der von vielen gelobt und von einigen enthusiastisch als der beste Roman in englischer Sprache des 20. Jahrhunderts betrachtet wird, aber auch als der am schwersten zu lesende angesehen wird. Wir sprechen über den Roman "Ulysses" von James Joyce. Wir werden nicht über die Schönheit der Sprache oder den Sprachstil sprechen. Wir werden den Roman untersuchen, indem wir uns die im Roman verwendeten Wörter genau ansehen. Unser Ansatz wird rein statistisch sein. Die Behauptung ist, dass James Joyce in seinem Roman mehr Wörter verwendet als jeder andere Autor. Tatsächlich ist sein Wortschatz größer als der aller anderen Autoren, vielleicht sogar als der von Shakespeare.

Außer dem Roman "Ulysses" werden wir noch die folgenden Romane verwenden "Sons and Lovers" von D.H. Lawrence, "The Way of All Flesh" von Samuel Butler, "Robinson Crusoe" von Daniel Defoe, "To the Lighthouse" von Virginia Woolf, "Moby Dick" von Herman Melville and the Short Story "Metamorphosis" von Franz Kafka.

Wörter in einem Text

Um alle Wörter aus dem Roman "Ulysses" herauszuschneiden, können wir die Funktion findall aus dem Modul "re" verwenden:

In [3]:
import re

# uns ist die Groß- und Kleinschreibung egal und wir verwenden Kleinbuchstaben:
ulysses_txt = open("books/james_joyce_ulysses.txt").read().lower()

words = re.findall(r"\b[\w-]+\b", ulysses_txt)
print(f"Der Roman Ulysses enthält {len(words)} Wörter.")
Der Roman Ulysses enthält 272452 Wörter.

Diese Zahl ergibt sich aus der Summe aller Wörter sowie der vielen Wörter, die mehrfach vorkommen:

In [7]:
for word in ["the", "while", "good", "bad", "ireland", "irish"]:
    outstr = f"Das Wort '{word}' kommt {words.count(word)}"
    outstr += f" Mal in dem Roman vor!"  
    print(outstr)
Das Wort 'the' kommt 15112 Male in dem Roman vor!
Das Wort 'while' kommt 123 Male in dem Roman vor!
Das Wort 'good' kommt 321 Male in dem Roman vor!
Das Wort 'bad' kommt 90 Male in dem Roman vor!
Das Wort 'ireland' kommt 90 Male in dem Roman vor!
Das Wort 'irish' kommt 117 Male in dem Roman vor!

272452 Wörter sind sicherlich eine enorme Anzahl von Wörtern für einen Roman, aber auf der anderen Seite gibt es viele Romane mit noch mehr Wörtern. Interessanter und aussagekräftiger über die Qualität eines Romans ist die Anzahl der verschiedenen Wörter. Das ist der Moment, in dem wir endlich "Set" brauchen. Wir werden die Wortliste "words" in eine Menge verwandeln. Wenden wir "len" auf diese Menge an, erhalten wir die Anzahl der verschiedenen Wörter:

In [8]:
diff_words = set(words)
print(f"'Ulysses' enthält {len(diff_words)} verschiedene Wörter!")
'Ulysses' enthält 29422 verschiedene Wörter!

Das ist in der Tat eine beeindruckende Zahl. Das sieht man umso besser, wenn man es mit den Worthäufigkeiten der anderen unten stehenden Romane vergleicht:

In [10]:
novels = ['sons_and_lovers_lawrence.txt', 
          'metamorphosis_kafka.txt', 
          'the_way_of_all_flash_butler.txt', 
          'robinson_crusoe_defoe.txt', 
          'to_the_lighthouse_woolf.txt', 
          'james_joyce_ulysses.txt', 
          'moby_dick_melville.txt']

print(f"{'Name des Romans':38s}: Anz. verschiedene Wörter")
for novel in novels:
    txt = open("books/" + novel).read().lower()
    words = re.findall(r"\b[\w-]+\b", txt)
    diff_words = set(words)
    n = len(diff_words)
    print(f"{novel[:-4]:38s}: {n:5d}")
Name des Romans                       : Anz. verschiedene Wörter
sons_and_lovers_lawrence              : 10822
metamorphosis_kafka                   :  3027
the_way_of_all_flash_butler           : 11434
robinson_crusoe_defoe                 :  6595
to_the_lighthouse_woolf               : 11415
james_joyce_ulysses                   : 29422
moby_dick_melville                    : 18922

Besondere Wörter in Ulysses

In dem folgenden kleinen Python-Programm werden wir alle Wörter, die in den anderen Romanen vorkommen, von "Ulysses" subtrahieren. Es ist erstaunlich, wie viele Wörter von James Joyce verwendet werden, die keiner der anderen Autoren verwendet:

In [11]:
words_in_novel = {}
for novel in novels:
    txt = open("books/" + novel).read().lower()
    words = re.findall(r"\b[\w-]+\b", txt)
    words_in_novel[novel] = words
    
words_only_in_ulysses =  set(words_in_novel['james_joyce_ulysses.txt'])
novels.remove('james_joyce_ulysses.txt')
for novel in novels:
    words_only_in_ulysses -= set(words_in_novel[novel])
    
with open("books/words_only_in_ulysses.txt", "w") as fh:
    txt = " ".join(words_only_in_ulysses)
    fh.write(txt)
    
print(len(words_only_in_ulysses))
15314

Übrigens hat amerikanische Kinderbuchautor Dr. Seuss ein Buch mit nur 50 verschiedenen Wörtern geschrieben: "Green Eggs and Ham"

Die Datei mit den Wörtern, die nur in Ulysses vorkommen enthält seltsame oder selten verwendete Wörter wie:

huntingcrop tramtrack pappin kithogue pennyweight undergarments scission nagyaságos wheedling begad dogwhip hawthornden turnbull calumet covey repudiated pendennis waistcoatpocket nostrum

Gemeinsame Wörter

Es ist auch möglich, die Wörter zu finden, die in jedem Buch vorkommen. Dazu benötigen wir die Schnittmenge:

In [12]:
# Wir starten mit den Wörtern in ulysses
common_words = set(words_in_novel['james_joyce_ulysses.txt'])
for novel in novels:
    common_words &= set(words_in_novel[novel])
    
print(len(common_words))
1745

Richtig machen

Bei den vorherigen Berechnungen ist uns ein kleiner Fehler unterlaufen. Wenn Sie sich die Texte ansehen, werden Sie feststellen, dass sie einen Kopf- und Fußteil haben, der von Project Gutenberg hinzugefügt wurde und nicht zu den Texten gehört. Die Texte sind zwischen den Zeilen positioniert:

START OF THE PROJECT GUTENBERG EBOOK THE WAY OF ALL FLESH

und

END OF THE PROJECT GUTENBERG EBOOK THE WAY OF ALL FLESH

oder

START OF THIS PROJECT GUTENBERG EBOOK ULYSSES

und

END OF THIS PROJECT GUTENBERG EBOOK ULYSSES

Die Funktion read_text nimmt sich dieses Problemes an, d.h. sie schneidet die Textblöcke aus:

In [14]:
def read_text(fname):
    beg_e = re.compile(r"\*\*\* ?start of (this|the) project gutenberg ebook[^*]*\*\*\*")
    end_e = re.compile(r"\*\*\* ?end of (this|the) project gutenberg ebook[^*]*\*\*\*")
    txt = open("books/" + fname).read().lower()
    beg = beg_e.search(txt).end()
    end = end_e.search(txt).start()
    return txt[beg:end]

words_in_novel = {}
for novel in novels + ['james_joyce_ulysses.txt']:
    txt = read_text(novel)
    words = re.findall(r"\b[\w-]+\b", txt)
    words_in_novel[novel] = words

words_in_ulysses =  set(words_in_novel['james_joyce_ulysses.txt'])
for novel in novels:
    words_in_ulysses -= set(words_in_novel[novel])
    
with open("books/words_in_ulysses.txt", "w") as fh:
    txt = " ".join(words_in_ulysses)
    fh.write(txt)
    
print(len(words_in_ulysses))


# we start with the words in ulysses
common_words = set(words_in_novel['james_joyce_ulysses.txt'])
for novel in novels:
    common_words &= set(words_in_novel[novel])
    
print(len(common_words))
15341
1279

Die Wörter der Menge "common_words" sind Wörter, die zu den am häufigsten verwendeten Wörtern der englischen Sprache gehören. Schauen wir uns 30 beliebige Wörter aus dieser Menge an:

In [15]:
counter = 0
for word in common_words:
    print(word, end=", ")
    counter += 1
    if counter == 30:
        break
beginning, thanks, sense, sick, ceiling, around, out, directly, all, everything, breathing, pity, startled, strict, morning, quite, rather, line, turning, direct, losing, think, instead, lay, various, begin, handle, for, set, possible,