int21h

Práce se soubory v pascalu

Pascal je DOSový programovací jazyk, a proto jsou zákonitosti práce se soubory odvozeny od realizace správy souborů v systému DOS. Tedy, každý soubor můžeme chápat buď jako textový nebo jako binární a záleží jen na nás, v jakém režimu k němu budeme přistupovat.
Další podstatný detail je skutečnost, že standardní funkce Turbo pascalu, včetně verze 7.01, neznají dlouhá jména souborů. Pokud je chceme používat, musíme si sami napsat rozhraní k příslušným službám DOSu. K dispozici naštěstí je knihovna, která dané funkce, sice kostrbatě, ale implementuje.
Freepascal je novější a jeho standardní funkce LFN znají. Pokud z jakéhokoliv důvodu chceme použití LFN zakázat, uděláme to nastavením proměnné LFNsupport.
LFNsupport:=false;
Implicitně je samozřejmě true.

Klasické prostředky

Vyjmenoval jsem všechny standardní procedury a funkce jazyka pascal, které mají co do činění se soubory.
V první řádce jsou vyjmenovány prostředky, které lze použít na oba způsoby práce - na textové i binární soubory. Výjimkou je funkce Eoln, již lze použít výlučně na textové soubory. Jak se tedy pracuje s textovými soubory?

Textové soubory

Tento prográmek zkopíruje jeden soubor do druhého:
const zdroj = 'text.txt';
      cil   = 'text.new';

var f,g:text;      {chceme-li pracovat se soubory jako textovymi, pouzijeme typ TEXT}
    s:string;
    p:longint;
begin
p:=0;
Assign(f,zdroj);   {prirazeni jmena souboru k promenne - nic jineho nedela}
Assign(g,cil);
Reset(f);          {otevre soubor F}
Rewrite(g);        {zalozi soubor G - jestli uz takovy existuje, bude prepsan}
while not Eof(f) do     {dokud nejsme na konci souboru...}
   begin
   inc(p);
   Readln(f,s);         {z F nacti radku textu}
   Writeln(g,s);        {...a uloz ji do G}
   end;
{hotovo, zpracovali jsme vsechny radky souboru}
Close(f);          {uzavru zdrojovy soubor}
Close(g);          {a cilovy taky}
writeln('Soubor mel ',p,' radek.');
end.
Tento způsob kopírování má výhodu, že pracujeme po jednotlivých řádcích a proto i v TP dokážeme zpracovat soubory větší než 64 KB. Nevýhodou je, že typ string udrží max. 255 znaků. Vyskytne-li se v souboru řádka delší, jsou všechny znaky za 255. ignorovány. Je nepravděpodobné, že byste se s takovýmto textovým souborem setkali, ale stane se vám to, když budete tímto způsobem kopírovat jiné soubory než texty (soubory EXE, JPG, atd.)
Všimněte si řádku Readln(f,s)
Parametr F zajišťuje, že se data načtou ze souboru a ne z klávesnice jako obvykle.
Analogicky funguje parametr G u příkazu Writeln(g,s)
Soubory, které otvíráme jako textové, jsou obvykle prostě text - jen výjimečně mají nějakou vnitřní strukturu. Raritně se to ale přihodit může:
Předpokládejme , že máte takovýto soubor SACHISTI.TXT:
ELO vek jméno
-----------------------------------------------------
2312 62 Kačírek Petr
2300 24 Petrášek Ondřej
2277 33 Ščasná Iveta
2239 50 Hoznour René
2215 22 Lapša Vilém
Zpracujeme ho následovně:
const zdroj = 'test.txt';
      max = 50;
type sachista = record
     elo:integer;       {pro ty, co nevedi, ELO je vykonnost sachisty}
     vek:byte;
     jmeno:string[30];
     end;
var f:text;      {chceme-li pracovat se soubory jako textovymi, pouzijeme typ TEXT}
    s:string;
    p,q:integer;
    u:char;
    hrac:array[1..max] of sachista;
