Text Analyse (Kritik erwünscht)

Code-Stücke können hier veröffentlicht werden.
Antworten
codecode123
User
Beiträge: 2
Registriert: Mittwoch 4. April 2018, 16:57

Hallo zusammen :wink: ,
ich habe vor kurzem begonnen mir Python beizubringen. Ich habe im Laufe der letzten Jahre schon verschiedenen Programmiersprachen (kennen)gelernt (eigentlich hauptsächlich die Basics), also bin ich kein kompletter Neueinsteiger. Jedoch ist das letzte (kleine) Programm was ich geschrieben mindestens ein Jahr her.
Also habe ich mich mal hingesetzt und zur Übung ein bisschen was gebastelt.
Ich freue mich über Kritik und Verbesserungsvorschläge, vorallem geht es mir darum ob der Code "sauber" geschrieben ist, oder wenn nicht, in welchen Punkten es elegantere Lösungen gibt.
P.S. Um einer Anmerkung zuvor zu kommen, ich habe teilweise "überflüssige" while True Schleifen eingebaut um den Code im PyCharm Editor einklappen zu können (geht das auch anders ?)
Viele Grüße

Code: Alles auswählen

# -*- coding: UTF-8 -*-
# Name:         Textalyzer
# Autor:        codecode123
# Version:      1.0 03.04.2018
# Beschreibung: Textanalysewerkzeug, überprüft einen Text nach der Häufigkeit aller vorkommender Buchstaben (in Arbeit) und Wörter
#               und gibt eine umfangreiche Statistik aus.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Todo
# ...
#  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# BUGS:
#  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #


# Überprüft ob ein Text "text" nur gültige Zeichen (Standard: Alphabet, wahlweise 0-9 und 'Leerzeichen') enthält
def enthaelt_sonderzeichen(text, leerzeichenok=False, zahlen_ok=False):
    gueltig = "abcdefghijklmnopqrstuvwxyzßäöüABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜ"
    if leerzeichenok is True:
        gueltig += " "
    if zahlen_ok is True:
        gueltig += "0123456789"
    if all(c in gueltig for c in text):
        return False
    else:
        return True


# Entfernt Sonderzeichen vom Textanfang /-ende
def entferne_sonderzeichen(text):
    while True:
        if len(text) == 1:
            break
        # Sonderzeichen am ENDE
        if enthaelt_sonderzeichen(text[len(text) - 1]) is True:
            text = text[:(len(text) - 1)]
        # Sonderzeichen am ANFANG
        elif enthaelt_sonderzeichen(text[0]) is True:
            text = text[1:]
        else:
            break
    return text


# Sortiert ein Dictonarie nach den Value Werten!
def dictsort(dict_in, reverse_=True):
    sort = []
    for v in dict_in:
        sort.append([dict_in[v], v])
    sort.sort(key=None, reverse=reverse_)
    return sort


# Ähnlich input(), zusätzlich optionale Gültigkeitsüberprüfung mit optionaler manueller Fehlermeldung
def iinput(mystr="?:", gueltig=(), err="Ungültige Eingabe!"):
    if gueltig == ():
        return input(mystr)
    else:
        while True:
            tmp = input(mystr)
            if tmp in gueltig:
                break
            else:
                print(err, )
        return tmp


# Variablen festlegen
while True:
    Pos = 0
    Dict = {}
    WortFilter = [""]
    Zaehler = 0
    Wortliste = []
    break

# I: Abfrage: Texteingabe oder Text aus Datei einlesen.
TextEingabe = iinput("Text eingeben (1) oder Text aus Datei einlesen (2)? ", ("1", "2"))

# II: Auswertung: Texteingabe oder Text aus Datei einlesen.
while True:
    # (2) String aus Datei einlesen
    if int(TextEingabe) == 2:
        DateiPfad = input("Dateinamen bzw. Pfad eingeben: ")
        try:
            File = open(DateiPfad, "r")
            Text = File.read()
            File.close()
            break
        except FileNotFoundError:
            print("Datei nicht gefunden!")
    # (1) String manuell eingeben
    else:
        Text = input("Bitte zu analysierenden Text eingeben: ")
        # Mindestlänge 2 Zeichen
        if len(Text) < 2:
            print("Text zu kurz!")
        else:
            break

