Binarne datoteke - in nizi bajtov

Obstajajo datoteke, ki niso besedilne. Očiten primer so datoteke, ki shranjujejo slike, zvok ali filme. Manj očiten primer je datoteka z Wordovim dokumentom. Ta sicer vsebuje besedilo, vendar ni opisano tako, da bi bilo v njej le besedilo, lepo od prvega do zadnjega znaka (96 je mali a in tako naprej...), temveč vsebuje še kup dodatnih reči. (Da ne govorimo o tem, da je v novejših različicah še zazipano, kar lahko preverite tako, da ga preimenujete iz .docx v .zip in odzipate.) Vsebine teh datotek torej ne moremo brati kot besedilo - torej kot številke, ki pomenijo znake - temveč le kot številke.

Odprimo datoteko .gif in jo preberimo v niz ter izpišimo prvih 30 znakov.

>>> gif = open("FRI.gif")
>>> s = gif.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/janezdemsar/env/o3/bin/../lib/python3.4/codecs.py", line 313, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 6: invalid continuation byte

Na tem mestu se spomnim, da še nisem povedal, kaj se sploh zgodi, če poskusimo datoteko odpreti z napačnim načinom kodiranja. Mesto je kar primerno, saj lahko rečem preprosto: no, tole. 'utf-8' codec can't decode...

Tega sicer ne bo znal "dekodirati" noben "kodek", ker "kodeki" iz številk razberejo znake, tole pa pač ni besedilna datoteka in se je ne da z nobenim kodekom spremeniti v kaj podobnega besedilu.

Ko odpremo datoteko, moramo povedati, da ne gre za besedilno temveč "številsko" ali, bolj učeno, binarno datoteko.

>>> gif = open("wafl.gif", "rb")
>>> s = gif.read()
>>> s[:30]
b'GIF89a\xe9\x00\xc7\x00\xf7\x00\x00\xe5\x93\x90\xe0|y\x8c\x8b\x8a\xe2\x86\x83\xd6\xd6\xd5\xf9\xe7'

Da gre za binarno datoteko, smo povedali tako, da smo kot drugi argument podali "rb": r za branje in b za binarno. Tako kot prej smo z read() prebrali celotno datoteko, le da smo zdaj previdno izpisali le prvih 30 znakov.

Kar smo dobili, je videti kot niz; začne se s črkami GIF89a, sledijo pa znaki z nekimi čudnimi kodami; ker so izven običajnega obsega ASCII (32-127), je Python izpisal njihove kode. Prvi trije za GIF89a imajo kot 233, 0 in 199, zato jih je, po šestnajstiško, izpisal kot \x01, \x04 in \xe5.

V resnici pa ne gre za čisto pravi niz - sumljiv je tisti b pred začetnim narekovajem. Gre za nov podatkovni tip: imenuje se bytes. Navzven je zelo zelo podoben nizu; ne le, da se podobno izpisuje, temveč ima tudi običajne metode nizov, kot so strip, find in join. Razlikuje se v par podrobnostih.

Če želimo dobiti prvi, tretji, osemnajsti... znak tega "niza", ne dobimo črke, temveč številko.

>>> s[0]
71
>>> s[1]
73
>>> s[2]
70
>>> s[6]
233
>>> s[8]
199

Reči tipa bytes so torej križanec med seznami in nizi: navzven so videti kot nizi, navznoter pa so - kot nam razodene indeksiranje - pravzaprav seznami 8-bitni števil, torej števil med 0 in 255.

Ker so s[0], s[1] in s[2] številke 71, 73 in 70, ki (po ASCII) ustrezajo znakom G, I in F, jih je Python izpisal kot G, I in F. Tisto, kar ni podobno ničemur, je izpisal z onimi \x.

Mimogrede, gif, ki smo ga naložili, je velik 233x199 točk. Kar je slučajno ravno vsebina s[6] in s[8]. (Sem rekel, da so bytes številke med 0 in 255? GIF pa je lahko tudi večji. Kako je shranjena velikost gifov, večjih od 255 točk, je velika skrivnost, ki jo znajo razriti samo tisti, ki znajo uporabljati Google.)

