28. Binárne súbory

prezentácia

video prezentácia


V minulom semestri sme sa zoznámili s prácou s textovými súbormi:

  • textový súbor môžeme chápať ako postupnosť riadkov, pričom každý riadok je znakový reťazec ukončený znakom '\n'

  • takýto súbor sa najčastejšie nachádza na nejakom externom médiu, napríklad na disku alebo USB (najčastejšie majú príponu '.txt', '.py', '.html', '.js', '.css', '.xml', '.svg', …) a môžeme ich pozrieť alebo upravovať v rôznych textových editoroch, napríklad aj v pythonovskom IDLE

  • aby sme mohli pracovať so súborom, musíme ho najprv otvoriť (jedna z možností):

    • na čítanie - ďalej sa umožní čítať riadok za riadkom (alebo postupnsoť znakov)

    • na zápis - vymaže doterajší obsah a umožní postupne zapisovať riadok za riadkom (alebo postupnsoť znakov)

    • na zápis s ponechaním pôvodného obsahu

  • po ukončení práce so súborom by sme ho mali zatvoriť,aby sme ho neblokovali pre operačný systém

Nie všetky súbory v súborových systémoch sú ale textové. Sú to napríklad rôzne grafické súbory, napríklad '.png', '.bmp', '.jpg', … ale aj komprimované súbory ako '.zip', '.rar', '.cab', …, vykonateľné súbory ako '.exe', '.com', '.dll', … alebo rôzne dátové súbory so známou alebo s neznámou štruktúrou '.doc', '.odt', '.pdf', '.dat', …

Takéto súbory, ak otvoríme v textovom editore, sa zobrazia s množstvom divných znakov. Ak by sme ich ale zobrazili v editoroch, ktoré zvládajú šestnástkový výpis, videli by sme niečo takéto:

_images/28_01.png

Zobrazuje sa postupnosť bajtov, pričom niektoré z nich zodpovedajú niektorým ASCII znakom.


Typ bytes

Binárny súbor je postupnosť bajtov. V Pythone budeme zapisovať do binárneho súboru a čítať z neho nie znakové reťazce, ale nový typ bytes. Tento typ je nemeniteľnou (immutable) postupnosťou bajtov, teda celých čísel z intervalu <0, 255>. V Pythone zapisujeme takéto postupnosti v tvare znakového reťazca, pred ktorým je prilepený znak b:

>>> b'+ A'                         # trojbajtová postupnosť
b'+ A'
>>> list(b'+ A')                   # tento zápis reprezentuje ASCII-hodnoty týchto znakov
[43, 32, 65]
>>> bytes((45, 49, 70, 100))       # postupnosť bajtov vieme skonštruovať aj zo zoznamu čísel
b'-1Fd'
>>> bytes()                        # prázdna postupnosť bajtov
b''
>>> bytes('Python', 'utf8')        # postupnosť bajtov vieme skonštruovať aj zo znakového reťazca
b'Python'                          # vtedy musíme uviesť aj kódovanie
>>> tuple(b'Python')
(80, 121, 116, 104, 111, 110)

Takto vidíme zobrazené bajty, ktoré majú v ASCII svoju reprezentáciu. Ak potrebujeme zadať bajt, ktorý nemá v ASCII svoje zobrazenie, použije sa zadávanie kódu bajtu v šestnástkovej sústave:

>>> b'A\x05B'                      # trojbajtová postupnosť
b'A\x05B'
>>> tuple(b'A\x05B')               # '\x05' označuje jeden bajt s hodnotou 5
(65, 5, 66)
>>> b = bytes(range(130, 250, 10)) # dvanásť bajtová postupnosť čísel
>>> b
b'\x82\x8c\x96\xa0\xaa\xb4\xbe\xc8\xd2\xdc\xe6\xf0'  # čísla sú v šestnástkovej sústave
>>> tuple(b)
(130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240)
>>> for i in b:                    # postupnosť vieme prechádzať for-cyklom
        print(i, end=' ')

130 140 150 160 170 180 190 200 210 220 230 240

Okrem nemeniteľného (immutable) štandardného typy bytes v Pythone môžete využiť meniteľný typ bytearray, ktorý funguje skoro presne rovnako. Viac informácií bytearray.


Práca s binárnym súborom

S binárnym súborom pracujeme veľmi podobne ako s textovým. Zápis do súboru:

with open('subor.dat', 'wb') as subor:     # 'wb' označuje zápis do binárneho súboru
    subor.write(b'student')
    subor.write(bytes(range(10)))