# III: Wortliste generieren
while Pos < len(Text):
    # Suche nach Leerzeichen
    Fund = Text.find(" ", Pos)

    # Kein Leerzeichen-Fund =>
    if Fund == -1:
        # Kein (weiterer) Fund, markiere letztes Wort
        Wortliste.append(Text[Pos:])
        Fund = len(Text) - 1

    # Leerzeichen-Fund =>
    else:
        Wortliste.append(Text[Pos:Fund])

    # Position verschieben UND Zaehler hochsetzen
    Pos = Fund + 1
    Zaehler += 1

# IV: Wortliste modizizieren und filtern (map & filter)
while True:
    # Alle Wörter kürzer als 2 Zeichen entfernen
    Wortliste = list(filter(lambda x: len(x) > 1, Wortliste))

    # Alle Sonderzeichen vom Wortanfang / -ende entfernen
    Wortliste = list(map(entferne_sonderzeichen, Wortliste))

    # Alle Wörter mit innenliegenden Sonderzeichen entfernen
    Wortliste = list(filter(lambda x: enthaelt_sonderzeichen(x) is False, Wortliste))

    # Alle Wörter klein schreiben
    Wortliste = list(map(lambda x: x.lower(), Wortliste))

    # Wörter aus dem Wortfilter entfernen
    Wortliste = list(filter(lambda x: x not in WortFilter, Wortliste))
    break

# V: Wörter aus Wortliste in Dictonarie überführen und zählen
while not Wortliste == []:
    Wort = Wortliste.pop()

    if Wort in Dict:
        # Counter hochsetzen
        Dict[Wort] += 1
    else:
        # Neues Wort! Counter 1...
        Dict[Wort] = 1

# VI: Ausgabe
while True:
    # Wörter sortiert nach Häufigkeit ausgeben und Prozentualer Anteil am Zaehler
    for i in dictsort(Dict):
        Leerstellen = (20 - len(i[1])) * " "
        Leerstellen_2 = int((4 - len(str(i[0])))) * " "
        print(i[1], Leerstellen, i[0], Leerstellen_2, round((i[0] / Zaehler) * 100, 1), "%")

    print("Anzahl verschiedener Wörter im Text: ", len(Dict))
    print("Anzahl aller Wörter im Text: ", Zaehler)
    print("\nHinweis: Die Anzahl der Wörter betrifft nur solche die als gültig erkannt wurden.")
    print("\nWörter welche Sonderzeichen oder Zahlen enthalten werden nicht ausgewertet!")
    break
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@codecode123: hier meine Kritik.

Die Kommentare vor den Funktionsdefinitionen sind eigentlich Doc-Strings.

Man prüft nicht explizit auf True, weil das ja auch nur wieder einen Wahrheitswert gibt. Wenn man einen if-Block hat, wo das Ergebnis ein Wahrheitswert ist und der else-Block das negative davon, dann kann man auch gleich die Bedingung als Wahrheitswert benutzen.

Code: Alles auswählen

def enthaelt_sonderzeichen(text, leerzeichenok=False, zahlen_ok=False):
    """ Überprüft ob ein Text "text" nur gültige Zeichen (Standard: Alphabet,
    wahlweise 0-9 und 'Leerzeichen') enthält """
    gueltig = "abcdefghijklmnopqrstuvwxyzßäöüABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜ"
    if leerzeichenok:
        gueltig += " "
    if zahlen_ok:
        gueltig += "0123456789"
    return not all(c in gueltig for c in text)
Warum entfernt `entferne_sonderzeichen` kein Sonderzeichen, wenn der String nur aus einem Sonderzeichen besteht? Wenn man Indizes vom Ende her will, braucht man kein len, man kann einfach negative Indizes nehmen: `text[-1]` fürs letze Zeichen. Besser zwei while-Schleifen:

Code: Alles auswählen

def entferne_sonderzeichen(text):
    """ Entfernt Sonderzeichen vom Textanfang /-ende """
    # Sonderzeichen am ENDE
    while enthaelt_sonderzeichen(text[-1]):
        text = text[:-1]
    # Sonderzeichen am ANFANG
    while enthaelt_sonderzeichen(text[0]):
        text = text[1:]
    return text
