Eine Fallstudie

Zu dieser Fallstudie werden wir das Wörterzählprogramm erweitern, das wir schon früher entwickelt haben. Wir werden ein Programm erzeugen, welches das Unix wc - Programm nachahmt, indem es die Anzahl der Zeilen, Wörter und Schriftzeichen einer Datei ausgibt. Wir werden noch weiter als das gehen und auch die Anzahl der Sätze, Nebensätze, Wörter, Buchstaben und Satzzeichen in einer Textdatei ermitteln. Wir werden der Entwicklung dieses Programmes Schritt für Schritt folgen, wenn wir seine Fähigkeiten stufenweise erhöhen, es dann in ein Modul überführen, um es wiederverwendbar und endlich in eine OO-Implementation mit maximalen Erweiterungsmöglichkeiten umzuwandeln.

Es wird eine Python-Implementation werden, aber die Hauptschritte könnten stattdessen genausogut in BASIC oder Tcl geschrieben werden. Wenn wir in die komplexeren Teile hineinkommen und einen verstärkten Gebrauch von Python's eingebauten Datenstrukturen machen werden, wir die Verwendung von BASIC sehr schwierig werden, Tcl wird hingegen immer noch als Option bleiben. Zum Schluss können die OO-Aspekte nur noch auf Python angewendet werden.

Zusätzliche einzubauende Eigenschaften, die als Übungen für den Leser übrig bleiben, sind:

Zählen von Zeilen, Wörtern und Schriftzeichen

Schauen wir uns noch einmal den vorherigen Wortzähler an:

import string
def numwords(s):
    list = string.split(s)
    return len(list)

inp = open("menu.txt","r")
total = 0

# aufsummieren zur Gesamtsumme aus jeder Zeile
for line in inp.readlines():
    total = total + numwords(line)
print "File had %d words" % total

inp.close()

Wir wollen jetzt einen Zeilen- und Zeichen-Zähler hinzufügen. Der Zeilenzähler ist einfach, da wir eine Schleife für jede Zeile durchlaufen, benötigen wir nur eine Variable bei jeder Iteration der Schleife zu Inkrementieren. Der Zeichenzähler ist hingegen bedeutend schwieriger, da wir hierbei über die Wörterliste iterieren und jeweils ihre Länge in noch einer anderen Variable addieren müssen.

Wir müssen das Programm auch allgemeiner anwendbar machen, indem wir den Filenamen von der Kommandozeile einlesen oder falls das nicht erwünscht ist, den Anwender zur Namenseingabe auffordern. (Eine alternative Strategie wäre das Einlesen von der Standardeingabe, was das wirkliche wc tut.)

Somit sieht wc endlich so aus:

import sys, string

# Holt den Dateinamen entweder von der Kommandozeile oder vom Anwender
if len(sys.argv) != 2:
   name = raw_input("Enter the file name: ")
else:
   name = sys.argv[1]
 
inp = open(name,"r")

# initialisiert Die Zähle auf Null; erzeugt auch Variablen
words = 0
lines = 0
chars = 0

for line in inp.readlines():
    lines = lines + 1

# Zerteilt in eine wörterliste und zählt sie
list = string.split(line)
words = words + len(list)
chars = len(line)# Verwendung der Originalzeilenlänge, die auch Leerzeichen etc. beinhaltet.
print "%s has %d lines, %d words and %d characters" % (name, lines, words, chars)
inp.close()

Falls du mit dem Unix wc -Befehl vertraut bist, weisst du, dass du ihm einen Filenamen als Wildcard übergeben kannst, um eine Statistik für alle passenden Programme, aber auch um ein Gesamtergebnis zu erhalten. Dieses Programm kümmert sich nur um direkt eingegebene Filenamen. Falls du es erweitern möchtest, dass es sich auch um Wildcards kümmert, betrachte das glob-Modul und erzeuge dir eine Namensliste, um dann einfach über die File-Liste zu iterieren. Du wirst temporäre Zähler für jede Datei benötigen und dann einen kumulativen Zähler für das Gesamtergebnis. Oder du könntest stattdessen ein Dictionary verwenden...

Zählen von Sätzen anstatt von Zeilen

Als ich darüber nachzudenken begann, wie ich dies um das Zählen von Sätzen und Wörtern erweitern könnte, um nicht nur wie oben "Character-Gruppen" zu zählen, war meine zündende Idee, zuerst eine Schleife durch die Datei laufen zu lassen, um die Zeilen in eine Liste zu extrahieren und dann eine Schleife über jede Zeile zu erzeugen, um die Wörter in eine andere Liste zu überführen. Schließlich soll jedes Wort bearbeitet werden, um zusätzliche Zeichen zu entfernen.