begin
p:=0;
Assign(f,zdroj);
Reset(f);
readln(f);         {preskocim prvni dve radky}
readln(f);
while not Eof(f) do     {dokud nejsme na konci souboru...}
   begin
   inc(p);
   Read(f,hrac[p].elo);     {nechceme nacist celou radku, ale jen prvni cislo}
   Read(f,hrac[p].vek);     {proto Read, ne Readln}
   Read(f,u);                  {nacteme "prebytecny" znak - oddelovaci mezeru}
   Readln(f,hrac[p].jmeno);    {a cele to ukoncime nactenim jmena}
   end;
Close(f);          {uzavru zdrojovy soubor}
writeln('Na soupisce je ',p,' hracu:');
for q:=1 to p do
    writeln(hrac[q].jmeno,#9,hrac[q].vek,#9,hrac[q].elo);
readln;
end.
Většina programátorů by ale tuto úlohu řešila tak, že by načetli celou řádku a pak by ji rozebírali řetězcovými funkcemi. Postupné čtení má totiž závažná omezení:
1) je-li za řetězcovým typem číslo, obvykle se již nenačte, neboť je považováno za součást řetězce. To se dá obejít jenom explicitním deklarováním délky řetězce.
2) čísla v souboru musí být od sebe oddělena mezerami. Lhostejno, jestli jednou nebo více. (není možné toto: 2312, 62, Kačírek Petr toto ale možné je: 2312 62 Kačírek Petr)
3) když je porušen bod 2, tak program skončí s chybovou hláškou 106

Binární soubory

Binární soubory se rozdělují na typové a netypové. Typové soubory mají předepsanou a rigidní vnitřní strukturu a pracuje se s nimi pomocí Read/Write. Netypové jsou prostě hromada bajtů a pracuje se s nimi pomocí BlockRead/BlockWrite.
Pro problém kopírování souboru se více hodí netypové soubory, protože nás nezajímá, jaká data kopírujeme:
const zdroj = 'test.exe';
      cil   = 'test.ex~';
      delka_kusu = 65535;
type sachista = record
     elo:integer;       {pro ty, co nevedi, ELO je vykonnost sachisty}
     vek:byte;
     jmeno:string[30];
     end;
var f,g:file;      {netypove soubory jsou typu FILE}
                   {(typove by bylo FILE of <neco>}
                   {napr. file of sachista)}
    p,i:word;
    l:longint;
    buffer:array[1..delka_kusu] of byte;
begin
Assign(f,zdroj);
Assign(g,cil);
Reset(f,1);    {u netypovych souboru urcim delku bloku. Prakticky vzdy 1 bajt}
l:=FileSize(f);
Rewrite(g,1);
p:=0;
while not Eof(f) do     {dokud nejsme na konci souboru...}
   begin
   inc(p);
   BlockRead(f,buffer,delka_kusu,i); {zkusim nacist DELKA_KUSU bajtu, ale je}
                                     {mozne, ze se nacte mene, protoze soubor}
                                     {je kratsi nebo uz jsme neco nacetli predtim}
                           {pocet skutecne nactenych bajtu se nastesti ulozi do I}
   BlockWrite(g,buffer,i); {...a techto I bajtu zapisu do druheho souboru}
   end;
Close(f);
Close(g);
writeln('Soubor je velky ',l,' bajtu.');
writeln('Zkopiroval jsem ho v ',p,' krocich.');
readln;
end.
Tato metoda kopírování má výhodu, že dokáže bez poškození zkopírovat jakékoliv soubory - neplatí tu omezení o maximální délce řádku. Je ale třeba dávat pozor na omezení Turbo pascalu, na maximální velikost proměnné 64KB. Někdy to není problém (třeba při kopírování souborů), jindy je to obtížnější, třeba když načítáme obrázek. Je třeba stále hlídat dekomprimační rutinu, aby nedošla na konec zdrojového bufferu a průběžně "dočítat" další data.