Dass `dictsort` nach Werten sortiert, sollte im Funktionsnamen stehen, weil das ja überraschen ist. `reverse` ist kein Schlüsselwort, der Unterstrich also überflüssig. Eine Liste `sort` zu nennen, ist komisch, denn sie enthält (aber erst zum Schluß) sortierte Werte, sortiert also selbst nichts. key=None ist der Defaultwert, braucht also nicht angegeben zu werden. Wenn man sowohl Keys als auch Values will, benutzt man dict.items().

Code: Alles auswählen

def sort_values(dict, reverse=True):
    """ Sortiert ein Dictonarie nach den Value Werten! """
    result = [[v,k] for k, v in dict.items()]
    result.sort(reverse=reverse)
    return result
Was soll das `i` bei `iinput`, ich würde das nach seiner Funktion benennen, `validating_input`. Ein nicht-Wert ist None und nicht (). `mystr` wird `prompt` genannt. (`my` ist so ein nichtssagendes Präfix). `tmp` ist eigentlich für unbenutzte Variablen reserviert, Du benutzt es aber als Ergebnis.

Code: Alles auswählen

def validating_input(prompt="?:", gueltig=None, err="Ungültige Eingabe!"):
    """ Ähnlich input(), zusätzlich optionale Gültigkeitsüberprüfung
    mit optionaler manueller Fehlermeldung """
    while True:
        result = input(prompt)
        if not gueltig or result in gueltig:
            break
        print(err, )
    return result
Statt `while True` wäre wohl `if True` besser, das ist aber auch Quatsch. Wenn es Blöcke gibt, die Du "einklappen" willst, dann sollten das Funktionen sein.

Variablen werden grundsätzlich komplett klein geschrieben.

Bei II ist die while-Schleife und das if logisch falsch herum, weil sie die if-Bedingung nicht innerhalb der while-Schleife ändert. Dateien öffnet man am besten mit dem with-Statement. I und II sollten zusammen ein Funktion ergeben.

Code: Alles auswählen

def read_text():
    selection = validating_input("Text eingeben (1) oder Text aus Datei einlesen (2)? ", ("1", "2"))
    if selection == "2":
        while True:
            filename = input("Dateinamen bzw. Pfad eingeben: ")
            try:
                with open(filename) as text:
                    return text.read()
            except FileNotFoundError:
                print("Datei nicht gefunden!")
    else:
        while True:
            text = input("Bitte zu analysierenden Text eingeben: ")
            # Mindestlänge 2 Zeichen
            if len(text) < 2:
                print("Text zu kurz!")
            else:
                break
        return text
Variablen sollte man dann initalisieren, wenn sie auch gebraucht werden. `Pos`, `Zaehler` und `Wortliste` werden erst bei III gebraucht `Dict` sogar erst bei V gebraucht. `Zaehler` muß nicht gezählt werden, denn er entspricht der Länger der Liste `Wortliste`. Was Du hier so kompliziert per while-Schleife geschrieben hast ist einfach:

Code: Alles auswählen

Wortliste = Text.split()
Zu IV: statt immer wieder eine neue Liste zu erzeugen, würde ich die Umwandlung in einer Schleife machen.
[codebox=python file=Unbenannt.txt]
def cleanup_words(words):
result = []
for word in words:
# Alle Wörter kürzer als 2 Zeichen entfernen
if len(word) > 1:
# Alle Sonderzeichen vom Wortanfang / -ende entfernen und klein schreiben
word = entferne_sonderzeichen(word).lower()
# Alle Wörter mit innenliegenden Sonderzeichen und verbotene Wörter entfernen
if not enthaelt_sonderzeichen(word) and word not in FILTERED_WORDS:
result.append(word)
return result
[/code]

Zu V: die Liste solltest Du nicht ändern, sondern einfach mit einer for-Schleife durchgehen. Dafür gibt es aber auch schon was fertiges: collections.Counter

Zu VI: bei `i` erwartet man eine einfache Zahl, nicht eine Liste mit 2 Werten. Zwei Werte entpackt man am besten gleich in der for-Anweisung. Statt der seltsamen print-Anweisung mit Leerstellen nimmt man String-Formatierung.

Code: Alles auswählen