Denken wir ein bischen weiter, so wird es von Bedeutung sein,dass, wenn wir einfach Wörter und Satzzeichen sammeln, das wir die letzteren analysieren können, um damit Sätze und Nebensätze usw. zu zählen (wobei zu definieren ist was wir unter Satz und Nebensatz bezüglich der Satzzeichen verstehen). Das bedeutet, wir müssen nur einmal über die Datei iterieren und dann über die Satzzeichen iterieren - eine viel kleinere Liste. Wir wollen dies in Pseudo-Code skizzieren:

foreach line in file:
   increment line count
   if line empty:
      increment paragraph count
   split line into character groups

foreach character group:
   increment group count
   extract punctuation chars into a dictionary - {char:count}
   if no chars left:
      delete group
   else: increment word count
 
sentence count = sum of('.', '?', '!')
clause count = sum of all punctuation (very poor definition...)

report paras, lines, sentences, clauses, groups, words.
foreach puntuation char:
   report count

Das sieht so so aus, dass wir 4 Funktionen erzeugen können, wenn wir die obige Gruppierung verwenden möchten. Dies hilft uns ein Modul zu erstellen, das wir entweder teilweise oder vollständig wiederverwenden können.

Umwandlung in ein Modul

Die Schlüsselfunktionen sind: getCharGroups(infile), und getPunctuation(wordList). Schauen wir, was herauskommt, wenn wir den Pseudo-Code verwenden...


#############################
# Module: grammar
# Created: A.J. Gauld, 2000,8,12
# 
# Funktion:
# zählt Abschnitte, Zeilen, Sätze, Nebensätze, Zeichengruppen,
# Wörter und Satzzeichen einer Prosa-Textdatei. Es nimmt an,
# dass Sätze mit [.!?] enden und Abschnitte eine Leerzeile zwischen 
# sich haben. Ein 'Nebensatz' ist einfach ein Satzsegment, abgetrennt
# durch ein Satzzeichen ( schwachsinnig, aber irgendwann machen 
# wir es vielleicht besser!)
#
# Anwendung: Die Grundanwendung nimmt den Dateinamenparameter und gibt die
#            ganze Statistik aus. Es ist wirklich beabsichtigt, dass ein zweites
#            Modul mit solchen Funktionen ausgestattet ist, die Nützlicheres zulassen.
#############################
import string, sys

############################
# initilisiere globale Variablen
para_count = 1
line_count, sentence_count, clause_count, word_count = 0,0,0,0
groups = []
alphas = string.letters + string.digits
stop_tokens = ['.','?','!']
punctuation_chars = ['&','(',')','-',';',':',','] + stop_tokens
for c in punctuation_chars:
    punctuation_counts[c] = 0
punctuation_counts = {}
format = """%s enthält:
%d Abschnitte, %d Zeilen und %d Sätze.
Diese enthalten der Reihe nach %d Nebensätze und eine Gesamtzahl von %d Wörtern.""" 



############################
# Jetzt werden die Funktionen definiert, die die Arbeit leisten

def getCharGroups(infile):
    pass

def getPunctuation(wordList):
    pass
    
def reportStats():
    print format % (sys.argv[1],para_count, 
                    line_count, sentence_count,
		    clause_count, word_count)

def Analyze(infile):
    getCharGroups(infile)
    getPunctuation(groups)
    reportStats()


# Mache es von der kommandozeile aufrufbar  ( wobei in diesem 
# Fall die 'magische' __name__ -Variable auf '__main__' gesetzt wird

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Anwendung: python grammer.py <filename >"
        sys.exit()
    else:
        Document = open(sys.argv[1],"r")
        Analyze(Document)

Anstatt das ganze Ding als ein einziges langes Listing zu zeigen, werde ich dieses Skelett untersuchen und dann werden wir uns nacheinander jede der 3 signifikanten Funktionen anschauen. Um das Programm ans Laufen zu bringen, müssen wir das alles dann irgendwie am Ende zusammenfügen.

Die erste Sache die auffällt, ist die Kommentierung am Anfang. Dies ist eine übliche Vorgehensweise, um dem Leser der Datei eine Idee davon zu vermitteln, was sie enthält und wozu sie verwendet werden kann. Die Versions-Information (Autor und Datum) ist ebenfalls sehr nützlich, wenn man die Ergebnisse mit jemand vergleichen will, der eine mehr oder weniger neue Version verwendet.

Der letzte Abschnitt ist eine Eigenschaft von Python, die ein Modul in die Befehlszeile "__main__" lädt. Wir können diese spezielle, eingebaute __name__ - Variable testen und wenn wir das zugehörige Hauptprogramm kennen, wird das Modul nicht importiert aber ausgeführt und damit starten wir den "Trigger-Code" innerhalb des if.

Dieser Trigger-Code beinhaltet eine benutzerfreundlichen Hinweis darüber, wie das Programm ausgeführt werden soll, wenn kein Dateinamen angegeben wird, oder sogar zuviele Filenamen angegeben sind.