Zapíše 17 bajtov, najprv 7 bajtov ASCII kodov reťazca 'student' a za tým 10 bajtov s hodnotami 0, 1, 2, …, 9.

Čítanie zo súboru:

with open('subor.dat', 'rb') as subor:     # 'rb' označuje čítanie z binárneho súboru
    prvy = subor.read(7)
    bajt = subor.read(1)
    druhy = b''
    while bajt != b'':                     # kým nie je koniec súboru
        druhy += bajt
        bajt = subor.read(1)

Tento program najprv prečíta prvých 7 bajtov do premennej prvy (zrejme bude obsahovať postupnosť b'student') a do premennej druhy prečíta zvyšné bajty súboru (zrejme b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09', čo sa môže zobraziť ako b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t'). Dalo by sa to zapísať aj takto jednoducho:

with open('subor.dat', 'rb') as subor:
    prvy = subor.read(7)               # prvých 7 bajtov
    druhy = subor.read()               # celý zvyšok súboru až do konca

Konverzie dát z a do postupnosti bajtov

Na stránke Bytes Objects v Pythonovom helpe si môžete prečítať ďalšie možnosti práce s týmto typom (napríklad, užitočnú metódu decode), ale užitočné môžu byť aj niektoré metódy s celočíselným typom (napríklad, to_bytes a from_bytes). Preštudujte si nasledovné ukážky:

  • ako prerobiť postupnosť bajtov (typ bytes) na obyčajný znakový reťazec (typ str):

    >>> bajty = b'Kovac Igor'
    >>> ''.join(chr(i) for i in bajty)   # pomocou chr pre každý bajt a potom join
    'Kovac Igor'
    >>> ''.join(map(chr, bajty))         # pomocou mapovania každého bajtu na znak a join
    'Kovac Igor'
    >>> bajty.decode()                   # pomocou metódy decode
    'Kovac Igor'
    
  • ako z celého čísla vyrobiť postupnosť štyroch bajtov (na začiatku je najnižší bajt):

    >>> cislo = 1234567
    >>> bajty = b''
    >>> for i in range(4):               # pomocou for-cyklu a postupným delením 256
            bajty += bytes((cislo%256,))
            cislo //= 256
    
    >>> bajty
    b'\x87\xd6\x12\x00'
    

    alebo pomocou metódy:

    >>> cislo = 1234567
    >>> cislo.to_bytes(4, 'little')   # parameter 'little' označuje, že začína najnižším bajtom
    b'\x87\xd6\x12\x00'
    
  • ako z postupnosti štyroch bajtov vyrobiť celé číslo:

    >>> bajty = b'\xb1\x7f\x39\x05'
    >>> cislo = 0
    >>> for bajt in reversed(bajty):
         cislo = cislo*256 + bajt
    
    >>> cislo
    87654321
    

    alebo pomocou metódy:

    >>> bajty = b'\xb1\x7f\x39\x05'
    >>> int.from_bytes(bajty, 'little') # parameter 'little' označuje, že začína najnižším bajtom
    87654321
    

Príklady práce s binárnym súborom

