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
- Assign, Close, Read, Readln, Reset, Rewrite, Write, Writeln, IOResult, Eof, Eoln
- FilePos, FileSize, Seek
- BlockRead, BlockWrite
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;
s:string;
p:longint;
begin
p:=0;
Assign(f,zdroj);
Assign(g,cil);
Reset(f);
Rewrite(g);
while not Eof(f) do
begin
inc(p);
Readln(f,s);
Writeln(g,s);
end;
Close(f);
Close(g);
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;
vek:byte;
jmeno:string[30];
end;
var f:text;
s:string;
p,q:integer;
u:char;
hrac:array[1..max] of sachista;
begin
p:=0;
Assign(f,zdroj);
Reset(f);
readln(f);
readln(f);
while not Eof(f) do
begin
inc(p);
Read(f,hrac[p].elo);
Read(f,hrac[p].vek);
Read(f,u);
Readln(f,hrac[p].jmeno);
end;
Close(f);
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;
vek:byte;
jmeno:string[30];
end;
var f,g:file;
p,i:word;
l:longint;
buffer:array[1..delka_kusu] of byte;
begin
Assign(f,zdroj);
Assign(g,cil);
Reset(f,1);
l:=FileSize(f);
Rewrite(g,1);
p:=0;
while not Eof(f) do
begin
inc(p);
BlockRead(f,buffer,delka_kusu,i);
BlockWrite(g,buffer,i);
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;
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
Assign(f,soubor);
Rewrite(f);
for i:=1 to 5 do Write(f,soupiska1[i]);
Close(f);
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');
Reset(f,1);
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;
BlockRead(f,buffer,sizeof(buffer),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;
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;
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
begin
if (r.attr and directory)=0 then
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);
g.init(NOVYNAZEV,stCreate);
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;
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);
g.init(NOVYNAZEV,stCreate);
repeat
n:=f.GetPos;
f.Read(buffer,VELBUF);
if f.status=stReadError then
begin
f.reset;
n:=f.GetSize-n;
f.Read(buffer,n);
end
else n:=f.GetPos-n;
for i:=1 to n do
inc(buffer[i],KOD);
g.Write(buffer,n);
until n<>VELBUF;
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;
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);
g:=TFileStream.Create(NOVYNAZEV,fmCreate);
repeat
n:=f.Read(buffer,VELBUF);
for i:=1 to n do
inc(buffer[i],KOD);
g.Write(buffer,n);
until n<>VELBUF;
f.Destroy;
g.Destroy;
writeln('Soubor ',SOUBOR,' zakodovan a ulozen do ',NOVYNAZEV,'.');
readln;
end.