Beachte schließlich, dass die Analyze() - Funktion einfach die anderen Funktionen in der richtigen Reihenfolge aufruft. Auch das ist wieder eine übliche Praxis, dem Anwender die Wahl zu lassen, die gesamte Funktionalität in einer fortschrittlichen Weise zu nutzen (durch Analyze()) oder durch Direktaufruf der untergeordnete Primitiv-Funktionen.

getCharGroups()

Der Pseudo-Code für diesen Abschnit war:

foreach line in file:
  increment line count
  if line empty:
     increment paragraph count
  split line into character groups

Wir können dies mit etwas zusätzlicher Anstrengung in Python implementieren:


# verwende globale Zählvariablen und Liste von Zeichengruppen
def getCharGroups(infile):
    global para_count, line_count, groups
    try:
       for line in infile.readlines():
           line_count = line_count + 1
           if len(line) == 1: # only newline => para break
              para_count = para_count + 1
           else:
              groups = groups + string.split(line)
    except:
       print "Abbruch bei Einlesen der Datei ", sys.argv[1]
       sys.exit()

Bemerkung 1: Wir müssen das Schlüsselwort global hier verwenden, um die Variablen zu deklarieren, die außerhalb der Funktion erzeugt werden. Tun wir das nicht, so erzeugt Python bei ihrer Zuweisung neue Variablen mit gleichem Namen, aber local bezüglich dieser Funktion. Änderungen dieser lokalen Variablen haben keinen Effekt auf die Werte der Modulebene (globale Werte).

Bemerkung 2: Wir haben eine try/except - Klausel um einige Fehler abzufangen, die Abbruchursache zu melden und das Programm zu verlassen.

getPunctuation()

Dies bereitet etwas mehr Mühe und verwendet einige neue Eigenschaften von Python.

Der Pseudocode sieht so aus:

foreach character group:
  increment group count
  extract punctuation chars into a dictionary - {char:count}
  if no chars left:
     delete group
  else: increment word count

Mein erster Versuch ergab dies:

def getPunctuation(wordList):
    global punctuation_counts
    for item in wordList:
       while item and (item[-1] not in alphas):
          p = item[-1]
          item = item[:-1]
          if p in punctuation_counts.keys():
             punctuation_counts[p] = punctuation_counts[p] + 1
          else: punctuation_counts[p] = 1

Beachte, dass dies nicht die abschließende if/else-Klausel der Pseudocode-Version beinhaltet. Ich habe diese der Einfachheit halber weggelassen und weil ich dachte, dass in der Praxis nur sehr wenige Wörter, die Satzzeichen enthalten, gefunden werden. Wir werden dies dennoch der endgültigen Codeversion hinzufügen.

Bemerkung 1: Wir haben die Wortliste parameterisiert, so dass Anwender des Moduls ihre eigene Liste verwenden können, anstatt gezwungen sein zu müssen, mit einer Datei zu arbeiten.

Bemerkung 2: Wir weisen item den Ausdruck item[:-1] zu . Dies ist in Python als Slicing bekannt und der Doppelpunkt besagt lediglich, dass der Index als Bereich zu behandeln ist. Wir könnten zum Beispiel item[3:6] spezifiert haben, um item[3}, item[4] und item[5] in eine Liste hineinzubringen.

Der voreingestellte Bereich ist der Start oder das Ende der Liste, abhängig davon, welche Seite des Doppelpunktes leer ist. Das item[3:] spricht alle Listenbestandteile von item[3] bis zum Ende an. Dies ist wieder eine sehr nützliche Pythoneigenschaft. Die Original- item -Liste ist verloren (und ordnungsgemäß im Abfall gesammelt) und die ganz neu erzeugte Liste wird item zugwiesen.

Bemerkung 3: Wir verwenden einen negativen Index, um das letzte Teil von item zu entnehmen. Das ist eine sinnvolle Eigenschaft von Python. Auch bilden wir eine Schleife, wenn mehrere Satzzeichen am Ende der Gruppe auftauchen.

Beim Austesten, wird ersichtlich, dass wir das selbe auch am Anfang einer Gruppe tun müssen, da zwar abschließende Klammern entdeckt werden, solche die sich öffnen jedoch nicht! Um dieses Problem in den Griff zu bekommen, werde ich eine neue Funktion trim() erzeugen, die Satzzeichen am Beginn und Ende jeder Zeichengruppe entfernt:


#########################################################
# Beachte: trim verwendet eine Rekursion wobei die Abbruch-  
# bedingung entweder 0 oder -1 ist. Ein "InvalidEnd"-Fehler  
# wird bei allem verursacht, außer -1, 0 oder 2.
##########################################################
def trim(item,end = 2):
  """ löscht alle Nicht-alphanumerischen Zeichen vom linken(0), rechten(-1) oder beiden enden von item"""

  if end not in [0,-1,2]:
     raise "InvalidEnd"

  if end == 2:
     trim(item, 0)
     trim(item, -1)
  else:
     while (len(item) > 0) and (item[end] not in alphas):
        ch = item[end]
        if ch in punctuation_counts.keys():
           punctuation_counts[ch] = punctuation_counts[ch] + 1
        if end == 0: item = item[1:]
        if end == -1: item = item[:-1]

