4. Textové súbory


Posledná zmena: 1.10.2002

Banner Text 1.10.2002

    čo sa budeme dnes učiť

    • nový jednoduchý typ char a nový komplexnejší typ textový súbor
    • ukážeme, ako sa pracuje s textovým súborom, aké tu platia pravidlá, ako sa riešia jednoduché úlohy
    • ukážeme zopár úloh, v ktorých sa pracuje s korytnačkami a s textovými súbormi

Typ CHAR

  • ordinálny typ, ktorý obsahuje sadu 256 znakov (tzv. ASCII), tieto sú usporiadané
  • vnútorne sú reprezentované číselným kódom 0..255, t.j. zaberajú 1 bajt (8 bitov)
  • znakové konštanty v apostrofoch alebo #kód
  • v Pascale nie sú žiadne znakové operácie (iba relácie)
  • treba si pamätať:
    • ' '<...<'0'<'1'<...<'9'<...<'A'<'B'<...<'Z'<...<'a'<'b'<...<'z'

  • znakové funkcie: pred, succ, ord, char (v pascale funguje aj chr)
  • treba si pamätať:
    • ord(' ')=32; ord('0')=48; ord('1')=49; ...
      alebo #32=' '; #48='0'; #49='1'; ...
    • ord('A')=65; ord('a')=ord('A')+32=97; ...
      t.j. #65='A'; #97='a'; ...
  • ak c obsahuje znak cifry ('0'..'9'), tak ord(c)-ord('0')=cifra (podobne pre písmená)
  • príkazy inc, dec fungujú aj pre znaky (napr. c:='A'; inc(c,32); )
  • typ char využijeme najmä pri práci so súbormi
  • so všetkými ordinálnymi typmi (integer, boolean aj char) sa dá robiť for-cyklus

príklad for-cyklu pre rôzne typy:

var
  i,x:integer;
  b:boolean;
  z:char;
begin
  for i:=1 to 100 do       ... cyklus prejde 100-krát ...
  for b:=false to true do  ... cyklus prejde 2-krát ...
  for b:=x=1 to x=2 do     ... cyklus prejde 0, 1 alebo 2-krát ...
  for z:='A' to 'Z' do     ... cyklus prejde 26-krát ...
  for z:=pred('#') to succ('#') do     ... cyklus prejde 3-krát ...

Textová plocha

  • aby sme mohli experimentovať so znakmi, s textovou informáciou, naučíme sa v Delphi pracovať s textovou plochou
  • do prázdneho formulára položíme komponent Memo (trieda TMemo) - podobne ako grafickú plochu Image budeme ťahať aj túto plochu - vytvorí sa komponent s menom Memo1 (v ľavom hornom riadku sa objaví text Memo1)
  • zatiaľ sa naučíme len niekoľko príkazov na prácu s textovou plochou a rozumieť im budeme až neskôr. Podobne ako sme písali Image1.Canvas. a za tým príkaz, budeme teraz písať Memo1.Lines. a za to príkaz:
    • Memo1.Lines.Clear; - vyčistí všetky riadky
    • Memo1.Lines.Add('nejaký text'); - pridá ďalší riadok s daným textom na koniec

ukážka časti programu s jedným komponentom Memo1, ktorý vypisuje kódy písmen:

procedure TForm1.FormCreate(Sender: TObject);
var
  z:char;
begin
  Memo1.Lines.Clear;
  for z:='a' to 'z' do
    Memo1.Lines.Add(z+' '+IntToStr(ord(z)));
        // k písmenu prilepí medzeru a číselný kód znaku
end;

 Textové súbory TEXT

Súbor = postupnosť (sekvencia) prvkov rovnakého typu, zaujímavé sú postupnosti znakov, t.j. textové súbory + špeciálne znaky <Eoln>. Všetky údaje sú v textovom súbore zapísané ako ASCII znaky.

  • pascal umožňuje sekvenčný prístup do súboru ale aj priamy (uvidíme neskôr)
  • s textovými súbormi sa dá pracovať len sekvenčným prístupom
  • musí sa rozlišovať, či zo súboru čítame alebo do neho zapisujeme údaje:
    • vstupný súbor - len čítať pripravené údaje
    • výstupný súbor - len zapisovať, na koniec súboru!
    • do textového súboru sa nedá aj zapisovať aj súčasne z neho čítať

