Bizonyos esetekben szükséges vagy előnyös lehet, hogy fájlban tárolt adatsorok sorrendjét a feldolgozásuk előtt megfordítsuk (a fájlba írt utolsó sort lehessen elsőként beolvasni). Ilyen igény merülhet fel például naplófájlok vagy más idősoros adatok (pl. tőzsdei árfolyamok) feldolgozásakor, amikor is a jelenhez időben közelebbi (de a fájlban az utolsók között szereplő) adatok érdekesebbek lehetnek, mint a régebbiek.
A kérdés az, hogy mi van akkor, ha olyan nagy adathalmazzal (nagyon sok sorból álló fájlal) kell dolgozni, amely már nem tölthető be egyszerre a memóriába, és így pl. a reversed() beépített függvény nem alkalmazható?
Hasonló problematikával már foglalkoztunk a „Nagyméretű adathalmazok rendezése” című bejegyzésben, ahol az alkalmazott megoldás az úgynevezett külső rendezés volt. Ennek alapelvét a sorrendfordítás esetén is használhatjuk, amit az alábbi ábra szemléltet.


A módszernek most is két fázisa van, amelyek főbb lépései a következők:
1. Fázis: Darabolás és ideiglenes sorrendfordítás
- A megfordítandó szövegfájlból egymás után beolvassuk a sorokat, de egyszerre csak annyit, hogy ezek bájtban mért összmérete ne haladja meg az általunk előzetesen megszabott korlátot (pl. 40 MB). A sorok bájtban mért méretét úgy határozzuk meg, hogy megnézzük, hogy a sor hány karakterből áll, és e számot megszorozzuk 4 bájttal. Ennek indoka, hogy az UTF-8 maximum 4 bájton kódol egy karaktert. Ezzel ugyan felülbecsüljük a méretet, de ez a művelet sokkal gyorsabb, mintha a tényleges bájtokat számolnánk az encode() metódussal, vagy a valós memóriafoglaltságot számoltatnánk a sys modul getsizeof() függvényével.
- Minden alkalommal, amikor a megfelelő méretű adatsor-szakasz előáll, akkor azt egy listában eltároljuk, és e lista elemeit helyben megfordítjuk a reverse() metódussal.
- A fordított elemsorrendű lista tartalmát egy egyedi névvel rendelkező ideiglenes fájlba mentjük. A fájlok nevét a mentések sorrendjében egy listában eltároljuk.
Az ideiglenes fájlok egy ideiglenes mappában gyűlnek.
2. Fázis: Összefűzés fordított sorrendben
- Miután az összes ideiglenes fájl létrejött, azokat sorban egymás után megnyitjuk a mentési sorrendhez képest fordított sorrendben (tehát az utolsóként mentett ideiglenes fájlt olvassuk be először) és tartalmukat kiírjuk egy végső kimeneti eredményfájlba. Ez a fájl tehát a kiinduló, eredeti fájl sorait tartalmazza, de fordított sorrendben.
- Az ideiglenes mappát a tartalmával együtt eltávolítjuk.
E lépések programmal történű megvalósítása mind függvénnyel, mind osztállyal megtehető. Mi most az utóbbit választjuk. A FileReverser osztály definícióját, valamint a tesztsorokat láthatjuk alább. A működés megértését a részletes kommentek és az előző elvi ábra segíti.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
from itertools import count from tempfile import NamedTemporaryFile, gettempdir, TemporaryDirectory from pathlib import Path from random import choices, randint class FileReverser: def __init__(self, chunk_size_in_megabytes=40): # A memóriába egyszerre beolvasható adatsorok bájtban mért, megengedett maximális összmérete. (Maximális szakaszméret) self.chunk_size = chunk_size_in_megabytes * 1024 * 1024 # A memóriába maximálisan beolvasható számú adatsorokat gyűjtő lista. self._chunk_lines = [] # A memóriába egyszerre beolvasott adatsorok listájáinak és azokat tároló részfájlok számlálója. (Szakaszszámláló) self._chunk_counter = count(1) # A részfájlok útvonalainak listája. self._chunk_files_paths = [] # A megfordítandó és megfordított fájlok elérési útvonalának változói. self.input_filepath = self.output_filepath = '' # A részfájlokat tartalmazó ideiglenes mappaobjektum változója. self._temporary_directory: TemporaryDirectory | None = None def _write_lines_to_temp_chunk_file(self, lines: list): """A megadott lista elemeit mint adatsorokat egy ideiglenes fáljba írja. A fájl neve egy listában lesz eltárolva, hogy a példány másik metódusa azokat később használni tudja. Minden egyes meghíváskor az argumentum tartalma egy új, egyedi fájlnévvel rendelkező ideiglenes fájlba lesz elmentve. Minden így előálló ideiglenes fájlt egy közös ideiglenes könyvtár tartalmaz. """ with NamedTemporaryFile('w', encoding='utf8', newline='\n', prefix=f'out_{next(self._chunk_counter)}_', suffix='.txt', dir=self._temporary_directory.name, delete=False) as chunk_file: chunk_file.writelines(lines) self._chunk_files_paths.append(chunk_file.name) def _concat_reversed_chunks(self): """Veszi az egyes ideiglenes fájlokat az elmentésük fordított sorrendjében, és tartalmukat az eredményfájlba menti.""" with open(self.output_filepath, 'w', encoding='utf-8', newline='\n') as outfile: for chunk_file_path in reversed(self._chunk_files_paths): with open(chunk_file_path, 'r', encoding='utf-8', newline='\n') as chunkfile: for line in chunkfile: outfile.write(line) def reverse(self, path_to_file_to_be_reversed: str | Path) -> Path: """Az argumentumként megadott fájl tartalmát egy új fájlba menti úgy, hogy ebben az adatsorok fordított sorrendben szerepelnek. Az adatsorokat fordított sorrendben tartalmazó eredményfájl ugyanabban a mappában fog létrejönni, mint ahol az argumentumként megadott, megfordítandó fájl van. Az eredményfájl neve a 'reversed_' szóval kezdődik és a megfordítandó fájl nevével végződik. """ self.input_filepath = Path(path_to_file_to_be_reversed) self.output_filepath = self.input_filepath.parent / ('reversed_' + self.input_filepath.name) self._temporary_directory = TemporaryDirectory(dir=gettempdir()) cum_line_length = 0 # A korábban már beolvasott sorok halmozott mérete. with open(self.input_filepath, 'r', encoding='utf8', newline='\n') as f: for line in f: # Ha a megfordítandó fájlból aktuálisan beolvasott sor bájtmérete (sorhossz*4 byte) és a korábban már beolvasott # sorok halmozott mérete együttesen meghaladja a bájtban mért maximális szakaszméretet, akkor a sort nem adjuk # a beolvasott sorokat tároló listához, hanem e lista elemeit megfordítjuk és egy ideiglenes fájlba írjuk. # Ezután a listát ürítjük, majd első elemként az aktuális sort adjuk hozzá. if 4 * (cum_line_length + len(line)) > self.chunk_size: self._chunk_lines.reverse() self._write_lines_to_temp_chunk_file(self._chunk_lines) self._chunk_lines = [line] cum_line_length = len(line) # Egyéb esetben pedig az aktuális sort felvesszük a listába és a sorok halmozott bájtméretét tároló változó # értékét növeljük az aktuális sor hosszával. else: self._chunk_lines.append(line) cum_line_length += len(line) # A maradék utolsó adatsor-szakaszt is fordított sorrendben ideiglenes fájlba írjuk. if self._chunk_lines: self._chunk_lines.reverse() self._write_lines_to_temp_chunk_file(self._chunk_lines) # Az elmentett adatsor-szakaszokat a mentési sorrendhez képest fordított sorrendben, vagyis az utolsóként mentett # fájllal, kezdve az eredményfájlba mentjük. self._concat_reversed_chunks() # Alaphelyzetre állítás egy következő híváshoz. self._temporary_directory.cleanup() # Az ideiglenes könyvtár és tartalmának törlése. self._chunk_counter = count(1) # A szakaszszámlálót kezdőhelyzetbe állítjuk. self._chunk_files_paths.clear() self._chunk_lines.clear() # Az eredményfájl abszolút útvonalával térünk vissza. return self.output_filepath.resolve() # TESZT def make_large_number_sequence_text_file(file_path: str, number_of_lines: int): """Az első argumentumként megadott fájlt hozza létre, amelyben soronként a nem negatív egész számok szerepelnek. A sorok számát a második argumentummal lehet megadni. """ with open(file_path, 'w', encoding='UTF8', newline='\n') as f: for n in range(number_of_lines): f.write(str(n) + '\n') def make_large_random_text_file(file_path: str, number_of_lines: int): """Az első argumentumként megadott fájlt hozza létre, amelyben soronként véletlenszerűen előállított, legfeljebb 15 karakter hosszú karakterláncok szerepelnek. A sorok számát a második argumentummal lehet megadni. """ with open(file_path, 'w', encoding='UTF8', newline='\n') as f: for n in range(number_of_lines): f.write(''.join(map(chr, choices(range(65, 127), k=randint(1, 15)))) + '\n') # Nagy számú sort tartalmazó tesztfájlok előállítása. make_large_random_text_file('large_file_random_strings_10_000_000.txt', 10_000_000) make_large_number_sequence_text_file('large_file_integers_100_000_000.txt', 100_000_000) # A tesztfájlok adatsorainak megfordítása és fájlokba mentése. file_reverser = FileReverser(30) file_reverser.reverse('large_file_random_strings_10_000_000.txt') file_reverser.reverse('large_file_integers_100_000_000.txt') # Az adasorokat fordított sorrendben tartalmazó létrejött új fájlok neve # reversed_large_file_random_strings_10_000_000.txt és reversed_large_file_integers_100_000_000.txt' |
Annak, aki jobban el akar mélyülni e feladatban, érdemes lehet gyakorlásképpen a fájltartalom sorrendfordítását függvénnyel is megvalósítani, és annak definícióját megalkotni.
E bejegyzésben elsődlegesen a fájlkezelés és az ideiglenes fájlokkal és könyvtárakkal való munka voltak a középpontban. Ezekkel a Python tudásépítés lépésről lépésre című e-könyv „Mentsük, ami menthető! – fájlok és mappák” fejezete foglalkozik részletesen.