Beachte wie der Gebrauch der Rekursion kombiniert mit vorgewählten Parametern uns das Definieren einer einzigen Trimmfunktion ermöglicht, bei der in der Grundeinstellung beide Enden abgeschnitten werden, die aber beim Passieren eines Ende-Wertes auch auf das Bearbeiten eines Endes angewendet wird. Die Ende-Werte sind ausgewählt, um das Indexsystem von Python widerzugeben: 0 für das linke Ende und -1 für das rechte. Ursprünglich habe ich zwei trimfunktionen geschrieben, aber aufgrund der großen Ähnlichkeit wurde mir bewußt, dass ich diese unter der Verwendung eines Parameters kombinieren konnte.

Und getPunctuation wird nun nahezu trivial:

def getPunctuation(wordList):
   for item in wordList:
      trim(item)
   # Jetzt lösche leere 'Wörter'
   for i in range(len(wordList)):
      if len(wordList[i]) == 0:
         del(wordList[i])

Bemerkung 1: Dies beinhaltet nun das Löschen von leeren Worten.

Bemerkung 2: Im interesse der Wiederverwendbarkeit haben wir "trim" doch besser in kleinere Teilstücke aufgetrennt. Dies ermöglicht es uns, eine Funktion zu erzeugen, um ein einziges Satzzeichen am Anfang oder Ende eines Wortes zu löschen und das gelöschte Zeichen wieder zurückzugeben. Dann könnte eine andere Funktion diese wiederholt aufrufen, um das Endergebnis zu erhalten. Da unser Modul jedoch nur dazu da ist, eine Statistik zu produzieren, und nicht hauptsächlich zur Textverarbeitung dient, könnten wir ein separates, sauber getrenntes Modul erstellen, das wir dann importieren könnten. Aber da es nur eine Funktion haben würde, ist es doch nicht so sinnvoll. Ich lasse also alles so wie es ist!

Das endgültige grammar-Modul

Die einzige sache, die noch übrig bleibt, ist die Ausgabe so zu verbessern, dass sie die Satzzeichen und die Zähler beinhaltet. Ersetze die vorhandene reportStats() - Funktion durch diese:

def reportStats():
   global sentence_count, clause_count
   for p in stop_tokens:
      sentence_count = sentence_count + punctuation_counts[p]
   for c in punctuation_counts.keys():
      clause_count = clause_count + punctuation_counts[c]
   print format % (sys.argv[1], 
                   para_count, line_count, sentence_count, 
                   clause_count, len(groups))
   print "Folgende Satzteichen wurden verwendet:"
   for p in punctuation_counts.keys():
      print "\t%s\t:\t%3d" % (p, punctuation_counts[p])

Wenn du alle obigen Funktionen sorgfältig an ihre richtige Stelle zusammengestrickt hast, ist es dir möglich, nun einzugeben:

C:> python grammar.py myfile.txt

um einen Report über die Statistik deiner Datei myfile.txt (oder wie auch immer du sie wirklich nennst) zu erhalten. Man kann darüber streiten, wie nützlich dieses Pprogramm ist, aber ich hoffe, dass das Lesen durch die Entwicklunggeschichte des Codes hat dir geholfen, eine Vorstellung über das Erzeugen deineS eigenen Programme gegeben. Die Haupsache ist, irgendwelche Dinge auszuprobieren. Es ist keine schande, verschiedene Methoden auszuprobieren, oft erhältst du wertvolle Lektionen bei ihrer Erstellung.

Zum Abschluss unseres Kurses werden wir das Grammar-Modul überarbeiten und OO-Techniken verwenden. Bei der Durchführung wirst du erkennen, wie eine OO - Methode Module ergibt, die für den Anwender viel flexibler und auch erweiterbarer sind.

Klassen und Objekte

Eines der größten Probleme für den Anwender unseres modules ist seine Abhängigkeit von globalen variablen. Das Bedeutet er kann nur ein einzges Dokument gleichzeitig anlysieren, jeder Versuch, mehr als dieses zu behandeln, führt zu einem überschreiben der globalen Werte.

Durch die Umwandlung der globalen Werte in Klassen können wir nun mehrfache Instanzen der Klasse (eine pro Datei) erzeugen und jede Instanz erhält ihren eigenen Variablensatz. Weiterhin können wir dadurch, dass wir die Methoden genügend aufgeteilt machen, eine Architektur herstellen, bei der es für den Erzeuger eines neuen Dokumentobjekltes leicht wird, die Suchkriterien so zu modifizieren, dass sie mit neuen Regeln für die neuen Typen versorgt sind (z.B. durch Rückgabe aller HTML-Anhängsel von der Wortliste).