Súboru (file) musí byť priradené nejaké fyzické zariadenie, resp. musí sa nachádzať na nejakom zariadení:

  • na vonkajšej pamäti - diskový súbor
  • pomocná vnútorná pamäť - naučíme sa neskôr

"Operácie" so súbormi:

  • write = zápis informácie na koniec súboru (podobne ako do textovej ploche pomocou Memo1.Lines.Add(...))
  • read = čítanie nasledujúcej hodnoty zo súboru

Ako pracujeme s textovým súborom

  • deklarovanie premennej typu textový súbor:
        var t:TextFile; // premenná t je typu textový súbor
  • priradenie fyzického súboru najčastejšie na disku:
        AssignFile(t,'meno_súboru');
    • dávajte si pozor na uvádzanie plnej cesty na disku k nejakému súboru - ak sa takýto program prenesie na iný počítač, s veľkou pravdepodobnosťou tieto absolútne cesty k súborom nebudú fungovať - používajte radšej relatívne cesty od momentálnej adresy projektu (tam, kde sa nachádza EXE súbor)
  • otvorenie súboru
    • buď na čítanie (musí už existovať):
          Reset(t);
    • alebo zápis (ak už existuje, tak sa najprv vyprázdni):
          Rewrite(t);
  • práca so súborom, t.j. samotné čítanie alebo zapisovanie:
        read(t,...); readln(t,...);
        write(t,...); writeln(t,...);
  • zatvorenie súboru, t.j. ukončenie práce so súborom:
        CloseFile(t);
  • textový súbor = postupnosť riadkov (aj prázdna)
  • pričom riadok textového súboru = postupnosť znakov (aj prázdna) ukončená <Eoln>

Ak máme nejaký súbor:

vidíme:

v skutočnosti:

ab
 cde
f

|a|b|<Eoln>| |c|d|e|<Eoln>|f|
 ^
ukazovateľ

  • ukazovateľ = pozícia v súbore - na začiatku je na 1. znaku; po read sa posunie o 1 znak vpravo

Čítanie zo súboru

  • príkazom read(t, premenná ) sa prečíta jeden znak zo súboru z pozície ukazovateľa, priradí sa jeho hodnota do premennej a ukazovateľ sa posunie

Testovanie konca súboru a konca riadka

  • štandardná logická funkcia Eof(t) = skratka z End Of File
    • vráti true, ak je ukazovateľ nastavený za posledným znakom súboru
  • štandardná logická funkcia Eoln(t) = skratka z End Of LiNe
    • vráti true, ak je ukazovateľ na značke <Eoln> alebo aj za posledným znakom súboru, t.j. ak platí eof(t)=true, tak platí eoln(t)=true
    • značka <Eoln> sa vnútorne kóduje dvomi znakmi #13 a #10 (hovoríme im CR a LF)
    • príkazom readln(t) preskočíme všetky znaky v súbore až za najbližšiu značku <Eoln> (na konci súboru nerobí nič)
    • POZOR, pomocou read(t,z) je možné čítať aj značku <Eoln>, lenže táto sa potom chápe ako 2 znaky #13 a #10 a nie ako nejaký špeciálny znak
    • príkaz readln(t,z) je skrátený tvar pre read(t,z); readln(t);
  • čítanie na konci súboru vyvolá vstupno-výstupnú chybu
  • znak #26 má v pascale (z historických dôvodov) niekedy špeciálny význam: čítanie textového súboru si na ňom "myslí", že je na konci súboru (eof(t)=true) a nedovolí ho prečítať a ani čítať ďalšie znaky za ním...
    • môžete to otestovať tak, že si vytvoríte textový súbor, ktorý bude niekde v strede obsahovať znak #26, potom tento súbor vypíšte pomocou Memo1.Lines.LoadFromFile(...) a tiež ho čítajte a vypisujte pomocou while not eof(t) do begin read(t,z); ...end;

V nasledujúcom príklade zistíme počet medzier v textovom súbore medzery.txt:

var
  t:TextFile;
  z:char;
  pocet:integer;
begin
  AssignFile(t,'medzery.txt'); Reset(t);
  pocet:=0;
  while not Eof(t) do begin
    read(t,z);
    if z=' ' then inc(pocet);
  end;
  CloseFile(t);
  Memo1.Lines.Add('Počet medzier v súbore '+IntToStr(pocet));
       // do textovej plochy
end;

Príklad: Zistíme, počet riadkov textového súboru text.txt:

var
  t:TextFile;
  pocet:integer;
begin
  AssignFile(t,'text.txt'); Reset(t);
  pocet:=0;  
  while not Eof(t) do begin
    readln(t); inc(pocet);
  end;
  CloseFile(t);
  Memo1.Lines.Add('Počet riadkov v súbore '+IntToStr(pocet));
end;

Príklad: Zistíme, dĺžku najdlhšieho riadka súboru text.txt

var
  t:TextFile;
  z:char;
  max,dlzka:integer;
begin
  AssignFile(t,'text.txt'); Reset(t);
  max:=0;
  while not Eof(t) do begin
    dlzka:=0;
    while not Eoln(t) do begin
      read(t,z); inc(dlzka);
    end;
    readln(t);   // nesmie sa tu zabudnúť
    if dlzka>max then max:=dlzka;
  end;
  CloseFile(t);
  Memo1.Lines.Add('Dĺžka najdlhšieho riadka '+IntToStr(max));  
end;