Uvedieme niekoľko užitočných príkladov.

  • obsah binárneho súboru ako 16-ové hodnoty:

    def to_hex(subor, pocet=None):           # pocet urcuje pocet precitanych bajtov
        with open(subor, 'rb') as file:
            return ' '.join(f'{x:02X}' for x in file.read(pocet))
    
    >>> to_hex('python.exe', 1000)
    

    to isté by sa dalo zapísať aj takto pomocou metódy hex:

    def to_hex(subor, pocet=None):
        with open(subor, 'rb') as file:
            return file.read(pocet).hex(' ')
    
  • obsah binárneho súboru so zobrazitelnými znakmi:

    def to_str(subor, pocet=None):           # pocet urcuje pocet precitanych bajtov
        with open(subor, 'rb') as file:
            return ''.join(chr(x) if 32<=x<127 else '.' for x in file.read(pocet))
    
    >>> to_str('python.exe', 1000)
    
  • zapísať do súboru zoznam dvojbajtových čísel (kladné čísla od 0 do 65535) - čísla sa zapisujú tak, že prvý bajt z dvojice je ten vyšší:

    def zapis_zoznam(subor, zoznam):
        with open(subor, 'wb') as file:
            for x in zoznam:
                file.write(x.to_bytes(2, 'big'))
    
    >>> zapis_zoznam('zoznam.dat', range(1000, 2000, 7))
    
  • prečítať zo súboru zoznam dvojbajtových čísel (kladné čísla od 0 do 65535) - čísla sa čítajú tak, že prvý bajt z dvojice je ten vyšší:

    def citaj_zoznam(subor):
        zoznam = []
        with open(subor, 'rb') as file:
            x = file.read(2)
            while x != b'':
                zoznam.append(int.from_bytes(x, 'big'))
                x = file.read(2)
        return zoznam
    
    >>> zoz = citaj_zoznam('zoznam.dat')
    >>> zoz
    
  • zapísať do súboru zoznam znakových reťazcov - každý reťazec sa zapíše tak, že najprv je v jednom bajte dĺžka reťazca v bajtoch (po zakódovaní do bytes) a za tým samotný reťazec:

    def zapis_retazce(subor, retazce):
        with open(subor, 'wb') as file:
            for r in retazce:
                r = r.encode()
                file.write(bytes((len(r),))+r)
    
    >>> ret = ('prvý', 'druhý\na tretí', '', '\n\n\n')
    >>> zapis_retazce('retazce.dat', ret)
    
  • prečítať zo súboru zoznam znakových reťazcov - každý reťazec sa prečíta tak, že najprv je v jednom bajte dĺžka reťazca v bajtoch (po zakódovaní do bytes) a za tým samotný reťazec:

    def citaj_retazce(subor):
        ret = []
        with open(subor, 'rb') as file:
            dlzka = file.read(1)
            while dlzka != b'':
                ret.append(file.read(dlzka[0]).decode())
                dlzka = file.read(1)
        return ret
    
    >>> r = citaj_retazce('retazce.dat')
    >>> r
    
  • vytvoriť kópiu súboru:

    def kopia(subor1, subor2):
        with open(subor1, 'rb') as file1, open(subor2, 'wb') as file2:
            file2.write(file1.read())
    
    >>> kopia('subor.dat', 'subor1.dat')
    
  • skontrolovať, či majú dva súboru identický obsah:

    def zhoda(subor1, subor2):
        with open(subor1, 'rb') as file1, open(subor2, 'rb') as file2:
            return file1.read() == file2.read()
    
    >>> zhoda('subor.dat', 'subor1.dat')
    

Cvičenia

L.I.S.T.


  1. Napíš funkciu vyrob(meno_suboru, n, pocet), ktorá vygeneruje binárny súbor pocet náhodných celých n-bajtových čísel (čísla začínajú najvyšším bajtom). Náhodné hodnoty nech sú napríklad z rozsahu range(0, 256**n, 100). Riešenie otestuj pre rôzne n.


  1. Napíš funkciu citaj(meno_suboru, n), ktorá prečíta súbor z predchádzajúcej úlohy a vráti jeho prvky v tvare zoznamu (list).


  1. V binárnom súbore 'cisla.dat' je uložená postupnosť celých n-bajtových čísel (čísla začínajú najvyšším bajtom). Napíš funkciu vyhod(meno_suboru='cisla.dat', n=1, hodnota=0), ktorá z daného súboru vyhodí všetky výskyty zadanej hodnoty. Napríklad, volanie vyhod('cisla4.dat', 4, 1000) vyhodí z daného súboru 4-bajtových celých čísel všetky výskytu hodnoty 1000. Svoje riešenie otestuj s rôznymi binárnymi súbormi, v ktorých je rôzna veľkosť zapísaných celých čísel (napríklad pre jednobajtové, dvojbajtové, štvorbajtové).


  1. Napíš funkciu pocet_bajtov(x), ktorá pre kladné celé číslo x zistí minimálny počet bajtov, ktoré dané číslo zaberá. Napríklad:

    >>> pocet_bajtov(0)
    1
    >>> pocet_bajtov(200)
    1
    >>> pocet_bajtov(2000)
    2
    

  1. Napíš funkciu zapis_zoznam(meno_suboru, zoznam), ktorá do binárneho súboru zapíše prvky daného zoznamu (list). Prvkami sú buď kladné celé čísla alebo znakové reťazce. Navrhni formát binárneho súboru tak, aby sa dal spätne prečítať a vytvoriť pôvodný zoznam: asi najlepšie tak, že v ňom bude pre každý prvok informácia o tom, či je to celé číslo alebo znakový reťazec (napríklad znakom 'i' alebo 's') a tiež počet bajtov, ktoré zaberá (môžeš predpokladať, že tento počet bude do 255). Otestuj, napríklad pre:

    zapis_zoznam('zoznam.dat', ['Abc', 42, '', '*'*100, 7**100])
    

  1. Napíš funkciu citaj_zoznam(meno_suboru), ktorá vráti prečítaný zoznam z binárneho súboru z predchádzajúcej úlohy.


  1. Napíš funkciu zapis_desatinne(meno_suboru, zoznam), ktorá do binárneho súboru zapíše zoznam desatinných čísel (float). Navrhni takú reprezentáciu, aby sa takýto súbor dal naspäť prečítať. Otestuj, napríklad:

    zapis_desatinne('float.dat', list(1/i for i in range(1, 8)))
    

  1. Napíš funkciu citaj_desatinne(meno_suboru), ktorá vráti prečítaný zoznam z binárneho súboru z predchádzajúcej úlohy.