Unser erster Ansatz in diese Richtung führt zu Folgendem:

#! /usr/local/bin/python
################################
# Modul: document.py
# Autor: A.J. Gauld
# Datum:   2000/08/12
# Version: 2.0
################################
# diese Module enthält eine Dokumentklasse, die für
# verschiedene okumentkategorien ( Text, HTML, Latex usw.) 
# unterklassiert werden kann. Text und HTML sind als
# Beispiele ausgeführt.
#
# verwendbare Primärdienste sind:
#    - getCharGroups(),
#    - getWords(), 
#    - reportStats().
################################
import sys,string

class Document:
  def __init__(self, filename):
    self.filename = filename
    self.para_count = 1
    self.line_count, self.sentence_count, self.clause_count, self.word_count = 0,0,0,0
    self.alphas = string.letters + string.digits
    self.stop_tokens = ['.','?','!']
    self.punctuation_chars = ['&','(',')','-',';',':',','] + self.stop_tokens
    self.lines = []
    self.groups = []
    self.punctuation_counts = {}
    for c in self.punctuation_chars + self.stop_tokens:
       self.punctuation_counts[c] = 0
    self.format = """%s enthalten:
%d Abschnitte, %d Zeilen und %d Sätze.
Diese enthalten in ihrer Reihenfolge %d Nebensätze und eine gesamtzahl von %d Wörtern.""" 

  def getLines(self):
    try:
      self.infile = open(self.filename,"r")
      self.lines = self.infile.readlines()
    except:
      print "Failed to read file ",self.filename
      sys.exit()

  def getCharGroups(self, lines):
    for line in lines:
      line = line[:-1]  # lose the '\n' at the end
      self.line_count = self.line_count + 1
      if len(line) == 0: # empty => para break
         self.para_count = self.para_count + 1
      else:
         self.groups = self.groups + string.split(line)


  def getWords(self):
    pass

  def reportStats(self, paras=1, lines=1, sentences=1, words=1, punc=1):
    pass

  def Analyze(self):
    self.getLines()
    self.getCharGroups(self.lines)
    self.getWords()
    self.reportStats()

class TextDocument(Document):
  pass

class HTMLDocument(Document):
  pass

if __name__ == "__main__":
  if len(sys.argv) != 2:
     print "Anwendung: python document.py <filename>"
     sys.exit()
  else:
    D = Document(sys.argv[1])
    D.Analyze()

Jetzt implementieren wir die Klasse, die wir benötigen, um die getWords-Methode zu erzeugen. Wir können einfach das kopieren, was wir in der vorhergehenden version gemacht haben und erzeugen eine Trimm-Methode. Da wir aber eine leicht erweiterbare OO-Version möchten, teilen wir getWords in eine Reihe von Schritten auf. Dann brauchen wir nur in Unterklassen die Unterschritte zu überschreiben und nicht die ganze getWords-Methode. Dies eröffnet uns einen viel größern Spielraum, um mit unterschiedlichen Dokumenttypen umzugehen.

Insbesondere werden wir Methoden hinzufügen, um Zeichengruppen zu verwerfen, die als fehlerhaft erkannt werden, wobei wir unerwünschte Zeichen am Anfang und Ende abschneiden. Dazu fügen wir 3 Methoden zu Document hinzu und implementieren getWords mit der Ausdrucksweise dieser Methoden.

class Document:
   # .... wie oben
  def getWords(self):
    for w in self.groups:
       self.ltrim(w)
       self.rtrim(w)
    self.removeExceptions()

  def removeExceptions(self):
    pass

  def ltrim(self,word):
    pass

  def rtrim(self,word):
    pass

Beachte, wie wir die Funktionsrümpfe mit dem einzelnen Befehl pass definieren, der absolut nichts bewirkt. Anstelle diesem werden wir dann definieren, wie die Methode für jeden konkreten Dokument-Typ arbeiten soll.

Textdokument

Ein Textdokument sieht so aus:

class TextDocument(Document):
   def ltrim(self,word):
      while (len(word) > 0) and (word[0] not in self.alphas):
         ch = word[0]
         if ch in self.c_punctuation.keys():
            self.c_punctuation[ch] = self.c_punctuation[ch] + 1
         word = word[1:]
      return word

   def rtrim(self,word):
      while (len(word) > 0) and (word[-1] not in self.alphas):
         ch = word[-1]
         if ch in self.c_punctuation.keys():
            self.c_punctuation[ch] = self.c_punctuation[ch] + 1
         word = word[:-1]
      return word
      
  def removeExceptions(self):
     top = len(self.groups)
     n = 0
     while n < top:
        if (len(self.groups[n]) == 0):
           del(self.groups[n])
           top = top - 1
         n = n+1

Die Trimmfunktionen sind scheinbar identisch mit der trim-Funktion unseres grammar.py-Modules, jedoch zweigeteilt. Die removeExceptions-Funktion wurde definiert, um leere Worte zu entfernen.

