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 $