O čem si budeme povídat?
Zpracování souborů začátečníky často přivádí do úzkých, ačkoliv důvody jsou pro mne tak trochu záhadou. Z pohledu programátora se soubory opravdu nijak neliší od souborů, které používáme při práci s textovým editorem nebo s jinou aplikací: musíme je otevřít, provedeme n>jaké operace s obsahem a zase je zavřeme.
Nejv>tší rozdíl spočívá v tom, že v programu se k souboru přistupuje sekvenčn>. To znamená, že čteme od jeho začátku, postupn> po jednom řádku. Textový editor často d>lá totéž, jenže si obsah celého souboru nejdříve načte do pam>ti, kde jej upravujete, a v okamžiku ukončení práce se souborem obsah pam>ti zapíše zp>t do souboru a uzavře jej. (Proto se vám může zdát, že textový editor nepoužívá postupné čtení obsahu souboru.) Další rozdíl spočívá v tom, že z programu obvykle soubor otvíráme jen pro čtení nebo jen pro zápis. Při zápisu můžeme vytvořit zcela nový soubor (nebo můžeme přepisovat obsah již existujícího souboru) nebo obsah připojujeme na konec (append) existujícího souboru.
Další operace, kterou můžeme při zpracování souboru použít, je skok zp>t na začátek.
Podívejme se na to v praxi. Budeme předpokládat, že existuje soubor zvaný
menu.txt a že obsahuje seznam jídel:
spam & vajíčka spam & opékané brambory spam & spam
Poznámka překladatele: Pojem spam jsme si již vysv>tlovali. Pokud nečtete texty učebnice postupn>, naleznete vysv>tlení zde. V jedné z epizod série Monty Python's Flying Circus se slovo spam objevovalo velmi hojn>. S překvapiv> podobnou frekvencí se v českých titulcích objevovalo slovo prejt. Není to totéž. Je to jen podobn> stručné, což je pro titulky (a pro dabing) asi důležité.
Nyní napíšeme program, který obsah souboru přečte a zobrazí jej na
výstupu — podobn> jako to v Unixu d>lá příkaz cat nebo v
DOSu příkaz type.
# Nejdříve soubor otevřeme ke čtení (r jako read). vstup = file("menu.txt", "r") # Soubor načteme do seznamu řádků a pak # každou položku seznamu (řádek) vytiskneme. for radek in vstup.readlines(): print radek # A nyní soubor zase zavřeme. vstup.close()
Poznámka 1: Operace file() vyžaduje dva argumenty.
Prvním z nich je jméno souboru. Můžeme je předat prostřednictvím prom>nné
nebo je můžeme zapsat přímo jako řet>zec, jako jsme to učinili zde.
(Takovému zápisu řet>zce se říká literál. Jde vlastn> o přímo zapsanou řet>zcovou
konstantu.) Druhý argument určuje režim. Ten říká, zda
soubor otvíráme pro čtení (r jako read) nebo pro zápis (w jako write).
Můžeme též určit, zda se jedná o ASCII text nebo o binární data —
přidáním 'b' za 'r' nebo za 'w' takto:
open(jm_soub, "rb").
Poznámka 2: K otevření souboru jsme použili funkci
file(). Starší verze jazyka Python místo ní používaly funkci
open(). Parametry obou funkcí jsou naprosto shodné. Používání
open() se stále dává přednost, takže v dalších ukázkách budeme
obvykle používat open(). Ale pokud se vám zdá používání
file() logičt>jší, používejte file().
Poznámka překladatele: V dokumentaci se dočteme, že
open() je alias pro file() a dočteme se zde
také:
Zám>rem je, aby byla funkciopen()i nadále dávána přednost, pokud ji používáme jako factory function[1], která vrací nový objekt typu soubor. Zápisfilese lépe hodí pro testování typu (například při voláníisinstance(f, file)).
Z objektového pohledu (viz dále) ale můžeme v zápisu file()
vid>t také vytváření objektu voláním jeho konstruktoru. Záleží tedy na tom,
jak se na v>c chcete dívat. Funkčn> jsou ob> volání naprosto shodná.
Poznámka 3: Ze souboru jsme četli a uzavírali jsme jej voláním funkcí, před které jsme připsali souborovou prom>nnou. Tomuto zápisu se říká volání metody a je to naše další setkání s objektovou orientací. Teď si s tím nelamte hlavu. Jenom si všimn>te, že to má svým způsobem vztah k modulům. O použité souborové prom>nné můžete uvažovat, jako kdyby to byla reference na modul, který obsahuje funkce pro práci se soubory a který se jakoby automaticky importuje pokaždé, když vytvoříme souborovou prom>nnou.
Poznámka 4: Na konci soubor uzavíráme voláním metody
close(). Python (a nejen Python) sice všechny
otevřené
soubory na konci programu uzavírá, ale mezi dobré zvyky patří předepisování
uzavírání souborů přímo. Proč? No, operační systém může odkládat zápis dat
do souboru až do doby, kdy je soubor uzavírán (kvůli zvýšení výkonu
systému). Pokud náhodou váš program skončí neočekávaným způsobem, riskujete,
že vaše drahocenná data nebudou do souboru zapsána vůbec. Takže poučení zní:
Jakmile ukončíte zápis do souboru, zavřete jej.
A teď uvažme, jak bychom se mohli vypořádat s dlouhými soubory. V prvé
řad> bychom museli soubor číst řádek po řádku. (V jazyce Python bychom místo
použití readlines() a cyklu for museli použít
readline() a cyklus while.) V takové situaci
bychom mohli bychom použít prom>nnou poc_radku, kterou bychom
zvyšovali při načtení každého řádku a testovali bychom, zda dosáhla
hodnoty 25 (počet řádku na obrazovce). Pokud by tato situace nastala,
požádáme uživatele, aby stiskl n>jaké tlačítko (dejme tomu Enter). Potom
bychom poc_radku nastavili na nulu a pokračovali bychom dál.
Můžete si to vyzkoušet jako cvičení….
Od verze 2.2 se Python k souborovému objektu umí chovat, jako kdyby to
byl seznam řádků. To znamená, že v cyklu for nemusíme používat
readlines(), ale jednoduše procházíme všemi řádky souboru. S
využitím této vlastnosti můžeme předchozí příklad přepsat takto:
# Nejdříve soubor otevřeme ke čtení (r jako read). vstup = file("menu.txt", "r") # Procházíme souborem a tiskneme každou položku (řádek). for radek in vstup: print radek # A nyní soubor zase zavřeme. vstup.close()
Výhoda tohoto stylu spočívá v tom, že nenarazíme na žádná omezení daná
velikostí pam>ti, jako v případ> použití readlines(). Takže se
vlastn> kombinují výhody cyklu for a výše zmín>ného řešení
využívajícího while/readline().
Ukázali jsme si skutečn> všechno, co pro zpracování souboru potřebujeme.
Otevřeme soubor, čteme z n>j a manipulujeme s načtenými daty jak potřebujeme.
Jakmile skončíme, soubor uzavřeme. V předchozím příkladu jste si mohli
všimnout jednoho malého zádrhele. Načtené řádky již na konci obsahují znak
konce řádku, takže když je vytisknete příkazem print, který
přidá navíc své konce řádků, bude výstup proložen prázdnými řádky. Abychom
se tomu vyhnuli, můžeme použít metodu zabudovaného řet>zcového typu
rstrip(), která z konce řet>zce odstraní všechny bílé
znaky — říká se jim také netisknutelné znaky. (Existují
i příbuzné metody lstrip() a strip(), které
odstraňují bílé znaky zleva, respektive z obou konců řet>zce.) Pokud tedy
část výše uvedeného příkladu upravíme do podoby...
for radek in vstup:
print radek.rstrip()
... m>lo by to dopadnout podle očekávání.
Když budeme v jazyce Python chtít zapsat program pro příkaz
copy, jednoduše otevřeme nový soubor pro zápis a místo tisku
načtených řádků na displej je budeme zapisovat do tohoto souboru:
# Vytvoříme obdobu příkazu: COPY MENU.TXT MENU.BAK # Nejdříve otevřeme soubory pro čtení (r) a pro zápis (w). vstup = open("menu.txt", "r") vystup = open("menu.bak", "w") # Řádky vstupního souboru kopírujeme do nového souboru. for radek in vstup: vystup.write(radek) print "1 soubor okopírován..." # Nyní soubory zavřeme. vstup.close() vystup.close()
Všimli jste si, že jsem na konci použil příkaz print k tomu,
aby uživatel poznal, že se n>co stalo? Podobná zp>tná vazba pro
uživatele je obvykle vhodná.
Protože jsme v tomto případ> zapisovali stejný řádek, který jsme před tím
načetli, nenastanou žádné problémy z konci řádků. (Metoda write() nepřidává
další konec řádku.) Pokud bychom ale cht>li zapisovat řet>zce, které
jsme si sami vygenerovali nebo které jsme před tím zbavili pravostranných
bílých znaků metodou rstrip(), pak bychom museli na konec
výstupního řet>zce znak nového řádku přidat. Ud>lali bychom to takto:
vystup.write(radek + '\n') # \n reprezentuje přechod na nový řádek
Podívejme se, jak metodu write() využijeme v našem
kopírovacím programu. Abychom soubor jen nekopírovali, přidáme na začátek
souboru dnešní datum. Tím z jednoduše upravovatelného textového souboru s
nabídkou jídel vygenerujeme denní menu. Stačí, když před vlastním kopírováním
obsahu souboru menu.txt připíšeme na začátek nového souboru pár
řádků:
# -*- coding: cp1250 -*- # Vytvoříme denní menu podle obsahu souboru menu.txt. import time # Nejdříve otevřeme soubory pro čtení (r) a pro zápis (w). vstup = open("menu.txt", "r") vystup = open("menu.prn", "w") # Připravíme si řet>zec s dnešním datem. dnes = time.localtime(time.time()) datum = time.strftime(u"%A %d. %B", dnes) # Přidáme řádek s nadpisem a prázdný řádek. vystup.write(u"Denní nabídka pro %s\n\n" % datum) # Řádky vstupního souboru kopírujeme do nového souboru. for radek in vstup: vystup.write(radek) print u"Menu pro %s bylo vytvořeno..." % datum # Nyní soubory zavřeme. vstup.close() vystup.close()
Povšimn>te si, že jsme použili modul time k získání aktuálního
data a času
(time.time()) a k převodu na n-tici souvisejících hodnot
(time.localtime()), které jsou zase použity funkcí
time.strftime() pro zformátování řet>zcové podoby data. Ten je
přes další formátovací řet>zec vložen do nadpisu. Výsledný soubor pak vypadá n>jak
takto:
Denní nabídka pro Sunday 07. August spam & vajíčka spam & opékané brambory spam & spam
Ačkoliv jsme na konec formátovacího řet>zce nadpisu
vložili dva znaky '\n', objevil se jen jeden prázdný řádek. Je to
tím, že první znak způsobil ukončení řádku s nadpisem a teprve ten druhý způsobil vygenerování
prázdného řádku. Správné vytváření a odstraňování znaků pro nový řádek
patří při zpracování textových souborů k jedné z t>ch otravn>jších v>cí.
Poznámka překladatele: Pokud se vám v nadpisu a ve vypisovaném
upozorn>ní objevily anglické názvy dne a m>síce — jak je naznačeno v
příkladu výstupu výše —, je to tím, že jste Pythonu neoznámili, jaké
místní jazykové zvyklosti (locale) se mají používat. Zkuste za řádek
import time přidat následující dva řádky:
import locale locale.setlocale(locale.LC_ALL, 'cz')
Výstupní soubor pak nabude o n>co lepší české podoby:
Denní nabídka pro ned>le 07. srpen spam & vajíčka spam & opékané brambory spam & spam
Musíme se však smířit, že Python neumí skloňovat česky. I kdyby to n>jakým
zázrakem um>l, při formátování data jsme stejn> nijak neuvedli, v jakém pádu
se má výsledek objevit, takže jména dne a m>síce budou uvedena v prvním pádu.
Povšimn>te si, že jsme na prvním řádku programu (formou speciálního komentáře)
uvedli kódování, ve kterém je uložen zdrojový text (zde pro Microsoft Windows,
podle potřeby je upravte). Povšimn>te si také, že řet>zce, které obsahují nebo
mohou obsahovat české znaky, uchováváme v kódování Unicode (před úvodní
uvozovkou píšeme písmeno u).
Celé téma konce řádků v textových souborech patří k temným stránkám nestandardizované implementace v různých operačních systémech. Rozdíly mají své kořeny v dávných dnech úsvitu datových komunikací, v magii ovládání mechanických dálnopisů. Nové řádky se indikují v podstat> třemi různými způsoby:
V různých operačních systémech se používají všechny tři techniky. V MS DOS (a odtud i v MS Windows) se používá třetí způsob. Unix (včetn> Linuxu) používá druhou metodu. Apple ve svém původním systému MacOS používá první metodu, ale v současnosti používá metodu druhou. Je to dáno tím, že MacOS X je ve skutečnosti variantou systému Unix.
Takže jak se má chudák programátor s takovou rozdílností zakončování
řádků vyrovnávat? V mnoha jazycích musí prost> více testovat a provád>t jiné
akce v závislosti na konkrétním operačním systému. V modern>jších jazycích,
včetn> Pythonu, máme k dispozici prostředky, které nám umožní tento zmatek
zvládnout. V případ> jazyka Python tato pomoc přichází v podob> modulu
os. Ten definuje prom>nnou zvanou linesep, která
obsahuje posloupnost konce řádku pro daný operační systém. Takže přidávání
nových řádků není složité. Pokud je chceme naopak odstranit, použijeme
rstrip(), který při odstraňování konce řádku zohlední
vlastnosti operačního systému. Takže pokud si chceme při zpracování konců
řádků zachovat příčetnost: k odstraňování konců řádků používejte vždy
rstrip() a před zápisem do souboru zakončujte řádky přidáním
os.linesep.
Stále zde zůstává nepříjemná situace, kdy byl soubor vytvořen v jednom
operačním systému a zpracovává se naj jiném, neslučitelném. Bohužel s tím
nic moc nenad>láme. Můžeme jen porovnat konce řádků s
os.linesep a zjistit, v čem se liší.
Poznámka překladatele: Podle mého názoru mají výše
vyjádřená skepse a uvedené rady smysl pouze v situaci, kdy obsah textového
souboru načítáme v binárním režimu. V textovém režimu se v systému
MS Windows posloupnost konců řádků převádí při čtení na \n
automaticky (a to nejen v Pythonu), při zápisu se zase automaticky provádí
převod na dvojznakovou posloupnost. Osobn> jsem se nikdy nemusel zabývat
popisovanou situací a nikdy jsem nemusel přímo používat
os.linesep.
V systému Unix se s tímto problémem můžeme setkat jen v případ>, kdy načítáme soubor vytvořený v jiném operačním systému. Aniž bych si to n>jak ov>řoval, předpokládám, že jde o starý problém, který se při práci se soubory v textovém režimu řeší už dávno. Se systémem MacOS ovšem nemám žádné zkušenosti.
Při zpracování souboru byste mohli ješt> chtít, aby se načtená data
přidávala na konec existujícího souboru. Jednou z možností by bylo otevřít
výstupní soubor pro čtení, načíst jeho obsah do seznamu, připojit k seznamu
data ze vstupního souboru a nakonec celý seznam zapsat jako novou verzi
původního výstupního souboru. Pokud by byly soubory krátké, pak to nezpůsobí
žádné problémy. Ale pokud je výstupní soubor velmi velký, třeba v>tší než
100MB, pak vám prost> při vytváření seznamu řádků dojde pam>ť. (I kdybyste m>li dostatečn>
velkou pam>ť, takový postup by byl časov> náročný.) Našt>stí můžeme
operaci open() určit další režim "a" (jako
append), který zajistí připojení dat na konec souboru — do souboru
prost> zapisujeme. Je to dokonce ješt> vylepšené tím, že pokud soubor
neexistuje, bude vytvořen nový soubor — jako kdybyste použili režim
"w".
Uveďme si příklad, kdy používáme takzvaný log[2] soubor, do kterého zapisujeme chybová hlášení. Přitom ale nechceme smazat předchozí záznamy, takže nové záznamy připisujeme na konec souboru (error = chyba; msg = message [mesidž] = zpráva):
def logError(msg):
err = open("Errors.log", "a")
err.write(msg)
err.close()
V reálném sv>t> bychom ovšem rádi n>jakým způsobem omezili velikost
souboru. B>žn> se používá technika, kdy se jméno souboru odvodí z aktuálního
data. Takže když se datum zm>ní, vytvoří se automaticky nový soubor. Správce
systému pak může snadno najít chyby, které se staly v určitý den. Může
snadno rozhodnout, které soubory jsou staré, archivovat je a odstranit v
případ>, kdy už nebudou potřebné. (Připomeňme si, že aktuální datum můžeme
zjistit pomocí funkcí modulu time — stejn> jako ve výše
uvedeném příkladu generování denní nabídky.)
Pamatujete si na příklad záznamníku s adresami, který jsme poprvé nakousli v tématu Data, datové typy a prom>nné a poté vylepšili v kapitole Konverzace s uživatelem? Teď z n>j ud>lám n>co opravdu užitečného tím, že obsah záznamníku budeme ukládat do souboru. Při startu programu jej samozřejm> budeme také načítat. Pro tyto účely si napíšeme pár funkcí. V tomto příkladu tedy spojíme n>kolik prvků a dovedností, kterými jsme se zabývali v předešlých tématech.
Náš základní návrh bude vyžadovat funkci, která při startu přečte obsah souboru, a další funkci, která jej při ukončování programu op>t do souboru zapíše. Prostřednictvím další funkce uživateli nabídneme možnost volby ze zobrazeného menu. A každou volbu položky z menu budou obsluhovat další funkce. Menu bude uživateli umožňovat:
jmeno_souboru = 'adresy.dat'
def nactiObsah(zaznamnik):
import os
if os.path.exists(jmeno_souboru):
soubor = open(jmeno_souboru, 'r')
for radek in soubor:
jmeno = radek.rstrip()
polozka = soubor.next().rstrip()
zaznamnik[jmeno] = polozka
else:
soubor = open(jmeno_souboru, 'w') # vytvoř nový prázdný soubor
soubor.close()
Povšimn>te si, že znaky konců řádků odstraňujeme voláním
rstrip(). Povšimn>te si také, že k získání dalšího řádku
souboru uvnitř t>la cyklu využíváme operace next(). A všimn>te
si také toho, že jsme jméno souboru uložili do prom>nné, která je definována
na úrovni modulu. To znamená, že prom>nnou jmeno_souboru můžeme
využít jak při načítání, tak při ukládání dat.
Poznámka překladatele k příkladu: Osobn> nejsem příznivcem řešení,
kdy se v jednom cyklu for načítají dva řádky souboru najednou.
První problém spočívá v tom, že by druhý řádek již nemusel být v souboru
přítomen (například díky chyb> při implementaci zápisu do souboru). V
takovém případ> vznikne při volání next() výjimka, kterou zde
neošetřujeme. To by ale nebylo nejhorší — chyba by se rychle
ukázala.
Za závažn>jší prohřešek považuji to, že zneužíváme znalosti
vnitřní implementace cyklu for a spoléháme se, že funguje práv>
tak, jak momentáln> funguje. Tuto znalost v kódu zveřejňujeme voláním metody
next() a mlčky předpokládáme, že je vše v pořádku. Jinými
slovy, ve zdrojovém textu tím vyjadřujeme přímou souvislost fungování cyklu
for a metody next(). Úvahy podobného typu se nám
mohou v budoucnu vymstít, protože by se například chování cyklu
for mohlo zm>nit. V tomto případ> to není pravd>podobné. Berte
to jako teoretickou možnost.
V tomto případ> bych se pravd>podobn> uchýlil k řešení, které by se tomuto problému vyhýbalo. Jeden záznam s celou adresou bych ukládal na jeden řádek. Jeho části bych vhodným způsobem na řádku při zápisu odd>loval a při načítání bych je (z jednoho řádku) odpovídajícím způsobem získal.
Za zbytečnou považuji celou v>tev else. Vytváření souboru
ani po logické stránce neodpovídá operaci, při které bychom čekali pouze
čtení ze souboru.
def ulozObsah(zaznamnik):
soubor = open(jmeno_souboru, 'w')
for jmeno, polozka in zaznamnik.items():
soubor.write(jmeno + '\n')
soubor.write(polozka + '\n')
soubor.close()
Povšimn>te si, že při zápisu dat musíme přidávat znak konce řádku
('\n').
def nactiVolbu(menu):
print menu
volba = int(raw_input(u'Zvolte možnost (1-4): '))
return volba
Poznámka překladatele: Python verze 2.4.1 a pravd>podobn> i
předchozí verze obsahují chybu v implementaci zabudované funkce
raw_input(). Pokud použijeme parametr v kódování Unicode a
program spustíme pod MS Windows v konzolovém okn>, vypíše se výzva v
neočekávaném kódování. Je to dáno tím, že MS Windows z historických
důvodů používají v konzolovém (DOSovém) okn> jiné kódování, než v oknech
grafického uživatelského rozhraní. Detailní popis chyby můžete nalézt
(anglicky) na SourceForge u projektu Python pod číslem 1099364.
Jeden z vývojářů Pythonu navrhuje dočasné řešení, kdy řet>zec s výzvou
převedeme do kódování, které používá stdout (souborový objekt
pro standardní výstup) takto:
import sys vyzva.encode(sys.stdout.encoding)
Nyní máme dv> možnosti. Buď si definujeme vlastní funkci, která co do
funkčnosti nahradí raw_input() a přid>líme jí vlastní jméno,
nebo upravíme funkčnost původní raw_input() jejím
předefinováním. Vybral jsem druhou možnost, protože ji lze využít pro opravu
stávajících programů. Vytvoříme novou stejnojmennou funkci, která bude
uvnitř volat její zabudovanou variantu (viz předpona
__builtins__):
def raw_input(vyzva):
import sys
return __builtins__.raw_input(vyzva.encode(sys.stdout.encoding))
Obecnou nevýhodou tohoto i alternativního přístupu je to, že předefinovaná funkce má omezenou oblast platnosti. Pokud se chcete zeptat na podrobnosti, učiňte tak odkazem na konci této stránky.
def pridejPolozku(zaznamnik):
jmeno = raw_input(u'Vložte jméno: ')
polozka = raw_input(u'Vložte ulici, m>sto a telefonní číslo: ')
zaznamnik[jmeno] = polozka
def odstranPolozku(zaznamnik):
jmeno = raw_input(u'Vložte jméno: ')
del(zaznamnik[jmeno])
def najdiPolozku(zaznamnik):
jmeno = raw_input(u'Vložte jméno: ')
if jmeno in zaznamnik: # v originále je zaznamnik.keys(), ale je to zbytečné
print jmeno, zaznamnik[jmeno]
else:
print u"Lituji. Pro '%s' nebyla nalezena žádná položka." % jmeno
Pro ukončení programu nebudeme psát n>jakou zvláštní funkci. Místo toho
budeme volbu na ukončení testovat v podmínce cyklu while. Takže
hlavní program bude vypadat takto:
def main():
menu = u'''
1) Přidej položku
2) Odstraň položku
3) Najdi položku
4) Uložit a konec
'''
zaznamnik = {}
nactiObsah(zaznamnik)
volba = nactiVolbu(menu)
while volba != 4:
if volba == 1:
pridejPolozku(zaznamnik)
elif volba == 2:
odstranPolozku(zaznamnik)
elif volba == 3:
najdiPolozku(zaznamnik)
else:
print u'Neočekávaná volba. Zkuste to znovu.'
volba = nactiVolbu(menu)
ulozObsah(zaznamnik)
Teď už zbývá jen zavolat při spušt>ní programu funkci
main(). Zajistíme to při použití trošky pythonovské magie:
if __name__ == '__main__':
main()
Tento záhadný úsek kódu nám umožní spoušt>t pythonovský soubor buď jako
modul tím, že ho importujeme, nebo jako program tim, že ho
spustíme. Rozdíl při
uvedených dvou použitích pythonovského souboru spočívá v tom, že při
importování modulu je vnitřní prom>nné __name__ (dva znaky podtržení na
začátku jména a dva na konci) přiřazeno jméno modulu. Pokud je soubor
spušt>n přímo (tj.
použit jako samostatný program), je prom>nná __name__
nastavena na hodnotu '__main__'. Tajemné, že?
Pokud nyní vložíte všechny kousky kódu do nového textového souboru a
uložíte jej jako adresy.py, m>lo by to jít spustit z
příkazového řádku operačního systému tím, že napíšete:
C:\PROJEKTY> python adresy.py
Nebo prost> v Průzkumníku (Explorer, MS Windows) poklepete na ikonu. M>lo by se spustit nové DOSové okno a po ukončení programu by m>lo zas zmizet.
V Linuxu by to vypadalo podobn>:
$ python adresy.py
Prostudujte si uvedený kód, zkuste v n>m najít chyby (nechal jsem tam přinejmenším dv>, ale může jich tam být i více) a zkuste je opravit. Výsledný cca 70 řádkový program je typickým představitelem programů, které byste mohli začít psát pro svou vlastní potřebu. Pár v>cí se v n>m dá vylepšit — dostaneme se k tomu v další části —, ale i v této podob> jde o rozumn> užitečný, malý nástroj.
Ani jeden z jazyků VBScript a JavaScript nepodporuje práci se soubory.
Jde o rys související s bezpečností. Zajišťuje, že nikdo nebude schopen číst
vaše soubory v situaci, kdy jste si nevinn> stáhli n>jakou webovou stránku.
Na druhou stranu se tím však omezuje obecná použitelnost obou jazyků. V
části zabývající se znovupoužitelností modulů jsme se ale dozv>d>li, že si v
tomto sm>ru můžeme pomoci, když použijeme Windows Script Host. WSH nám dává k dispozici
objekt FileSystem, který jakémukoliv WSH jazyku umožní čtení
souborů. Nejdříve se podíváme na příklad v JavaScript a potom si jej
srovnáme s řešením v jazyce VBScript. Ale znovu uvidíme, jako v předchozím
případ>, že klíčovými prvky řešení budou volání WScript objektů.
Než se dostaneme k podrobnostem, m>li bychom se zmínit o objektovém
modelu FileSystem. Objektovým modelem rozumíme sadu
vzájemn> souvisejících objektů (tříd), které může programátor
přímo využívat. V rámci WSH se objektový model FileSystem
skládá z objektů FSO a z řady objektů typu File, včetn>
objektu TextFile, který budeme používat. Najdeme zde také
pomocné objekty. Pro naše účely z nich budeme využívat objekt
TextStream. V podstat> budeme postupovat tak, že vytvoříme
instanci objektu třídy FSO, tu
použijeme pro vytvoření objektů třídy TextFile a z
nich vytvoříme objekty TextStream. Do nich budeme zapisovat
nebo z nich budeme číst.
The TextStream objects
themselves are what we actually read/write from the files.
Následující kód uložte do souboru nazvaného zkusSoubory.js a
spusťe jej pomocí cscript způsobem, který jsme použili v úvodu
k WSH (tedy
cscript zkusSoubory.js).
Abychom ve WSH mohli otevřít soubor, musíme si vytvořit objekt typu
FSO a poté jeho prostřednictvím vytvořit objekt
TextFile.
var jmenoSouboru, fso, inSoubor, outSoubor, radek; // Získáme jméno souboru. fso = new ActiveXObject("Scripting.FileSystemObject"); WScript.Echo("Jak se bude soubor jmenovat? "); jmenoSouboru = WScript.StdIn.Readline(); // Otevřeme inSoubor pro čtení a outSoubor pro zápis. inSoubor = fso.OpenTextFile(jmenoSouboru, 1); // režim 1 = čtení jmenoSouboru = jmenoSouboru + ".BAK" outSoubor = fso.CreateTextFile(jmenoSouboru);
// Cyklus přes vstupní soubor, dokud nenarazíme na konec.
while ( ! inSoubor.AtEndOfStream){
radek = inSoubor.ReadLine();
WScript.Echo(radek);
outSoubor.WriteLine(radek);
}
inSoubor.close(); outSoubor.close();
<?xml version="1.0" encoding="UTF-8" ?> <job> <script language="VBScript"> Dim fso, inSoubor, outSoubor, inJmenoSouboru, outJmenoSouboru Set fso = CreateObject("Scripting.FileSystemObject") WScript.Echo "Zadejte jméno souboru pro zálohu." inJmenoSouboru = WScript.StdIn.ReadLine outJmenoSouboru = inJmenoSouboru & ".BAK" ' Otevřeme soubory. Set inSoubor = fso.OpenTextFile(inJmenoSouboru, 1) Set outSoubor = fso.CreateTextFile(outJmenoSouboru) ' Čteme soubor a vytváříme záložní kopii. While not inSoubor.AtEndOfStream line = inSoubor.ReadLine outSoubor.WriteLine(line) Wend ' Uzavřeme oba soubory. inSoubor.Close outSoubor.Close WScript.Echo inJmenoSouboru & " zálohován do " & outJmenoSouboru </script> </job>
Poznámka překladatele: Soubor uložíme do souboru
zkusSoubory.wsf a spustíme:
C:\PROJEKTY> cscript zkusSoubory.wsf
Zpracování textu patří k jednomu z nejb>žn>jších programátorských úkolů. Občas ale potřebujeme zpracovávat i binární data. V jazycích VBScript nebo JavaScript se s tímto problémem setkáme velmi zřídka — už jen proto, že nemají přímou podporu práce se soubory —, takže se budeme zabývat jen tím, jak se s tím vypořádáme v jazyce Python.
Klíčový rozdíl mezi textovými a binárními soubory spočívá v tom, že
textové soubory jsou složené z oktetů (nebo bajtů) s binárními
daty, kde každý bajt reprezentuje znak. Konec souboru je označen speciálním
bajtem, kterému se anglicky obecn> říká end of file [end of fajl] nebo
eof (čili konec souboru). Binární soubor obsahuje libovolná binární
data, takže pro identifikaci konce souboru nemůže být použita žádná
speciální hodnota. Pro čtení takových souborů se proto musí použít jiný
režim. Pokud tedy v Pythonu (nebo v jiném programovacím jazyce) otvíráme
binární soubor, musíme jej otevřít v binárním režimu. V opačném případ>
riskujeme, že soubor bude ukončen v míst> prvního výskytu znaku
eof, který Python nalezne mezi binárními daty. Binárního
otevření souboru v jazyce Python dosáhneme tak, že k parameru režimu
přidáme 'b':
binarniSoubor = file('binSoubor.bin', 'rb')
Jedinou odlišnost od otvírání textového souboru představuje hodnota
režimu 'rb'. Písmeno 'b' můžeme přidat i k
ostatním režimům: 'wb' pro zápis, 'ab' pro
připojování za konec souboru.
Uzavírání binárního souboru se provádí stejn>, jako u textového souboru.
Jednoduše zavoláme metodu otevřeného souborového objektu
close():
binarniSoubor.close()
Protože soubor byl otevřen v binárním režimu, nemusíme Pythonu poskytovat n>jakou další zvláštní informaci — Python ví, jak má soubor korektn> uzavřít.
Poznámka překladatele: Můj osobní pohled na rozdíl v binárních a textových souborech je trochu jiný. Hlavní odlišnost spatřuji v tom, zda existuje obecn> přijímaná interpretace uložených dat, či nikoliv. Jinými slovy se dá říci, že odlišnost spočívá v tom, zda existuje obecn> přijímaný abstraktní pohled na uložená data. Obecn> se přijímá to, že v textovém souboru jsou uloženy znaky a že se textový soubor člení na řádky. Jde o technický pohled. Z jazykového hlediska bychom mohli text členit na odstavce, v>ty, slova, znaky uvnitř slov a znaky, které nejsou součástí slov. Zůstaňme u technického pohledu.
Textový soubor lze v současnosti považovat za pouhou variantu binárních souborů. Dohodnutá interpretace si vynucuje, aby obsahoval jen určitou podmnožinu všech možných binárních kombinací. Pokud víme, že se na soubor máme dívat jako na textový, jsme například schopni vytisknout nebo jinak zobrazit jeho lidsky čitelnou podobu.
Pokud bychom cht>li být přísní, m>l by textový soubor obsahovat pouze
písmena, další tisknutelné znaky, mezeru a sekvence pro odd>lování řádků (CR
a LF — viz poznámka výše). Odd>lovače řádků patří mezi takzvané
řídicí znaky. Tento pojem pochází z doby dálnopisů — tyto
znaky řídily činnost dálnopisu, pokud zrovna nem>l n>co tisknout. Historické
a technické souvislosti způsobily, že se za součást textových souborů
považují i další řídicí znaky, jako je znak tabulační ('\t'),
znak zp>tného posuvu ('\b' z anglického back), znak
alarm ('\a', dálnopis cinknul), a další. Patří sem i
znak, který ukončuje konec souboru, ale v současných textových souborech se
nepovažuje za povinný.
Binární soubor žádnou dohodnutou interpretaci nemá. To znamená, že bez dalších znalostí nevíme, co data znamenají, jak s nimi máme zacházet, kolik bajtů nebo bitů tvoří jeden informační celek, zda soubor obsahuje posloupnost informačních jednotek o stejné velikosti, či nikoliv, atd.
Stupn> abstrakce se tedy při práci se soubory v binárním a v textovém režimu liší. Práci se soubory v binárním režimu musíme z hlediska zpracování systémem považovat za práci na nižší úrovni abstrakce. S vyšší úrovní abstrakce textových souborů souvisí existence n>kterých operací, jako je například načtení jednoho řádku.
Než si řekneme, jak můžeme přistupovat k datům v binárním souboru, m>li bychom se dozv>d>t n>co o tom, jakým způsobem jsou data reprezentována a ukládána v počítači. Veškerá data jsou ukládána jako posloupnosti binárních číslic (binary digit), bitů. Bity se sdružují do skupin po 8 nebo po 16 a nazývají se bajty (bytes), respektive slova (words). (Skupiny po 4 bitech se n>kdy nazývají nibble.) Bajt může obsahovat jeden z 256 různých vzorků, kterým jsou přiřazeny hodnoty 0–255.
Veškeré informace, se kterými v našich programech manipulujeme —
řet>zce, čísla a další —, musí být převedeny na posloupnosti bajtů. To
znamená, že pro znaky, které jsme použili v řet>zci, musíme vyhradit
odpovídající vzorek bajtů. V minulosti se používalo n>kolik způsobů
kódování, ale nejpoužívan>jším se stalo takzvané ASCII
kódování (American Standard Coding for
Information Interchange). Čisté ASCII je nanešt>stí definováno
jen pro 128 hodnot, což nestačí pro použití v neanglických jazycích. Pozd>ji
byl navržen nový kódovací standard, známý jako Unicode. Ten pro
ukládání datové reprezentace znaků používá místo bajtů slova, což umožňuje
kódovat přes 65000 znaků. Pokud použijeme kódovací formát UTF-8, pak původní
soubory v ASCII kódování představují korektní reprezentaci Unicode textu.
Python standardn> podporuje kódování ASCII. Pokud před zápis řet>zce uvedeme
písmeno u, bude se řet>zec považovat za řet>zec v
kódování Unicode.
Poznámka překladatele: O v>cech souvisejících se standardem Unicode se můžeme podrobn>ji dočíst na stránkách http://www.unicode.org/. Technický úvod ke standardu Unicode naleznete na stránce http://www.unicode.org/standard/principles.html (principy, formáty).
Výše uvedená informace o počtu bajtů na znak a o počtu kódovaných znaků je nepřesná. Standard Unicode verze 4.0 definuje kódy pro 96447 znaků. Unicode verze 4.1.0 přidává dalších 1273 znaků. Standard definuje jednoznačné kódy pro každý znak. Pro ukládání do souboru se používají kódovací formáty UTF-8, UTF-16 a UTF-32. Určují způsob, jakým se jednoznačné číslo znaku převede do binární podoby pro uložení v souboru. Pokud použijeme formát UTF-32, pak je každý znak kódován na 4 bajtech. Pokud použijeme UTF-16, pak je v>tšina znaků kódována na 2 bajtech a n>které na 4 bajtech. Pokud použijeme kódování UTF-8, pak jsou ASCII znaky kódovány na jednom bajtu, ale n>které znaky vyžadují až 4 bajty.
Pokud chceme pracovat s Unicode řet>zci v neanglických jazycích, musíme na začátku zdrojového textu pythonovského programu uvést speciální komentář, který říká, v jakém kódování je zdrojový text uložen. Při překladu zápisu řet>zce pak může dojít ke korektnímu převodu zápisu do Unicode.
Do binárního kódování musíme převád>t i čísla. Pro malá celá čísla stačí přímo využít hodnoty jednoho bajtu. Ale pro čísla v>tší, než 255 (nebo pro záporná čísla, nebo pro racionální čísla) musíme učinit n>co navíc. B>hem doby se objevila celá řada standardů pro kódování numerických dat. Využívá je v>tšina programovacích jazyků a operačních systémů. Řadu způsobů kódování čísel s plovoucí řádovou čárkou vydal například americký Institute of Electrical and Electronic Engineering (IEEE).
Pointa spočívá v tom, že při čtení binárního souboru musíme v našem programu zajistit převod surových bitových vzorků na hodnotu správného datového typu. Sérii bajtů, kterou jsme původn> zapsali jako řet>zec znaků, můžeme klidn> načítat jako sérii čísel v plovoucí řádové čárce. Původní význam se tím samozřejm> ztratí. Chci jen naznačit, že stejný bitový vzorek může reprezentovat oba případy. Pokud tedy načítáme binární data, je velmi důležité, abychom je převedli na správný datový typ.
structPro kódování a dekódování binárních dat můžeme v Pythonu využít modulu
struct (zkratka pro structure, tedy struktura). Tento
modul pracuje podobn>, jako když jsme používali formátovací řet>zec pro tisk
dat různého typu. Zadáváme řet>zec, který reprezentuje typ načítaných dat, a
ten je použit pro proud bajtů, který se pokoušíme interpretovat. Modul
struct můžeme použít také pro převod dat na proud bajtů určených
k zápisu do binárního souboru (nebo dokonce do komunikační linky).
Modul definuje řadu kódů pro převod formátů, ale my zde použijeme jen kódy
pro celá čísla a pro řet>zce. (Ostatní kódy si můžete vyhledat v dokumentaci k
modulu struct, který je součástí distribuce Pythonu. Kódy pro
celé číslo a řet>zec jsou i respektive s.
Formátovací řet>zec modulu struct se skládá z posloupnosti kódů,
kterým jsou předřazena čísla, určující kolik prvků příslušného typu chceme
získat. Tak například zápis 4s znamená, že chceme řet>zec o délce
4 znaky.
Poznámka překladatele: Číslo před řet>zcovou značkou
se chápe jinak, než čísla před značkami pro jiné typy. U řet>zce udává délku
získávaného řet>zce, u značek ostatních typů jde skutečn> o počet hodnot
daného typu. Tak například značka 10s vyjadřuje jeden
řet>zec o délce 10 znaků, zatímco značka 10c vyjadřuje
deset jednoznakových řet>zců (Python nezná typ znak).
Dejme tomu, že bychom cht>li detaily adresy ve výše zmín>ném záznamníku adres zapisovat jako binární data, kde by číslo domu bylo uloženo jako celé číslo a zbytek by byl uložen jako řet>zec. (Z praktického hlediska to zase není tak dobrý nápad, protože čísla domů n>kdy obsahují i písmena.) Formátovací řet>zec by pak vypadal n>jak takto:
'i34s' # Předpokládáme, že na adresu je vyhrazeno 34 znaků.
Pokud bychom potřebovali pracovat s různou délkou adresy, mohli bychom si napsat funkci, která vytvoří její binární podobu takto:
def formatujAdresu(adresa):
# split rozd>lí řet>zec na seznam 'slov'.
slova = adresa.split()
cislo = int(slova[0])
zbytek = ' '.join(slova[1:])
format = "i%ds" % len(zbytek) # vytvoř formátovací řet>zec
return struct.pack(format, cislo, zbytek)
Takže adresu jsme rozsekali na kousky metodou split() zabudovaného typu
řet>zec. První slovo jsme převedli na číslo a ostatní slova jsme op>t
spojili mezerami do jednoho řet>zce. Jeho délku potřebujeme pro vygenerování
formátovacího řet>zce pro metodu modulu struct.
Funkce formatujAdresu() vrací posloupnost bajtů, které
zachycují binární vyjádření zadané adresy. Když už tedy máme potřebná binární
data, podívejme se, jak je můžeme do binárního souboru zapsat a zase je zp>t
přečíst.
structVytvoříme si binární soubor, který bude obsahovat jediný řádek adresy převedený do binární
podoby výše nadefinovanou funkcí formatujAdresu(). Soubor
musíme otevřít pro zápis v binárním režimu ('wb'), zakódujeme
data, zapíšeme je do souboru a ten následn> uzavřeme. Vyzkoušejme si to:
import struct
f = file('adresa.bin','wb')
data = "10 Ulice, M>sto, 0171 234 8765"
bindata = formatujAdresu(data)
f.write(bindata)
f.close()
Otevřením souboru adresa.bin v Poznámkovém bloku (notepad,
případn> v jiném
editoru) si můžete ov>řit, že data byla skutečn> zapsána v binárním
tvaru. Znaky adresy sice budou čitelné, ale neuvidíme zde žádné číslo 10.
Abychom adresu ze souboru op>t přečetli, musíme jej otevřít v režimu
'rb', načíst data jako posloupnost bajtů, uzavřít soubor a
nakonec data rozbalit metodou unpack() modulu
struct. K tomu op>t potřebujeme formátovací řet>zec. Otázka zní,
jak by m>l vypadat? V našem případ> víme, že musí být stejný jako ten, který
jsme si připravili uvnitř funkce formatujAdresu() —
konkrétn> iNs, kde N musíme nahradit konkrétním číslem. Ale jak
hodnotu N zjistíme?
V modulu struct najdeme také pomocné funkce, které vracejí
velikost každého datového typu. Když si spustíme Python v interaktivním
režimu, pak po pár pokusech zjistíme, kolik bajtů zabírají hodnoty různého
datového typu:
>>> import struct
>>> print struct.calcsize('i')
4
>>> print struct.calcsize('s')
1
Takže teď už víme, že číslo zabere 4 bajty a každý znak řet>zce zabere jeden bajt. To znamená, že N spočítáme jako délku dat mínus 4. Vyzkoušejme si načíst obsah našeho souboru:
import struct
f = file('adresa.bin','rb')
data = f.read()
f.close()
format = "i%ds" % (len(data) - 4)
cislo, zbytek = struct.unpack(format, data)
adresa = str(cislo) + ' ' + zbytek
print adresa
Co se týká binárních souborů je to vše, k čemu jsem se cht>l vyjádřit. Jist> jste si všimli, že používání binárních dat vede ke komplikacím. Pokud k tomu nemáte velmi dobrý důvod, pak uvedený přístup rozhodn> nedoporučuji. Pokud ovšem skutečn> potřebujete číst binární soubor, je to možné. V takovém případ> ovšem musíte v>d>t, co data reprezentují.
Poznámka překladatele: V uvedeném příkladu předpokládáme, že pracujeme s řet>zci, kde je každý znak uložen na jednom bajtu. Pokud bychom navíc potřebovali pracovat s Unicode řet>zci, pak se při použitém kódování UTF-8 může počet bajtů pro uložení znaku m>nit. Nov>jší verze jazyka Python navíc podporují i celočíselný typ, kde hodnota tohoto typu může být v>tší, než jakou můžeme zachytit na 4 bajtech. Pořadí ukládaných bajtů čísla se navíc řídí pravidly konkrétního výpočetního prostředí (little/big endian). Pokud tedy chceme zařídit přenositelnost takto vygenerovaných binárních souborů do na jiné systémy, musíme si pomoci explicitním uvedením dalších formátovacích značek, které předepíší konkrétní pořadí ukládání bajtů. V>ci mohou být mnohem komplikovan>jší, než se na první pohled zdá.
Zapamatujte si
readlines() jazyka Python přečte všechny řádky
souboru najednou, zatímco funkce readline() přečte jen jeden
řádek. Může nám to pomoci šetřit pam>tí.b'.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: cztutfiles.html,v 1.9 2005/10/07 19:16:51 petr Exp $