Beachte, dass ich die Struktur der letzten Methode so verändert habe, dass ich eine while-Schleife anstelle der for-Schleife in der vorhergehenden verwendet habe. Dies geschah deshalb, weil während des Testens ein fehler ghefunden wurde, wobei wenn wir Elemte aus der Liste gelöscht haben, der range-Wert (zu Beginn errechnet) immer noch die Originallänge hatte und wir uns beim versuch Teile hinter dem Listenende anzusprechen, aufhängten. Um die verwendung einer while-Schleife und das Nachregulieren des Indexmaximums zu vermeiden, entfernen wir ein Element.

HTML-Dokument

Für HTML werden wir eine Eigenschaft von Python verwenden, die wir schon vorher gesehen haben: reguläre Ausdrücke. Dies sind spezielle String-Muster, die wir zum Auffinden von komplexen Strings verwenden können. Hier benutzen wir sie, um etwas zwischen < und > zu entfernen. Dies bedeutet, dass wir getWords umdefinieren müssen. Das momentane Abschneiden von Satzzeichen soll das Gleiche sein, als für Volltext, so dass, anstatt direkt von Document , wir von TextDocument erben und seine trim - Methoden wiederverwenden werden.

Dann erhält man für das HTMLDocument:

class HTMLDocument(TextDocument):
   def removeExceptions(self):
      """ Verwendung von regulären Ausdrücken, um alle <.+?> zu entfernen """
      import re
      tag = re.compile("<.+?>")# gebrauche kein gieriges re
      L = 0
      while L < len(self.lines):
        if len(self.lines[L]) > 1: # wenns nicht leer ist
           self.lines[L] = tag.sub('', self.lines[L])
           if len(self.lines[L]) == 1:
              del(self.lines[L])
           else: L = L+1
        else: L = L+1


   def getWords(self):
     self.removeExceptions()
     for i in range(len(self.groups)):
        w = self.groups[i]
        w = self.ltrim(w)
        self.groups[i] = self.rtrim(w)
     TextDocument.removeExceptions(self)# streiche leere Wörter

Bemerkung 1: Die einzige zu beachtende Sache hier hier ist das Aufrufen von self.removeExceptions vor dem Trimmen und dann der Aufruf von TextDocument.removeExceptions. Wenn wir uns auf das geerbte getWords verlassen hätten, hätte dies unser removeExceptions nach dem Trimmen aufgerufen, was wir nicht wollen.

Hinzufügen eines GUI

Um ein GUI zu erzeugen werden wir Tkinter verwenden, das kurz im Kapitel über ereignisgesteuerte Programmierung vorgestellt wurde. Diesmal wird das GUI etwas anspruchsvoller und wir verwenden mehr von den grafischen Steuerelementen oder Widgets, mit denen Tkinter ausgestattet ist.

Umbau der Document - Klasse

Bevor wir zu dieser Stufe gelangen, müssen wir unsere Document-Klasse modifizieren. Die momentane version gibt das etrgebnis an stdout als Teil der Analysemethode aus. Aber für ein GUI möchten wir das so nicht haben. Stattdessen möchten wir die Analysemethode so, dass sie einfach die gesamtsummen in den Zählerattributen speichert und wir auf sie zugreifen können, wenn es von Nöten ist. Um dies zu tun, teilen oder bauen wir einfach die reportStats()-Methode in zwei Teile um: generateStats() , die die Werte berschnen und sie in den zählern speichern wird und printStats(), die nach stdout ausgibt.

Schließlich müssen wir Analyze modifizieren, um generateStats() aufzurufen und die Hauptbefehlsfolge zum spezifizierten Aufruf von call printStats() im Anschluss an Analyze . Mit diesen Änderungen an ihrer entsprechenden Stelle wird der bestehende Code genauso wie vorher arbeiten, zumindest soweit es den Kommandozeilenbenutzer angeht. Andere programmierer werden geringfügige Änderungen durchführen müssen, um printStats() hinter Analyze zu verwenden - eine nicht zu schwierige Änderung.

Die überarbeiteten Code-Segmente sehen so aus:

  def generateStats(self):
    self.word_count = len(self.groups)
    for c in self.stop_tokens:
       self.sentence_count = self.sentence_count + self.punctuation_counts[c]
    for c in self.punctuation_counts.keys():
       self.clause_count = self.clause_count + self.punctuation_counts[c]


  def printStats(self):
    print self.format % (self.filename, self.para_count, 
    self.line_count, self.sentence_count, 
    self.clause_count, self.word_count)
    print "Die folgenden satzzeichen wurden verwendet:"
    for i in self.punctuation_counts.keys():
       print "\t%s\t:\t%4d" % (i,self.punctuation_counts[i])

and:

if __name__ == "__main__":
       if len(sys.argv) != 2:
          print "Anwendung: python document.py <filename>"
          sys.exit()
       else:
         try:
            D = HTMLDocument(sys.argv[1])
            D.Analyze()
            D.printStats()
         except:
            print "Fehler bei Dateianalyse: %s" % sys.argv[1]

