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ápisfile
se 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 import
ujeme, 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.
struct
Pro 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.
struct
Vytvoří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 $