O branju binarnih datotek nimamo povedati kaj prida več. Razen tega, da se je po njih smiselno sprehajati: z metodo seek lahko skočimo na poljubno mesto v datoteki, metoda tell pa pove, kje v datoteki smo.

Pretvarjanje med str in bytes

Vzemimo niz

>>> s = "Demšar"
>>> len(s)
6

Če bi ga želeli zapisati v datoteko, bi morali ob odpiranju datoteke povedati, na kakšen način naj ga zakodira (predvsem zaradi š-ja) ali pa pustiti operacijskemu sistemu, da se odloči. "Zakodirati" tu pomeni spremeniti v številke.

Včasih pa želimo to pretvorbo opraviti sami. Iz niza s bomo naredili zaporedje številk, skladno z določenim kodekom. Temu rečemo kodiranje, opravi pa ga metoda encode, ki ji kot argument podamo kodek.

>>> s = "Večna pot"
>>> len(s)
9
>>> b = s.encode("cp1250")
>>> b
b'Ve\xe8na pot'
>>> len(b)
9
>>> b[0]
86
>>> b[2]
232

Še enkrat (tole ni zapleteno, se pa zna zazdeti takšno, če ne bomo pozorno spremljali): nizi (str) imajo metodo encode, s katero jih spremenimo v zaporedje števil (bytes). V gornjem primeru smo iz šestih znakov dobili šest številk. Prva je 86, saj je to koda (ki v cp1250, pa tudi v ASCII) pripada veliki črki V. Druga številka je 232 (\xe8), saj le-ta pripada malemu č - v kodeku cp1250.

Če bi poskusili s kakim drugim kodekom, recimo "cp1252", se to ne bi obneslo.

>>> c = s.encode("cp1252")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/janezdemsar/env/o3/bin/../lib/python3.4/encodings/cp1252.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_table)
UnicodeEncodeError: 'charmap' codec can't encode character '\u010d' in position 2: character maps to <undefined>

Črke "č" v tem kodnem razporedu ni.

Zdaj pa obrnimo. Imamo zaporedje bajtov, b. Konkretno, imamo zaporedje števil

>>> list(b)
[86, 101, 232, 110, 97, 32, 112, 111, 116]

Če želimo spremeniti b v niz, v besedilo, moramo te črke "dekodirati". Ob tem moramo povedati, s kakšnim kodekom. Vemo: cp1250.

>>> b.decode("cp1250")
'Večna pot'

Pa če zgrešimo? Če namesto cp1250 uporabimo cp1252? Tokrat bo delovalo. V bo V in e bo e, saj sta tadva znaka v vseh kodekih na istem mestu (lepo je biti Američan). Kot tretji znak pa bomo namesto č dobili pač tisti znak, ki ima v izbranem kodeku kodo 232.

>>> b.decode("cp1252")
'Veèna pot'

Znano? Ste že kdaj videli è namesto č? Recimo v podnapisih v VLC? No, zdaj veste, zakaj: zato, ker je nek program dobil besedilo, zapisano v cp1250, mislil pa je, da je v cp1252, zato je kodo 232 prebral kot è in ne kot č.

Kako pa deluje ta stran? V katerem kodeku je zapisana, da ima tako è-je kot č-je? V UTF-8, v katerem je lahko zapisano vse.

>>> b = s.encode("utf-8")
>>> len(s)
9
>>> len(b)
10

Čeprav je besedilo dolgo 9 znakov, je zakodirano z 10 števili.

>>> b
b'Ve\xc4\x8dna pot'
>>> b[2]
196
>>> b[3]
141

Znak č se zapiše s zaporedjem 196, 141. Če bi malo pobrskali, bi videli, da se è zapiše kot 195, 168. V pa se zapiše z eno samo številko, 86, tako kot prej. (Lepo je biti Američan.)

Dekodiranje je seveda takšno kot prej, le z drugim kodekom.

>>> b.decode("utf-8")
'Večna pot'
Zadnja sprememba: torek, 14. april 2026, 20.19