Nun sind wir soweit, um eine GUI-Hülle um unsere Dokument-Klassen zu erzeugen.

Entwurf eines GUI

Der erste Schritt ist der Versuch, sich vorzustellen, wie es aussehen sollte. Wir müssen einen Filenamen angeben, damit benötigen wir also ein Edit- oder Entry-Steuerelement. Auch benötigen wir die Spezifikationsmöglichkeit, ob wir eine Text- oder HTML-Analyse durchführen wollen; dieser Auswahltyp der Art 'Eins von Mehreren' wird gewöhnlich durch einen Satz von Radiobuttons gesteuert. Diese Steuerelemente sollten zusammen gruppiert werden, um darzustellen, dass sie zueinander gehören.

Die nächste anforderung ist eine Anzeigemöglichkeit der Ergebnisse. Wir könnten ein mehrfaches Label -Steuerelement nehmen, jeweils eins je Zähler. Stattdessen möchte ich ein einfaches Text-steuerelement verwenden, in das wir Strings einfügen können, dies kommt näher an die kommandozeilenausgabe heran, aber letztendlich ist die Auswahl eine Sache der Vorlieben des Entwerfers.

Schließlich benötigen wir etwas, um die Analyse auszulösen und die Anwendung zu verlassen. Da wir ein Text-Kontrollelement zur Ergebnisanzeige verwenden, wäre es auch zweckmäßig, etwas zu haben, das die anzeige zurücksetzt. Diese Befehlsmöglichkeiten können alle durch Button-Kontrollelemente representiert werden.

Das Skizzieren dieser Ideen für ein GUI liefert uns etwa das Folgende:

+-------------------------+-----------+
|    FIILENAME            | O TEXT    |
|                         | O HTML    |
+-------------------------+-----------+
|                                     |
|                                     |
|                                     |
|                                     |
|                                     |
+-------------------------------------+
|                                     |
|   ANALYZE        RESET      QUIT    |
|                                     |
+-------------------------------------+

Jetzt können wir etwas Code schreiben, nehmen wir uns das Schritt für Schritt vor:

from Tkinter import *
import document

################### CLASS DEFINITIONS ######################
class GrammarApp(Frame):
  def __init__(self, parent=0):
    Frame.__init__(self,parent)
    self.type = 2 # create variable with default value
    self.master.title('Grammar counter')
    self.buildUI()

Hier haben wir das Tkinter- und das document-Module importiert. Bezüglich des Ersteren haben wir alle Tkinter-Namen innerhalb unseres laufenden Moduls sichtbar gemacht, wobei wir wir für das Zweite bei allen namen 'document' als Prefix voranstellen müssen.

Wir haben auch eine __init__ - Methode definiert, die die Frame.__init__ Superklassenmethode aufruft, um sicher zu gehen, dass Tkinter intern sauber installiert ist. Wir erzeugen dann ein Attribut, das den Wert für den Dokumenttyp speichert und endlich die buildUI - Methode aufruft, die alle Widgets für uns erzeugt.

   def buildUI(self):
     # Jetzt die Datei-Information: Filename and -typ
     fFile = Frame(self)
     Label(fFile, text="Filename: ").pack(side="left")
     self.eName = Entry(fFile)
     self.eName.insert(INSERT,"test.htm")
     self.eName.pack(side="left", padx=5)

     
     # um die Radiobuttons  in gleicher Linie mit dem namen
     #  zu halten, benötigen wir ein anderes Frame
     fType = Frame(fFile, borderwidth=1, relief=SUNKEN)
     self.rText = Radiobutton(fType, text="TEXT",
                              variable = self.type, value=2, 
                              command=self.doText)
     self.rText.pack(side=TOP)
     self.rHTML = Radiobutton(fType, text="HTML",
                              variable=self.type, value=1,
                              command=self.doHTML)
     self.rHTML.pack(side=TOP)
     # make TEXT the default selection
     self.rText.select()
     fType.pack(side="right", padx=3)
     fFile.pack(side="top", fill=X)

     
     # Die Textbox erhält die Ausgabe, mit pad wird ihr ein Rand gegeben
     self.txtBox = Text(fApp, width=60, height=10)
     self.txtBox.pack(side=TOP, padx=3, pady=3)
     
     
     # zum schluss einige  Command-Buttons , die echte arbeit leisten
     fButts = Frame(self)
     self.bAnal = Button(fButts, text="Analyze",
                         command=self.AnalyzeEvent)
     self.bAnal.pack(side=LEFT, anchor=W, padx=50, pady=2)
     self.bReset = Button(fButts, text="Reset",
                          command=self.doReset)
     self.bReset.pack(side=LEFT, padx=10)
     self.bQuit = Button(fButts, text="Quit",
                         command=self.doQuitEvent)
     self.bQuit.pack(side=RIGHT, anchor=E, padx=50, pady=2)

     fButts.pack(side=BOTTOM, fill=X)
     self.pack()

