Datoteke

Branje datotek

Recimo, da imamo datoteko stavki.txt s sledečo vsebino:

To je prvi stavek. To je drugi
stavek. To je tretji stavek. To
je četrti stavek.

Če vsebino želimo prebrati, moramo datoteko najprej odpreti. To storimo s funkcijo open. Klicu open lahko podamo tudi neobvezni argument encoding, ki poda kodno tabelo, v kateri je napisana datoteka. Težava je, da je privzeta vrednost parametra na Windowsih cp1250, kar je precej zastarel standard, zato morate tam pisati open(..., encoding='UTF-8').

dat = open('stavki.txt', encoding='UTF-8')
dat
<_io.TextIOWrapper name='stavki.txt' mode='r' encoding='UTF-8'>

Dobili smo objekt dat, ki predstavlja dostop do vsebine datoteke. Najosnovnejša metoda je read, ki ob vsakem klicu prebere dano število znakov.

dat.read(35)
'To je prvi stavek. To je drugi\nstav'
dat.read(20)
'ek. To je tretji sta'

Če števila ne podamo, datoteko preberemo do konca:

dat.read()
'vek. To\nje četrti stavek.\n'
dat.read()
''

Datoteko je treba po koncu uporabe treba zapreti, da lahko do nje dostopajo tudi drugi programi. Se vam je kdaj zgodilo, da USB ključka niste mogli varno odstraniti, ker naj bi ga uporabljal še nek program? Težava je bila, da slabo napisan program ni zaprli dostopa do datotek, zato je operacijski sistem mislil, da jih še vedno uporablja. Datotetko lahko zapremo z metodo close.

dat.close()

Da ne pozabimo zapreti datoteke, ali pa da nam zapiranja ne prepreči napaka v programu, Python ponuja varnejši način odpiranja datotek. Če napišemo

with open('stavki.txt') as dat:
    ...

bo Python samodejno poskrbel za zapiranje datoteke, ko se bodo izvedli vsi stavki .... To bo veljalo tudi v primeru, če kakšen izmed stavkov sproži izjemo in prekine izvajanje.

Za branje po vrsticah lahko uporabimo metodi readline in readlines:

with open('stavki.txt') as dat:
    prva = dat.readline()
    druga = dat.readline()
    ostale = dat.readlines()
prva
'To je prvi stavek. To je drugi\n'
druga
'stavek. To je tretji stavek. To\n'
ostale
['je četrti stavek.\n']

Po vrsticah se lahko zapeljemo z zanko for:

with open('stavki.txt') as dat:
    for vrstica in dat:
        print(len(vrstica), vrstica)
31 To je prvi stavek. To je drugi

32 stavek. To je tretji stavek. To

18 je četrti stavek.

Vidimo, da so med izpisanimi vrsticami tudi prazne. Te so posledice tega, da vsaka vrstica datoteke na koncu vsebuje znak za novo vrstico, en tak znak pa doda še print.

def izpisi_datoteko(ime_datoteke):
    print(ime_datoteke)
    with open(ime_datoteke) as dat:
        for st_vrstice, vrstica in enumerate(dat, 1):
            print(st_vrstice, vrstica, end='')
izpisi_datoteko('stavki.txt')
stavki.txt
1 To je prvi stavek. To je drugi
2 stavek. To je tretji stavek. To
3 je četrti stavek.

Pisanje datotek

Datoteke lahko tudi pišemo, vendar jih moramo ustrezno odpreti, kar naredimo kot: Na Windowsih, kjer je treba podat

with open(..., 'w') as d:
    ...

oziroma

with open(..., 'w', encoding='UTF-8') as d:
    ...

Ko datoteko odpremo za pisanje, lahko vanjo pišemo z metodo write

with open('izpis.txt', 'w') as dat:
    dat.write('To je ')
    dat.write('en stavek.\nTo je drugi.')
izpisi_datoteko('izpis.txt')
izpis.txt
1 To je en stavek.
2 To je drugi.

V datoteko pišemo tudi s print, ki mu podamo neobvezni argument file:

with open('izpis.txt', 'w') as dat:
    print('To je en stavek.', file=dat)
    print('1 + 1 =', 1 + 1, file=dat)
izpisi_datoteko('izpis.txt')
izpis.txt
1 To je en stavek.
2 1 + 1 = 2

Vsakič, ko datoteko odpremo za pisanje, povozimo obstoječo vsebino:

with open('izpis.txt', 'w') as dat:
    print('To je en stavek.', file=dat)
    print('To je drugi.', file=dat)
with open('izpis.txt', 'w') as dat:
    print('To je tretji stavek.', file=dat)
    print('To je četrti.', file=dat)
