Operacije sa tekstualnim datotekama u programskim jezicima C i Python
Uvod
Sposobnost obavljanja operacija čitanja iz tekstualnih datoteka i pisanja u tekstualne datoteke, predstavlja važan deo osnovne pismenosti svakog programera, i stoga - u cilju upoznavanja sa navedenim operacijama - razmotrićemo nekoliko osnovnih primera iz programskih jezika C i Python (dva popularna jezika koja se koriste u fazi učenja).
Programski kodovi iz C-a (koji se tiču učitavanja datoteka i obrade podataka), ni u kom slučaju ne deluju "skroz jednostavno" iz perspektive mlađih programera koji se sa takvim kodovima prvi put sreću, ali, verujemo i nadamo se da neće biti ni teški za razumevanje.
Sa druge strane, primeri iz Python-a (kao što je i inače svojstveno ovom popularnom skriptnom jeziku), svakako su - bar naizgled - daleko jednostavniji ....
Operacije sa datotekama u C-u
Za početak ćemo se usmeriti na teži zadatak, * tj. sagledaćemo nekoliko primera iz programskog jezika C, ali - što je još važnije - upoznaćemo se sa opštim principima za pristup tekstualnim datotekama (koji važe u gotovo svim programskim jezicima).
Čitanje datoteke (osnovni primer)
Najjednostavniji primer otvaranja tekstualne datoteke i čitanja sadržaja znak-po-znak, uz ispis na standardni izlaz (tj. 'konzolu' ili 'terminal'), podrazumeva sledeće korake:
- otvaranje datoteke u režimu čitanja (preko funkcije
fopen
) if
grananje preko koga se ispituje da li je otvaranje datoteke proteklo na predviđeni načindo-while
petlju preko koje se znaci čitaju iz datoteke i ispisuju na ekran
Iako primer ne deluje komplikovano, osvrnimo se na određene delove; pre svega, na samu funkciju za otvaranje fopen
(uz komentare u kodu):
Ukoliko otvaranje datoteke nije proteklo u skladu sa očekivanjima, bitno je odreagovati na odgovarajući način (što je u gornjem primeru rešeno preko jednostavnog if
grananja):
Sama operacija čitanja datoteke (onako kako smo prikazali), vrlo je jednostavna ....
.... ali - za sobom povlači i pitanje: kako (inače) postupiti ako sadržaj datoteke treba sačuvati u memoriju i obraditi na određeni način (a ne samo "ispisati u konzoli")?!
Na kraju, veoma (!) je bitno da ne zaboravimo da zatvorimo datoteku:
U suprotnom, može doći do grešaka u radu programa koji pišemo - ali i do grešaka usled pokušaja drugih programa da naknadno pristupe datoteci koja je otvorena.
Složeniji primer: učitavanje datoteke u RAM
Primer koji smo prethodno videli, sasvim uspešno prikazuje osnovnu funkcionalnost mehanizma za čitanje podataka iz datoteke, ali, ako postoji potreba da se tekst (dalje) obrađuje u programu, podaci iz datoteke prvo se moraju kopirati u operativnu memoriju.
Za manje datoteke (nekoliko desetina redova i sl), mogu se koristiti i statički nizovi (znakova) ....
.... međutim, za čuvanje sadržaja iole većih datoteka, u praksi se gotovo uvek koristi dinamička alokacija memorije, najčešće preko funkcije malloc
("memory allocation"), uz prethodno očitavanje veličine datoteke.
Funkcija main
, u implementaciji koju ćemo razmotriti, ima sledeći sadržaj:
U prikazanom kodu, posebnu pažnju treba obratiti na sledeće detalje:
- veličina datoteke očitava se preko zasebne funkcije (radije nego da kod bude zapisan unutar funkcije
main
) - za smeštaj podataka potrebno je obezbediti "bafer" (blok memorije, odgovarajuće veličine, kome se pristupa preko pokazivača)
- kreiranje bafera takođe se obavlja preko zasebne funkcije (u kojoj se poziva funkcija
malloc
)
Obrada i ovog puta podrazumeva prost ispis sadržaja datoteke (ali, u primeru koji razmatramo, najvažnija operacija je učitavanje, i stoga nećemo nepotrebno skretati pažnju čitalaca). *
Na kraju, potrebno je (ponovo) obratiti posebnu pažnju na oslobađanje pokazivača koji je korišćen za datoteku (f
) i, takođe, na oslobađanje pokazivača koji je vezan za tekstualni bafer koji je alociran preko funkcije malloc
(promenljiva buffer
).
Preostaje da implementiramo funkcije za očitavanje veličine datoteke i alokaciju bafera.
Funkcija ocitavanje_velicine_datoteke
vraća:
- veličinu datoteke u bajtovima - u slučaju da je predat pokazivač koji uredno pokazuje na datoteku (ili)
- vrednost
-1
- ukoliko se preda pokazivač sa vrednošćuNULL
(Objašnjenja su u komentarima.)
Funkcija ucitavanje_datoteke
vraća:
- pokazivač na blok memorije u koji je učitan sadržaj datoteke - ukoliko nema "ispada" (ili)
- sistemsku vrednost
NULL
- ukoliko dođe do greške
Promena veličine bafera (realloc)
Pri radu sa tekstom neretko se javlja potreba za promenom veličine postojećeg bafera i, ukoliko bafer (koji je nastao izvršavanjem funkcija malloc
, calloc
* ili realloc
**) - treba proširiti (što je najtipičniji slučaj), ili suziti - najčešće se koristi funkcija realloc
("re-allocation"):
Kao što vidimo, ideja je jednostavna (sintaksa takođe), ali, budući da operacija promene veličine bafera može proći neuspešno, potrebno je postupati vrlo pažljivo.
Ukoliko se naredba koju smo videli na prethodnoj slici, ne izvrši uspešno - pokazivač buffer
će dobiti vrednost NULL
, ali - bafer će zapravo biti očuvan u memoriji (neće biti oslobođen).
Međutim, nepovoljna situacija (koju smo opisali pre napomene), može se izbeći uz 'razmišljanje unapred':
Za početak (kao što vidimo u gornjem primeru), uputno je da se povratna vrednost funkcije realloc
za svaki slučaj poveže sa (novim) pomoćnim pokazivačem:
- ako sve 'prođe po planu', "novi bafer" će zapravo biti proširena verzija dotadašnjeg bafera (posle čega se novi bafer lako može povezati sa starim/prvobitnim pokazivačem) i - u praksi - ukoliko postoji dovoljno slobodne memorije, funkcija
realloc
gotovo uvek uredno vraća ('novi') bafer - ukoliko dođe do greške, pokazivač
buffer_p
će dobiti vrednostNULL
, ali (što je važnije), stari bafer (i sav sadržaj) - biće očuvan(i) - što ostavlja mogućnost da se pravilno odreaguje, u skladu sa okolnostima
Kao što vidimo, programski jezik C nije nimalo sklon tome da od programera sakrije šta se dešava "ispod haube". :)
Upis u datoteku
Upis u datoteku podrazumeva (za početak), da se datoteka otvara uz argumente: "w"
ili "a"
("write" - prepisivanje preko postojeće datoteke; "append" - dodavanje sadržaja na kraj postojeće datoteke).
Bitni "sigurnosni" elementi iz prethodnih primera:
- grananje preko koga se proverava da li je datoteka uspešno otvorena
- zatvaranje datoteke nakon obavljanja operacija
.... i dalje su prisutni.
Sam upis u datoteku obavlja se preko funkcije fprintf
("file printf"), koja, u odnosu na "običnu" funkciju printf
, ima dodatni parametar (prvi po redu), koji predstavlja pokazivač na datoteku.
Što se tiče sadržaja koji se upisuje u datoteku (u konkretnom primeru) ....
.... u pitanju je n
nasumično generisanih celobrojnih vrednosti.
Ostale funkcije za rad sa memorijskim blokovima
Osim funkcija malloc
i realloc
(sa kojima ćete se sretati najčešće), u programskom jeziku C koristi se još i nekolicina drugih funkcija za rad sa memorijskim blokovima (zarad obrade teksta, ali i u drugim okolnostima).
U nastavku, osvrnućemo se na neke od takvih funkcija.
Funkcija calloc
Ukoliko je za određeni 'programerski zahvat' potreban bafer koji je inicijalizovan nulama, može se koristiti funkcija calloc
('contiguous allocation'), koja se od funkcije malloc
razlikuje po sledećim svojstvima:
- svaki bajt je inicijalizovan vrednošću 0 (što nije slučaj sa baferom koji je inicijalizovan preko funkcije
malloc
) - pri inicijalizaciji se predaju dva argumenta - broj elemenata (strukture zarad koje se rezerviše memorijski prostor), i veličina pojedinačnog elementa
U praksi, bafer (koji će biti korišćen za tekst), inicijalizuje se preko funkcije calloc
na sledeći način:
Međutim, iako naizgled deluje kao 'idealna zamena za funkciju malloc', može se reći da funkcija calloc
nije toliko zastupljena u programskim kodovima kao funkcija malloc
, iz sledećih razloga:
- ako nije obavezno inicijalizovati ceo bafer nulama, postavlja se pitanje u vezi sa vremenom koje se nepotrebno troši na upisivanje nula u memorijske adrese *
- ako jeste potrebno inicijalizovati ceo bafer nulama, '(ne)pisana pravila programiranja u C-u', nalažu da inicijalizaciju programer treba da obavi sam ('ručno')
Funkcija memcpy
Ukoliko je potrebno kopirati određeni broj bajtova iz jednog bafera u drugi, može se koristiti funkcija memcpy
:
Pre svega, funkcija memcpy
često se koristi za inicijalizaciju memorijskih blokova koji su definisani preko funkcija malloc
ili calloc
....
.... a ako se pitate zašto se ne upotrebljava samo središnja naredba ....
.... podsetićemo vas na to da je u pitanju inicijalizacija 'string konstante' (to jest, u pitanju je niska koja se ne može naknadno menjati).
Pogledajmo još jedan primer:
Preko prvog argumenta, pokazivac_2
, određeno je da upis u nisku, koja se referencira preko pokazivača buffer_2
, počinje od 16. znaka.
Drugi argument, pokazivac_1
, određuje da kopiranje znakova iz niske, koja se referencira preko pokazivača buffer_1
, počinje (praktično) od 1. znaka.
Treći argument, strlen(niska_1)
, određuje da će ceo sadržaj prve niske biti kopiran u drugu nisku.
Posle izvršavanja, niska #2 ima sledeći sadržaj: "Ko se to bavi matematikom?!"
.
Funkcija memset
Ukoliko je potrebno velikom brzinom upisati niz bajtova u susedne memorijske lokacije (unutar određenog bafera), tipično se koristi funkcija memset
:
Da pojasnimo parametre funkcije:
adresa
- pokazivač na memorijsku lokaciju od koje počinje upis (očekuje se da pripada uredno inicijalizovanom baferu)vrednost
- celobrojna promenljiva, koja praktično predstavlja znak koji će biti upisan u svaku memorijsku adresubroj_pozicija
- ukupan broj bajtova u koje će (počevši od lokacije koja je definisana preko prvog parametra, tj. argumenta), biti upisan znak (koji je definisan kao drugi argument)
Primer:
Počevši od 10. pozicije ....
.... u sledećih 6 pozicija (dužina niske "memset"), biće upisan znak '*'
.
Rezultat izvršavanja je niska: "Funkcija ****** je sjajna"
.
Kao što ste verovatno i sami naslutili, funkcija memset
može se iskoristi i za 'brzinsko' resetovanje bafera.
Sledeći poziv:
.... upisaće vrednost '\0' u sve bajtove (u okviru bafera).
Operacije sa datotekama u Python-u
Ako je programski jezik C bio sklon da prikaže "sve" * što se dešava pri učitavanju datoteka, sa Pythonom to nije slučaj, i može se reći da je obavljanje operacija sa datotekama - znatno jednostavnije.
Čitanje datoteke
U Python-u, proces učitavanja datoteke je automatizovan (o detaljima implementacije brine interpretator), i stoga - u praktičnom smislu - nekoliko desetina linija koda i komentara koje smo videli u odeljku o učitavanju datoteka u C-u, staje u svega nekoliko linija koda:
Upis u datoteku
Programski kod koji se tiče upisa u datoteku, takođe je vrlo jednostavan:
Za kraj ....
Poslednji odeljak (koji se tiče operacija sa datotekama u Python-u), predstavlja svojevrsnu priliku za osvrt na dva različita pristupa:
- Python je pogodan za "brzinsko kreiranje" (sasvim funkcionalnih) programa i skripti
- pisanje kompleksnijih primera u C-u, omogućava da se nauči mnogo više o tome kako programi i računari zapravo obrađuju podatke
Diskusijama na temu "C vs Python" bavićemo se u budućnosti (prva sledeća prilika biće uvodni članak o Python-u, a spremamo i članak o izboru prvog programskog jezika), međutim, već i na osnovu ovakvog ("ne-baš-preterano-obimnog") članka, mogu se doneti određeni zaključci.
Na primer, ako se neko prvo usmeri na komplikovaniji jezik (C) i komplikovanije primere, a tek posle (!) na skriptne jezike sa pojednostavljenom sintaksom (Python), mnogo više će naučiti i mnogo bolje će razumeti programske kodove kojima se bavi (bez obzira na to, za koji jezik se odluči na kraju).
U svakom slučaju, sada možete (uz malo truda), čitati podatke iz datoteka i upisivati podatke u datoteke, a to su veštine koje veoma dobro dođu ....