Ich werde das jetzt nicht alles erläutern, stattdessen empfehle ich dir einen Blick in das Tkinter-Tutorial zu werfen, das auf der Python-Webseite zu finden ist. Dies ist eine exzellente Einführung und Referenz zu Tkinter (Anm.d.Ü.: Das kann ich bestätigen, leider ist alles auf Englisch). Das Hauptprinzip besteht darin, dass du Widgets aus ihren bestehenden Klassen erzeugst, die mit Optionen - Namensparametern- ausgestattet sind, wobei dann das Widget in den ihn umgebenden "Frame" gepackt wird.

Die anderen Schlüsselpunkte die zu erwähnen sind, sind der Gebrauch von untergeorneten Frame-Widgets, die die Radiobuttons und Koommandobuttons beinhalten. Die Radiobuttons besitzen auch ein Optionen-Paar, die variable & value genannt werden; die erste verbindet die Radiobuttons durch das Spezifizieren der gleichen externen Variablen (self.type) und die zweite gibt einen entsprechenden Einzelwert. Beachte auch die command=xxx - Option, die der Buttonsteuerung übergeben werden. Dies sind die Methoden, die von Tkinter aufgerufen werden, wenn der Button gedrückt wird. Der Code dazu kommt als nächstes:

   
   ################# EREIGNISBEHANDLUNGSMETHODEN ####################
   # Zeit zu sterben ...
   def doQuitEvent(self):
     import sys
     sys.exit()

   
   # Wiederspeichern der Grundeinstellungen
   def doReset(self):
     self.txtBox.delete(1.0, END)
     self.rText.select()

   
   # setzen der Radiowerte
   def doText(self):
     self.type = 2

   def doHTML(self):
     self.type = 1

Diese methoden sind eigentlich trivial und hoffentlich von jetzut an selbsterklärend. Der abschließende Ereignisbehandler ist derjenige, der die Analyse durchführt:

   
   # Erzeuge zugehörigen Dokumenttyp und analysiere ihn.
   # zeige dann das ergebnis in der Form an
   def AnalyzeEvent(self):
     filename = self.eName.get()
     if filename == "":
        self.txtBox.insert(END,"\nKein Filname angegeben!\n")
        return
     if self.type == 2:
        doc = document.TextDocument(filename)
     else:
        doc = document.HTMLDocument(filename)
     self.txtBox.insert(END, "\nAnalysiere...\n")
     doc.Analyze()
     str = doc.format % (filename,
                         doc.c_paragraph, doc.c_line,
                         doc.c_sentence, doc.c_clause, doc.c_words)
     self.txtBox.insert(END, str)

Du solltest wieder in der Lage sein, dies zu lesen und zu erkennen, was es macht. Die Kernpunkte sind diese:


Alles was jetzt noch benötigt wird, ist das Erzeugen einer Instanz des Applikationsobjektes und die Ereignisschleife ans Laufen zu bekommen; wir tun das hier:

myApp = GrammarApp()
myApp.mainloop()

Werfen wir einen Blick auf das Endergebnis, wie es unter MS Windows aussieht, das die Ergebnisse der Analyse einer Test-HTML-Datei anzeigt, zuerst im Text- dann im HTML-Modus:

Das war's. Wenn du möchtest, kannst du die HTML- Bearbeitung noch weiter entwickeln. Du kannst neue Module für andere Dokumenttypen erzeugen. Du kannst versuchen die Textbox gegen mehrere Label, die in einem einzigen Rahmen gepackt sind, auszutauschen. Aber für unsere Zwecke haben wir alles getan. Der nächste Abschnitt bietet einige Ideen an, in welche Richtung du in Abhängigkeit von deinen Programmierbestrebungen weitermachen kannst. Die Hauptsache ist jedoch das Vergnügen und merke dir für immer: Der Computer ist dumm!

(Anmerkung des Übersetzers: Im Gegensatz zu den vorangegangenen Kapiteln habe ich es diesmal unterlassen, die übersetzbaren Teile der Programmbeispiele - ausgenommen der Kommentare - ins Deutsche zu übersetzen. Du bist als Leser des Bisherigen soweit fortgeschritten, dass du sowieso das meiste verstehst, wobei die Bedeutung ohnehin aus dem Kontext hervorgeht. Python verwendet , wie alle mir bekannten Programmiersprachen, englische Ausdrücke und auch die meiste weiterführende Literatur ist auch auf Englisch. Also warum nicht gleich ein bischen daran gewöhnen ?)


Zurück Referenzen Inhalt
 

Wenn du Fragen oder Hinweise zu dieser seite hast, sende bitte eine E-Mail auf Englisch an alan.gauld@yahoo.co.uk oder aud Deutsch an bup.schaefer@freenet.de