Níže uvedený text pochází z prvního vydání. Nad tímto textem se nachází aktuální stav po revizi sm>řující k druhému vydání.
V této případové studii rozšíříme funkčnost programu pro počítání slov,
který jsme vyvinuli již dříve. Vytvoříme program, který napodobí funkci
unixovského programu wc v tom smyslu, že bude vypisovat počet
řádků, slov a znaků v souboru. Ale půjdeme ješt> dál a budeme vypisovat také
počet v>t, klauzulí (viz
poznámka dále), slov, písmen a interpunkčních znamének v textovém
souboru. Vývoj programu budeme provád>t po etapách. Postupn> budeme zvyšovat
jeho schopnosti. Převedeme jej do podoby modulu, abychom zvýšili jeho
znovupoužitelnost. A upravíme jeho implementaci do objektov> orientované
podoby, čím zvýšíme možnosti dalšího rozšiřování funkčnosti.
Implementovat jej budeme v jazyce Python, ale přinejmenším počáteční fáze mohou být napsány i v jazycích BASIC nebo Tcl. S tím, jak budeme řešit složit>jší části problému, budeme stále více používat zabudované datové struktury jazyka Python. Proto se bude obtížnost případného zápisu v jazyce BASIC zvyšovat, ačkoliv použití Tcl bude stále možné. Objektov> orientované stránky konečného řešení budou vhodné pouze pro jazyk Python.
Jako cvičení pro čtenáře bude ponechána možnost implementace dalších rysů, jako jsou:
((prům>rný počet slov na v>tu) + (procento
slov s více než 5 písmeny) * 0.4). Toto číslo vyjadřuje složitost
textu.Podívejme se znovu na dříve vytvořený program pro počítání slov (viz Práce se soubory — Počítání slov):
import string
def pocetSlov(s):
seznam = string.split(s)
# Poznámka překladatele: Od verze jazyka Python 2
# je pro standardní řet>zec definována metoda split(), takže místo výše
# uvedeného zápisu můžeme psát s.split() a není nutné provád>t import
# modulu string.
return len(seznam) # vrátíme počet prvků seznamu
vstup = open("menu.txt", "r")
celkem = 0 # vytvoříme prom>nnou a nastavíme jí počáteční hodnotu nula
for radek in vstup.readlines():
celkem = celkem + pocetSlov(radek) # sečti počty za každý řádek
print "Soubor má %d slov." % celkem
vstup.close()
Potřebujeme přidat počítadla řádků a znaků. Počítání řádků je snadné, protože cyklus zpracovává vstup po řádcích. Jednoduše přidáme n>jakou prom>nnou a budeme ji zvyšovat při každé obrátce cyklu. Počítadlo znaků je pouze mírn> složit>jší, protože můžeme procházet seznam slov a jejich délky přičítat do další prom>nné.
Rádi bychom také zvýšili obecnost použití programu tím, že jméno
zkoumaného souboru zjistíme z parametru příkazového řádku nebo, pokud není
zadáno, vyžádáme si zadání jména dotazem na uživatele. (Alternativní řešení
by spočívalo ve čtení textu ze standardního vstupu, což práv> d>lá opravdový
program wc.)
Takže konečné řešení ve stylu wc vypadá takto (poznámka překladatele: abychom se vyhnuli komplikacím s
českými řet>zci ve zdrojovém textu programu, zjednodušíme si řešení tím, že
použijeme cestinu bez hacku a carek):
import sys, string # Získáme jméno souboru buď z příkazového řádku # nebo si je vyžádáme od uživatele. if len(sys.argv) != 2: jmenoSouboru = raw_input("Zadejte jmeno souboru: ") else: jmenoSouboru = sys.argv[1] vstup = open(jmenoSouboru, "r") # Poznámka překladatele: Od verze Python 2 by se m>la dávat # přednost zápisu vstup = file(jmenoSouboru, "r") # Počáteční hodnoty počítadel nastavíme na nuly. # Tím se také vytvoří příslušné prom>nné. slov = 0 radku = 0 znaku = 0 for radek in vstup.readlines(): radku = radku + 1 # Řádek rozložíme na slova a spočítáme je. seznamSlov = string.split(radek) # Poznámka překladatele: U Python 2 lze psát # seznamSlov = radek.split() slov = slov + len(seznamSlov) # Počet znaků určíme z délky původního řádku, # čímž započítáme mezery atd. znaku = znaku + len(radek) print "%s ma %d radku, %d slov a %d znaku" % (jmenoSouboru, radku, slov, znaku) vstup.close()
Pokud znáte unixovský příkaz wc, pak víte, že mu můžete
jméno souboru zadat v podob> masky. Tím získáte hledané údaje pro všechny
soubory, které masce vyhovují, a získáte také celkový součet t>chto údajů.
Výše uvedený program pracuje pouze s přímo zadanými jmény souborů. Pokud
chcete, aby zpracovával i soubory zadané maskou, podívejte se na modul
glob. Ten vám umožní vytvořit seznam jmen vyhovujících souborů,
který pak můžete zpracovat. Budete k tomu potřebovat dočasná počítadla pro
každý soubor a navíc kumulativní počítadla pro celkové součty (součty součtů
za jednotlivé soubory). Nebo místo toho můžete použít slovník…
Když jsem začal přemýšlet o tom, jak bychom mohli rozšířit funkčnost, abychom počítali v>ty a slova místo "skupin znaků" (což činíme ve výše uvedeném řešení), napadlo m> nejprve, že bychom m>li ze souboru nejdříve v cyklu načíst jednotlivé řádky a pak bychom m>li v cyklu zpracovat každý řádek a získat z n>j slova do dalšího seznamu. Nakonec bychom m>li zpracovat každé "slovo" za účelem odstran>ní nadbytečných znaků.
Když jsem o tom uvažoval o n>co déle, začalo být zřejmé, že pokud bychom jednoduše shromažďovali slova a interpunkční znaménka, mohli bychom práv> interpunkční znaménka použít pro počítání v>t, klauzulí atd. (tím, že řekneme, co považujeme za v>tu nebo klauzuli s ohledem na použitá interpunkční znaménka). (Poznámka překladatele: V anglické gramatice se pojem clause (klauzule) používá ve významu v>tného členu, typicky hlavní a vedlejší v>ty. Pravidla pro výstavbu v>ty a pro psaní interpunkčních znamének jsou v anglickém jazyce mnohem propracovan>jší, takže se z nich dá strojov> lépe určit stavba v>ty. V českém jazyce je to mnohem obtížn>jší. Proto od dále uvedeného programu neočekávejte zázraky.) To znamená, že stačí, když souborem projdeme pouze jednou a poté budeme procházet přes mnohem kratší seznam interpunkčních znamének. Zkusme si to načrtnout v pseudokódu:
pro každý řádek v souboru:
zvýšit počítadlo řádků
if je řádek prázdný:
zvýšit počítadlo odstavců
rozložit řádek na skupiny znaků
pro každou skupinu znaků:
zvýšit počítadlo skupin
odstranit interpunkční znaky a přidat do slovníku - {znak: počet}
if ve skupin> nezbyl žádný znak:
zrušit skupinu
else: zvýšit počítadlo slov
počet v>t = počet znaků ('.', '?', '!')
počet klauzulí = součet všech interpunkčních znaků (pon>kud ubohá definice...)
vypsat počty odstavců, řádků, v>t, klauzulí, skupin znaků, slov.
pro každý interpunkční znak:
vypsat počet (ze slovníku)
Vypadá to, že bychom mohli vytvořit asi 4 funkce, odpovídající výše uvedeným skupinám. To by nám pomohlo vybudovat modul, který by mohl být opakovan> použitelný buď jako celek nebo po částech.
Klíčové funkce budou tyto: ziskejSkupinyZnaku(vstSoubor) a
ziskejInerpunkci(seznamSkupin). Podívejme se, co vytvoříme na
základ> uvedeného pseudokódu…
############################# # Modul: gramatika # Vytvořil: A.J. Gauld, 2000,8,12 # # Funkce: # Počítá odstavce, řádky, v>ty, 'klauzule', skupiny znaků, slova # a interpunkční znaménka pro textové soubory s textem předpokládajícím # prózu. Předpokládá se, že v>ty končí znaky [.!?] a odstavce jsou odd>leny # prázdným řádkem. Za 'klauzuli' se jednoduše považuje část v>ty, která je # odd>lena interpunkčním znakem (což je pon>kud hloupá definice, ale jednoho # dne můžeme doplnit n>co lepšího). # # Použití: Při základním použití se čte jméno souboru z parametru a vypisují # se všechny získané údaje. Předpokládá se vytvoření druhého modulu, # který by používal zde definované funkce a poskytoval užitečn>jší # příkazy. ############################# import string, sys ############################ # Počáteční nastavení globálních prom>nných. poc_odstavcu = 1 # Předpokládáme existenci nejmén> jednoho odstavce. poc_radku, poc_vet, poc_klauzuli, poc_slov = 0, 0, 0, 0 skupiny = [] poc_interpunkcnich_znaku = {} alfanumericke_znaky = string.letters + string.digits # Poznámka překladatele: Pro české texty bychom mezi # alfanumerické znaky m>li přidat i znaky s diakritickými znaménky. V>c se # ale komplikuje tím, že bychom navíc m>li uvažovat i způsob kódování # vstupního souboru. Obecné řešení by nebylo úpln> jednoduché, takže zatím # nebudeme překlad originálního zdrojového textu z angličtiny upravovat. koncove_znaky = ['.', '?', '!'] interpunkcni_znaky = ['&', '(', ')', '-', ';', ':', ','] + koncove_znaky for c in interpunkcni_znaky: poc_interpunkcnich_znaku[c] = 0 format = """%s obsahuje: %d odstavcu, %d radku a %d vet. Ty obsahuji %d klauzuli a celkem %d slov.""" ############################ # Nyni nadefinujeme funkce, které tvoří jádro činnosti. def ziskejSkupinyZnaku(vstSoubor): pass def ziskejInerpunkci(seznamSkupin): pass def vypsatStatistiky(): print format % (sys.argv[1], poc_odstavcu, poc_radku, poc_vet, poc_klauzuli, poc_slov) def Analyzuj(vstSoubor): ziskejSkupinyZnaku(vstSoubor) ziskejInerpunkci(skupiny) vypsatStatistiky() # Pokud je modul volán z příkazového řádku, zajisti spušt>ní následujícího # kódu. (V tomto případ> je magická prom>nná __name__ nastavena na hodnotu # "__main__".) if __name__ == "__main__": if len(sys.argv) != 2: print "Pouziti: python gramatika.py <soubor>" sys.exit() else: Dokument = open(sys.argv[1], "r") # Poznámka překladatele: Od verze Python 2 by se m>la dávat # přednost zápisu Dokument = file(sys.argv[1], "r") Analyzuj(Dokument) Dokument.close()
V tomto míst> jsem necht>l ukázat celé řešení v podob> jednoho dlouhého výpisu. Proberu rad>ji uvedenou kostru a potom se podíváme na každou z uvedených tří významných funkcí. Nicmén> k tomu, abyste program uvedli do chodu, budete muset na konci vše slepit dohromady.
První v>cí, která stojí za povšimnutí, je komentář na začátku. Jeho uvedení patří k b>žným praktikám. Má čtenáři naznačit, co soubor obsahuje a jak má být používán. Užitečná je rovn>ž informace o verzi (autor a datum) a to zejména pro n>koho, kdo již možná používá nov>jší nebo starší verzi.
Poslední úsek souvisí s rysem systému Python, který každému modulu
spušt>nému z příkazového řádku vnitřn> říká "__main__" (čti
mein; hlavní). Můžeme si otestovat obsah speciální zabudované prom>nné
__name__ (čti nejm; jméno). Pokud je v ní uveden zmín>ný
řet>zec, pak víme, že modul nebyl importován, ale že byl spušt>n. Takže v
takovém případ> můžeme provést spoušt>cí kód, který je uveden v t>le příkazu
if.
Náš spoušt>cí kód obsahuje uživatelsky přív>tivou nápov>du o způsobu spušt>ní programu pro případ, kdybychom jej spustili bez zadání jména souboru, nebo kdybychom naopak uvedli příliš mnoho jmen souborů.
Na záv>r poznamenejme, že funkce Analyzuj() jednoduše zavolá
ostatní funkce ve správném pořadí. K b>žným praktikám patří op>t to, že si
uživatel může vybrat, zda bude chtít používat celkovou funkčnost přímočarým
způsobem (voláním funkce Analyzuj()) nebo zda bude přímo volat
nízkoúrovňové, primitivní funkce.
Pseudokód pro tento úsek vypadal následovn>:
pro každý řádek v souboru:
zvýšit počítadlo řádků
if je řádek prázdný:
zvýšit počítadlo odstavců
rozložit řádek na skupiny znaků
V jazyce Python to můžeme implementovat velmi snadno:
# Použijeme globální prom>nné počítadel a globální seznam skupin znaků. def ziskejSkupinyZnaku(vstSoubor): global poc_odstavcu, poc_radku, skupiny try: for radek in vstSoubor.readlines(): poc_radku = poc_radku + 1 if len(radek) == 1: # prázdný řádek => odd>lení odstavce poc_odstavcu = poc_odstavcu + 1 else: skupiny = skupiny + string.split(radek) # Poznámka překladatele: Od verze Python 2 lze psát # skupiny = skupiny + radek.split() except: print "Nepodarilo se cist ze souboru ", sys.argv[1] sys.exit()
Poznámka 1: Abychom oznámili, že budeme používat prom>nné, které
byly vytvořeny vn> t>la funkce, musíme použít klíčové slovo
global. Pokud bychom tak neučinili, pak by při přiřazování do
nich Python vytvořil nové prom>nné se stejnými jmény, které by ovšem byly
lokální uvnitř t>la funkce. Zm>ny obsahu takových lokálních
prom>nných by se u globálních prom>nných (na úrovni modulu)
neprojevily.
Poznámka 2: K případnému oznámení chyb při čtení souboru a ukončení b>hu
programu jsme použili konstrukci try/except.
Zde budeme muset vyvinout o n>co v>tší úsilí. Použijeme také pár nových rysů jazyka Python. Příslušný pseudokód vypadal následovn>:
pro každou skupinu znaků:
zvýšit počítadlo skupin
odstranit interpunkční znaky a přidat do slovníku - {znak: počet}
if ve skupin> nezbyl žádný znak:
zrušit skupinu
else: zvýšit počítadlo slov
Můj první pokus vypadal n>jak takto:
def ziskejInerpunkci(seznamSkupin):
global poc_interpunkcnich_znaku
for skupina in seznamSkupin:
while skupina and (skupina[-1] not in alfanumericke_znaky):
p = skupina[-1]
skupina = skupina[:-1]
if p in poc_interpunkcnich_znaku.keys():
poc_interpunkcnich_znaku[p] = poc_interpunkcnich_znaku[p] + 1
else: poc_interpunkcnich_znaku[p] = 1
Povšimn>te si, že tato verze nezahrnuje záv>rečnou konstrukci
if/else, kterou obsahoval pseudokód. Vynechal jsem
ji kvůli zjednodušení. M>l jsem také pocit, že se v praktických případech
objeví velmi málo skupin, které obsahují pouze interpunkční znaky. Ale do
konečné verze kódu tuto konstrukci doplníme.
Poznámka 1: Seznam skupin jsme se rozhodli předávat parametrem, takže uživatelé tohoto modulu mohou předat svůj vlastní seznam místo toho, aby byli nuceni pracovat se souborem.
Poznámka 2: Za zmínku stojí obrat, kdy jsme prom>nné
skupina přiřadili hodnotu skupina[:-1]. V jazyce
Python je tento obrat znám jako slicing (čti slajsing; odřezávání,
odkrajování). Dvojtečka říká, aby byl index chápán jako rozsah. Pokud bychom
například cht>li získat seznam položek skupina[3],
skupina[4] a skupina[5], vyjádřili bychom jej jako
skupina[3:6]. (Poznámka překladatele:
tento obrat lze používat pro posloupnosti různých druhů, konkrétn> pro
řet>zce a pro seznamy.)
Pokud neuvedeme číslo, pak se to chápe jako začátek nebo konec seznamu
podle toho, na které stran> dvojtečky necháme prázdné místo. Takže zápis
skupina[3:] by m>l význam všech členů skupina od
skupina[3:] až do konce. Jde o jeden z velmi užitečných rysů
jazyka Python. V našem příkladu je originální posloupnost
skupina ztracena (a tím pádem náležit> uklizena, uvoln>na).
Nov> vytvořená posloupnost (v našem případ> řet>zec) je přiřazena do skupina.
Poznámka 3: Pro získání posledního znaku z prom>nné
skupina používáme záporný index. Jde op>t o velmi užitečný rys
jazyka Python. Pro případ, že by se na konci skupiny objevilo více
interpunkčních znaků, provádíme zpracování v cyklu.
B>hem testů se ukázalo, že totéž musíme provád>t i pro začátek skupiny,
protože, ačkoliv uzavírací závorky detekovány jsou, otvírací závorky
nikoliv. Pro vyřešení tohoto problému vytvoříme novou funkci
trim(), která odstraní interpunkční znaky ze začátku a z konce
jedné skupiny znaků:
#########################################################
# Poznámka: trim používá rekurzi, kde podmínkou ukončení je buď hodnota
# 0 nebo -1. Pokud se objeví n>co jiného, než hodnoty -1, 0 nebo 2,
# je vyvolána chyba "InvalidEnd".
# Poznámka překladatele: Od verze Python 2.0 se doporučuje pro výjimky
# používat instance tříd odvozených od třídy Exception. Používání
# řet>zců pro výjimky se již nedoporučuje.
##########################################################
def trim(polozka, end = 2):
""" Odstraní nealfanumerické znaky zleva (end = 0), zprava (-1) nebo
z obou stran prom>nné polozka."""
if end not in [0, -1, 2]:
raise "InvalidEnd"
if end == 2:
trim(polozka, 0)
trim(polozka, -1)
else:
while (len(polozka) > 0) and (polozka[end] not in alfanumericke_znaky):
ch = polozka[end]
if ch in poc_interpunkcnich_znaku.keys():
poc_interpunkcnich_znaku[ch] = poc_interpunkcnich_znaku[ch] + 1
if end == 0: polozka = polozka[1:]
if end == -1: polozka = polozka[:-1]
Povšimn>te si, jak nám kombinace použití rekurze a implicitní
(přednastavené) hodnoty parametru umožnila definovat jedinou funkci
trim, která standardn> ošetří oba konce. Pokud ale zadáme
hodnotu parametru end, můžeme ji použít k ošetření pouze
jednoho konce. Hodnoty parametru end jsou zvoleny tak, aby
odpovídaly způsobu indexování v jazyce Python: nula pro levou stranu a -1
pro pravou. Původn> jsem vytvořil dv> funkce trim, jednu pro
každý konec. Ale díky množství pozorovaných podobností jsem zjistil, že je
při použití parametru mohu zkombinovat dohromady.
Funkce ziskejInerpunkci se tím velmi zjednoduší:
def ziskejInerpunkci(seznamSkupin):
for skupina in seznamSkupin:
trim(skupina)
# Nyní vypustíme prázdná 'slova'.
for i in range(len(seznamSkupin)):
if len(seznamSkupin[i]) == 0:
del(seznamSkupin[i])
Poznámka 1: Nová implementace navíc provádí vypoušt>ní prázdných slov.
Poznámka 2: V zájmu znovupoužitelnosti by možná bývalo bylo lepší, kdybychom funkci trim rozbili na ješt> menší kousky. To by nám umožnilo vytvořit funkci pro odstran>ní jediného interpunkčního znaménka — buď ze začátku, nebo z konce slova — a odstran>ný znak bychom mohli vracet jako výsledek. Takovou funkci by pak mohla opakovan> volat jiná funkce, čímž bychom získali požadovaný výsledek. Ale náš modul se má zabývat zjišťováním konkrétních statistických údajů ze zadaného textu a ne n>jakým obecným zpracováním textu. Pro uvedené funkce bychom tedy správn> m>li vytvořit samostatný modul, který bychom pak importovali. Jenže ten by obsahoval jen jednu funkci, která se navíc nezdá být příliš užitečnou. Takže to rad>ji nechejme, jak to je.
gramatikaTeď už nám zbývá jenom vylepšit výpis výsledků tím, že do n>j zahrneme
další počítadla a vliv interpunkčních znaků. Existující funkci
vypsatStatistiky() nahraďte následujícím kódem:
def vypsatStatistiky():
global poc_vet, poc_klauzuli
for c in koncove_znaky:
poc_vet = poc_vet + poc_interpunkcnich_znaku[c]
for c in poc_interpunkcnich_znaku.keys():
poc_klauzuli = poc_klauzuli + poc_interpunkcnich_znaku[c]
print format % (sys.argv[1], poc_odstavcu,
poc_radku, poc_vet,
poc_klauzuli, poc_slov)
print "Byly pouzity nasledujici interpunkcni znaky:"
for c in poc_interpunkcnich_znaku.keys():
print "\t%s\t:\t%3d" % (c, poc_interpunkcnich_znaku[c])
Pokud jste pečliv> vložili všechny výše uvedené funkce na správná místa, m>li byste nyní po napsání
C:> python gramatika.py mujsoubor.txt
obdržet výpis statistických údajů pro váš soubor
mujsoubor.txt (ať už jej nazvete jak chcete). Užitečnost tohoto
programu je diskutabilní, ale snad vám sledování vývoje jeho kódu pomohlo
získat představu o tom, jak můžete tvořit své vlastní programy. Za hlavní
považuji to, abyste si vše zkoušeli. A ovšem, m>li byste si je také pečliv>
otestovat. V případ> tohoto programu například rychle zjistíte způsoby, jak
z n>j vylákat falešné odpov>di. Pokud například v>tu zakončíte třemi
tečkami, bude počítadlo v>t nabývat příliš vysoké hodnoty. Pro rozpoznání
takových situací můžete doplnit k tomu určený kód. Můžete se také
rozhodnout, že s ohledem na občasné použití programu vám podobné v>ci
nevadí. Je to na vás.
Nepovažuji za žádnou ostudu, když si vyzkoušíte n>kolik různých přístupů. Často b>hem toho získáte cenné zkušenosti.
Náš kurs ukončíme přepracováním modulu gramatika na použití
technik objektov> orientovaného programování. B>hem tohoto procesu uvidíte,
že objektov> orienovaný přístup vede k modulům, které jsou pro koncového
uživatele ješt> pružn>jší a také rozšířiteln>jší.
Jeden z nejv>tších problémů, se kterým se uživatel našeho modulu setká, spočívá ve spoléhání se na globální prom>nné. Vede to k tomu, že můžeme analyzovat vždy jen jeden dokument najednou. Jakýkoliv pokus o zpracování více dokumentů by vedl k přepisování hodnot globálních prom>nných.
Pokud přeneseme tyto globální údaje dovnitř třídy, můžeme vytvořit n>kolik instancí dané třídy (pro každý soubor jednu). Každá instance tím získá svou vlastní sadu prom>nných. Když navíc metody třídy dostatečn> rozčleníme, můžeme vytvořit architekturu, u které bude moci tvůrce objektu pro nový typ dokumentu snadno upravit kritéria tak, aby vyhovovala novým pravidlům (ze seznamu slov můžeme například vyloučit všechny HTML značky).
Náš první pokus vypadá takto:
#! /usr/local/bin/python ################################ # Modul: dokument.py # Autor: A.J. Gauld # Datum: 2000/08/12 # Verze: 2.0 ################################ # Tento modul poskytuje třídu Dokument, ze které # lze odvozovat další třídy pro různé kategorie # dokumentů (text, HTML, LaTeX, atd.). Jako vzor # jsou uvedeny třídy pro text a HTML. # # K nejdůležit>jším službám patří # - ziskejSkupinyZnaku(), # - ziskejSlova(), # - vypsatStatistiky(). ################################ import sys, string class Dokument: def __init__(self, jmenoSouboru): self.jmenoSouboru = jmenoSouboru self.poc_odstavcu = 1 self.poc_radku, self.poc_vet = 0, 0 self.poc_klauzuli, self.poc_slov = 0, 0 self.alfanum = string.letters + string.digits self.koncove_znaky = ['.','?','!'] self.interpunkcni_znaky = ['&', '(', ')', '-', ';', ':', ','] + self.koncove_znaky self.radky = [] self.skupiny = [] self.poc_interp_znaku = {} for c in self.interpunkcni_znaky + self.koncove_znaky: self.poc_interp_znaku[c] = 0 self.format = """%s obsahuje: %d odstavcu, %d radku a %d vet. Ty zase obsahuji %d klauzuli a celkem %d slov.""" def nactiRadky(self): try: self.vstSoubor = open(self.jmenoSouboru, "r") # Poznámka překladatele: Od verze Python 2 by se # m>la dávat přednost zápisu # self.vstSoubor = file(self.jmenoSouboru, "r") self.radky = self.vstSoubor.readlines() self.vstSoubor.close() except: print "Chyba pri cteni ze souboru ", self.jmenoSouboru sys.exit() def ziskejSkupinyZnaku(self, radky): for radek in radky: radek = radek[:-1] # odstraníme koncový '\n' self.poc_radku = self.poc_radku + 1 if len(radek) == 0: # prázdný => další odstavec self.poc_odstavcu = self.poc_odstavcu + 1 else: self.skupiny = self.skupiny + string.split(radek) # Poznámka překladatele: Od verze Python 2 lze psát # self.skupiny = self.skupiny + radek.split() def ziskejSlova(self): pass def vypsatStatistiky(self, odstavcu=1, radku=1, vet=1, slov=1, interpun=1): pass def Analyzuj(self): self.nactiRadky() self.ziskejSkupinyZnaku(self.radky) self.ziskejSlova() self.vypsatStatistiky() class TextovyDokument(Dokument): pass class HTMLDokument(Dokument): pass if __name__ == "__main__": if len(sys.argv) != 2: print "Pouziti: python dokument.py <jmeno souboru>" sys.exit() else: D = Dokument(sys.argv[1]) D.Analyzuj()
Následujícím krokem při implementaci této třídy je definice metody
ziskejSlova. Mohli bychom jednoduše okopírovat to, co jsme
vytvořili v předchozí verzi a vytvořit n>jakou metodu trim. Jenže my chceme,
aby byla objektov> orientovaná verze snadno rozšiřitelná. Takže místo toho
rozd>líme metodu ziskejSlova na posloupnost n>kolika kroků. V
odvozených třídách pak stačí přepsat jen nové verze t>chto podkroků a
nikoliv celou metodu ziskejSlova. To by m>lo usnadnit
zpracování mnohem širšího rozsahu typů dokumentů.
Konkrétn> přidáme metodu pro odmítnutí skupin, které budou rozpoznány
jako chybné, metodu pro odstran>ní necht>ných znaků ze začátku a z konce. To
znamená, že do třídy Dokument přidáme tři metody a metodu
ziskejSlova implementujeme pomocí nich.
class Dokument:
# ... viz výše
def ziskejSlova(self):
for w in self.skupiny:
self.orezatLeve(w)
self.orezatPrave(w)
self.odstranitVyjimky()
def odstranitVyjimky(self):
pass
def orezatLeve(self, slovo):
pass
def orezatPrave(self, slovo):
pass
Povšimn>te si, že t>la uvedených funkcí definují použití jediného příkazu
pass (čti pás; projít), který ned>lá vůbec nic. Místo n>j
budeme pozd>ji definovat způsob zpracování pro každý konkrétní typ
dokumentu.
Třída pro textový dokument vypadá takto:
class TextovyDokument(Dokument):
def orezatLeve(self, slovo):
while (len(slovo) > 0) and (slovo[0] not in self.alfanum):
ch = slovo[0]
if ch in self.poc_interp_znaku.keys():
self.poc_interp_znaku[ch] = self.poc_interp_znaku[ch] + 1
slovo = slovo[1:]
return slovo
def orezatPrave(self, slovo):
while (len(slovo) > 0) and (slovo[-1] not in self.alfanum):
ch = slovo[-1]
if ch in self.poc_interp_znaku.keys():
self.poc_interp_znaku[ch] = self.poc_interp_znaku[ch] + 1
slovo = slovo[:-1]
return slovo
def odstranitVyjimky(self):
self.skupiny = filter(lambda g: len(g) > 0, self.skupiny)
Ořezávací funkce (anglicky se tato funkčnost označuje slovem trim) jsou v
podstat> shodné s funkcí trim v našem modulu
gramatika.py, která byla ovšem rozd>lena na dv>. Funkce
odstranitVyjimky byla definována tak, aby odstraňovala prázdná
slova. Povšimn>te si použití funkce filter(), o které jsme se
zmínili v části v>nované funkcionálnímu
programování.
Při zpracování HTML dokumentů použijeme funkčnost jazyka Python, se
kterou jsme se ješt> nesetkali — regulární výrazy. Jde o
speciální řet>zcové vzorky, které se dají použít pro nalezení složit>jších
řet>zců. Použijeme je zde k odstran>ní čehokoliv mezi znaky < a >. To
znamená, že budeme muset předefinovat metodu ziskejSlova.
Odstraňování interpunkčních znamének by m>lo být shodné jako v případ>
zpracování holého textu. Takže místo abychom d>dili přímo z třídy
Dokument, odvodíme novou třídu z TextovyDokument a
použijeme jí definované metody pro ořezávání.
Takže třída HTMLDokument bude vypadat takto:
class HTMLDokument(TextovyDokument):
def odstranitVyjimky(self):
""" Použijeme regulární výrazy pro odstran>ní všech <.+?>"""
import re
tag = re.compile("<.+?>")# použijeme non greedy re
L = 0
while L < len(self.radky):
if len(self.radky[L]) > 1: # pokud řádek není prázdný
self.radky[L] = tag.sub('', self.radky[L])
if len(self.radky[L]) == 1:
del(self.radky[L])
else: L = L + 1
else: L = L + 1
def ziskejSlova(self):
self.odstranitVyjimky()
for i in range(len(self.skupiny)):
slovo = self.skupiny[i]
slovo = self.orezatLeve(slovo)
self.skupiny[i] = self.orezatPrave(slovo)
TextovyDokument.odstranitVyjimky(self)# odstraní prázdná slova
Poznámka: V tomto míst> stojí za zmínku pouze volání
self.odstranitVyjimky před ořezáváním a poté volání
TextovyDokument.odstranitVyjimky. Pokud bychom se spoléhali na
zd>d>nou metodu ziskejSlova, byla by po provedení ořezání
volána naše metoda odstranitVyjimky, což nechceme.
Poznámka překladatele: Pojem greedy (čti
grídy), který se objevil v poznámce u regulárního výrazu, odpovídá významu
anglického slovíčka — chamtivý, lakomý, žravý. Vzorek
<.+>, který bychom použili pro regulární výraz lze číst jako
řet>zec, který začíná levou úhlovou závorkou (znak 'menší'), pokračuje jedním a více
(znak plus) libovolných znaků (znak tečka) a končí pravou úhlovou závorkou
(znak 'v>tší'). Pokud neurčíme jinak, snaží se Python najít
odpovídající řet>zec, který je co nejv>tší (tj. greedy, neboli
žravé chování). To ovšem znamená, že by se k prvnímu znaku 'menší' na daném
řádku nalezl až poslední znak 'v>tší'. Při přeskakování libovolných znaků by
mohly být přeskočeny i znaky menší/v>tší, které se nacházejí mezi nimi. My
ovšem potřebujeme, aby se přeskakování zastavilo na nejbližším znaku
'v>tší', který ukončuje HTML značku. To znamená, že chceme předepsat opačné
chování (tj. non greedy), kdy se nalezne co nejkratší vyhovující
řet>zec. V jazyce Python tento požadavek vyjádříme tím, že za popis skupiny
doplníme otazník. Použijeme tedy vzorek <.+?>.
Pro vytvoření grafického uživatelského rozhraní použijeme Tkinter, který jsme stručn> představili v kapitole Událostmi řízené programování a kterému jsme se v>novali podrobn>ji v rámci tématu v>novanému grafickému uživatelskému rozhraní. Tentokrát vytvoříme o n>co propracovan>jší grafické uživatelské rozhraní a použijeme více ovládacích prvků, zvaných také widget, které nám Tkinter poskytuje.
Dříve než se do tohoto stadia dostaneme, musíme upravit naši třídu
Dokument. Dosavadní verze provádí zobrazení výsledků tiskem na
standardní výstup (stdout) v rámci metody
Analyzuj. Při vytváření grafického uživatelského rozhraní to
ale není to pravé. Místo toho bychom rad>ji přivítali, kdyby metoda Analyzuj
jednoduše uložila výsledky v atributech s charakterem počítadel, ke kterým
bychom pak přistupovali podle potřeby. Dosáhneme toho jednoduše rozd>lením
nebo refaktorizací metody vypsatStatistiky() na dv>
části: metodu vypocetStatistik(), která vyhodnotí výsledky a
uloží je v počítadlech, a na metodu tiskStatistik(), která
vytiskne výsledky na standardní výstup.
Nakonec musíme upravit metodu Analyzuj() tak, aby volala
metodu vypocetStatistik(), a hlavní sekvenci příkazů tak, aby
po volání metody Analyzuj() volala
tiskStatistik(). Po provedení t>chto úprav bude stávající kód
pracovat stejným způsobem, jako předtím — přinejmenším z pohledu
uživatele, který takový program spouští z příkazového řádku. Ostatní
uživatelé (tj. ti, kteří
zdrojový soubor používají jako modul, vytvářejí si sami instance třídy
Dokument a volají sami jeho metody) budou muset provést ve svém kódu
drobné zm>ny, kdy po použití metody Analyzuj() zavolají metodu
tiskStatistik() — a to není příliš obtížné.
Revidované úseky kódu vypadají takto:
def vypocetStatistik(self):
self.poc_slov = len(self.skupiny)
for c in self.koncove_znaky:
self.poc_vet = self.poc_vet + self.poc_interp_znaku[c]
for c in self.poc_interp_znaku.keys():
self.poc_klauzuli = self.poc_klauzuli + self.poc_interp_znaku[c]
def tiskStatistik(self):
print self.format % (self.jmenoSouboru, self.poc_odstavcu,
self.poc_radku, self.poc_vet,
self.poc_klauzuli, self.poc_slov)
print "Byly pouzity nasledujici interpunkcni znaky:"
for c in self.poc_interp_znaku.keys():
print "\t%s\t:\t%4d" % (c, self.poc_interp_znaku[c])
a t>lo provád>né při spušt>ní z příkazového řádku:
if __name__ == "__main__":
if len(sys.argv) != 2:
print "Pouziti: python dokument.py <jmeno souboru>"
sys.exit()
else:
try:
D = HTMLDokument(sys.argv[1])
D.Analyzuj()
D.tiskStatistik()
except:
print "Chyba pri analyze souboru: %s" % sys.argv[1]
Nyní jsme připraveni k tomu, abychom naše třídy dokumentů obalili grafickým uživatelským rozhraním.
V prvním kroku si pokusíme představit, jak to vše bude vypadat. Musíme zadat jméno souboru, takže budeme potřebovat ovládací prvky Edit nebo Entry. Musíme určit, zda chceme provád>t analýzu holého textu nebo obsahu v podob> HTML. Takový způsob výb>ru jedné z n>kolika možností je obvykle reprezentován sadou prvků typu Radiobutton. Tyto ovládací prvky by m>ly být sdruženy dohromady, aby bylo vid>t, že spolu souvisejí.
Dále požadujeme n>jaký způsob zobrazení výsledků. Mohli bychom použít n>kolik prvků typu Label — jeden pro každé počítadlo. Místo toho použiji jednoduchý prvek typu Text, do kterého můžeme vkládat řet>zce. Tento přístup se přibližuje duchu dřív>jšího řádkového výstupu, ale konkrétní způsob výstupu je v>cí volby návrháře.
Na záv>r, potřebujeme n>jaké prostředky pro zahájení analýzy a pro ukončení aplikace. Protože pro zobrazení výsledků použijeme ovládací prvek typu text, mohlo by být užitečné, kdybychom mohli do počátečního stavu uvést i zobrazování. Všechny uvedené příkazy mohou být vyjádřeny prvky typu Button (tlačítko).
Pokud si načrtneme podobu odpovídajícího grafického uživatelského rozhraní, dostaneme n>co takového:
+-------------------------+-----------+ | Jméno souboru | (*) text | | | ( ) HTML | +-------------------------+-----------+ | | | | | | | | | | +-------------------------------------+ | | | Analyzuj Vymazat Konec | | | +-------------------------------------+
Teď můžeme přistoupit k psaní kódu — krok po kroku:
from Tkinter import * import dokument ################### Definice tříd ###################### class AplikaceGramatika(Frame): def __init__(self, rodic=0): Frame.__init__(self, rodic) self.typ = 2 # vytvoř prom>nnou s počáteční hodnotou self.master.title('Pocitadlo gramatickych prvku') self.vybudovatUI()
Nejdříve jsme provedli import modulů Tkinter a dokument. V prvním případ>
jsme si v rámci vytvářeného modulu zajistili viditelnost všech jmen z
Tkinter, zatímco v druhém případ> budeme muset před jména přidávat předponu
'dokument'.
Definovali jsme i metodu __init__, která volá metodu
Frame.__init__ své bázové třídy. Tím se zajistí správná vnitřní
nastavení v rámci Tkinter. Poté vytváříme atribut, ve kterém bude uložena
hodnota typu dokumentu. A nakonec voláme metodu vybudovatUI,
která nám vytvoří všechny potřebné ovládací prvky.
def vybudovatUI(self):
# Informace o souboru: Jméno a typ
fSoubor = Frame(self)
Label(fSoubor, text="Jmeno souboru: ").pack(side=LEFT)
self.eJmeno = Entry(fSoubor)
self.eJmeno.insert(INSERT, "test.htm")
self.eJmeno.pack(side=LEFT, padx=5)
# Pro zajišt>ní zarovnání přepínacích tlačítek (radio buttons)
# se jménem potřebujeme další rámec.
fTyp = Frame(fSoubor, borderwidth=1, relief=SUNKEN)
self.rText = Radiobutton(fTyp, text="text",
variable = self.typ, value=2,
command=self.udalostText)
self.rText.pack(side=TOP, anchor=W)
self.rHTML = Radiobutton(fTyp, text="HTML",
variable=self.typ, value=1,
command=self.udalostHTML)
self.rHTML.pack(side=TOP, anchor=W)
# Na počátku vybereme 'text'
self.rText.select()
fTyp.pack(side=RIGHT, padx=3)
fSoubor.pack(side=TOP, fill=X)
# V textovém okn> se zobrazuje výstup. Použijeme vycpávku, abychom
# získali rámeček. Rodičovským rámcem bude rámec celé aplikace
# (tj. self)
self.txtBox = Text(self, width=60, height=10)
self.txtBox.pack(side=TOP, padx=3, pady=3)
# Nakonec umístíme příkazová tlačítka, která budou spoušt>t činnosti.
fTlacitka = Frame(self)
self.bAnalyzuj = Button(fTlacitka, text="Analyzuj",
command=self.udalostAnalyzuj)
self.bAnalyzuj.pack(side=LEFT, anchor=W, padx=50, pady=2)
self.bReset = Button(fTlacitka, text="Reset",
command=self.udalostReset)
self.bReset.pack(side=LEFT, padx=10)
self.bKonec = Button(fTlacitka, text="Konec",
command=self.udalostUkonceni)
self.bKonec.pack(side=RIGHT, anchor=E, padx=50, pady=2)
fTlacitka.pack(side=BOTTOM, fill=X)
self.pack()
Nebudu zde vysv>tlovat všechny detaily. Místo toho vám doporučuji k
nahlédnutí učebnici Tkinter, kterou naleznete na webových stránkách jazyka
Python. Jde o vynikající úvod i referenční příručku k Tkinter. Obecný
princip uvedeného
kódu spočívá ve vytváření ovládacích prvků odpovídajících tříd, při
kterém zadáváme nastavení formou pojmenovaných parametrů. Poté je
příslušný prvek umíst>n do svého obklopujícího rámce voláním metody
pack.
Poznamenejme, že k dalším klíčovým bodům patří použití pomocných prvků
typu Frame, které sdružují přepínací a příkazová tlačítka.
U přepínacích tlačítek (radio buttons) se mimo jiné uvádí dvojice parametrů
s názvy variable (prom>nná) a value (hodnota).
První z uvedených svazuje přepínací tlačítka dohromady tím, že udává stejnou
vn>jší prom>nnou (self.typ). Druhý parametr přid>luje každému
přepínacímu tlačítku jednoznačnou hodnotu. Všimn>te si také parametru
command=xxx, který se předává prvkům tlačítek. Jde o metody,
které bude Tkinter volat v okamžiku stisku tlačítka. Jejich kód je uveden
níže:
################# Metody pro ošetření událostí ####################
# je načase vše skoncovat...
def udalostUkonceni(self):
import sys
sys.exit()
# nastavíme vše do počátečního stavu
def udalostReset(self):
self.txtBox.delete(1.0, END)
self.rText.select()
# nastavíme hodnotu přepínacího tlačítka
def udalostText(self):
self.typ = 2
def udalostHTML(self):
self.typ = 1
Uvedené metody jsou velmi jednoduché a doufám, že není nutné je vysv>tlovat. Poslední metoda pro ošetření události zajišťuje provedení analýzy:
# Vytvoříme odpovídající typ dokumentu a provedeme analýzu.
# Poté zobrazíme výsledky v podob> řet>zců.
def udalostAnalyzuj(self):
jmenoSouboru = self.eJmeno.get()
if jmenoSouboru == "":
self.txtBox.insert(END,"\nNo filename provided!\n")
return
if self.typ == 2:
doc = dokument.TextovyDokument(jmenoSouboru)
else:
doc = dokument.HTMLDokument(jmenoSouboru)
self.txtBox.insert(END, "\nAnalyzuji...\n")
doc.Analyzuj()
str = doc.format % (doc.jmenoSouboru,
doc.poc_odstavcu, doc.poc_radku,
doc.poc_vet, doc.poc_klauzuli, doc.poc_slov)
self.txtBox.insert(END, str)
I výše uvedený text byste již m>li být schopni přečíst a pochopit, co d>lá. Jeho klíčové body jsou následující:
self.typ, která je nastavena podle přepínacích tlačítek.END metody insert. To znamená, že můžeme provést
n>kolik analýz a porovnávat výsledky. Ve srovnání s dříve zmín>ným
přístupem, kdy by byly výsledky zobrazovány formou ovládacích prvků s
popisným textem (label), jde o jednu z výhod použití výstupu do okna s
textem.Teď už nám zbývá jen vytvořit hlavní objekt aplikace a spustit smyčku pro zpracování událostí:
mojeAplikace = AplikaceGramatika() mojeAplikace.mainloop()
Podívejme se, jak vypadá konečný výsledek při spušt>ní v systému MS Windows. Zobrazeny jsou výsledky analýzy testovacího HTML souboru; nejdříve v režimu text a poté v režimu HTML:

A je to. Pokud chcete, můžete pokračovat ve zdokonalování zpracování HTML. Můžete vytvořit nové moduly pro nové typy dokumentů. Můžete zkusit zam>nit okno s textem za n>kolik prvků s popisným textem vložených do rámce. Ale z pohledu našeho původního zám>ru jsme hotovi. Následující kapitola nabízí nám>ty k dalšímu studiu v závislosti na vašich programátorských tužbách. Hlavní je, aby vás to bavilo. A vždycky si pamatujte: Počítač je hloupý!
Pokud vás napadne, co by se dalo na překladu této kapitoly vylepšit, zašlete e-mail odklepnutím Tím budou do dopisu automaticky vloženy informace o tomto HTML dokumentu.
$Id: cztutcase.html,v 1.7 2005/10/07 19:07:04 petr Exp $