Typové binární soubory
Řeknu to takhle: já jsem je ještě nikdy nepoužil. Jejich problém je ten, že se předpokládá, že v celém souboru mají data jednotný tvar. V praxi je ale obvyklejší, že soubor obsahuje hlavičku, několik podhlaviček a bloky dat. Nicméně, pokud bychom se vrátili k příkladu s šachisty, tak zde by se typový soubor dal použít dobře.
const soubor = 'test.txt';
type sachista = record
     elo:integer;       {pro ty, co nevedi, ELO je vykonnost sachisty}
     vek:byte;
     jmeno:string[20];
     end;
const soupiska1:array[1..5] of sachista =
      ((elo:2113; vek:35; jmeno:'Mrázek Jan'),
       (elo:2104; vek:56; jmeno:'Křepelka Václav'),
       (elo:2088; vek:24; jmeno:'Stéblová Klára'),
       (elo:2039; vek:40; jmeno:'Pichťour Otakar'),
       (elo:1992; vek:25; jmeno:'Šarfová Ilona'));

var f,g:file of sachista;
    soupiska2:array[1..5] of sachista;
    i:word;
begin
{Takhle soupisku ulozime}
Assign(f,soubor);
Rewrite(f);      {opet neuvadim velikost bloku}
for i:=1 to 5 do Write(f,soupiska1[i]);
Close(f);