def output_words(counted_words, total_amount):
    """ Wörter sortiert nach Häufigkeit ausgeben und
    Prozentualer Anteil am Zaehler """
    for word, count in counted_words.most_common():
        print("{:5d} {:20s} {:4.1%}".format(count, word, count / total_amount))
    print("Anzahl verschiedener Wörter im Text: ", len(counted_words))
    print("Anzahl aller Wörter im Text: ", total_amount)
    print("\nHinweis: Die Anzahl der Wörter betrifft nur solche die als gültig erkannt wurden.")
    print("\nWörter welche Sonderzeichen oder Zahlen enthalten werden nicht ausgewertet!")
Fehlt noch das Hauptprogramm:

Code: Alles auswählen

from collections import Counter
FILTERED_WORDS = [""]

# hier kommen die ganzen Funktionen von oben hin

def main():
    words = read_text().split()
    number_of_words = len(words)
    words = cleanup_words(words)
    counted_words = Counter(words)
    output_words(counted_words, number_of_words)

if __name__ == '__main__':
    main()
codecode123
User
Beiträge: 2
Registriert: Mittwoch 4. April 2018, 16:57

Hallo vielen Dank erstmal für die umfangreiche Kritik. :!:
Ich kann soweit alle Punkte gut nachvollziehen und habe den Code entsprechend angepasst.
Einige Dinge sind mir selbst noch aufgefallen.


1. Funktion enthaelt_sonderzeichen()
Hier habe ich in bestimmten Fällen (z.B. text = 1 [Integer]) einen TypeError: 'int' object is not iterable erhalten.
Die Methode all() sieht jetzt so aus:

Code: Alles auswählen

 return not all(c in gueltig for c in str(text))
Dadurch konnte ich den Fehler beheben.


2. Funktion entferne_sonderzeichen()
Bei einem text ohne inhalt gibt es einen IndexError: string index out of range.
Ich habe die while Schleifen in eine try/except Anweisung gepackt:

Code: Alles auswählen

def entferne_sonderzeichen(text):
    """ Entfernt Sonderzeichen vom Textanfang /-ende """
    try:
        # Sonderzeichen am ENDE
        while enthaelt_sonderzeichen(text[-1]):
            text = text[:-1]
        # Sonderzeichen am ANFANG
        while enthaelt_sonderzeichen(text[0]):
            text = text[1:]
    except IndexError:
        return text
    return text

3. Worthäufigkeit
Die Methode most_common des Moduls collections kannte ich bisher nicht, ist aber genau das was ich brauche.


4. Einige Fragen
  • I. Alle Variablen sollten klein geschrieben werden, das habe ich verstanden. Liege ich damit richtig, dass FILTERED_WORDS ausschließlich groß geschrieben ist um die Aufmerksamkeit darauf zu lenken? Oder steckt da ein anderer (tieferer) Sinn hinter?
  • II.

    Code: Alles auswählen

    if __name__ == '__main__':
        main()
    Was genau bedeutet die Variable __name__ ? Ist das so etwas wie der Name des Fensters in dem das Programm läuft? Was bewirkt diese Abfrage?
  • III.

    Code: Alles auswählen

    print("{:5} {:20s} {:4.1%}".format(count, word, count / total_amount))
    Ist vielleicht nicht kriegsentscheidend, aber muss das d aufgeführt werden? Ist die dezimalschreibweise nicht standard? Oder macht man das der Vollständigkeit halber? UND Was bedeutet das s in

    Code: Alles auswählen

    {:20s}

Zum Schluss nochmal der vollständige neue Programmcode:

Code: Alles auswählen

# -*- coding: UTF-8 -*-
# Name:         Textalyzer
# Autor:        ...
# Version:      1.1 06.04.2018
# Beschreibung: Textanalysewerkzeug, überprüft einen Text nach der Häufigkeit aller vorkommender Buchstaben und Wörter
#               und gibt eine umfangreiche Statistik aus.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Todo
#  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# BUGS:
#  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #


from collections import Counter
FILTERED_WORDS = [""]


def enthaelt_sonderzeichen(text, leerzeichen_ok=False, zahlen_ok=False):
    """
    Überprüft ob ein Text 'text' nur gültige Zeichen (Standard: Alphabet,
    wahlweise 0-9 und 'Leerzeichen') enthält
    """
    gueltig = "abcdefghijklmnopqrstuvwxyzßäöüABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜ"
    if leerzeichen_ok:
        gueltig += " "
    if zahlen_ok:
        gueltig += "0123456789"
    return not all(c in gueltig for c in str(text))


