Binarne zadeve
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'