Zápis do súboru

  • súbor treba otvoriť pomocou Rewrite(t)
  • ak súbor už existoval, tak hneď po Rewrite sa zruší jeho obsah
  • ak súbor ešte neexistoval, vytvorí sa s prázdnym obsahom
  • ukazovateľ v súbore je vždy nastavený na jeho koniec
  • príkaz write(t, znak ) zapíše do súboru jeden znak; write(t, reťazec ) zapíše kompletný reťazec
  • príkaz writeln(t) zapíše do súboru značku <Eoln>, t.j. robí to isté ako write(t,#13#10)
  • príkaz writeln(t, reťazec ) je skrátený tvar pre write(t, reťazec ); writeln(t); alebo aj write(t, reťazec ,#13#10);
  • v nasledujúcom príklade využijeme
        Memo1.Lines.LoadFromFile(meno_súboru)
    pomocou ktorého zapíšeme obsah celého súboru do textovej plochy

Príklad: Vytvoríme súbor text.txt z písmen 'a' až 'z' a vypíšeme jeho obsah do Textovej plochy

procedure TForm1.Button1Click(Sender: TObject);
var
  t:TextFile;
  z,z1:char;
begin
  AssignFile(t,'text.txt'); Rewrite(t);
  for z:='a' to 'z' do begin
    for z1:=z to 'z' do write(t,z1);
    writeln(t);
  end;
  CloseFile(t);

  Memo1.Lines.LoadFromFile('text.txt');
end;

Príklad: Vytvoríme kópiu súboru unit1.pas do súboru text.txt

var
  t1,t2:TextFile;
  z:char;
begin
  AssignFile(t1,'unit1.pas'); Reset(t1);
  AssignFile(t2,'text.txt'); Rewrite(t2);
  while not Eof(t1) do
    if Eoln(t1) then begin
      readln(t1); writeln(t2);
    end
    else begin
      read(t1,z);
      // spracuj načítaný znak
      write(t2,z);
    end;
  CloseFile(t1);
  CloseFile(t2);
  Memo1.Lines.LoadFromFile('text.txt');
end;

iný variant kopírovania súboru - nevšímame si konce riadkov a prerábame malé písmená na veľké

var
  t1,t2:TextFile;
  z:char;
begin
  AssignFile(t1,'unit1.pas'); Reset(t1);
  AssignFile(t2,'text.txt'); Rewrite(t2);
  while not Eof(t1) do begin
    read(t1,z);
    if (z>='a') and (z<='z') then
      z:=char(ord(z)-ord('a')+ord('A'));            // alebo dec(z,32);
    write(t2,z);
  end;
  CloseFile(t1);
  CloseFile(t2);
  Memo1.Lines.LoadFromFile('text.txt');
end;

Pozn:

  • na prerábanie malých písmen na veľké môžeme použiť štandardnú znakovú funkciu z:=upcase(z);

NDÚ:

  • postupnosť medzier nahraď 1 medzerou
  • vyhoď len medzerové riadky
  • vyhoď medzery na konci riadkov
  • postupnosť znakov 'end' nahraď '***'

Reset a Rewrite na ten istý súbor na disku

      var f,g:TextFile;
      ...
      AssignFile(f,'a.txt'); Reset(f);
      AssignFile(g,'a.txt'); Rewrite(g);

  • nepredvídateľné - rôzne verzie Pascalu reagujú rôzne (Delphi hlási I/O chybu)

Čítanie čísel

  • pomocou read môžeme čítať aj čísla (celé aj reálne), ale v súbore musia byť tieto čísla ukončené medzerou, koncom riadka alebo tabulátorom (znak s kódom #9)
  • príkaz read(t, číselná_premenná ) najprv preskočí všetky medzerové znaky (medzera, <Eoln> alebo #9), potom prekonvertuje znaky zo vstupu na číslo a ak je číslo ukončené nemedzerovým znakom (napr. ',' alebo ';'), tak vyhlási chybu Invalid numeric format
  • čítanie čísla na konci súboru (t.j. platí eof, ale aj ak sú tam len medzery) vráti hodnotu 0 - treba sa tohoto vyvarovať!
  • profesionálny softvér takýto read na čítanie čísel nepoužíva, lebo chyba v súbore spôsobí chybovú správu (výpočet je ďalej nekorektný) - vy môžete používať takéto čítanie, len ak je v zadaní výslovne povedané, že je súbor korektný a číslo je ukončené medzerovým znakom
  • neskôr uvidíme aj iný (bezpečný) spôsob čítania čísel

Príklad: Program nájde maximálne číslo v súbore celých čísel:

var
  t:TextFile;
  cislo,max:integer;
begin
  AssignFile(t,'text.txt'); Reset(t);
  max:=-maxint;       // preddefinovaná konštanta 2147483647
  while not Eof(t) do begin     // vyskúšajte SeekEof
    read(t,cislo);
    if cislo>max then max:=cislo;
  end;
  CloseFile(t);
  if max=-maxint then
    Memo1.Lines.Add('súbor je prázdny')
  else
    Memo1.Lines.Add('maximálne číslo v súbore je '+IntToSTr(max));
end;
  • ak súbor obsahoval napr. len prázdny riadok (alebo len medzery), tak program vypíše, že maximum bolo 0
  • toto isté sa stane, ak súbor obsahuje len záporné čísla a za posledným číslom sú ešte nejaké medzerové znaky - funkcia Eof(t) za posledným číslom vráti false - ešte nie je koniec súboru, ale už tam nie je žiadne číslo, teda načíta sa hodnota 0
  • v takejto situácii môžeme namiesto Eof(t) použiť štandardnú funkciu SeekEof(t), ktorá skôr ako odpovie na stav súboru, odfiltruje všetky medzerové znaky
  • podobne existuje SeekEoln(t), ktorá testuje koniec riadka, ale najprv odfiltruje medzery a tabulátory (znaky s kódom #9)
  • Pozn. Pri použití funkcie SeekEoln(t) a SeekEof(t) sa odignorujú všetky medzery, tabulátory a v prípade SeekEof(t) i konce riadkov. Preto tieto funkcie nie sú veľmi vhodné pri čítaní znakov, používame ich hlavne pri čítaní čísel z textového súboru.

Zápis čísel

  • pomocou write(t, ...) môžeme do textového súboru zapisovať aj čísla (hodnoty číselných výrazov)
  • celé čísla sa zapíšu bez medzery pred číslom aj za číslom, t.j. write(t,i,i+1) pre i=17 zapíše 1718
  • reálne čísla sa do súboru zapisujú v semilogaritmickom tvare s medzerou pred číslom

Príklad: Zo súboru text1.txt budeme kopírovať všetky čísla do súboru text2.txt, pričom ich budeme zaraďovať po troch do riadka:

var
  t1,t2:TextFile;
  cislo,pocet:integer;
begin
  AssignFile(t1,'text1.txt'); Reset(t1);
  AssignFile(t2,'text2.txt'); Rewrite(t2);
  pocet:=0;
  while not SeekEof(t1) do begin
    read(t1,cislo);
    if pocet=3 then begin
      writeln(t2); pocet:=1;
    end
    else begin
      if pocet>0 then write(t2,' ');
      inc(pocet);
    end;
    write(t2,cislo);
  end;
  CloseFile(t1);
  CloseFile(t2);
  Memo1.Lines.LoadFromFile('text2.txt');
end;

Príklad: V súbore sú reálne čísla, máme zistiť počet čísel, ktoré majú hodnotu menšiu ako je priemer všetkých čísel v súbore

var
  t:TextFile;
  cislo,suma,priemer:real;
  pocet:integer;
begin
  AssignFile(t,'text1.txt'); Reset(t);
        // priradenie vstupného súboru a jeho otvorenie na čítanie
  suma:=0; pocet:=0;
  while not SeekEof(t) do begin
    read(t,cislo);
    suma:=suma+cislo; inc(pocet);
  end;
  Reset(t);        // !!! nastavenie pozície na začiatok súboru !!!
  priemer:=suma/pocet; pocet:=0;
  // pocet už nepotrebujeme
  while not SeekEof(t) do begin
    read(t,cislo);
    if cislo<priemer then inc(pocet);
  end;
  Memo1.Lines.Add('počet podpriemerných='+IntToSTr(pocet));
end;

Formátovací parameter vo write

  • za znakom alebo znakovým reťazcom
       write(t,'*':10);
    označuje, že znak sa vypíše na šírku 10, t.j. najprv 9 medzier a potom '*'
        write(t,'delphi':3);
    nakoľko reťazec je dlhší ako formátovací parameter, zapíše sa kompletný reťazec, t.j. ignoruje sa formát
  • formátovací parameter za celým číslom označuje šírku, do ktorej sa má zapísať číslo, ak by nevošlo do danej šírky, formát sa ignoruje
        write(t,25*25:5);
    zapíše dve medzery, za ktoré dá číslo 625
  • formátovací parameter za reálnym číslom tiež označuje šírku, číslo sa vypíše v semilogaritmickom tvare; druhý formátovací parameter označuje počet desatinných miest
        write(t,sin(2):15);
    zapíše  9.092974E-0001
        write(t,cos(2):7:4);
    zapíše -0.4161

Textové súbory a korytnačky

Pripomeňme príkaz case, ktorý pre ordinálnu hodnotu zabezpečí vykonanie nejakej vetvy podľa príslušnej konštanty – všeobecne:

      case  ordinálny_výraz  of
        
      konštanta1: príkaz1;
        
      konštanta2: príkaz2;
        ...
        else  
      príkazy  // táto vetva môže chýbať
      end;

  • Príklad: daný je súbor, v ktorom sú príkazy pre korytnačku: d,l,p + číslo, napr.
        d 100 p 120 d 100 p 120 d 100
    pričom písmená a čísla sú navzájom oddelené aspoň jednou medzerou alebo novým riadkom

korytnačka interpretuje tento súbor:

var
  t:TextFile;
  k:TKor;
  z:char;
  p:integer;
begin
  AssignFile(t,'kor1.txt'); Reset(t);
  k:=TKor.Create;
  while not eof(t) do begin
    if eoln(t) then begin readln(t); z:=' '; end
    else read(t,z);
    if z<>' ' then read(t,p);
    case z of
      'd': k.dopredu(p);
      'p': k.vpravo(p);
      'l': k.vlavo(p);
    end;
  end;
  CloseFile(t);
end;

vylepšená verzia s použitím SeekEof(t):

var
  t:TextFile;
  k:TKor;
  z:char;
  p:integer;
begin
  AssignFile(t,'kor1.txt'); Reset(t);
  k:=TKor.Create;
  while not SeekEof(t) do begin
    read(t,z,p);
    case z of
      'd': k.dopredu(p);
      'p': k.vpravo(p);
      'l': k.vlavo(p);
    end;
  end;
  CloseFile(t);
end;

Príklad: v súbore sú slová dopredu, vpravo, vlavo, ph, pd (z každého aspoň 2 písmená) – za niektorými nasleduje číslo:

var
  t:TextFile;
  k:TKor;
  z,z1,z2:char;
  p:integer;
begin
  AssignFile(t,'kor2.txt'); Reset(t);
  k:=TKor.Create;
  while not SeekEof(t) do begin
    read(t,z,z1); z2:=z1;
    while (z2<>' ') and not eoln(t) do read(t,z2);
    if (z='d') and (z1='o') then begin
      read(t,p); k.dopredu(p);
    end
    else if (z='v') and (z1='p') then begin
      read(t,p); k.vpravo(p);
    end
    else if (z='v') and (z1='l') then begin
      read(t,p); k.vlavo(p);
    end
    else if (z='p') and (z1='h') then
      k.PH
    else if (z='p') and (z1='d') then
      k.PD;
  end;
  CloseFile(t);
end;

Pozn.

  • riešenie tejto úlohy môžeme zjednodušiť, ak si budeme všímať len 2. písmeno slov - v tomto prípade by sa dal použiť príkaz case

Príklad

  • počas kreslenia nejakého obrázka pomocou korytnačky (napr. kvetinka) si budeme do textového súboru zapamätávať momentálne súradnice korytnačky, t.j. (k.X,k.Y) - zrejme stačí sledovať príkaz dopredu

vytváranie súboru s postupnosťou súradníc:

var
  t:TextFile;
  k:TKor;

  procedure poly(n,d,u:integer);
  begin
    while n>0 do begin
      k.dopredu(d);
      writeln(t,k.X:0:2,' ',k.Y:0:2);
      k.vpravo(u); dec(n);
    end;
  end;

var
  i:integer;
begin
  AssignFile(t,'kor3.txt'); Rewrite(t);
  k:=TKor.Create;
  poly(1,100,0);
  for i:=1 to 7 do begin
    poly(9,5,10); k.vpravo(90);
    poly(9,5,10); k.vpravo(90);
    k.vpravo(360/7);
  end;
  CloseFile(t);
end;

korytnačka interpretuje súbor z predchádzajúceho príkladu:

var
  t:TextFile;
  k:TKor;
  x,y:real;
begin
  AssignFile(t,'kor3.txt'); Reset(t);
  k:=TKor.Create;
  while not eof(t) do begin
    readln(t,x,y);
    k.ZmenXY(x,y);
  end;
  CloseFile(t);
end;

to isté, ale do súboru sa ukladajú relatívne posuny korytnačky:

var
  t:TextFile;
  k:TKor;
  x,y:real;

  procedure poly(n,d,u:integer);
  begin
    while n>0 do begin
      k.dopredu(d);
      writeln(t,k.X-x:0:2,' ',k.Y-y:0:2);
      x:=k.X; y:=k.Y;
      k.vpravo(u); dec(n);
    end;
  end;

var
   i:integer;
begin
  AssignFile(t,'kor4.txt'); Rewrite(t);
  k:=TKor.Create; x:=k.X; y:=k.Y;
  poly(1,100,0);
  for i:=1 to 7 do begin
    poly(9,5,10); k.vpravo(90);
    poly(9,5,10); k.vpravo(90);
    k.vpravo(360/7);
  end;
  CloseFile(t);
end;

korytnačku vygenerujeme na náhodnú pozíciu a určíme náhodnú veľkosť obrázka (random(23)/10+0.3):

var
  t:TextFile;
  k:TKor;
  x,y,f:real;
  i:integer;
begin
  AssignFile(t,'kor4.txt');
  k:=TKor.Create;
  randomize;
  for i:=1 to 10 do begin
    Reset(t);
    k.PresunXY(random(400),random(300));
    f:=random(23)/10+0.3;    // náhodná veľkosť
    while not eof(t) do begin
      readln(t,x,y);
      k.zmenxy(k.X+x*f,k.Y+y*f);
    end;
  end;
  CloseFile(t);
end;


© 2002 AB, KVI
blaho@fmph.uniba.sk