def entferne_sonderzeichen(text):
    """ Entfernt Sonderzeichen vom Textanfang /-ende """
    try:
        # Sonderzeichen am ENDE
        while enthaelt_sonderzeichen(text[-1]):
            text = text[:-1]
        # Sonderzeichen am ANFANG
        while enthaelt_sonderzeichen(text[0]):
            text = text[1:]
    except IndexError:
        return text
    return text


def dictsort_values(mydict, reverse=True):
    """ Sortiert ein Dictonarie entsprechend seiner 'Value' Werte """
    result = [[v, k] for k, v in mydict.items()]
    result.sort(reverse=reverse)
    return result


def validating_input(prompt="?:", gueltig=None, err="Ungültige Eingabe!"):
    """ Ähnlich input(), zusätzlich optionale Gültigkeitsüberprüfung
   mit optionaler manueller Fehlermeldung """
    while True:
        result = input(prompt)
        if not gueltig or result in gueltig:
            break
        print(err, )
    return result


def read_text():
    """ List die Benutzereingabe endweder direkt oder aus einer Textdatei """
    selection = validating_input("Text eingeben (1) oder Text aus Datei einlesen (2)? ", ("1", "2"))
    if selection == "2":
        while True:
            filename = input("Dateinamen bzw. Pfad eingeben: ")
            try:
                with open(filename) as text:
                    return text.read()
            except FileNotFoundError:
                print("Datei nicht gefunden!")
    else:
        while True:
            text = input("Bitte zu analysierenden Text eingeben: ")
            # Mindestlänge 2 Zeichen
            if len(text) < 2:
                print("Text zu kurz!")
            else:
                break
        return text


def cleanup_words(words):
    """  """
    result = []
    for word in words:
        # Alle Wörter kürzer als 2 Zeichen entfernen
        if len(word) > 1:
            # Alle Sonderzeichen vom Wortanfang / -ende entfernen und klein schreiben
            word = entferne_sonderzeichen(word).lower()
            # Alle Wörter mit innenliegenden Sonderzeichen und verbotene Wörter entfernen
            if not enthaelt_sonderzeichen(word) and word not in FILTERED_WORDS:
                result.append(word)
    return result


def output_words(counted_words, total_amount):
    """ Wörter sortiert nach Häufigkeit ausgeben und
   Prozentualer Anteil am Zaehler """
    for word, count in counted_words.most_common():
        print("{:5} {:20s} {:4.1%}".format(count, word, count / total_amount))
    print("Anzahl verschiedener Wörter im Text: ", len(counted_words))
    print("Anzahl aller Wörter im Text: ", total_amount)
    print("\nHinweis: Die Anzahl der Wörter betrifft nur solche die als gültig erkannt wurden.")
    print("\nWörter welche Sonderzeichen oder Zahlen enthalten werden nicht ausgewertet!")


def main():
    words = read_text().split()
    number_of_words = len(words)
    words = cleanup_words(words)
    counted_words = Counter(words)
    output_words(counted_words, number_of_words)


if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Zu 1.: warum sollte enthaelt_sonderzeichen mit einer Zahl gefüttert werden? Woher kommt die?
Zu 2.: besser dem Fall erst gar nicht auftreten lassen:

Code: Alles auswählen

def entferne_sonderzeichen(text):
    """ Entfernt Sonderzeichen vom Textanfang /-ende """
    # Sonderzeichen am ENDE
    while enthaelt_sonderzeichen(text[-1:]):
        text = text[:-1]
    # Sonderzeichen am ANFANG
    while enthaelt_sonderzeichen(text[:1]):
        text = text[1:]
    return text
Zu 4.: Konstanten werden komplett groß geschrieben, um sie von Variablen unterscheiden zu können
In __name__ ist der Name des aktuellen Moduls, der, wenn man die Datei als das Programm und nicht als Modul benutzt den Wert '__main__' enthält. Diese if-Abfrage dient also dazu, main() nur aufzurufen, wenn die Datei als Programm benutzt wird.
Ja Format-Strings sollten selbsterklärend sein, ohne das `d` ist die Formatangabe vom Typ abhängig, also 'd' für int, 's' für String usw.


`dictsort_values` wird nicht mehr benötigt.
Antworten