izpisi_datoteko('izpis.txt')
izpis.txt
1 To je tretji stavek.
2 To je četrti.

Če želimo, lahko namesto w podamo parameter a, ki datoteko odpremo za dodajanje:

with open('izpis.txt', 'w') as d:
    print('To je en stavek.', file=d)
    print('To je drugi.', file=d)
with open('izpis.txt', 'a') as d:
    print('To je tretji stavek.', file=d)
    print('To je četrti.', file=d)
izpisi_datoteko('izpis.txt')
izpis.txt
1 To je en stavek.
2 To je drugi.
3 To je tretji stavek.
4 To je četrti.

Delo z datotečnim sistemom

Tako, kot si daljše programe shranjujemo v datoteke, si tudi večje količine podatkov shranimo v datoteke. Poglejmo si najprej, kako so datoteke na računalniku sploh organizirane. Vsak nosilec podatkov (trdi disk, SSD, DVD, USB ključek) ima podatke zapisane v določenem datotečnem sistemu, ki je odvisen od vrste nosilca in operacijskega sistema. Na primer, trdi diski pod Windowsi so običajno formatirani v sistemu NTFS, pod Linuxom v sistemu Ext, na Macintoshu pa v sistemu HFS. USB ključki so zaradi lažje prenosljivosti ponavadi vsi formatirani v sistemu FAT (ki se je včasih uporabljal pod Windowsi). Če želimo, lahko na nosilcu naredimo več particij in vsako od njih ločeno formatiramo s svojim datotečnim sistemom.

Datotečni sistem določa, v kakšni obliki je na nosilcu shranjena vsebina datotek in v kakšni obliki je predstavljena uporabniku. S prvim se ne bomo ukvarjali, pri drugem pa je pomembna le razlika med operacijskim sistemom Windows in sistemi, osnovanimi na UNIXu (torej Linux ali OS X).

Datotečni sistem vsebuje datoteke, razporejene po mapah (oz. direktorijih), ki so lahko tudi gnezdene. Na vrhu imamo korensko mapo, ki jo v operacijskih sistemih, osnovanih na UNIXu, označujemo z /, na operacijskem sistemu Windows pa z C:\, kjer je C ime particije: C običajno označuje glavni pogon, D drugi pogon ali CD/DVD/BlueRay enoto, A in B sta se uporabljali za diskete, kasnejše črke pa se uporabljajo za USB ključke in podobno.

Za primer vzemimo datotečni sistem s sledečimi mapami in datotekami:

/
    uvp
        datoteke
            vhodna.txt
            izhodna.txt
        funkcije.py
        seznami.py
        slovarji.py
        zanke.py
    praktikum
        latex
            pismo.tex
            pismo.pdf
            pismo.aux
        mathematica
            grafi.nb
            kolokvij.nb
    analiza
        plonkec.tex

Vsaka datoteka ima absolutno pot, na kateri jo lahko najdemo. Na primer absolutna pot datoteke pismo.tex je /praktikum/latex/pismo.tex oz. C:\praktikum\latex\pismo.tex na Windowsih. Do datotek kaže tudi relativna pot. Na primer, glede na imenik praktikum je pot do pismo.tex kar latex/pismo.tex. Če želimo, gremo z .. tudi ven iz trenutnega imenika. Na primer, glede na imenik mathematica je relativna pot do vhodna.txt enaka ../../uvp/datoteke/vhodna.txt.

Za delo z datotečnim sistemom je na voljo knjižnica os:

import os

Trenutni imenik dobimo z os.getcwd:

os.getcwd()
'/home/runner/work/uvod-v-programiranje/uvod-v-programiranje/zapiski'

zamenjamo pa ga z os.chdir, ki sprejme absolutno ali relativno pot:

os.chdir('..')
os.getcwd()
'/home/runner/work/uvod-v-programiranje/uvod-v-programiranje'
os.chdir('zapiski')
os.getcwd()
'/home/runner/work/uvod-v-programiranje/uvod-v-programiranje/zapiski'

Imena vseh datotek v danem imeniku dobimo z os.listdir:

os.listdir()
['_toc.yml',
 '00-uvod.md',
 'izpis.txt',
 '09-tekstovni-vmesnik.md',
 '04-zanke.md',
 '06-slovarji-in-mnozice.md',
 '02-rekurzija.md',
 '08-datoteke.md',
 'slike',
 '07-razredi.md',
 '11-spletni-vmesnik.md',
 '12-spletna-storitev.md',
 '05-seznami-in-nabori.md',
 '_config.yml',
 'stavki.txt',
 '_build',
 '01-uvod-v-python.md',
 '10-nadzor-razlicic.md',
 '03-nizi.md']
os.listdir('slike')
['readme-md.png', 'skica-modela.png', 'avtoceste.png', 'skica-vmesnika.png']