{A takhle nacteme a zobrazime}
Assign(g,soubor);
Reset(g);
for i:=1 to 5 do
    begin
    Read(g,soupiska2[i]);
    writeln(soupiska2[i].jmeno,#9,soupiska2[i].vek,'   ',soupiska2[i].elo);
    end;
Close(g);
readln;
end.

Zpracování chyb

Nene, nemám na mysli, že mí čtenáři jsou lemra, kteří nedokážou napsat nic správně. Chyby v souvislosti se soubory nesouvisí s kvalitou kódu. Mám na mysli chybové stavy, které vzniknou při nedostatku místa na disku při ukládání, při neexistenci souboru při načítání a podobně. Z hlediska programátora tedy nepředvídatelné chyby.
Chování pascalu při vzniku takovéto běhové chyby souvisí s nastavením direktivy $I (a ekvivalentní položky v nastavení). Pokud je nastaveno $I+, tak se při vzniku chyby prostě ukončí a vypíše chybovou hlášku. Prosté, funkční, ale nepůsobí to zrovna profesionálně. Jestliže si chceme takové situace zpracovat sami, musíme se přepnout do režimu $I-.
V knížkách o programování se většinou doporučuje dávat $I- jenom k operacím vstupu/výstupu a jinak být ve zbytku programu v režimu $I+
Tedy takto:
...
Assign(f,'c:\pascal\soubor.dat');
{$I-}
Reset(f,1);
{$I+}
n:=IOresult;
if n<>0 then
   begin
   if n=2 then writeln('Soubor neexistuje (ale cesta jo)') else
   if n=3 then writeln('Neexistující cesta') else
   if n=4 then writeln('Prilis mnoho poskrabanych souboru') else
      writeln('Nejaka chyba');
   Exit;
   end;
{$I-}
BlockRead(f,buffer,sizeof(buffer),i);
{$I+}
n:=IOresult;
if n<>0 then
   begin
   if n=100 then writeln('Chyba pri cteni z disku (poskrabane CD?)') else
      writeln('Nejaka chyba');
   Exit;
   end;
Podle mě je neustálé přepínání $I+/- blbost. Lepší mi příjde dát prostě na začátek programu $I- a dál už s tím nečarovat. Ovšem pozor! Jestliže se v $I- režimu vyskytne chyba, jsou všechny vstupně/výstupní operace zablokovány do té doby, než zavoláte funkci IOresult. IOresult totiž není proměnná, je to funkce, která kromě toho, že vrací kód chyby ještě odblokuje vnitřní pojistku a umožní tak znovu pracovat se soubory. Jestliže se tedy vyskytne nějaká chyba a vy ji pomocí IOresult nezpracujete, tak váš program přestane pracovat se soubory na disku a vy si toho nevšimnete.
Nejčastějším "mimořádným stavem" bývá neexistence souboru. Funkce IOresult to odhalí až zpětně, lepší je to ale řešit v předstihu. Jestliže pracujeme ve Freepascalu, tak je nejjednodušší použít funkci FileExists z jednotky SysUtils.
V Turbo pascalu si to musíme zjistit sami:
Function ExistFile(s:string):boolean;
{Zjisti,zda dany soubor existuje }
{potrebuje unit DOS}
var r:searchrec;
begin
if s='' then begin ExistFile:=false;Exit;end;
FindFirst(s,archive+hidden+readonly+sysfile,r);
ExistFile:=DosError=0;
end;
Tento kód bude samozřejmě fungovat i ve Freepascalu.

Důležité může být i zjištění existence adresáře. Tady si to musíme zjistit sami v každém případě, protože jednotka SysUtils takovou funkci postrádá:
Function ExistDir(s:string):boolean;
{Zjisti,zda dany adresar existuje }
{potrebuje unit DOS}
var r:searchrec;
    a:byte;
begin
if s='' then begin ExistDir:=false;Exit;end;
if Copy(s,a,1)='\' then dec(s[0]);
r.attr:=0;
FindFirst(s,directory,r);
if DosError=0 then
   begin
   if ExistFile(s+'\nul') then ExistDir:=true else ExistDir:=false;
   end else ExistDir:=false;
end;
Poslední věc, kterou bych chtěl v tomto oddíle zmínit, je volání funkce FindFirst. I když hledáte jenom adresáře, tak doporučuju brát v první chvíli všechno a výstup filtrovat až potom. Do parametru atributy ale v žádném případě nedávejte konstantu AnyFile, jinak budete dostávat prapodivné chyby, které jsou o to záludnější, že se objevují jen na některých počítačích (systémech) a na jiných ne.
Používejte proto tohle:
Procedure Nacti_soubory_a_ne_adresare(adresar:string);
var r:registers;
if maska='' then maska:='*.*';
findfirst(adresar+'*.*',readonly+directory+sysfile+archive,r);
while doserror=0 do{dokud je neco nalezeno...}
   begin
   if (r.attr and directory)=0 then{...a neni to adresar (ma se delat seznam souboru, ne adresaru)}      
      ZpracujSoubor(r.name);
   findnext(r);
   end;

Objektové prostředky

Práce se soubory pomocí objektů z rodiny TStream se podobá netypovým binárním souborům. Nicméně umožňuje zapisovat i textové řetězce. Objekt TStream a jeho potomci jsou v jednotce Objects.
Objektová práce se soubory má několik obrovských výhod, které plynou z polymorfizmu potomků objektu TStream.
Potomek TMemoryStream čte a zapisuje nikoliv do souboru, ale do bufferu v operační paměti. Ideální pro dočasné ukládání jakýchkoli dat - není třeba se babrat s buffery, poli, spojovými seznamy, vše jde samo.
TDosStream odpovídá klasické cestě pomocí Assign, Reset a spol.
TBufStream je jako TDosStream, ale V/V operace jsou bufferované a tudíž rychlejší.
Turbo pascal má ještě TEmsStream, který se podobá TMemoryStreamu, ovšem pracuje s pamětí EMS.
Na internetu se dají sehnat ještě další rozšíření, například zde, které přidávají další potomky, např. TXMSstream a jiné.
Podívejme se na streamy prakticky. Zase vám napřed ukážu, jak zkopírovat soubor:
uses objects;
const SOUBOR='soubor.dat';
      NOVYNAZEV='soubor.new';

var f,g:TDOSstream;
begin
f.init(SOUBOR,stOpenRead);      {pristup jen pro cteni}
g.init(NOVYNAZEV,stCreate);     {vytvori novy soubor}
g.CopyFrom(f,f.GetSize);
f.Done;
g.Done;
writeln('Soubor ',SOUBOR,' byl zkopirovan do ',NOVYNAZEV,'.');
readln;
end.
Streamy mají geniální metodu CopyFrom, která vše významně ulehčí. V následujícím příkladu budeme soubor nejenom kopírovat, ale i kódovat, takže nás čeká více práce:
uses objects;
{$R-,$O-}                  {pri kodovani nechci dostavat priblble chyby o}
                           {preteceni/podteceni bajtu}
const SOUBOR='soubor.dat';
      NOVYNAZEV='soubor.new';
      VELBUF = 8192;
      KOD:longint = 1;

var f,g:TDOSstream;
    buffer:array[1..VELBUF] of byte;
    n,i:longint;
begin
f.init(SOUBOR,stOpenRead);      {pristup jen pro cteni}
g.init(NOVYNAZEV,stCreate);     {vytvori novy soubor}
repeat
n:=f.GetPos;               {zaznamenam pozici ve streamu pred ctenim}
f.Read(buffer,VELBUF);        {pokusim se precist VELBUF bajtu}
if f.status=stReadError then  {nedoslo ke cteni za koncem souboru?}
   begin
   f.reset;                {resetuj chybovy stav}
   n:=f.GetSize-n;         {a zjisti, jak velky byl tento posledni usek}
   f.Read(buffer,n);       {neni mi jasne, zda je to nutne. nic tim ale nezkazime}
   end
   else n:=f.GetPos-n;     {o kolik bajtu jsme se soupli?}

for i:=1 to n do
    inc(buffer[i],KOD);    {zakoduju}

g.Write(buffer,n);         {a zapisu}
until n<>VELBUF;           {opakuj, dokud jsme nezpracovali cely soubor}

f.Done;
g.Done;
writeln('Soubor ',SOUBOR,' zakodovan a ulozen do ',NOVYNAZEV,'.');
readln;
end.
Vidíte, že metoda TStream.Write je podobná proceduře BlockWrite a TStream.Read proceduře BlockRead. Vidíte ale, že metoda Read postrádá parametr, kolik bajtů se doopravdy přečetlo. Proto si to musíme sami, poměrně těžkopádně, hlídat.
Toto povídání o streamech berte jen jako takové nakopnutí podívat se pro další informace do manuálu. Stejně jako klasické prostředky má své analogy funkcí jako Seek a Truncate apod. Streamy získají ještě další sílu ve spojení s potomky typu TObject. Ukládání položek objektů, ale i záznamů může být poměrně pracná zaležitost, protože leckdy je potřeba ukládat položku po položce. Streamy mají ale mechanismus, jak napsat univerzální metody pro ukládání a načítání položek objektů. Pro daný objekt napíšete dané procedury, registrujete je a pomocí metod TStream.Get a TStream.Get je můžete načítat, aniž byste museli stream informovat o přesném formátu dat. Znovu odkazuji na manuál.
Všechny tyto věci fungují i v TP i ve FP. FreePascal umí kromě objektů používat i třídy. Třídy TP nezná, objevují se až v Delphi, které naopak neznají objekty. FreePascal zná oboje.
Takto bude vypadat předchozí úloha pomocí nikoliv objektu TStream, ale třídy TStream:
uses Classes;
{$R-,$O-}                  {pri kodovani nechci dostavat priblble chyby o}
                           {preteceni/podteceni bajtu}
const SOUBOR='soubor.dat';
      NOVYNAZEV='soubor.new';
      VELBUF = 8192;
      KOD:longint = 1;

var f,g:TFileStream;
    buffer:array[1..VELBUF] of byte;
    n,i:longint;
begin
f:=TFileStream.Create(SOUBOR,fmOpenRead);      {pristup jen pro cteni}
g:=TFileStream.Create(NOVYNAZEV,fmCreate);     {vytvori novy soubor}
repeat
n:=f.Read(buffer,VELBUF);
for i:=1 to n do
    inc(buffer[i],KOD);    {zakoduju}

g.Write(buffer,n);         {a zapisu}
until n<>VELBUF;           {opakuj, dokud jsme nezpracovali cely soubor}

f.Destroy;
g.Destroy;
writeln('Soubor ',SOUBOR,' zakodovan a ulozen do ',NOVYNAZEV,'.');
readln;
end.
2007-06-07 | Laaca
Reklamy:
„Rozdávat rady je zbytečné. Moudrý si poradí sám a hlupák stejně neposlechne.“ Mark Twain