Práce se soubory

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.

Soubory — vstup a výstup

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 funkci open() i nadále dávána přednost, pokud ji používáme jako factory function[1], která vrací nový objekt typu soubor. Zápis file 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).

Konce řádků v různých operačních systémech

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:

  1. Znak návratu vozíku (Carriage Return — CR; '\r').
  2. Znak posunu o řádek (Line Feed — LF; '\n').
  3. Dvojice CR/LF ('\r\n').

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.)

Oprášený příklad záznamníku s adresami

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:

Načtení obsahu záznamníku

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.

Uložení obsahu záznamníku

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').

Načtení uživatelského vstupu

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.

Přidání položky

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

Odstran>ní položky

def odstranPolozku(zaznamnik):
    jmeno = raw_input(u'Vložte jméno: ')
    del(zaznamnik[jmeno])

Nalezení položky

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

Ukončení programu

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.

VBScript a JavaScript

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).

Otevření souboru

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);

Čtení a zápis

// Cyklus přes vstupní soubor, dokud nenarazíme na konec.
while ( ! inSoubor.AtEndOfStream){
    radek = inSoubor.ReadLine();
    WScript.Echo(radek);
    outSoubor.WriteLine(radek);
    }

Uzavření souborů

inSoubor.close();
outSoubor.close();

Teď v jazyce VBScript

<?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 &amp; ".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 &amp; " zálohován do " &amp; outJmenoSouboru
  </script>
</job>

Poznámka překladatele: Soubor uložíme do souboru zkusSoubory.wsf a spustíme:

C:\PROJEKTY> cscript zkusSoubory.wsf 

Práce s netextovými soubory

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.

Otvírání a uzavírání binárních souborů

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.

Reprezentace dat a jejich ukládání

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.

Modul 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.

Čtení a zápis s využitím modulu 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

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 $