Vse funkcije za delo z datotekami lahko najdete v [uradni dokumentaciji], ostale pogosto uporabljene pa so:

  • os.mkdir(pot), ki naredi imenik z dano potjo.

  • os.makedirs(pot, exist_ok=False), ki naredi imenik z dano potjo in vse vmesne imenike. Če je argument exist_ok nastavljen na True, ne javi napake, če ciljna mapa že obstaja.

  • os.rename(stara_pot, nova_pot) datoteko ali imenik s potjo stara_pot preimenuje v nova_pot.

  • os.remove(pot) pobriše datoteko z dano potjo.

  • os.rmdir(pot) pobriše imenik z dano potjo.

  • os.removedirs(pot) pobriše imenik z dano potjo in vse vmesne imenike.

Poleg knjižnice os je na voljo tudi knjižnica os.path za delo z datotečnimi potmi:

  • os.path.join(pot1, pot2) stakni poti pot1 in pot2, pri čemer ustrezno poskrbi za prava ločila glede na operacijski sistem.

  • os.path.isdir(pot) vrne True, kadar pot vodi do imenika.

  • os.path.exists(pot) vrne True, kadar pot obstaja v datotečnem sistemu.

  • os.path.splitext(pot) loči pot datoteke na del pred končnico in del za njo:

os.path.splitext('/imenik/podimenik/test.txt')
('/imenik/podimenik/test', '.txt')
  • os.path.split(pot) loči na pot do zadnje imenika in na ime datoteke. Do prve komponente lahko dostopamo tudi z os.path.dirname(pot), do druge pa z metodo os.path.basename(pot).

os.path.split('/imenik/podimenik/test.txt')
('/imenik/podimenik', 'test.txt')
os.path.dirname('/imenik/podimenik/test.txt')
'/imenik/podimenik'
os.path.basename('/imenik/podimenik/test.txt')
'test.txt'
  • os.path.abspath(pot) dano pot pretvori v absolutno:

os.path.abspath('../02-rekurzija/')
'/home/runner/work/uvod-v-programiranje/uvod-v-programiranje/02-rekurzija'

JSON datoteke

Za zapis strukturiranih podatkov je uveljavljen standard JSON, ki ga podpirajo skoraj vsa orodja za delo s podatki. Vrednosti v njem so lahko:

  • števila,

  • logični vrednosti true in false (pozorni bodite na malo začetnico),

  • nizi (ki jih obvezno pišemo med narekovaje "),

  • ničelna vrednost null (ki igra enako vlogo kot None),

  • seznami (ki jih pišemo enako kot v Pythonu) ter

  • objekti (ki so podobno kot slovarji v Pythonu, le da so ključi lahko le nizi).

Na primer:

{
  "ime": "Anka Cvetnik",
  "vpisnaStevilka": 27123456,
  "visina": 167.8,
  "prijatelji": [27154321, 27165432],
  "predmetnik": [
    {"predmet": "Analiza 1", "ocena": 10},
    {"predmet": "Algebra 1", "ocena": 10},
    {
      "predmet": "Uvod v programiranje",
      "ocena": null
    }
  ]
}

V Pythonu je delu z JSONom namenjena knjižnica json. Najenostavnejša funkcija v njej je loads, ki prebere niz z JSONom in vrne ustrezno Pythonovo vrednost:

import json
json.loads('[1, {"3": true, "4": null}]')
[1, {'3': True, '4': None}]

V obratno smer deluje funkcija json.dumps, ki Pythonovo vrednost pretvori v JSON:

>>> json.dumps([1, {3: True, 4: None}])
'[1, {"3": true, "4": null}]'
'[1, {"3": true, "4": null}]'

Vidimo, da je funkcija ključe slovarjev tudi ustrezno spremenila v nize.

Če želimo delati z JSON datotekami, imamo na voljo funkcijo dump, ki poleg vrednosti sprejme datoteko, v katero naj zapiše JSON vrednost.

with open('primer.json', 'w') as dat:
    json.dump([1, {3: True, 4: None}], dat)
izpisi_datoteko('primer.json')
primer.json
1 [1, {"3": true, "4": null}]

Če želimo, lahko datoteko tudi lepo oblikujemo z zamiki:

with open('primer.json', 'w') as dat:
    json.dump([1, {3: True, 4: None}], dat, indent=4)
izpisi_datoteko('primer.json')
primer.json
1 [
2     1,
3     {
4         "3": true,
5         "4": null
6     }
7 ]

Podobno obstaja funkcija load, ki datoteko prebere:

with open('primer.json') as datoteka:
    vrednost = json.load(datoteka)
vrednost
[1, {'3': True, '4': None}]