4. Týždenný projekt

L.I.S.T.


Toto domáce zadanie bude riešiť takúto úlohu:

  • v súbore sa nachádzajú informácie o študentoch nejakej školy (osobné číslo, meno a priezvisko, zoznam udelených známok);

  • tento súbor treba prečítať a vytvoriť z neho jednosmerný spájaný zoznam (metoda read), v ktorom bude v každom vrchole informácia o jednom študentovi; vrcholy budú v spájanom zozname v tom poradí, v akom boli v súbore;

  • z tohto zoznamu budeme vedieť nájsť študenta (metóda __getitem__) podľa zadaného osobného čísla

  • tento zoznam bude treba vedieť zapísať späť do súboru (metóda write), ale v inom poradí:

    • metóda remove_min nájde študenta s najmenším osobným číslom, tento vrchol zo spájaného zoznamu odstráni a vráti ho ako výsledok funkcie

    • údaje tohto študenta zapíše v požadovanom formáte do súboru

    • toto opakuje, kým nebude spájaný zoznam prázdny

Formát súboru so študentmi nebude textový, ale binárny. Pre každého študenta bude v súbore postupnosť takýchto bajtov:

  • najprv 4 bajty s osobným číslom (osobné číslo je celé číslo z intervalu <0, 4294967295>, teda 256**4-1), kde najnižší bajt je prvý v štvorici, napríklad číslo 74565 bude v štyroch bajtoch ako (69, 35, 1, 0), alebo v šestnástkovej sústave ako (0x45, 0x23, 0x01, 0x00)

  • potom nasleduje postupnosť znakov (postupnosť bajtov s ASCII-kódmi, znaky nebudú obsahovať diakritiku), pričom prvý bajt postupnosti obsahuje dĺžku reťazca, napríklad meno a priezvisko 'Abc Def', bude uložené v 8 bajtoch: (7, 65, 98, 99, 32, 68, 101, 102), čo vieme zapísať aj v 16-ovej sústave: (0x07, 0x41, 0x62, 0x63, 0x20, 0x44, 0x65, 0x66); zrejme takéto reťazce môžu mať maximálnu dĺžku 255 znakov; ASCII-kódy znakov budú len z intervalu <32, 126>

  • na záver je to postupnosť známok v tvare čísel od 1 do 6, ktorá je ukončená číslom 0, pričom 1 zodpovedá známke A, 2 zodpovedá B, atď. až 6 zodpovedá Fx, 0 tu označuje koniec postupnosti a nereprezentuje známku, napríklad postupnosť (3, 1, 6, 4, 0) popisuje 4 známky (C, A, Fx, D); ak budeme niekedy potrebovať vypočítať priemer, tak použijeme prepočet, v ktorom známka A má hodnotu 1, známka B má hodnotu 1.5, známka C má hodnotu 2, atď. až známka Fx má hodnotu 4; potom priemer známok je (2+1+4+2.5)/4, teda 2.375

  • ak sa v súbore hneď na začiatku objaví táto postupnosť bajtov (zapísali sme ju v 16-ovej sústave):

    0x45, 0x23, 0x01, 0x00, 0x07, 0x41, 0x62, 0x63, 0x20, 0x44, 0x65, 0x66, 0x03, 0x01, 0x06, 0x04, 0x00
    

    týchto 17 bajtov reprezentuje informácie o jednom študentovi s osobným číslom 74565, s menom a priezviskom 'Abc Def' a so známkami (C, A, Fx, D); za týmito bajtami môže v súbore nasledovať ďalšia postupnosť bajtov, ktorá popisuje ďalšieho študenta

Napíš modul s menom riesenie.py, ktorý bude obsahovať jedinú triedu s ďalšou vnorenou podtriedou a týmito metódami:

class LinkedList:
    class Student:
        def __init__(self, number, name, grades):  # osobné číslo, meno a priezvisko, zoznam známok
            self.number = number                   # celé číslo z <0, 4294967295>
            self.name = name                       # znakový reťazec
            self.grades = grades                   # n-tica (tuple) celých čísel z <1, 6>
            self.next = None

        def __repr__(self):
            return f'Student({self.number}, {self.name!r}, {self.grades})'

    def __init__(self):
        self.zac = self.kon = None
        ...

    def read(self, file_name):
        ...

    def write(self, file_name):
        ...

    def remove_min(self):
        ...
        return ...

    def __getitem__(self, number):
        ...
        return None

    def __len__(self):
        ...
        return 0

Trieda LinkedList implementuje jednosmerný spájaný zoznam študentov, pričom metódy triedy by mali mať takúto funkčnosť (môžeš si dodefinovať aj ďalšie pomocné metódy):

  • metóda __init__() inicializuje atribúty zac a kon pre referencie na začiatok a koniec zoznamu;

  • metóda read(file_name) otvorí binárny súbor a informácie o študentoch pridá na koniec momentálneho spájaného zoznamu; zrejme by takto mohol z viacerých súborov vyrobiť jeden spájaný zoznam;

  • metóda write(file_name) zapíše kompletný spájaný zoznam do binárneho súboru; použije na to metódu remove_min, pomocou ktorej získa študenta s najmenším osobným číslom, študenta zapíše do súboru; ak túto metódu bude volať, kým nebude spájaný zoznam prázdny, zapíše do súboru všetkých študentov v usporiadanom poradí;

  • metóda remove_min() vyhľadá študenta s najmenším osobným číslom, tohto študenta vyhodí zo spájaného zoznamu a samotný vrchol (typu self.Student) vráti ako výsledok funkcie; ak bol zoznam už prázdny, funkcia vráti None;

  • metóda __getitem__(number) vráti informácie o študentovi s daným osobným číslom v tvare dvojice (meno, priemer), kde meno je znakový reťazec s menom a priezviskom študenta, priemer je priemer jeho známok; ak je zoznam študentových známok prázdny, jeho priemer je 0; ak študenta s daným osobným číslom nenajde, funkcia vráti None;

  • metóda __len__() vráti aktuálny počet prvkov zoznamu.

Obmedzenia

  • svoje riešenie odovzdaj v súbore riesenie.py, pričom sa v ňom bude nachádzať len jedna definícia triedy LinkedList, trieda Student bude vnorená v triede LinkedList

  • prvé tri riadky tohto súboru budú obsahovať:

    # 4. zadanie: binarny subor
    # autor: Janko Hrasko
    # datum: 16.3.2021
    
  • zrejme ako autora uvedieš svoje meno

  • atribúty zac a kon v triede LinkedList musia obsahovať referencie na začiatok a koniec zoznamu

  • tvoj program by nemal počas testovania testovačom nič vypisovať (žiadne testovacie print())

Testovanie

Keď budeš spúšťať svoje riešenie na počítači, môžeš do súboru riesenie.py pridať testovacie riadky, ktoré ale testovač vidieť nebude, napríklad, ak subor1.dat obsahuje takúto postupnosť bajtov (zapísali sme ich v 16-ovej sústave a rozsekali sme ich tak, aby bolo lepšie vidieť informácie o štyroch študentoch):

4523010007416263204465660301060400
0004000007476820496A6B6C0200
B17F3905094D204E6F707172737400
87D61200075576777879205A0101010300

môžeme zapísať takýto test:

if __name__ == '__main__':
    zoz = LinkedList()
    zoz.read('subor1.dat')
    print('pocet =', len(zoz))
    p = zoz.zac
    while p:
        print(p)
        p = p.next
    print()
    for cislo in 74565, 87654321, 8765432:
        print(f'student[{cislo}] =', zoz[cislo])
    print('min =', zoz.remove_min())
    print('pocet po remove_min =', len(zoz))
    zoz.write('subor2.dat')
    print('pocet po write =', len(zoz))

Tento test by mal vypísať:

pocet = 4
Student(74565, 'Abc Def', (3, 1, 6, 4))
Student(1024, 'Gh Ijkl', (2,))
Student(87654321, 'M Nopqrst', ())
Student(1234567, 'Uvwxy Z', (1, 1, 1, 3))

student[74565] = ('Abc Def', 2.375)
student[87654321] = ('M Nopqrst', 0)
student[8765432] = None
min = Student(1024, 'Gh Ijkl', (2,))
pocet po remove_min = 3
pocet po write = 0

Projekt riesenie.py odovzdaj na úlohový server https://list.fmph.uniba.sk/ najneskôr 28. marca do 23:00. Za tento projekt môžeš získať 5 bodov.